運に左右されるゲームには、ランダムさの源が必要です。Factorのrandomボキャブラリーには、渡したものに合わせて姿を変える汎用的なワードrandomをはじめ、シャッフルやサンプリングのためのヘルパー、そしてテストのためにランダムさを再現可能にする仕組みが用意されています。
inlineにするこの演習のタスクには、クオーテーション([ … ]のブロック)を引数として受け取り、それを実行するワードを定義するものがあります。それについてFactorには厳格なルールが1つあります。**引数として受け取ったクオーテーションをcallするワードは、inlineとして宣言しなければなりません。**書き方としては、閉じる;のすぐ後ろにinlineを置きます。
inlineは、ワードの本体を一度だけ独立してコンパイルする代わりに、それぞれの呼び出し箇所にそのまま差し込むようコンパイラーに指示します。
10を積んでから、渡されたクオーテーションを実行する小さなワードを紹介します。
USING: kernel math prettyprint ;
: with-ten ( quot -- ) 10 swap call ; inline
このワードのスタック効果( quot -- )には出力がありません。それでも、異なる効果を持つ2つのクオーテーションを渡して呼び出すと、結果はそれぞれ異なり、どちらも呼び出し元へ流れていきます。
[ 1 + ] with-ten . ! => 11 (quotation ( n -- n ): one value back)
[ dup 2 * ] with-ten . . ! => 20
! => 10 (quotation ( n -- n n ): two values back)
random:1つの要素random(random内)は、引数に対して汎用的に働きます。
random ( obj -- elt )
nを与えると、[0, n)の範囲のランダムな整数を返します。USING: random ;
6 random . ! 4 (a value from 0 to 5)
{ "rock" "paper" "scissors" } random . ! "paper"
普通のサイコロを振るには、範囲を1つずらします。6のrandomは0から5を返すので、6 random 1 +とすれば1から6になります。
決まった回数だけランダムな値を取り出して集めるには、replicate(sequences内)でクオーテーションを繰り返します。
replicate ( n quot -- seq )
USING: random sequences ;
3 [ 6 random 1 + ] replicate . ! { 4 1 6 } (three dice)
randomize:その場でシャッフルするrandomize(random内)は、要素をランダムな順序に並べ替えたシーケンスを返します。
randomize ( seq -- randomized )
{ 1 2 3 4 5 } randomize . ! { 3 1 5 2 4 }
これはその場でシャッフルし、同じシーケンスを返します。元の順序を保ちたい場合は、先にcloneしてください。
sample:重複しない複数の要素sample(random内)は、シーケンスから重複しないn個の要素を取り出します。トランプを配るように、同じカードは二度出ません。
sample ( seq n -- seq' )
{ 10 20 30 40 50 } 3 sample . ! { 40 10 30 }
シーケンスが持っている数より多くの要素を求めると、エラーになります。
既定では、randomはシステムクロックを種にして初期化されたグローバルなジェネレーターから値を取り出すため、実行するたびに結果が変わります。テストのため、あるいは再現できるゲームが欲しいときはいつでも、固定のシードでジェネレーターを束縛します。
<mersenne-twister>(random.mersenne-twister内)は、整数のシードからジェネレーターを組み立て、with-random(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を定義して、景品のシーケンスを受け取り、その中から1つをランダムに返すようにしましょう。
{ "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