週末で2048を作った話——ゲームループのすべて
2026-06-29
最近 2048 を ToolKoala に追加したのですが、一番うれしい驚きは、本当のゲーム部分がどれだけ小さいかでした。アニメーションや仕上げには時間がかかりますが、ルール——スライド、マージ、出現、ゲームオーバー判定——はおよそ60行に収まります。ループ全体がどう動くかを紹介します。
盤面はただのフラット配列
グリッドは4×4ですが、16個の数字からなるフラット配列として保持し、0 は空を意味します:
[2, 0, 0, 4,
0, 2, 0, 0,
0, 0, 0, 0,
4, 0, 0, 2]
フラット配列はコピー、比較、localStorage への保存が簡単です。行 r、列 c はインデックス r * 4 + c にあります。
本当の仕事をするのは1つの関数
すべての移動——左、右、上、下——は、4つのタイルの並びに対する同じ操作です:すべてを一方の端へ押し込み、等しい隣り合うタイルを一度だけマージする。それが1つの関数です:
function compress(line) {
const arr = line.filter(v => v) // ゼロを取り除く
const res = []
let gained = 0
for (let i = 0; i < arr.length; i++) {
if (arr[i] === arr[i + 1]) { // 等しいタイルが2つ接触 → マージ
res.push(arr[i] * 2)
gained += arr[i] * 2
i++ // マージで取り込んだ分をスキップ
} else {
res.push(arr[i])
}
}
while (res.length < 4) res.push(0) // 長さ4に詰め直す
return { line: res, gained }
}
マージ後の i++ が、4 4 4 4 が単一の 16 につぶれてしまうのを止めるルールです。これは 8 8 になります——各タイルは1回の移動で最大1回しかマージしません。
1方向から4方向
私は「左へスライド」しか書いていません。残りの3つはそれを再利用します:
- 右 — 行を反転し、左へ圧縮し、また反転して戻す。
- 上 / 下 — 盤面を行ではなく列ごとに読み、あとは同じ左右のロジック。
つまり移動は各行または各列を取り出し、必要なら反転し、compress を走らせ、書き戻します。矢印ごとの個別コードはありません——どの並びを読むか、それを反転するかどうかだけです。
出現させ、それから詰みかどうかを確認
実際に盤面が変わった移動のあと、新しいタイルがランダムな空セルに現れます——たいていは 2、たまに 4。それから、まだ移動が可能かどうかを確認します:空セルはあるか、それとも等しい隣り合うタイルがあるか?なければゲームオーバーです。
function canMove(g) {
if (g.includes(0)) return true // 空セルがある → 可能
// 横/縦に等しい隣接タイルがある → まだマージが可能
// ...小さなループでチェック...
return false
}
「盤面は実際に変わったか?」のチェックが重要です:移動が何もしないなら、ただで新しいタイルをもらえるべきではありません。壁に向かって左を押すのはノーオペでなければなりません。
refを使うことを教えてくれたバグ
最初のバージョンは、キーハンドラの中で React の state から盤面を直接読んでいました。高速なキーリピートでは、React が再レンダリングする前に2回の矢印押しが発生し、2回目が古い盤面を読んで移動が取りこぼされました。修正は、最新の盤面とスコアを映す ref を保持し、移動ハンドラにその ref を読ませることでした。state は見た目を駆動し、ref はロジックを駆動します。それ以降は、矢印を連打しても正しく動きます。
再開とベストスコア、すべてローカル
localStorage には2つが入っています:ベストスコアと、現在の盤面です。盤面は移動のたびに保存し、ゲームが終わったらクリアします。読み込み時、有効な未完了のゲームがあれば、それを復元して小さな「続ける」の表示を出します——数独やワードサーチで使っているのと同じパターンです。何もアップロードされません。あなたのゲームは、あなたのデバイス上の、あなたのものです。
試してみる
ここで2048をプレイできます——キーボードかスワイプで、ページが読み込まれれば一度オフラインでも動きます。こういうのが好きなら、数独ジェネレーターはもっと面白いアルゴリズムを背後に持っています。
よくある質問
2048はプログラミングが難しいですか? 核心は驚くほど小さいです——「並びをスライドしてマージする」関数を1つ、4方向すべてに再利用し、加えてタイルの出現とゲームオーバー判定。仕上げ(アニメーション、タッチ操作、進捗の保存)にこそ大半の時間がかかります。
マージのルールはどう動きますか? 同じ数字の2つのタイルが押し合わさると、値が2倍の1つのタイルに合体します——そして各タイルは1回の移動で1回しかマージできないので、2が4つ並んだ行は8が1つではなく4が2つになります。
4つの方向はどう扱いますか? 実装が必要なのは1方向だけです。右は行を反転した左、上と下は同じロジックを行ではなく列に適用したものです。
ToolKoala の2048は私のゲームを保存しますか? はい——ベストスコアと現在の盤面はブラウザのローカルストレージに保存されるので、タブを閉じてあとで再開できます。デバイスを離れることは決してありません。
— Milo 🐨