マガジン

「同じものが3つ揃うと消える」を数で設計する:連鎖の確率

遊びをつくる

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

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

3つ揃えて消すパズルを作るとき、盤面を「見た目のバランスで」8×8、色数を5色と決めたくなります。ところがこの2つの数字は、開始直後に勝手に消える組の数や、1手で連鎖が起きる頻度を大きく左右します。ここで考えるのは、盤面の大きさと色数を決める前に、消去の確率を紙の上と短いコードで見積もれるか、という問いです。 最初の見積もりは暗算でできます。ランダムに色を並べたとき、決まった3マスが同じ色になる確率は、色数をKとして1/K²です。5色なら1/25、つまり4%です。盤面には3マスの並び(窓)がたくさんあるので、全部の窓について足し合わせると、最初から揃っている窓の数の期待値が出ます。 ただし期待値は「平均」でしかなく、連鎖が何回続くかの分布は教えてくれません。そこでこの記事では、期待値の計算と、短いシミュレーションの設計をセットにします。結果は載せません。手元で動かして、自分のゲームの目標値と照らし合わせるための設計書です。

この記事の目次
  1. 問いを数に落とす
  2. 準備と手順
  3. 記録の取り方
  4. 結果の読み方
  5. 起こりやすい誤り
  6. 次の行動

問いを数に落とす

盤面の幅をW、高さをHとします。横向きの3マス窓は H×(W−2) 個、縦向きは W×(H−2) 個あり、合計は 2×H×(W−2)(正方形の場合)です。完全にランダムな盤面での「揃っている窓の期待値」は、窓の数÷K² で求まります。

  • 6×6、窓の数:48、4色:3、5色:1.92、6色:約1.33
  • 8×8、窓の数:96、4色:6、5色:3.84、6色:約2.67
  • 10×10、窓の数:160、4色:10、5色:6.4、6色:約4.44

数字は「ランダムに並べた直後に、すでに揃っている窓がいくつあるか」の平均です。4個並びは窓が2つ重なるので2個と数えます。

もう一つ覚えておきたいのは、すでに2つ同じ色が並んでいる場所に、新しい1マスが来て揃う確率は1/Kだということです。3マスとも未確定なら1/K²ですが、2マスが確定済みなら1/Kに跳ね上がります。連鎖は、補充された新しいマスが、既存の「同色の2連」を完成させることで起きます。盤面が大きく色が少ないほど、同色の2連が多く、連鎖が起きやすくなります。

この見積もりを決める前に、目標も数で置きます。たとえば次のように決めておきます。

  • 1手で連鎖が1回以上起きる割合を、何%にしたいか
  • 3連鎖以上は、何手に1回くらいにしたいか

準備と手順

準備するものは、ブラウザの開発者ツールのコンソール(またはNode.js)と、手元のメモだけです。外部ライブラリは使いません。

次のコードは、連鎖のモデルを最小限に絞ったものです。まず、揃いが一つもない安定した盤面を作ります。次に、横3マスをランダムに消し、上のマスを落とし、空いたところを乱数で補充します。そこから揃いがなくなるまでの追加の連鎖回数を数えます。

javascript
const rnd = K => Math.floor(Math.random() * K);
function marks(b, W, H) {
  const s = new Set();
  for (let r = 0; r < H; r++) for (let c = 0; c < W; c++) {
    const v = b[r][c];
    if (c + 2 < W && b[r][c+1] === v && b[r][c+2] === v)
      [0,1,2].forEach(i => s.add(r * W + c + i));
    if (r + 2 < H && b[r+1][c] === v && b[r+2][c] === v)
      [0,1,2].forEach(i => s.add((r + i) * W + c));
  }
  return s;
}
function fall(b, W, H, K) {
  for (let c = 0; c < W; c++) {
    const col = [];
    for (let r = H - 1; r >= 0; r--) if (b[r][c] >= 0) col.push(b[r][c]);
    while (col.length < H) col.push(rnd(K));
    for (let r = 0; r < H; r++) b[H - 1 - r][c] = col[r];
  }
}
function stable(W, H, K) {
  const b = Array.from({length: H}, () => Array.from({length: W}, () => rnd(K)));
  let m;
  while ((m = marks(b, W, H)).size)
    m.forEach(k => { b[Math.floor(k / W)][k % W] = rnd(K); });
  return b;
}
function sim(W, H, K, N) {
  const h = {};
  for (let t = 0; t < N; t++) {
    const b = stable(W, H, K);
    const r = rnd(H), c = rnd(W - 2);
    for (let i = 0; i < 3; i++) b[r][c + i] = -1;
    let n = 0;
    for (;;) {
      fall(b, W, H, K);
      const m = marks(b, W, H);
      if (!m.size) break;
      n++;
      m.forEach(k => { b[Math.floor(k / W)][k % W] = -1; });
    }
    h[n] = (h[n] || 0) + 1;
  }
  return h;
}
for (const K of [4, 5, 6]) console.log('8x8 色' + K, sim(8, 8, K, 10000));

