マガジン

乱数の種:同じ盤面を何度でも再現するには、どう作ればよいのか

遊びをつくる

執筆にClaudeを使用しています。

チェスのナイトから枝分かれした影が盤面へ伸び、先読みの候補を表す版画
記事のテーマをもとに制作したAI生成イラスト。

パズルゲームを作っていて、遊んだ人から「さっきの配置をもう一度やりたい」と言われる場面を考えてみます。シャッフルのたびに Math.random() で盤面を作っていると、あの配置は二度と出てきません。配置そのものを保存しておく方法もありますが、もっと小さく済ませる方法があります。「種(シード)」と呼ばれる数字を1つだけ覚えておく方法です。 この記事の問いは、「数字1つから、なぜ毎回まったく同じ並びが生まれるのか」です。答えは、コンピューターの乱数が本当のでたらめではなく、前の値から次の値を決める計算の連鎖だからです。短いコードを自分のブラウザで動かして、同じ種なら同じ並び、違う種なら違う並びになることを確かめます。そのうえで、種を使うときに起こりやすい失敗と、公式作品の設計から読み取れることを整理します。

この記事の目次
  1. 乱数は「前の値から次の値を決める計算」です
  2. 短いコードで種を試します
  3. 結果の読み方と、自分で確かめる手順
  4. 種を使うときに起こりやすい誤り
  5. 公式作品の設計から読み取れること
  6. 次の行動

乱数は「前の値から次の値を決める計算」です

ゲームで使う乱数の多くは疑似乱数と呼ばれます。仕組みは単純で、内部に「状態」という数字を1つ持ち、呼び出されるたびに決まった計算でその状態を更新し、更新後の値から0以上1未満の小数を取り出します。

ここで重要なのは、計算に偶然の要素がないことです。最初の状態が同じなら、1回目も2回目も100回目も、出てくる数は必ず同じ順番になります。この最初の状態が「種」です。

ブラウザ標準の Math.random() は、種を自分で指定できません。再現したい場面では、種を受け取れる乱数を自前で用意します。小さな実装なら数行で書けます。

短いコードで種を試します

次のHTMLを seed-test.html のような名前で保存し、ブラウザで開いてください。外部ライブラリは使わず、単独で動きます。1〜9の数字を、種から決まる順番に並べ替えます。同じ種で2回並べて、結果を比べます。

html
<input id="seed" value="12345">
<button id="go">並べる</button>
<pre id="out"></pre>
<script>
function makeRng(seed) {
  let a = seed >>> 0;
  return () => {
    a = (Math.imul(a, 1664525) + 1013904223) >>> 0;
    return a / 4294967296;
  };
}
function shuffled(seed, n) {
  const rng = makeRng(seed);
  const list = Array.from({ length: n }, (_, i) => i + 1);
  for (let i = n - 1; i > 0; i--) {
    const j = Math.floor(rng() * (i + 1));
    [list[i], list[j]] = [list[j], list[i]];
  }
  return list;
}
document.getElementById("go").onclick = () => {
  const seed = Number(document.getElementById("seed").value);
  const a = shuffled(seed, 9);
  const b = shuffled(seed, 9);
  document.getElementById("out").textContent =
    "1回目: " + a.join(" ") + "\n2回目: " + b.join(" ") +
    "\n一致: " + (a.join() === b.join());
};
</script>

makeRng が乱数の本体です。状態 a に定数を掛けて別の定数を足すという計算を繰り返し、32ビットの範囲に収めています。これは学習用に選んだ最小の方式(線形合同法)で、品質を厳しく求める用途には向きません。ただ、仕組みを目で追うには十分です。

shuffled はフィッシャー・イェーツ法と呼ばれる標準的な並べ替えです。後ろから1枚ずつ、残りの中からランダムに選んだ位置と入れ替えます。選ぶ位置を決める部分で、Math.random() の代わりに rng() を使っています。この差し替えがすべてです。

結果の読み方と、自分で確かめる手順

