운에 따라 결과가 달라지는 게임에는 무작위성이 필요해요. Factor의 random 보캐뷸러리는 넘겨받은 대상에 따라 알맞게 동작하는 random이라는 범용 워드 하나와, 섞기와 표본 추출을 위한 도우미들, 그리고 테스트를 위해 무작위성을 재현 가능하게 만드는 방법을 제공해요.
inline 표시하기이 연습 문제의 몇몇 과제에서는 쿼테이션([ … ] 블록)을 인자로 받아 실행하는 워드를 정의해요. 여기에 Factor가 정해 둔 확고한 규칙이 하나 있는데, 바로 인자로 받은 쿼테이션을 call하는 워드는 반드시 inline으로 선언해야 한다는 것이에요. 방법은 닫는 ; 바로 뒤에 inline을 붙여 쓰는 거예요.
inline은 컴파일러에게 워드의 본문을 한 번 따로 컴파일하는 대신 각 호출 지점에 그대로 끼워 넣으라고 알려줘요.
다음은 10을 푸시한 다음 넘겨받은 쿼테이션을 실행하는 작은 워드예요:
USING: kernel math prettyprint ;
: with-ten ( quot -- ) 10 swap call ; inline
이 워드의 스택 효과는 ( quot -- )인데, 출력을 하나도 선언하지 않아요. 그런데도 효과가 서로 다른 두 쿼테이션을 넘겨 호출하면 서로 다른 결과가 나오고, 그 결과는 모두 호출자에게 흘러 돌아와요:
[ 1 + ] with-ten . ! => 11 (quotation ( n -- n ): one value back)
[ dup 2 * ] with-ten . . ! => 20
! => 10 (quotation ( n -- n n ): two values back)
random: 원소 하나random에 있는 random은 인자에 따라 동작이 달라지는 범용 워드예요:
random ( obj -- elt )
n을 주면 [0, n) 범위의 무작위 정수를 반환해요:USING: random ;
6 random . ! 4 (a value from 0 to 5)
{ "rock" "paper" "scissors" } random . ! "paper"
보통 주사위를 굴리려면 범위를 하나 위로 옮기면 돼요. 6에 대한 random은 0부터 5까지를 주니까, 6 random 1 +는 1부터 6까지를 줘요.
정해진 개수만큼 무작위로 값을 뽑아 모으려면 sequences에 있는 replicate로 쿼테이션을 반복해요:
replicate ( n quot -- seq )
USING: random sequences ;
3 [ 6 random 1 + ] replicate . ! { 4 1 6 } (three dice)
randomize: 제자리에서 섞기random에 있는 randomize는 원소들을 무작위 순서로 배열한 시퀀스를 반환해요:
randomize ( seq -- randomized )
{ 1 2 3 4 5 } randomize . ! { 3 1 5 2 4 }
제자리에서 섞고 같은 시퀀스를 반환하니, 원래 순서를 유지해야 한다면 먼저 clone을 해요.
sample: 서로 다른 원소 여러 개random에 있는 sample은 시퀀스에서 서로 다른 원소를 n개 뽑아요. 카드를 나눠 줄 때처럼 같은 카드가 두 번 나오지 않아요:
sample ( seq n -- seq' )
{ 10 20 30 40 50 } 3 sample . ! { 40 10 30 }
시퀀스에 들어 있는 것보다 많은 원소를 요청하면 오류가 발생해요.
기본적으로 random은 시스템 시계로 시드를 정한 전역 생성기에서 값을 뽑기 때문에 실행할 때마다 결과가 달라져요. 테스트를 위해서든, 다시 실행해 볼 수 있는 게임을 원할 때든, 고정된 시드를 가진 생성기를 묶어서 사용해요.
random.mersenne-twister에 있는 <mersenne-twister>는 정수 시드로 생성기를 만들고, random에 있는 with-random은 그 생성기를 설치한 상태로 쿼테이션을 실행해요:
<mersenne-twister> ( seed -- rnd )
with-random ( rnd quot -- )
USING: random random.mersenne-twister ;
42 <mersenne-twister> [ 6 random 1 + ] with-random .
! always the same value for seed 42
쿼테이션 안의 모든 것, 즉 random, randomize, sample은 시드를 지정한 생성기에서 값을 뽑아요. 그래서 같은 시드는 언제나 같은 결과를 재현해요.
쿼테이션을 인자로 받아 with-random으로 실행하는 워드는 워드에 inline 표시하기의 규칙이 적용돼요. with-random이 결국 그 쿼테이션을 call하기 때문에, 그런 래퍼도 inline으로 표시해야 해요.
해변 산책로의 오락실에서 운에 따라 승부가 갈리는 게임들을 운영해요. 손님들은 주사위를 굴리고, 인형 뽑기 기계는 상품을 집어 올리고, 카드 테이블에서는 카드를 섞어서 패를 돌려야 해요. 이 모든 게 무작위성 덕분에 돌아가죠. 영업이 끝난 뒤에는 "리플레이" 기능도 필요해요. 시비가 붙은 게임을 같은 시드로 다시 돌려볼 수 있어야 하거든요.
roll-die는 면의 수를 받아서 1부터 sides까지(양 끝 포함)의 값을 반환하도록 정의해요.
6 roll-die .
! => 4 (some value from 1..6)
pick-prize는 상품들의 시퀀스를 받아서 그중 하나를 무작위로 반환하도록 정의해요.
{ "teddy" "goldfish" "keyring" } pick-prize .
! => "goldfish"
shuffle-deck는 덱(시퀀스)을 받아서 카드를 무작위 순서로 재배열한 덱을 반환하도록 정의해요.
{ 1 2 3 4 5 } shuffle-deck .
! => { 3 1 5 2 4 }
deal-hand는 덱과 개수 n을 받아서 덱에서 서로 다른 카드 n장을 반환하도록 정의해요.
{ 10 20 30 40 50 } 3 deal-hand .
! => { 40 10 30 }
play-seeded는 시드와 쿼테이션을 받아서, 그 정수로 시드를 설정한 생성기로 쿼테이션을 실행하도록 정의해요. 그래야 같은 시드가 항상 같은 결과를 만들어 내거든요. 쿼테이션의 결과는 호출한 쪽으로 반환돼야 해요.
42 [ 6 roll-die ] play-seeded .
! => the same value every time for seed 42
! Same seed, same game:
99 [ 5 deal-hand ] play-seeded 99 [ 5 deal-hand ] play-seeded = .
! => t
Exercism에 가입하고 Factor 트랙을 개념 47개연습 문제 163개, 그리고 실제 사람의 멘토링과 함께 배우고 익혀 보세요. 모두 무료예요.