【ゲーム】JavaScript:31 数独(Sudoku)

 「🔢 数独ゲーム 🔢」は、9×9 のマスに 1~9 の数字を配置するパズルゲームです。行・列・3×3 ブロックのいずれも同じ数字を重複させず、すべての空きマスを正しく埋めるとクリアとなります。

遊び方と操作方法

  1. タイトル画面の「スタート 🚀」ボタンをクリック
  2. 9×9 グリッドが表示され、あらかじめ配置された数字は編集不可
  3. 空のセルをクリックして数字を入力(1~9)
  4. 入力時に自動で同じ行・列・ブロック内の重複をチェックし、重複があると赤くハイライト
  5. すべてのセルに数字が入り、重複エラーなしになったら「クリアチェック」ボタンをクリック
  6. 正しく埋まっていれば「クリアしました!」の画面へ

ルール

  • サイズ:9 行 × 9 列
  • 数字:1~9
  • 制約
    ・各行に同じ数字を 2 つ以上入れない。
    ・各列に同じ数字を 2 つ以上入れない。
    ・各 3×3 ブロックに同じ数字を 2 つ以上入れない。
  • クリア条件:空マスがなく、すべての制約を満たす

🎮ゲームプレイ

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

31 数独(Sudoku)

素材のダウンロード

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

sudoku_title.pngsudoku_bg.png

ゲーム画面イメージ

