카테고리 보관물: 게임개발

게임개발

루프없이 많은 주사위 롤을 정확하게 시뮬레이션합니까? % 3-5.55 % 4-8.33

게임에서 주사위를 많이 굴리면 임의의 숫자 생성기를 루프로 호출 할 수 있습니다. 그러나 주사위를 자주 굴리면 분포 곡선 / 히스토그램을 얻을 수 있습니다. 그래서 내 질문은 그 분포에 맞는 숫자를 줄 수있는 멋진 간단한 계산이 있습니까?

예 : 2D6-점수-확률 %

2-2.77 %

3-5.55 %

4-8.33 %

5-11.11 %

6-13.88 %

7-16.66 %

8-13.88 %

9-11.11 %

10-8.33 %

11-5.55 %

12-2.77 %

위의 내용을 알면 단일 d100을 굴려서 정확한 2D6 값을 계산할 수 있습니다. 그러나 일단 10D6, 50D6, 100D6, 1000D6으로 시작하면 많은 처리 시간을 절약 할 수 있습니다. 그래서 이것을 빨리 할 수있는 튜토리얼 / 방법 / 알고리즘이 있어야합니까? 주식 시장, 카지노, 전략 게임, 난쟁이 요새 등에 유용 할 것입니다.이 함수를 몇 번만 호출하고 몇 가지 기본 수학을 수행하는 데 몇 시간이 걸리는 완전한 전략적 전투 결과를 시뮬레이션 할 수 있다면 어떨까요?



답변

위의 의견에서 언급했듯이 코드를 복잡하게 만들기 전에 이것을 프로파일 링하는 것이 좋습니다. 빠른 for루프 합산 주사위는 복잡한 수학 공식 및 테이블 작성 / 검색보다 이해하고 수정하기가 훨씬 쉽습니다. 중요한 문제를 해결하려면 항상 먼저 프로파일 링하십시오. 😉

즉, 한 번에 복잡한 확률 분포를 샘플링하는 두 가지 주요 방법이 있습니다.


1. 누적 확률 분포

하나의 균일 한 무작위 입력 만 사용하여 연속 확률 분포에서 샘플링하는 깔끔한 트릭 이 있습니다 . 그것은 함께 할 수있다 누적 분포 함수를 그 “값을 얻기없는 확률 무엇 답변 더 큰 X보다가?”

이 함수는 감소하지 않고 0에서 시작하여 도메인에서 1로 증가합니다. 6 면체 주사위 2 개를 합한 예는 다음과 같습니다.

누적 분포 함수에 계산하기 편리한 역수가있는 경우 (또는 베 지어 곡선과 같은 부분 함수로 근사 할 수있는 경우)이를 사용하여 원래 확률 함수에서 샘플링 할 수 있습니다.

역함수는 0에서 1 사이의 도메인을 원래 랜덤 프로세스의 각 출력에 매핑 된 간격으로 파 셀링하고 각 영역의 원래의 확률과 일치하는 포획 영역을 처리합니다. (이것은 연속 분포의 경우 무한히 사실입니다. 주사위 롤과 같은 이산 분포의 경우 신중한 반올림을 적용해야합니다)

이것을 사용하여 2d6을 에뮬레이트하는 예는 다음과 같습니다.

int SimRoll2d6()
{
    // Get a random input in the half-open interval [0, 1).
    float t = Random.Range(0f, 1f);
    float v;

    // Piecewise inverse calculated by hand. ;)
    if(t <= 0.5f)
    {
         v = (1f + sqrt(1f + 288f * t)) * 0.5f;
    }
    else
    {
         v = (25f - sqrt(289f - 288f * t)) * 0.5f;
    }

    return floor(v + 1);
}

이것을 다음과 비교하십시오.

int NaiveRollNd6(int n)
{
    int sum = 0;
    for(int i = 0; i < n; i++)
       sum += Random.Range(1, 7); // I'm used to Range never returning its max
    return sum;
}

코드 선명도와 유연성의 차이에 대해 무엇을 의미합니까? 순진한 방법은 루프로 순진하지만 짧고 간단하며 작동 방식을 즉시 알 수 있으며 다양한 다이 크기와 수로 쉽게 확장 할 수 있습니다. 누적 배포 코드를 변경하려면 사소한 수학이 필요하며 명확한 실수없이 쉽게 깨져서 예기치 않은 결과가 발생할 수 있습니다. (내가 위에서 만들지 않았 으면 좋겠다)

따라서 명확한 루프를 없애기 전에 실제로 이런 종류의 희생에 가치가있는 성능 문제인지 반드시 확인하십시오.


2. 별칭 방법

누적 분포 함수의 역수를 간단한 수학 식으로 표현할 수있을 때 누적 분포 방법이 효과적이지만 항상 쉬운 것은 아닙니다. 불연속 분포의 신뢰할 수있는 대안 은 Alias ​​Method 입니다.