動かしたら、次の順に確かめてください。

  1. 種を変えずに「並べる」を何度か押します。「1回目」と「2回目」が一致し、押し直しても同じ並びが出れば、再現は成功です。
  2. 種を 12345 から 12346 に変えて押します。並びが大きく変わるか、記録しておきます。1違いの種で並びがどの程度似るかは、実際に見て判断してください。
  3. 種を5つほど試して、結果をメモします。見る点は、どの種でも1〜9が1回ずつ入っていること(欠けや重複がないこと)と、種ごとに並びが違うことです。
  4. ページを再読み込みして、同じ種で同じ並びが出るか確かめます。出れば、並びが画面を開いた時刻などに依存していないと判断できます。

判断の目安は次のとおりです。

  • 同じ種で毎回一致する、読み取れること:乱数が種だけで決まっている
  • 同じ種でも、押すたびに変わる、読み取れること:どこかで Math.random() や時刻が混ざっている
  • 種が違うのに並びがよく似ている、読み取れること:乱数の質、または種の範囲に偏りがある可能性がある

3番目の「よく似ている」は主観で決まるので、サンプルを増やして、自分のゲームで困る程度かどうかで判断してください。

種を使うときに起こりやすい誤り

種による再現は、仕組みが単純なぶん、崩れ方も単純です。よくある原因を挙げます。

  • 別の乱数が混ざる。 盤面生成の途中に Math.random() が1か所でもあると、再現できなくなります。盤面に関わる乱数は、すべて自前の rng に通します。
  • 呼び出す順番が変わる。 乱数は順番に消費されます。たとえば、敵の配置のあとに宝箱の配置を決める処理を、あとから逆順に書き換えると、同じ種でも盤面が変わります。コードを直したら、過去の種で作った盤面が変わりうることを覚えておきます。
  • 演出の乱数と盤面の乱数を共有する。 紙吹雪の位置に同じ rng を使うと、プレイ中の演出の回数で、次の盤面が変わってしまいます。盤面用と演出用で、乱数の流れを分けます。
  • 種を入力欄から読むとき、型を確かめない。 空欄や文字列は Number() で 0 や NaN になります。NaN を >>> 0 にかけると 0 になるため、思わぬ種で動き続けます。入力値の検査を入れておくと安心です。
  • ただ並べ替えただけで遊べない盤面ができる。 スライドパズルのような盤面は、任意の並べ替えの半分が、どう動かしても完成しない配置になることが知られています。種から盤面を作る場合は、完成形から決まった回数だけ合法な動きを逆に重ねる作り方にすると、必ず解ける盤面になります。ゲームの種類ごとに、「ランダムに作った盤面が成立するか」を別に検討してください。

公式作品の設計から読み取れること

アソバースの公式作品「かたちのあいだ」は、数字を順にそろえるスライドパズルです。確認済みの機能に、「同じ配置の再挑戦」があります。ここから読み取れるのは、「一度遊んだ配置を、もう一度最初から出せる」という設計の意図です。

ただし、この再挑戦がどう実現されているかは、公開されている情報からは分かりません。種から配置を作り直しているのか、最初の配置そのものを覚えているのかは、この記事では判断できません。作品を開いて、「同じ配置の再挑戦」を押したときの動きを確かめてください。新しい配置を始めたあとで再挑戦を押すとどうなるかも、見る価値があります。

アソバースの仕組みの面でも、種の考え方は相性がよさそうです。確認済みの範囲では、投稿作品のクラウドセーブは作品ごとにJSONで128KiBまでで、アップロード作品内の localStorage は一時メモリーとしての互換処理です。種は数字1つなので、保存したい情報が小さく済みます。進行の保存方法は、自分のゲームの規模に合わせて設計してください。保存の細かい使い方は、投稿画面の案内で確認してください。

なお、13の公式作品はどれも外部通信なしで動きます。遊んだ人どうしで盤面を共有したい場合でも、今のところ共通の仕組みは案内されていません。種の数字を画面に表示して、手で伝えてもらう形が最も素直な設計です。

次の行動

まず上のコードを保存して、同じ種で2回並べ、「一致」が出ることを確かめてください。次に、自分のゲームの盤面生成から Math.random() を洗い出し、rng() に置き換えてみてください。画面の隅に種を表示するだけで、「この盤面をもう一度」を実現する土台になります。