落ち物パズルの作り方:消去判定と連鎖
遊びをつくる執筆にClaudeを使用しています。

落ち物パズルを自作しようとするとき、最初に詰まるのは「ブロックが揃ったかどうかの判定」と「消えた後に起きる連鎖」の実装です。ゲームの見た目を作るより、この二点がロジックの核心であり、ここが正しく動けばゲームとして成立します。 この記事では、テトリス型(横ライン消去)とぷよぷよ型(隣接グループ消去)の二種類を例に取り、消去判定を書くための考え方と、連鎖を「自然に生まれる仕組み」として設計する手順を解説します。ゲームエンジンや言語は問いませんが、コード例はJavaScript(ブラウザで動く素のJS)で示します。
盤面をどう持つか:2次元配列の設計
消去判定の前に、盤面のデータ構造を決めます。落ち物パズルの盤面は行数×列数の2次元配列で表すのが定番です。各セルには「空(0)」か「何色のブロックか(1以上の整数)」を入れます。
board[row][col] = 色番号 // 0 = 空落下中のピースは盤面配列に書き込まず、「現在のピース」として別に持ちます。ピースが着地した瞬間に盤面配列へ転写し、その直後に消去判定を走らせる、という順番が基本です。この「着地→転写→判定→消去→落下→再判定」のサイクルが連鎖の土台になります。
配列の向きは統一しておくことが重要です。board[0] が画面の一番上か一番下かで座標計算が逆になるため、チーム開発でも個人開発でも最初に決めてコメントに残しておきます。
テトリス型:横ライン消去の実装
テトリス型の消去判定は「行をすべてスキャンして、空きセルが一つもない行を消す」という単純なルールです。
判定と消去の手順
- 盤面の一番下の行から上に向かってループする。
- その行の全セルが 0 でないかを確認する。
- 条件を満たす行のインデックスをリストに集める。
- リストの行を配列から削除し、先頭に同じ列数の空行を追加する。
function clearLines(board) {
const filled = [];
for (let r = 0; r < board.length; r++) {
if (board[r].every(cell => cell !== 0)) filled.push(r);
}
for (const r of filled) {
board.splice(r, 1);
board.unshift(new Array(board[0].length).fill(0));
}
return filled.length; // 消した行数を返す
}失敗しやすい点は「複数行を一度に消すときの順序」です。上の例では splice のたびに行インデックスがずれます。対策として、フラグを使って消した行だけを除外した新しい配列を生成し、不足した行数分だけ先頭に空行を追加する方法が安全です。
function clearLines(board) {
const cols = board[0].length;
const kept = board.filter(row => row.some(cell => cell === 0));
const cleared = board.length - kept.length;
const empty = Array.from({ length: cleared }, () => new Array(cols).fill(0));
board.splice(0, board.length, ...empty, ...kept);
return cleared;
}テトリス型に連鎖はありませんが、一度に多くの行を消したときのスコア倍率(いわゆるテトリスボーナス)は cleared の値で判断できます。
ぷよぷよ型:隣接グループ消去と連鎖の仕組み
ぷよぷよ型では「同じ色が4個以上隣接していたら消える」がルールです。テトリス型と比べてロジックが複層になります。
グループ探索:幅優先探索(BFS)
隣接するブロックのまとまりを「グループ」と呼びます。盤面全体を走査してグループを列挙するには、幅優先探索(BFS)か深さ優先探索(DFS)を使います。
function findGroup(board, startR, startC, visited) {
const color = board[startR][startC];
if (color === 0) return [];
const group = [];
const queue = [[startR, startC]];
visited[startR][startC] = true;
const dirs = [[-1,0],[1,0],[0,-1],[0,1]];
while (queue.length > 0) {
const [r, c] = queue.shift();
group.push([r, c]);
for (const [dr, dc] of dirs) {
const nr = r + dr, nc = c + dc;
if (nr >= 0 && nr < board.length &&
nc >= 0 && nc < board[0].length &&
!visited[nr][nc] &&
board[nr][nc] === color) {
visited[nr][nc] = true;
queue.push([nr, nc]);
}
}
}
return group;
}盤面全体のグループを集めるには、すべてのセルをループして未訪問かつ非空のセルで findGroup を呼び、返ってきたグループのサイズが閾値(標準的には4)以上ならそのグループを消去対象に追加します。
消去→落下→再判定のサイクルが連鎖を生む
連鎖は「消去判定の結果、何かが消えた」→「上にあったブロックが落下する」→「もう一度消去判定をする」のループによって自然に発生します。特別な「連鎖カウンタ」を先に作る必要はなく、このループが続いた回数が連鎖数になります。
async function resolveChain(board) {
let chain = 0;
while (true) {
const toDelete = getGroupsToDelete(board); // 閾値以上のグループを返す
if (toDelete.length === 0) break;
deleteBlocks(board, toDelete);
await animateClear(); // 消去演出(任意)
applyGravity(board); // 空きセルに上のブロックを落とす
await animateFall(); // 落下演出(任意)
chain++;
}
return chain;
}applyGravity は各列を下からスキャンして、空きを詰める処理です。
function applyGravity(board) {
const rows = board.length, cols = board[0].length;
for (let c = 0; c < cols; c++) {
let writeRow = rows - 1;
for (let r = rows - 1; r >= 0; r--) {
if (board[r][c] !== 0) {
board[writeRow][c] = board[r][c];
if (writeRow !== r) board[r][c] = 0;
writeRow--;
}
}
}
}連鎖スコアの設計
連鎖数に応じてスコアを増やすときの判断基準は「連鎖数が上がるほど非線形に大きくなる」設計です。シンプルな例として score += base * 2 ** (chain - 1) という指数式がよく使われます。1連鎖ならbase点、2連鎖なら2倍、3連鎖なら4倍と増えていきます。ゲームバランスに合わせて底を変えるか、連鎖ボーナステーブルを別途用意する方法もあります。
詰まりやすいポイントと対処
判定のタイミングがずれる:ピースが着地した直後ではなく描画ループ内で判定を呼んでいると、フレームをまたいで二重消去が起きます。着地イベントとして明示的に関数を呼ぶ設計にすると防げます。
落下速度と判定のループが競合する:setInterval で落下を動かしながら連鎖中に次のピースが降ってくるケースがあります。連鎖処理中は落下タイマーを一時停止するフラグを用意して、resolveChain の完了後に再開します。
浮きブロックが発生する:ぷよぷよ型で左右に別れた柱が中間だけ消えると、柱の上部が宙に浮きます。applyGravity をグループ消去のたびに呼ぶことで解消できます。
おじゃまぷよ(特殊ブロック)の扱い:おじゃまぷよは色グループには含まれませんが、隣接するグループが消えたときに一緒に消えるルールが一般的です。これを実装するには、消去対象グループの周囲セルに特殊ブロックがないかを消去前に走査し、あれば消去リストに追加します。
次の行動
ここで示した盤面配列・BFS・重力の三点セットをHTMLファイル一本に書いてブラウザで動作確認するところから始めてみてください。消去と落下が視覚的に確認できたら、そのファイルをZIPにまとめてアソバースに投稿し、他の人に実際に遊んでもらうとバランス調整の感覚が一気につかめます。連鎖数の上限・落下速度の変化曲線・ゲームオーバー条件の三要素を後から調整しやすい定数として分離しておくと、公開後のチューニングが楽になります。

