【ゲーム】JavaScript:34 2048ゲーム

 2048(トゥエンティフォーティ)は、2014年3月にイタリア人のガブリエレ・チルリ氏によって公開されたシンプルかつ中毒性の高いパズルゲームです。4×4 のグリッド上で同じ数字のタイルをスライド&マージし、最終的に「2048」のタイルを作成することを目指します。

遊び方と操作方法

  • 矢印キー(↑↓←→)でグリッド上のすべてのタイルを同じ方向にスライド
  • タイル移動後、空きマスに「2」または「4」の新規タイルがランダムに出現
  • 同じ数字のタイル同士が接触すると合体し、タイルの数字が2倍になる

ルール

  1. 4×4 のグリッドでプレイ
  2. 空きマスに新しいタイル(2または4)が必ず1つ出現
  3. 同じ数字のタイル同士がスライド中に重なると合体
  4. 合体したタイルは合計値(例:2+2→4)となり、その分スコアが加算される
  5. すべてのマスが埋まっていて、かつどの方向にも移動・合体できなくなるとゲームオーバー
  6. 「2048」タイルの誕生でクリア(続けて上位タイルを作ることも可能)
  7. 理論上は「131072」までのタイルを作成可能

🎮ゲームプレイ

以下のリンク先から実際にプレイできます。

34 2048ゲーム

素材のダウンロード

以下のリンクから使用する素材をダウンロードできます。

2048game_title.png2048game_bg.png

ゲーム画面イメージ

