トラック
/
Factor
Factor
/
演習
/
ボードウォークのゲーム
ボードウォークのゲーム

ボードウォークのゲーム

学習演習

はじめに

運に左右されるゲームには、ランダムさの源が必要です。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)
  • シーケンスを与えると、ランダムな要素を1つ返します。
{ "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を付ける必要があります。

説明

海辺のボードウォークにあるゲームセンターで、運任せのゲームを切り盛りしています。お客さんがサイコロを振り、クレーンゲームが景品をつかみ、カードテーブルではデッキをシャッフルして手札を配る必要があります。これらはすべてランダム性によって動いています。一晩の営業が終わったら、判定でもめたゲームを同じシードから再現できるように、「リプレイ」機能も欲しくなります。

1. サイコロを振る

roll-dieを定義して、面の数を受け取り、1からsidesまでの範囲(両端を含む)の出目を返すようにしましょう。

6 roll-die .
! => 4   (some value from 1..6)

2. 景品をつかむ

pick-prizeを定義して、景品のシーケンスを受け取り、その中から1つをランダムに返すようにしましょう。

{ "teddy" "goldfish" "keyring" } pick-prize .
! => "goldfish"

3. デッキをシャッフルする

shuffle-deckを定義して、デッキ(シーケンス)を受け取り、カードをランダムな順序に並べ替えたものを返すようにしましょう。

{ 1 2 3 4 5 } shuffle-deck .
! => { 3 1 5 2 4 }

4. 手札を配る

deal-handを定義して、デッキと枚数nを受け取り、デッキからn枚の異なるカードを返すようにしましょう。

{ 10 20 30 40 50 } 3 deal-hand .
! => { 40 10 30 }

5. シードからリプレイする

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
GitHubで編集する リンクは新しいウィンドウまたはタブで開きます
Factor Exercism

ボードウォークのゲームを始める準備はできましたか?

Exercismに登録すれば、47個のコンセプト163個の演習、そして本物の人間によるメンタリングとともに、Factorを学んでマスターできます。すべて無料です。