出力は、追加の連鎖回数ごとの試行回数です。キーが0なら、最初の消去だけで盤面が落ち着いたことを表します。

手順は次のとおりです。

  1. 目標(連鎖率など)を先にメモへ書く
  2. 盤面を8×8に固定し、色数だけを4・5・6と変えて実行する
  3. 色数を5に固定し、盤面を6×6・8×8・10×10と変えて実行する(sim の引数を書き換えます)
  4. 各条件を3回ずつ実行し、毎回の値を記録する

一度に変えるのは盤面か色数のどちらか一つだけにします。両方を同時に変えると、どちらが効いたのか分からなくなります。

記録の取り方

条件ごとに、次の表を作ります。

  • 8×8・5色、実行回:1回目、連鎖0:、連鎖1:、連鎖2:、連鎖3以上:

記録の単位は、1条件につき10,000回の試行を1セットとし、3セット行います。割合は「各回数の試行数÷10,000」で出します。

試行回数が10,000回なら、割合pのばらつき(標準誤差)は √(p(1−p)/10000) で、最大でも0.5ポイント程度です。3セットの値が1ポイント以上離れていたら、試行回数を増やします。

結果の読み方

出てきた数字は、次の順で判断します。

  • 連鎖率が目標より高い場合:色数を1つ増やすのが最も素直な調整です。盤面を小さくしても下がりますが、手を打てる選択肢も減ります。
  • 連鎖率が目標より低い場合:色数を減らすか、盤面を広げます。ただし色が少なすぎると、手を打たなくても勝手に揃う場面が増えます。
  • 3連鎖以上の割合が極端に小さい場合:連鎖はほぼ「おまけ」の演出になります。連鎖を狙う遊びにしたいなら、補充の色を偏らせる、特殊なマスを作るなど、確率以外の設計が必要です。
  • 3セットのばらつきが大きい場合:結論を出さず、試行回数を増やします。

あわせて、表の期待値(たとえば8×8・5色の3.84)と、実際の盤面生成の方法を見比べます。「開始直後に揃っている窓がほぼゼロの盤面」を保証したいのか、「多少揃っていて自動で消える」ことを許すのか。この選択で、盤面生成の処理が変わります。

起こりやすい誤り

  • 安定盤面の偏りを忘れる:揃った場所を再抽選して作った盤面は、完全な乱数とは分布が少し違います。期待値の表は「完全ランダム」の値なので、シミュレーションの結果と一致しなくても、すぐに不具合とは判断できません。
  • 実際の操作を省いている:コードは「ランダムな横3マスの消去」で代用しています。実際のゲームの入れ替え操作を再現しているわけではないので、本番の操作に近づけたい場合はコードを拡張します。
  • 4つ並び・L字・T字の扱い:このコードは3つ以上の並びをすべて消す簡易判定です。自分のルールで特別な扱いをするなら、判定を書き換えます。
  • 連鎖回数の数え方がずれる:最初の消去を含めるかどうかで、数字が1ずれます。画面に出す「COMBO」の表示と同じ数え方に揃えてください。
  • 補充の色を均等に決めている前提:特定の色が出やすいルールにすると、1/Kの見積もりは使えなくなります。

次の行動

目標の連鎖率を数字でメモに書き、コードを実行して8×8の4〜6色の3セットを記録してみてください。結果に合わせて盤面と色数を決めたら、1つのHTMLにまとめます。直下の index.html を入口にしたZIPにして、アソバースに投稿すれば、数で設計した盤面を実際に遊んでもらえます。