プログラム全文(sudoku.html)

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>🔢 JavaScript:数独ゲーム 🔢</title>
  <style>
    /* ===== 全体背景設定 ===== */
    body {
      margin: 0;
      padding: 0;
      font-family: 'Arial', sans-serif;
      /* 背景画像を固定で全面に表示 */
      background: url('sudoku_bg.png') no-repeat center center fixed;
      background-size: cover;
    }
    /* ===== 中央オーバーレイ共通スタイル ===== */
    .overlay {
      position: absolute;           /* 画面中央に配置 */
      top: 50%;
      left: 50%;
      transform: translate(-50%, -50%);
      background-color: rgba(255,255,255,0.95); /* 半透明白背景 */
      padding: 20px;
      border-radius: 10px;
      text-align: center;
      width: 90%;
      max-width: 600px;
    }
    /* 非表示用クラス */
    .hidden { display: none; }
    /* タイトル画像サイズ調整 */
    .title-image {
      display: block;
      margin: 0 auto 10px;
      max-width: 80%;
      height: auto;
    }
    /* 説明文左寄せ */
    .instructions { text-align: left; margin-bottom: 10px; }
    .instructions p { margin: 5px 0; }
    /* 汎用ボタンスタイル */
    .btn {
      display: inline-block;
      padding: 10px 20px;
      margin: 10px 5px;
      font-size: 16px;
      font-weight: bold;
      color: #fff;
      background-color: #007BFF;
      border: none;
      border-radius: 5px;
      cursor: pointer;
      box-shadow: 0 2px 4px rgba(0,0,0,0.2);
      transition: background-color 0.2s, transform 0.1s;
    }
    .btn:hover { background-color: #0056b3; transform: translateY(-2px); }
    .btn:active { transform: translateY(0); box-shadow: 0 1px 2px rgba(0,0,0,0.2); }
    /* ===== 数独グリッド ===== */
    #sudoku-grid { border-collapse: collapse; margin: 10px auto; }
    #sudoku-grid td {
      border: 1px solid #999;
      width: 40px;
      height: 40px;
      text-align: center;
      vertical-align: middle;
      position: relative;
    }
    /* 3x3ブロック境界線を太くする */
    .thick-top { border-top: 3px solid #333; }
    .thick-bottom { border-bottom: 3px solid #333; }
    .thick-left { border-left: 3px solid #333; }
    .thick-right { border-right: 3px solid #333; }
    /* 3x3ブロック識別用の薄い背景色 */
    .block-shade { background-color: rgba(0, 0, 0, 0.05); }
    /* セル入力フィールド */
    .cell-input {
      width: 100%; height: 100%;
      border: none;
      font-size: 20px;
      text-align: center;
      background: transparent;
    }
    .cell-input:focus { outline: none; }
    /* バリデーションハイライト */
    .invalid { background-color: rgba(255,0,0,0.3); }
    /* メッセージ表示 */
    .message {
      margin-top: 10px;
      padding: 8px;
      background-color: rgba(0,0,0,0.7);
      color: #fff;
      border-radius: 5px;
      min-height: 1.5em;
    }
  </style>
</head>
<body>
  <!-- タイトル画面 -->
  <div id="title-screen" class="overlay">
    <!-- タイトル画像 -->
    <img src="sudoku_title.png" alt="数独タイトル" class="title-image">
    <!-- メイン見出し -->
    <h1>🔢 数独ゲーム 🔢</h1>
    <!-- ルール説明(左寄せ) -->
    <div class="instructions">
      <p>9×9のマスに1~9の数字を、一行・一列・3×3ブロックそれぞれに重複なく配置します。</p>
      <p>空きマスに数字を入力後、同じ行・列・ブロックに重複がないか自動でチェック。</p>
      <p>すべて埋めてルールを守ればクリア!</p>
    </div>
    <!-- スタートボタン -->
    <button id="start-btn" class="btn">スタート 🚀</button>
  </div>

  <!-- ゲーム画面 -->
  <div id="game-screen" class="overlay hidden">
    <!-- 数独グリッドを表示 -->
    <table id="sudoku-grid"></table>
    <!-- クリアチェックボタン -->
    <button id="check-btn" class="btn">クリアチェック</button>
    <!-- 状態メッセージ -->
    <div id="message" class="message">数字を入力してください</div>
  </div>

  <!-- 終了画面 -->
  <div id="end-screen" class="overlay hidden">
    <h2>🎉 クリアしました! 🎉</h2>
    <button id="restart-btn" class="btn">タイトルに戻る 🔄</button>
  </div>

  <script>
    /**
     * サンプルパズルデータ(0は空きセルを示す)
     */
    const PUZZLE = [
      [5,3,0, 0,7,0, 0,0,0],
      [6,0,0, 1,9,5, 0,0,0],
      [0,9,8, 0,0,0, 0,6,0],
      [8,0,0, 0,6,0, 0,0,3],
      [4,0,0, 8,0,3, 0,0,1],
      [7,0,0, 0,2,0, 0,0,6],
      [0,6,0, 0,0,0, 2,8,0],
      [0,0,0, 4,1,9, 0,0,5],
      [0,0,0, 0,8,0, 0,7,9]
    ];

    // 入力要素を格納する2D配列
    let gridInputs = [];

    // DOM要素取得
    const titleScreen   = document.getElementById('title-screen');
    const gameScreen    = document.getElementById('game-screen');
    const endScreen     = document.getElementById('end-screen');
    const startBtn      = document.getElementById('start-btn');
    const restartBtn    = document.getElementById('restart-btn');
    const sudokuGrid    = document.getElementById('sudoku-grid');
    const messageDiv    = document.getElementById('message');
    const checkBtn      = document.getElementById('check-btn');

    /**
     * タイトル画面を表示する
     */
    function showTitle() {
      titleScreen.classList.remove('hidden');
      gameScreen.classList.add('hidden');
      endScreen.classList.add('hidden');
    }

    /**
     * ゲームを開始する
     * - グリッドを初期化して表示
     * - タイトル画面を非表示に
     */
    function startGame() {
      initGrid();
      messageDiv.textContent = '数字を入力してルールに従ってください';
      titleScreen.classList.add('hidden');
      endScreen.classList.add('hidden');
      gameScreen.classList.remove('hidden');
    }

    /**
     * 数独グリッドを構築し、空セルに入力欄を配置する
     */
    function initGrid() {
      // グリッドクリア
      sudokuGrid.innerHTML = '';
      gridInputs = [];

      for (let r = 0; r < 9; r++) {
        const row = document.createElement('tr');
        const rowInputs = [];

        for (let c = 0; c < 9; c++) {
          const cell = document.createElement('td');

          // 3×3ブロックの外枠を太線にして識別しやすく
          if (r % 3 === 0) cell.classList.add('thick-top');
          if (r === 8)      cell.classList.add('thick-bottom');
          if (c % 3 === 0) cell.classList.add('thick-left');
          if (c === 8)      cell.classList.add('thick-right');

          // 3×3ブロックを交互に薄い背景でシェード
          if (((Math.floor(r/3) + Math.floor(c/3)) % 2) === 0) {
            cell.classList.add('block-shade');
          }

          if (PUZZLE[r][c] !== 0) {
            // 固定数字セルは太字表示
            cell.textContent = PUZZLE[r][c];
            cell.style.fontWeight = 'bold';
          } else {
            // 空セル:テキスト入力要素を生成
            const input = document.createElement('input');
            input.type = 'text';
            input.maxLength = 1;
            input.className = 'cell-input';
            // 入力イベントを設定して重複チェック
            input.addEventListener('input', () => onInput(r, c));
            cell.append(input);
            rowInputs.push(input);
          }

          row.append(cell);
        }

        sudokuGrid.append(row);
        gridInputs.push(rowInputs);
      }
    }

    /**
     * ユーザー入力時に行・列・ブロックの重複チェックを行う
     * r, c: 入力されたセルの行列番号
     */
    function onInput(r, c) {
      const input = getInputAt(r, c);
      const val = input.value.trim();

      // 1-9以外の文字が入ったらクリア
      if (!/^[1-9]$/.test(val)) {
        input.value = '';
        input.classList.remove('invalid');
        return;
      }

      const num = parseInt(val, 10);
      let valid = true;

      // 行チェック
      for (let cc = 0; cc < 9; cc++) {
        if (cc !== c && getCellValue(r, cc) === num) valid = false;
      }
      // 列チェック
      for (let rr = 0; rr < 9; rr++) {
        if (rr !== r && getCellValue(rr, c) === num) valid = false;
      }
      // 3×3ブロックチェック
      const br = Math.floor(r/3) * 3;
      const bc = Math.floor(c/3) * 3;
      for (let i = 0; i < 3; i++) {
        for (let j = 0; j < 3; j++) {
          const rr = br + i;
          const cc = bc + j;
          if ((rr !== r || cc !== c) && getCellValue(rr, cc) === num) valid = false;
        }
      }

      // 重複があれば赤背景でハイライト
      input.classList.toggle('invalid', !valid);
    }

    /**
     * 指定セル(r,c)の入力要素を返す
     */
    function getInputAt(r, c) {
      let idx = 0;
      for (let cc = 0; cc < 9; cc++) {
        if (PUZZLE[r][cc] === 0) {
          if (cc === c) return gridInputs[r][idx];
          idx++;
        }
      }
    }

    /**
     * 指定セル(r,c)の値を返す
     * 固定セルはPUZZLE配列から、空セルは入力欄から取得
     */
    function getCellValue(r, c) {
      if (PUZZLE[r][c] !== 0) return PUZZLE[r][c];
      const inp = getInputAt(r, c);
      const v = parseInt(inp.value, 10);
      return isNaN(v) ? null : v;
    }

    /**
     * クリアチェックを実行
     * 全セルが埋まり、エラーがないとクリア画面へ遷移
     */
    checkBtn.addEventListener('click', () => {
      let complete = true;
      for (let r = 0; r < 9; r++) {
        for (let c = 0; c < 9; c++) {
          if (PUZZLE[r][c] === 0) {
            const inp = getInputAt(r, c);
            if (!inp.value || inp.classList.contains('invalid')) complete = false;
          }
        }
      }
      // クリア判定
      if (complete) {
        gameScreen.classList.add('hidden');
        endScreen.classList.remove('hidden');
      } else {
        messageDiv.textContent = '未入力またはエラーがあります';
      }
    });

    // イベント登録
    startBtn.addEventListener('click', startGame);
    restartBtn.addEventListener('click', showTitle);
    document.addEventListener('DOMContentLoaded', showTitle);
  </script>
</body>
</html>

アルゴリズムの流れ

ステップ内容関数/命令
1. タイトル表示タイトル画面のみVisibleshowTitle()
2. ゲーム開始グリッド初期化、メッセージ設定、画面切替startGame()
3. グリッド生成PUZZLE 配列を走査し、0→入力欄、数字→固定セルを生成initGrid()
4. 入力検証入力欄の input イベントで 1–9かつ重複チェックonInput(r,c)
5. 重複チェック行・列・ブロック内を走査し、重複あれば .invalid 付与getCellValue(), onInput()
6. クリアチェック全セルに値があり、エラー無しであればクリア画面へcheckBtn のクリックイベント
7. 終了画面表示クリア成功時に終了画面を表示checkBtn ハンドラ

関数の詳細

関数名説明
showTitle()タイトル画面を表示し、ゲーム画面と終了画面を非表示に
startGame()ゲーム開始処理。グリッドの初期化、メッセージ更新、画面切替
initGrid()9×9 グリッドを PUZZLE に基づき動的に生成。0 → <input>、それ以外 → 固定数字セル
onInput(r,c)セルに入力された数字を検証し、同じ行・列・ブロック内の重複をチェックして .invalid を付与
getInputAt(r,c)空セルが何番目かを算出し、対応する <input> 要素を返す
getCellValue(r,c)固定セル or 入力セルの現在値を取得

改造のポイント

  • 複数パズル対応
    PUZZLE 配列を複数用意し、ランダムに選ぶかユーザーが選択できるように。
  • 自動解答ヒント
    空きセルの候補リストを自動で表示する「ナンプレ支援機能」を追加。
  • タイマー/記録機能
    クリアまでの時間を計測し、localStorage に保存してハイスコア化。
  • レスポンシブ対応
    グリッドセルやフォントサイズを画面幅に応じて調整するとモバイルでも快適。
  • 難易度調整
    提示される数字を増減(シン・ジェネレーター)して易しい/難しいモードを用意。

アドバイス:セルをクリックして数字選択する専用パッド(1〜9 のボタン)を追加すると、モバイル操作性が大幅に向上します。また、クリアチェックだけでなく「行チェック」「列チェック」「ブロックチェック」単位でボタンを分けると、段階的にパズルを進めやすくしてくれます!