プログラム全文(2048game.html)

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>🧩 2048ゲーム 🧩</title>
  <style>
    /* 全体リセット */
    * {
      box-sizing: border-box;
      margin: 0;
      padding: 0;
    }
    html, body {
      width: 100%;
      height: 100%;
    }
    body {
      /* 背景画像を全面に敷く */
      background: url('2048game_bg.png') no-repeat center center fixed;
      background-size: cover;
      font-family: 'Yu Gothic','Segoe UI',sans-serif;
      position: relative;
      overflow: hidden;
    }

    /* 画面(タイトル/ゲーム)共通 */
    #title-screen, #game-screen {
      position: absolute;
      top: 0; left: 0;
      width: 100%; height: 100%;
      display: flex;
      flex-direction: column;
      align-items: center;
      justify-content: center;
    }

    /* タイトル画面 */
    #rules {
      background: rgba(255,255,255,0.8);
      padding: 20px;
      border-radius: 8px;
      color: #333;
      text-align: left;
      width: 60%;
      max-width: 600px;
      margin-bottom: 20px;
      line-height: 1.5;
    }
    #title-img {
      max-width: 80%;
      margin-bottom: 20px;
    }
    #start-btn {
      padding: 10px 20px;
      font-size: 1em;
      border: none;
      border-radius: 4px;
      cursor: pointer;
      background: #8f7a66;
      color: #f9f6f2;
    }

    /* ゲーム画面 */
    #back-btn, #back-btn2 {
      position: absolute;
      top: 10px;
      right: 10px;
      padding: 8px 16px;
      font-size: 0.9em;
      border: none;
      border-radius: 4px;
      cursor: pointer;
      background: #8f7a66;
      color: #f9f6f2;
    }
    #back-btn2 {
      top: auto;
      bottom: 10px;
    }
    /* タイトルとスコアの読みやすさを強化 */
    #game-screen h1 {
      margin: 10px 0;
      color: #776e65;
      background: rgba(255,255,255,0.8);
      padding: 8px 12px;
      border-radius: 4px;
      text-shadow: 1px 1px 2px rgba(0,0,0,0.3);
    }
    #score {
      margin-bottom: 10px;
      font-size: 1.2em;
      color: #776e65;
      background: rgba(255,255,255,0.8);
      padding: 6px 10px;
      border-radius: 4px;
      text-shadow: 1px 1px 2px rgba(0,0,0,0.3);
    }

    /* グリッドコンテナ */
    #grid-container {
      display: grid;
      grid-template-columns: repeat(4, 100px);
      grid-template-rows: repeat(4, 100px);
      gap: 10px;
      background: #bbada0;
      padding: 10px;
      border-radius: 6px;
    }

    /* 各タイル */
    .tile {
      width: 100px;
      height: 100px;
      border-radius: 4px;
      display: flex;
      align-items: center;
      justify-content: center;
      font-size: 2em;
      font-weight: bold;
      color: #776e65;
      background: rgba(238, 228, 218, 0.35);
      transition: background 0.2s;
    }
    .tile-2    { background: #eee4da; }
    .tile-4    { background: #ede0c8; }
    .tile-8    { background: #f2b179; color: #f9f6f2; }
    .tile-16   { background: #f59563; color: #f9f6f2; }
    .tile-32   { background: #f67c5f; color: #f9f6f2; }
    .tile-64   { background: #f65e3b; color: #f9f6f2; }
    .tile-128  { background: #edcf72; color: #f9f6f2; }
    .tile-256  { background: #edcc61; color: #f9f6f2; }
    .tile-512  { background: #edc850; color: #f9f6f2; }
    .tile-1024 { background: #edc53f; color: #f9f6f2; }
    .tile-2048 { background: #edc22e; color: #f9f6f2; }

    /* ゲームオーバー表示 */
    #game-over {
      position: absolute;
      top: 50%; left: 50%;
      transform: translate(-50%, -50%);
      background: rgba(0,0,0,0.7);
      padding: 30px;
      border-radius: 8px;
      color: #f9f6f2;
      text-align: center;
      display: none;
    }
    #game-over p {
      margin-bottom: 20px;
      font-size: 1.5em;
    }
    #game-over button {
      padding: 8px 16px;
      font-size: 1em;
      border: none;
      border-radius: 4px;
      cursor: pointer;
      background: #8f7a66;
      color: #f9f6f2;
    }
  </style>
</head>
<body>

  <!-- ========== タイトル画面 ========== -->
  <div id="title-screen">
    <!-- タイトル画像 -->
    <img id="title-img" src="2048game_title.png" alt="2048 タイトル">
    <!-- ルール説明(左寄せ) -->
    <div id="rules">
      <p>🕹️ <strong>2048ゲーム の遊び方</strong></p>
      <ul>
        <li>矢印キーで全てのタイルを上下左右にスライドさせます。</li>
        <li>移動後、空いているマスに「2」または「4」のタイルが出現します。</li>
        <li>同じ数字のタイル同士がぶつかると合体し、数字が2倍になります。</li>
        <li>全てのマスが埋まる前に「2048」のタイルを作るとクリア!</li>
        <li>作れる最大のタイルは「131072」です。</li>
      </ul>
    </div>
    <!-- スタートボタン -->
    <button id="start-btn">▶️ スタート</button>
  </div>

  <!-- ========== ゲーム画面 ========== -->
  <div id="game-screen" style="display:none;">
    <!-- タイトル画面に戻るボタン -->
    <button id="back-btn">🏠 タイトル画面に戻る</button>
    <!-- ゲームタイトル -->
    <h1>🧩 2048ゲーム 🧩</h1>
    <!-- スコア表示 -->
    <div id="score">スコア: 0</div>
    <!-- グリッド -->
    <div id="grid-container"></div>
    <!-- ゲームオーバーオーバーレイ -->
    <div id="game-over">
      <p>😢 ゲームオーバー!😢</p>
      <button id="back-btn2">🏠 タイトル画面に戻る</button>
    </div>
  </div>

  <script>
  // ====== 画面切り替え ======
  const titleScreen = document.getElementById('title-screen');
  const gameScreen  = document.getElementById('game-screen');
  const startBtn    = document.getElementById('start-btn');
  const backBtn     = document.getElementById('back-btn');
  const backBtn2    = document.getElementById('back-btn2');

  // タイトル画面を表示
  function showTitleScreen() {
    gameActive = false;              // ゲーム停止フラグ
    titleScreen.style.display = 'flex';
    gameScreen .style.display = 'none';
    // 初期化しておく
    init();
    hideGameOver();
  }

  // ゲーム画面を表示
  function showGameScreen() {
    titleScreen.style.display = 'none';
    gameScreen .style.display = 'flex';
    gameActive = true;               // ゲーム開始フラグ
    updateView();
  }

  // ボタンイベント
  startBtn.addEventListener('click', showGameScreen);
  backBtn .addEventListener('click', showTitleScreen);
  backBtn2.addEventListener('click', showTitleScreen);

  // ====== 2048ゲーム本体 ======

  // グリッドサイズ
  const SIZE = 4;        // 4×4
  let grid  = [];        // ゲーム盤配列
  let score = 0;         // スコア
  let gameActive = false; // ゲーム中かどうか

  // DOM要素
  const container = document.getElementById('grid-container');
  const scoreEl   = document.getElementById('score');
  const gameOverEl= document.getElementById('game-over');

  // キー入力管理
  const keys = {};
  window.addEventListener('keydown', e => {
    keys[e.key] = true;
    if (!gameActive) return;  // ゲーム中のみ反応
    let moved = false;
    switch (e.key) {
      case 'ArrowUp':    moved = moveUp();    break;
      case 'ArrowDown':  moved = moveDown();  break;
      case 'ArrowLeft':  moved = moveLeft();  break;
      case 'ArrowRight': moved = moveRight(); break;
    }
    if (moved) {
      addRandomTile();
      updateView();
      if (isGameOver()) {
        showGameOver();
      }
    }
  });
  window.addEventListener('keyup', e => { delete keys[e.key]; });

  // 初期化:グリッド配列とタイル要素を生成
  function init() {
    score = 0;
    grid  = Array.from({length: SIZE}, () => Array(SIZE).fill(0));
    container.innerHTML = '';
    for (let y = 0; y < SIZE; y++) {
      for (let x = 0; x < SIZE; x++) {
        const tile = document.createElement('div');
        tile.classList.add('tile');
        container.appendChild(tile);
      }
    }
    // 最初の2枚を配置
    addRandomTile();
    addRandomTile();
    updateView();
  }

  // ランダムに「2」か「4」のタイルを空きマスに追加
  function addRandomTile() {
    const empty = [];
    for (let y = 0; y < SIZE; y++) {
      for (let x = 0; x < SIZE; x++) {
        if (grid[y][x] === 0) empty.push({x, y});
      }
    }
    if (empty.length === 0) return;
    const {x, y} = empty[Math.floor(Math.random() * empty.length)];
    grid[y][x] = (Math.random() < 0.9) ? 2 : 4;
  }

  // 画面更新:タイルとスコアを再描画
  function updateView() {
    const tiles = container.children;
    let idx = 0;
    for (let y = 0; y < SIZE; y++) {
      for (let x = 0; x < SIZE; x++) {
        const val = grid[y][x];
        const tile= tiles[idx++];
        tile.textContent = val === 0 ? '' : val;
        tile.className = 'tile';
        if (val > 0) tile.classList.add(`tile-${val}`);
      }
    }
    scoreEl.textContent = `スコア: ${score}`;
  }

  // スライド&マージ共通処理
  function slideAndMerge(arr) {
    let row = arr.filter(v => v !== 0);
    for (let i = 0; i < row.length - 1; i++) {
      if (row[i] === row[i+1]) {
        row[i] *= 2;
        score += row[i];
        row[i+1] = 0;
      }
    }
    row = row.filter(v => v !== 0);
    while (row.length < SIZE) row.push(0);
    return row;
  }

  // 各方向の移動処理
  function moveLeft() {
    let moved = false;
    for (let y = 0; y < SIZE; y++) {
      const old = [...grid[y]];
      const merged = slideAndMerge(old);
      grid[y] = merged;
      if (merged.some((v,i) => v !== old[i])) moved = true;
    }
    return moved;
  }
  function moveRight() {
    let moved = false;
    for (let y = 0; y < SIZE; y++) {
      const old = [...grid[y]].reverse();
      const merged = slideAndMerge(old).reverse();
      if (merged.some((v,i) => v !== grid[y][i])) moved = true;
      grid[y] = merged;
    }
    return moved;
  }
  function moveUp() {
    let moved = false;
    for (let x = 0; x < SIZE; x++) {
      const col = [];
      for (let y = 0; y < SIZE; y++) col.push(grid[y][x]);
      const merged = slideAndMerge(col);
      for (let y = 0; y < SIZE; y++) {
        if (grid[y][x] !== merged[y]) moved = true;
        grid[y][x] = merged[y];
      }
    }
    return moved;
  }
  function moveDown() {
    let moved = false;
    for (let x = 0; x < SIZE; x++) {
      const col = [];
      for (let y = 0; y < SIZE; y++) col.push(grid[y][x]);
      const merged = slideAndMerge(col.reverse()).reverse();
      for (let y = 0; y < SIZE; y++) {
        if (grid[y][x] !== merged[y]) moved = true;
        grid[y][x] = merged[y];
      }
    }
    return moved;
  }

  // ゲームオーバー判定
  function isGameOver() {
    // 空きセルがあれば継続
    for (let y = 0; y < SIZE; y++)
      for (let x = 0; x < SIZE; x++)
        if (grid[y][x] === 0) return false;
    // 隣接セルに同じ値があれば継続
    for (let y = 0; y < SIZE; y++) {
      for (let x = 0; x < SIZE; x++) {
        const v = grid[y][x];
        if ((x < SIZE-1 && grid[y][x+1] === v) ||
            (y < SIZE-1 && grid[y+1][x] === v)) {
          return false;
        }
      }
    }
    return true;
  }

  // ゲームオーバー時の表示
  function showGameOver() {
    gameActive = false;
    gameOverEl.style.display = 'block';
  }
  function hideGameOver() {
    gameOverEl.style.display = 'none';
  }

  // 初期表示はタイトル画面
  showTitleScreen();
  </script>
</body>
</html>

アルゴリズムの流れ

手順内容関数/処理
1. 初期化4×4 グリッドをゼロ埋め、最初のタイルを 2 枚出現init()addRandomTile() ×2
2. 描画DOM に tile 要素を配置し、数字と色を更新updateView()
3. ユーザ操作矢印キーで方向を検出し、スライド&マージ処理を実行moveUp() / moveDown() / moveLeft() / moveRight()
4. 合体処理同じ数字の連続するタイルを合体しスコアを加算slideAndMerge()
5. 新規生成移動があった場合、空きマスにランダムで「2」または「4」を追加addRandomTile()
6. ゲームオーバー/継続判定空きマスなし&合体可能位置なしなら終了、そうでなければ継続isGameOver()showGameOver()

関数の詳細

関数名役割
init()グリッド配列を初期化し、DOM 上に tile 要素を 16 個生成
addRandomTile()空きマスをリスト化し、1 つを選んで「2」か「4」を配置
updateView()各セル要素に数値と背景色クラスを反映し、スコア表示を更新
slideAndMerge(arr)1 行/1 列を左詰め→同値合体→再左詰めし、新配列を返す
moveLeft()moveRight()moveUp()moveDown()各方向のスライド&マージをグリッドに適用し、動きがあれば true を返す
isGameOver()空きセルの有無と隣接セルの同値有無をチェックし、終了判定を返す
showGameOver()ゲームを停止し、オーバーレイで「ゲームオーバー」を表示

改造のポイント

  • サイズ変更・カスタムグリッド
    SIZE を変更し、5×5 や 6×6 のバリエーションを追加可能。大きいグリッドは難易度アップにつながります。
  • タイルアニメーション
    合体時のエフェクトやスライドの動きに CSS アニメーションを追加し、視覚的に楽しく演出しましょう。
  • UNDO(取り消し)機能
    各操作前のグリッドとスコアをスタックに保存し、UNDO ボタンで一手戻せるようにすると上級者向けに便利です。
  • 目標値変更
    2048 以外に「4096」「8192」を目標としたモードを実装し、さらなる高得点を目指せるようにできます。
  • スコアランキング
    ローカルストレージにハイスコアを保存し、プレイごとの最高記録を表示するとリプレイ性が高まります。

最後に
2048 は「シンプルなルール × 奥深い戦略」が魅力です。プログラム構造も分かりやすく、カスタマイズや拡張にぴったりな教材になります。ぜひ上記のポイントをもとに、ご自身だけのオリジナルバージョンにチャレンジしてみてください!