이를 통해 두 개의 독립적으로 균일하게 분포 된 임의의 입력을 사용하여 임의의 이산 확률 분포에서 샘플링 할 수 있습니다.

왼쪽 아래의 것과 같은 분포를 취하고 ( 상대 가중치에 관심이있는 Alias ​​Method의 경우 면적 / 무게가 1로 합산되지 않을까 걱정하지 마십시오 ) 다음과 같은 표로 변환하여 작동합니다 오른쪽 위치 :

  • 각 결과에 대해 하나의 열이 있습니다.
  • 각 열은 최대 두 부분으로 나뉘며 각 부분은 원래 결과 중 하나와 연결됩니다.
  • 각 결과의 상대적 면적 / 무게가 유지됩니다.

( 샘플링 방법에 대한이 훌륭한 기사의 이미지를 기반으로 한 다이어그램 )

코드에서 우리는 각 열에서 대체 결과를 선택할 확률을 나타내는 두 개의 테이블 (또는 두 개의 속성을 가진 객체 테이블)과 대체 결과의 동일성 (또는 “별칭”)으로이를 나타냅니다. 그런 다음 분포에서 표본 추출 할 수 있습니다.

int SampleFromTables(float[] probabiltyTable, int[] aliasTable)
{
    int column = Random.Range(0, probabilityTable.Length);
    float p = Random.Range(0f, 1f);
    if(p < probabilityTable[column])
    {
        return column;
    }
    else
    {
        return aliasTable[column];
    }
}

여기에는 약간의 설정이 포함됩니다.

  1. 가능한 모든 결과의 상대 확률을 계산하십시오 (따라서 1000d6을 굴릴 경우 1000에서 6000까지 모든 합계를 얻는 방법의 수를 계산해야합니다)

  2. 각 결과에 대한 항목으로 테이블 쌍을 작성하십시오. 전체 방법은이 답변의 범위를 벗어나므로 Alias ​​Method 알고리즘에 대한이 설명을 참조하는 것이 좋습니다 .

  3. 이 분포에서 새로운 랜덤 다이 롤이 필요할 때마다 해당 테이블을 저장하고 다시 참조하십시오.

이것은 시공간 상충 관계 입니다. 사전 계산 단계는 다소 철저하며, 결과 수에 비례하여 메모리를 따로 보관해야합니다 (1000d6의 경우에도 한 자리 킬로바이트를 사용하므로 수면을 잃을 것은 없습니다). 분포가 아무리 복잡하더라도 상수 시간입니다.


나는 그 방법들 중 하나 또는 다른 방법이 다소 유용 할 수 있기를 바랍니다 (또는 순진한 방법의 단순성이 반복하는 데 가치가 있다고 확신합니다);)


답변

안타깝게도이 방법으로 성능이 향상되지는 않습니다.

난수가 어떻게 생성되는지에 대한 오해가있을 수 있다고 생각합니다. 아래의 예제를 보자 [Java] :

Random r = new Random();
int n = 20;
int min = 1; //arbitrary
int max = 6; //arbitrary
for(int i = 0; i < n; i++){
    int randomNumber = (r.nextInt(max - min + 1) + min)); //silly maths
    System.out.println("Here's a random number: " + randomNumber);
}

이 코드는 1에서 6 사이의 임의의 숫자를 인쇄하여 20 회 반복됩니다 (포함). 이 코드의 성능에 대해 이야기 할 때 Random 객체 (만들었을 때 컴퓨터의 내부 시계를 기준으로 의사 난수 정수 배열을 생성하는 데 소요됨)를 생성하는 데 약간의 시간이 소요됩니다. 각 nextInt () 호출에서 조회합니다. 각 ‘롤’은 일정한 시간 작업이므로 시간이 매우 저렴합니다. 또한 min에서 max까지의 범위는 중요하지 않습니다 (즉, 컴퓨터가 d6100을 굴리는 것만 큼 d6을 굴리는 것이 쉽습니다). 시간 복잡성 측면에서 말하면, 솔루션의 성능은 단순히 O (n)입니다. 여기서 n은 주사위 수입니다.

또는 단일 d100 (또는 해당 문제의 경우 d10000) 롤을 사용하여 원하는 개수의 d6 롤을 추정 할 수 있습니다. 이 방법을 사용하여 롤링하기 전에 먼저 s [얼굴 수] * n [다이스 수] 백분율을 계산해야합니다 (기술적으로는 s * n-n + 1 %이므로 대략적으로 나눌 수 있어야합니다). 2d6 롤 시뮬레이션 예제에서는 11 %와 6이 고유 한 것으로 계산되었습니다). 롤링 후 이진 검색을 사용하여 롤이 어느 범위에 속하는지 알아낼 수 있습니다. 시간 복잡성 측면에서이 솔루션은 O (s * n) 솔루션으로 평가됩니다. 여기서 s는 변의 수이고 n은 주사위의 수입니다. 보다시피, 이것은 이전 단락에서 제안 된 O (n) 솔루션보다 느립니다.

