우리가 플레이하는 게임에는 확률에 의한 요소가 들어가는 경우가 상당히 많습니다. 흔히 랜덤박스니 가챠니 하는 뽑기 요소는 말할 것도 없으며, 아이템을 강화하거나 혹은 적에게 공격하는 액션을 취할 때도 해당 액션이 성공할 확률이 몇 퍼센트다 하는 개념이 들어가는 경우가 많지요. 이처럼 게임에서 확률 요소는 빼놓을래야 빼놓을 수가 없는 것입니다.
그런데, 컴퓨터는 어떻게 확률에 의한 판정을 내릴 수가 있는 걸까요? 그건 0과 1 사이에 고르게 분포된 임의의 숫자(난수)를 얻을 수만 있다면 그리 어려운 일은 아닙니다. 예를 들어, 어떤 액션의 성공 확률을 25%로 가정해 봅시다. 0%는 0이고 100%는 1이므로, 25%는 0.25입니다. 앞에서 0과 1 사이에 고르게 분포되어 있는 난수를 뽑는다고 가정했으므로, 난수 하나를 뽑았을 때 그 난수가 0.25보다 작거나 같을 확률은 25%입니다. 따라서 25%의 성공 확률이 주어졌다면, 난수 하나를 뽑아서 해당 난수가 0.25보다 작거나 같으면 성공으로 판정하고 아니면 실패로 판정하면 됩니다. 생각보다 간단하죠. 이를 Python3 코드로 단순하게 옮기면 다음과 같습니다. 함수 이름에 신경쓰면 지는 겁니다.
import random # 파이썬 표준 라이브러리에서 의사난수 모듈을 불러온다. def gacha(percent): # 확률을 퍼센트로 받아, 확률에 따라 참과 거짓을 반환하는 함수를 선언한다. prob = percent * 0.01 # 퍼센트 값을 0과 1 사이의 숫자로 바꾼다. rand = random.random() # 의사난수 모듈을 사용하여, 0과 1 사이의 난수 하나를 뽑는다. if prob >= rand: # 확률보다 난수가 작거나 같으면 참(True)을 반환한다. 아니면 거짓(False)이다. return True else: return False
문제는 난수를 얻는 것 자체입니다. 현대의 디지털 컴퓨터는 결정론적 알고리즘(deterministic algorithm)에 의해 작동하는데, 이것의 특징은 똑같은 입력이 들어오면 언제나 똑같은 결과만을 내놓는다는 것이지요. 쉽게 말해서, 계산기에 1 + 1을 눌렀는데 뜬금없이 3이나 4나 -127이 나온다면 고장난 것이니 내다버려야 하지 않겠습니까. 바로 그런 이치로, 컴퓨터에 1 + 1을 입력하면 언제 어디서나 2가 나오는 것은 지극히 당연하고 우리에게 친숙한 결과입니다. 문제는, 난수란 것은 뽑을 때마다 바뀌어야 하는 숫자라는 점이죠. 주사위를 아무리 던져도 숫자가 1밖에 나오지 않는다면, 그 주사위는 쓸모없는 물건입니다.
그래서, 현대의 컴퓨터에서 난수가 필요하면 난수를 얻기 위한 별도의 하드웨어 장치를 사용하거나, 혹은 씨앗이 되는 숫자(seed number)를 특정한 공식에다 집어넣어서 마치 난수처럼 보이는 숫자(의사난수, pseudo-random number)를 얻어내는 방법을 사용합니다. 난수를 얻기 위한 하드웨어 장치는 여러 가지 종류(예를 들어, 로또 추첨시 사용되는 장치도 일종의 난수를 얻는 장치라고 볼 수 있습니다. 혹은, 진짜로 주사위를 던져서 난수를 얻는 장치도 있다고 합니다.)가 있지만, 장치 주변의 노이즈를 수신하여 디지털 숫자로 전환하는 방식이 흔히 사용된다고 합니다. 예를 들면 아날로그 TV나 라디오를 아무 방송도 하지 않는 채널이나 주파수에 맞춰두면 주변의 의미없는 노이즈 전파신호를 수신하여 내보내는 것을 볼 수 있는데, 이걸 그대로 디지털화하여 난수로 사용하는 것이죠. 아니면 디지털 카메라는 어두운 곳에서 노이즈가 늘어난다는 특징을 활용하여, 그 노이즈를 증폭하여 난수를 생성하는 방법도 있습니다. 투과성이 약한 방사성 동위원소와 검출기를 한데 묶어서, 방사선 입자가 검출되는 랜덤한 패턴을 난수로 뽑아내는 경우도 있고요. 이런 하드웨어를 사용하는 방법은 진짜 랜덤한 난수를 얻을 수 있지만, 별도의 하드웨어 비용이 들어가고 난수 생성 속도에 제약이 있으며 구현 방식에 따라 주변 환경의 영향을 받을 수 있는 등의 단점이 있어서 정말 중요한 곳이 아닌 이상에야 그리 널리 쓰이지는 않습니다.
일반적으로 가장 많이 사용되는 난수 얻기 수단은 의사난수를 생성하는 알고리즘을 사용하는 것입니다. 알고리즘에 씨앗이 되는 숫자를 집어넣으면 마치 아무 숫자나 나오는 것처럼 보이고, 그렇게 해서 나온 숫자를 다시 씨앗 숫자로 사용하는 방식이죠. 예를 들어, 예전부터 널리 사용되던 의사난수 생성방식인 선형 합동 생성기(linear congruential generator, LCG)의 구조는 실로 간단합니다. 곱할 숫자 a, 더할 숫자 c, 나눌 숫자 m을 미리 상수로 준비해 두고, 씨앗 숫자 s가 들어오면
s = (s * a + c) % m
이라는 실로 단순하기 짝이 없는 코드를 실행할 뿐이죠. 여기서 상수로 사용되는 a, c, m의 값은 각 구현마다 다른데, 예시로 C++11 표준의 minstd_rand
에서 사용하는 상수값을 보여드리겠습니다.
- 곱할 숫자 a = 48271 (소수)
- 더할 숫자 c = 0 (더하지 않음)
- 나눌 숫자 m = 231 - 1 (메르센 소수)
이 값들을 선형 합동 생성기 공식에 집어넣고, 최초의 씨앗 수로 1234를 넣어서 10번을 돌리면 숫자가 다음과 같이 차례대로 변합니다.
- 59566414
- 1997250508
- 148423250
- 533254358
- 982122076
- 165739424
- 1031150829
- 305696493
- 915275066
- 1061641155
어떤가요, 제법 랜덤해 보이지 않나요?
물론 이 방법은 한계가 명확합니다. 진짜 난수가 아니라 고정된 특정 공식에 의해 산출되는 것이기 때문에 공식의 상수와 씨앗 수만 알고 있다면 그 뒤에 이어질 모든 난수열을 모두 다 알고 있는 것이나 마찬가지일 뿐더러, 공식의 상수 설정이 난수의 품질에 직접적으로 영향을 줍니다. 예를 들어, 1960년대에 IBM 메인프레임에서 사용하던 RANDU라는 의사난수 생성기의 상수는 a = 216 + 3, c = 0, m = 231인데, 위키백과에서는 이를 두고 역사상 최악으로 설계된 유사난수 생성기 중 하나
라고 서술하고 있습니다. 왜냐하면, 이 유사난수 생성기로 만든 숫자들을 여럿 모아 (x, y, z) 좌표 형태로 만들어서 3차원 공간상에 나열하면 2차원 평면 15개가 늘어서 있는 형태의 명확한 패턴이 관찰되거든요. 이를 마서글리아 효과(Marsaglia effect)라고 부르는데, 이 평면 패턴이 나타나는 효과 자체는 선형 합동 생성기 방식의 근본적인 한계이지만 m = 231일 때 이론적으로 나타날 수 있는 평면의 수는 최대 1290개인데도 RANDU에서는 평면이 15개밖에 나타나지 않는 것은 순전히 상수 값의 설정이 잘못되었기 때문입니다. 공식의 틀 자체는 변한 게 없는데, 상수를 어떻게 선택하느냐에 따라 난수가 엉망진창이 될 수 있는 것이죠. 하여튼, 당시에 RANDU는 몬테카를로 시뮬레이션(난수를 이용하여 확률적으로 근사값을 구하는 방법) 등에 자주 사용되었는데, 이걸로 구한 난수에 패턴이 있다는 사실이 드러나자 이걸 사용한 몬테카를로 시뮬레이션의 결과 자체가 모두 의심받게 되는 사태로까지 발전하게 됩니다. 그래서 이후에는 선형 합동 생성기 방식 자체가 몬테카를로 시뮬레이션을 위한 난수 생성에 적합하지 않다고 보게 되었습니다.
RANDU 알고리즘에서 나타나는 마서글리아 효과의 시각화. 3차원 공간상에 15개의 2차원 평면 패턴이 드러난다.
요즘은 선형 합동 생성기보다 더 좋은 의사난수 생성 알고리즘이 여럿 나와 있습니다. 암호학적으로 안전하다는 것이 증명된 알고리즘도 있고, 암호학적으로 안전하지는 않아도 일반적인 목적에서는 충분히 랜덤한 알고리즘들도 많지요. 의사난수 생성 알고리즘은 지금도 개발되고 있습니다. PCG니 xoroshiro니 하는 것들로, 코드를 보면 갖가지 비트 연산을 써서 이진수 숫자를 카드 셔플하듯이 마구 섞어버리는 물건입니다. 여담으로, PCG와 xoroshiro의 개발자는 서로 상대방의 알고리즘에 결함이 있다고 자기네 홈페이지 및 레딧에서 디스를 하는 사이인 모양이더군요. 어쨌거나 좀 더 좋은 의사난수 생성 알고리즘들은 계속 나오고 있지만, 게임 같은 곳에서는 여전히 선형 합동 생성기와 같은 단순한 방법으로 의사난수를 생성하는 경우가 흔합니다.
그런데, 아무리 의사난수 알고리즘이 좋아도 거기에 최초의 씨앗 수가 들어가야 한다는 사실은 변하지 않습니다. 이 씨앗 수는 사전에 정해지지 않은 값이어야만 이후 나오는 의사난수가 다른 값이 될 수가 있지요. 가장 흔히 사용되는 씨앗 수는 바로 현재 시간입니다. 시간은 매순간 바뀌는 숫자값이니 씨앗 수로 사용하기에 적당하거든요. 시계가 내장되지 않은 기계(아케이드 또는 휴대용 게임기 등)의 경우에는 플레이어가 입력장치를 통해 입력한 값들을 메모리에 저장해 두었다가 씨앗 수로 삼는 경우가 보편적입니다. 플레이어가 뭘 입력할지는 프로그램이 사전에 알 수가 없으니까요.
어떤 게임 폐인들은 특정 게임에서 위에서 언급한 의사난수의 특징을 교묘하게 응용하기도 합니다. 만약 해당 게임이 플레이어가 지금까지 입력한 내용을 씨앗 수로 삼아 랜덤 값을 도출한다면, 리플레이를 할 때 이전 플레이와 완전히 똑같은 입력을 반복하면 똑같은 랜덤 값이 도출되리라는 것을 기대할 수 있지요. 더욱 나아가서, 입력값이나 입력 타이밍을 조절하여 씨앗 수를 조작하는 것으로 이후 게임에서 나오는 랜덤 요소들을 자신이 원하는 식으로 조작하는 것도 가능하지요. 이에 대해서는 나무위키의 관련 항목들을 읽어보는 것이 좋습니다.
사실, 이 이야기를 하는 것은 제가 나무위키에서 TAS(Tool-Assisted Speedrun) 관련 항목들을 읽다가 문득 게임에서의 확률에 대해 생각난 것이 있어서입니다. 개발자가 설정하는 확률이란 것이 과연 실제로는 얼마나 정확하게 적용되는 것일까요? 예를 들어 성공 확률이 50%로 설정된 액션이 있다고 가정해볼게요. 정말로 성공 확률이 50%라면 액션을 100번 반복했을 때 성공 횟수는 50번이어야 하겠지만, 실제로는 그렇지 않습니다. 48번밖에 성공하지 못할 수도 있고, 아니면 51번을 성공할 수도 있습니다. 그러면, 성공 확률이 극단적인 경우에는 어떨까요? 강화에 성공할 확률이 0.01%밖에 되지 않는 경우, 혹은 확률이 99.99%인 경우에는 어떨까요? 그것이 궁금해졌지요.
그래서, 위에서 제시한 파이썬 코드를 가지고 직접 실험을 해봤습니다. 성공 확률을 부여하고 그 확률을 10만번 시행하여 성공한 횟수를 1단위로 하여, 확률별로 1만 단위를 돌렸습니다. 실험에 사용한 확률은 50%, 0.01%, 99.99%입니다. 참고로 위에 제시된 파이썬 코드는 파이썬의 기본 의사난수 모듈을 사용하는데, 여기에 사용되는 의사난수 생성 알고리즘은 메르센 트위스터(Mersenne Twister)라고 합니다. 좀 더 게임스럽게 하려면 자체적으로 선형 합동 생성기 함수를 만들어 쓰는 게 좋겠지만, 그런 생각은 스크립트 다 돌리고 나서야 생각이 나기도 했고 또 정수를 0과 1 사이의 부동소수점으로 바꿀 간단한 방법이 바로 생각나지 않길래 그냥 다음에 생각나면 해보기로 했습니다. 그래프 작성은 엑셀 2016을 썼습니다.
먼저, 50% 확률입니다. 10만번 실행시 성공 횟수는 5만번이 찍혀야 하는데, 실제 분포는 어떻게 될까요. 확인해봅시다.
성공확률 50%로 100,000번을 돌리니, 성공 횟수는 49,300에서 50,700회 사이에서 분포합니다. 다시 말해, 확률 50%가 주어졌을 때 실제 분포는 ±0.7% 정도의 오차범위 안에 있다는 말이 됩니다. 대부분의 점들은 49,500에서 50,500회 사이에 분포하고 있으니, 대개의 경우는 ±0.5%의 오차범위 안에서 논다는 말이 되겠지요.
히스토그램으로 조금 더 자세히 살펴봤습니다. 이게 그 정규분포인가 뭔가 하는 그거죠? 문돌이는 그런 거 모릅니다. 데헷. (코 쓰윽)
그럼, 성공확률 0.01%는 어떨까요. 10만번 삽질했을 때 단순 계산으로 10번밖에 성공하지 못하는 극악 난이도입니다. 저보고 이런 확률에 도전하라고 하면 그런 똥망겜은 집어치우고 말 거에요.
역시, 예상대로 10번 성공을 중심으로 성공횟수가 모여 있습니다. 근데 운빨이 영 안 좋은 것인지, 10만번 시도해서 고작 1번밖에 성공하지 못한 사례가 있네요. 1만번 시도도 기운빠지는데, 10만번 삽질은 차라리 날 죽여라 소리가 절로 나올 겁니다. 그 행운은 저기 우상단에 위치한 28번 성공에 모조리 몰렸나 봐요. 그나마 아예 한번도 성공 못하는 경우는 없는 게 다행일까요.
확률 0.01%의 히스토그램.
그럼 이번에는 혜자롭게(?) 확률을 99.99%로 준 경우를 살펴볼까요. 이건 아무래도 실패를 하는 경우가 더 희귀하지요.
성공 확률이 아무리 99.99%라지만, 100%와는 다릅니다. 진짜 100%가 아닌 이상 !감나빗을 완전히 피해갈 수는 없는 법이죠. 그래도 99.98% 아래의 성공확률을 보여주는 경우는 비교적 드뭅니다.
그러고 보니 동생이 플레이하는 마비노기가 생각나네요. 제가 플레이하던 시절의 마비노기는 물 마시는 것조차 실패가 뜰 확률이 있던 게임이었는데, 요새도 그런지는 모르겠군요.
나무위키 돌아다니다 뜬금없이 생각난 발상을 가지고 스크립트를 짜서 돌리고 그 결과를 그냥 날려버리기 조금 아까워서 글을 쓰기 시작했는데, 이거 생각보다 시간이 오래 걸렸네요. 하여튼 오늘의 뻘짓은 여기까지. 게임 관련 글 보다가 생각난 것이니 게임 게시판에 올립니다.