何週間も待ち望んだ末、友達と集まって、初めてのDungeons & Dragons(D&D)のゲームを遊ぶことになりました。 これがゲームの最初のセッションなので、プレイヤーはそれぞれ、遊ぶためのキャラクターを作らなければなりません。 キャラクターの能力は6面ダイスを振って決まりますが、そのダイスは_どこに_あるのでしょうか? はっと気づくと、友達はダイスを用意してくれるのを待っています。何しろ、D&Dで遊ぼうと言い出したのは自分なのですから! 慌てて、ダイスを持ってくるのを忘れたことに気づきます。それではD&Dのゲームができません。 幸い少しはプログラミングの基礎を身につけていたので、すぐに解決策を思いつきます。ダイスを振る動きをシミュレートするプログラムを書くのです。
Dungeons & Dragonsのゲームでは、まずプレイヤーそれぞれが、遊ぶためのキャラクターを生成するところから始めます。 このキャラクターには、ほかにもいろいろな要素がありますが、なかでも6つの能力があります。strength、dexterity、constitution、intelligence、wisdom、charismaです。 これら6つの能力には、それぞれランダムに決まるスコアがあります。 スコアは、6面のサイコロを4つ振り、大きいほうから3つの出目の合計を記録して求めます。 これを6回、能力ごとに1回ずつ行います。
キャラクターの初期hitpointsは、10にconstitutionの修正値を足した値です。 constitutionの修正値は、constitutionから10を引き、その結果を2で割って切り下げると求められます。
上記のルールに従って、ランダムにキャラクターを生成するプログラムを書いてください。
たとえば、4つのサイコロを6回振った結果は、次のようになるかもしれません。
constitutionが3なので、constitutionの修正値は-4、hitpointsは6になります。
ほとんどのプログラミング言語には(疑似)乱数生成器がありますが、サイコロを振るために設計されているプログラミング言語はほとんどありません。 そのひとつがTrollです。
関数型言語では、ほとんどの関数は_純粋_です。 つまり、同じ引数の組に対してはいつも同じ出力を返し、それ以外のことは何もしません。 言い換えると、純粋な関数は決定的です。
しかし、乱数は定義上、予測できず、非決定的です。 乱数のように_見える_数列を返すこと、すなわち疑似乱数でさえ、純粋な文脈では簡単には実現できません。 なぜなら、疑似乱数生成器(PRNG)は、数列の次の数を返すために内部状態を追跡し続ける必要があるからです。
Leanには、この問題に対する2つの方法があります。
PRNGの内部状態を変更できるモナドを使う方法です。
まさにそのための、すぐに使える関数IO.randがあります。
ジェネレーターを引数に取り、生成された疑似乱数の値だけでなく、数列の次の数に向けて「準備された」更新済みのジェネレーターも返す、純粋な関数を使う方法です。
この演習では、2つ目の方法を使います。 疑似乱数の値を生成するすべての関数にはジェネレーターが渡され、その関数は値と更新済みのジェネレーターの両方を返すことが求められます。
あるジェネレーターは決定的である、つまり_いつも_同じ値を生成することに注意してください。 数列の中で次の疑似乱数の値を生成するには、更新済みのジェネレーターを使う必要があります。
この演習では、乱数はp < 0.0001という有意水準のカイ二乗検定で検証されます。
つまり、説明のとおりに能力値が生成されていれば、正しい実装が偶然によってテストに失敗する確率は0.01%未満です。
説明によると、能力値は_偏りのないd6(6面サイコロ)を4回振ったうち、大きいほうから3つの結果の合計_であることに注意してください。