거기에서 외삽하면 1000d20 롤을 시뮬레이션하기 위해이 두 프로그램을 모두 만들었다 고 가정 해보십시오. 첫 번째는 단순히 1,000 번 굴립니다. 두 번째 프로그램은 다른 작업을 수행하기 전에 먼저 19,001 % (잠재적 범위 1,000 ~ 20,000)를 결정해야합니다. 따라서 메모리 조회가 부동 소수점 연산보다 몇 배나 비싼 이상한 시스템을 사용하지 않는 한 각 롤마다 nextInt () 호출을 사용하는 것이 좋습니다.


답변

주사위 조합을 저장하고 싶다면 좋은 해결책은 해결책이 있다는 것입니다. 나쁜 점은 우리 컴퓨터가 이런 종류의 문제와 관련하여 어떻게 든 제한되어 있다는 것입니다.

좋은 뉴스:

이 문제에 대한 결정적인 접근 방식이 있습니다.

1 / 주사위 그룹의 모든 조합을 계산

2 / 각 조합의 확률 결정

3 /이 목록에서 오지를 던지는 대신 결과를 검색합니다.

나쁜 소식 :

반복과의 조합 수는 다음 공식으로 주어집니다

Γ엔케이=(엔+케이−1케이)=(엔+케이−1)!케이! (엔−1)!

( 프랑스 위키 백과에서 ) :

즉, 예를 들어 150 개의 오지에서는 698’526’906 조합이 있습니다. 확률을 32 비트 부동 소수점으로 저장한다고 가정하면 2,6GB의 메모리가 필요하며 인덱스에 대한 메모리 요구 사항을 추가해야합니다.

계산 용어로, 조합 수는 컨볼 루션 (convolution)에 의해 계산 될 수 있는데, 이는 편리하지만 메모리 제약을 해결하지 못한다.

결론적으로, 많은 수의 오지에 대해서는 각 조합과 관련된 확률을 미리 계산하는 대신 오지를 던져 결과를 관찰하는 것이 좋습니다.

편집하다

그러나 오지의 합계에만 관심이 있으므로 훨씬 적은 자원으로 확률을 저장할 수 있습니다.

컨벌루션을 사용하여 각 주사위 합에 대한 정확한 확률을 계산할 수 있습니다.

에프나는(미디엄)=∑엔에프1(엔)에프나는−1(미디엄−엔)

그런 다음 1/6의 각 결과에서 1/6부터 시작하여 주사위 수에 대한 모든 올바른 확률을 구성 할 수 있습니다.

다음은 일러스트레이션 용으로 작성한 대략적인 Java 코드입니다 (실제로 최적화되지는 않음).

public class DiceProba {

private float[][] probas;
private int currentCalc;

public int getCurrentCalc() {
    return currentCalc;
}

public float[][] getProbas() {
    return probas;
}

public void calcProb(int faces, int diceNr) {

    if (diceNr < 0) {
        currentCalc = 0;
        return;
    }

    // Initialize
    float baseProba = 1.0f / ((float) faces);
    probas = new float[diceNr][];
    probas[0] = new float[faces + 1];
    probas[0][0] = 0.0f;
    for (int i = 1; i <= faces; ++i)
        probas[0][i] = baseProba;

    for (int i = 1; i < diceNr; ++i) {

        int maxValue = (i + 1) * faces + 1;
        probas[i] = new float[maxValue];

        for (int j = 0; j < maxValue; ++j) {

            probas[i][j] = 0;
            for (int k = 0; k <= j; ++k) {
                probas[i][j] += probability(faces, k, 0) * probability(faces, j - k, i - 1);
            }

        }

    }

    currentCalc = diceNr;

}

private float probability(int faces, int number, int diceNr) {

    if (number < 0 || number > ((diceNr + 1) * faces))
        return 0.0f;

    return probas[diceNr][number];

}

}

원하는 매개 변수로 calcProb ()를 호출 한 다음 결과에 대한 proba 테이블에 액세스하십시오 (첫 번째 색인 : 1 주사위의 경우 0, 2 개의 주사위의 경우 1).

랩톱에서 1’000D6으로 확인했는데 1에서 1,000 개의 오지에서 가능한 모든 오지의 합계를 계산하는 데 10 초가 걸렸습니다.

사전 계산 및 효율적인 스토리지를 통해 많은 오지에 대한 빠른 답변을 얻을 수 있습니다.

도움이 되길 바랍니다.


답변