状態管理 | State Management
プログラムが「今どんな状況か」を記憶するのが 状態(State)。 数独ソルバーはゲームの盤面を記憶し続ける必要があるため、状態管理が設計の核心になる。
アプリの状態をひとつのオブジェクトにまとめる設計パターン。 バラバラな変数より管理しやすく、バグを減らせる。
const state = { grid: Array(81).fill(null), // 盤面の値(null = 空) initial: Array(81).fill(false), // 初期値かどうか candidates: Array(81).fill(null).map(() => [1,2,3,4,5,6,7,8,9]), showCandidates: false // UI の表示モード };
let grid = … let initial = … と別々に書くこともできるが、 オブジェクトにまとめると「状態のリセット」や「渡し回し」が簡単になる。 React・Vue など現代のフレームワークも同じ発想を持つ。9×9 をフラット配列で表す
2次元グリッドを 1次元配列(長さ 81) で表現するのはよくあるテクニック。 行と列は計算で求める。
// 1次元インデックス → 行・列・ブロックの変換 function getRow(idx) { return Math.floor(idx / 9); } function getCol(idx) { return idx % 9; } function getBlock(idx) { const row = getRow(idx); const col = getCol(idx); return Math.floor(row / 3) * 3 + Math.floor(col / 3); } // 例: idx=40(中央)→ row=4, col=4, block=4 // 式: 40 / 9 = 4.44... → floor → 4 // 式: 40 % 9 = 4
index = row * 9 + col、逆変換は上の式。 画像処理・ゲーム開発など多くの場面で登場する基本パターン。関数設計 | Function Design
同じ入力を与えれば必ず同じ出力を返し、外部の状態を変更しない関数。 テストしやすく、バグが見つけやすい。
getRow() getCol() getBlock()引数だけを使い、
state を読み書きしない。eliminateCandidates()外部の
state.candidates を書き換える。意図的だが、そのことを意識する必要がある。eliminateCandidates() は true/false/changed を返す。呼び出し側がその結果で分岐できる設計。Set で重複なしの集合を作る
getRelatedIndices() は「同じ行・列・ブロックのマス」をまとめて返す関数。 行・列・ブロックで重複するインデックスが出るため Set を使う。
function getRelatedIndices(idx) { const related = new Set(); // 重複なしコレクション // 同じ行の 9 マス for (let c = 0; c < 9; c++) related.add(row * 9 + c); // 同じ列の 9 マス(行と列の角は重複するが Set が自動で除く) for (let r = 0; r < 9; r++) related.add(r * 9 + col); // 同じ 3×3 ブロックの 9 マス const bRow = Math.floor(row / 3) * 3; const bCol = Math.floor(col / 3) * 3; for (let r = bRow; r < bRow+3; r++) for (let c = bCol; c < bCol+3; c++) related.add(r * 9 + c); related.delete(idx); // 自分自身は除外 return Array.from(related); // Set → Array に変換 }
push() し続けると同じインデックスが複数入る。 後で手動で filter() して重複除去もできるが、Set を使う方がシンプル。 最後に Array.from() で配列に戻すことで for...of や配列メソッドが使える。配列メソッド | Array Methods
このコードには JavaScript の重要な配列メソッドが多数登場する。 「元の配列を変えずに新しい配列を返す」メソッドと、「元の配列を変える」メソッドの違いに注目。
| メソッド | 何をするか | コードでの使用例 | 元の配列 |
|---|---|---|---|
.fill(v) | 全要素を値 v で埋める | Array(81).fill(null) | 変える |
.map(fn) | 各要素を変換した新配列を返す | example.map(v => v !== null) | 変えない ✅ |
.filter(fn) | 条件を満たす要素だけ残した新配列 | candidates.filter(c => c !== value) | 変えない ✅ |
.every(fn) | 全要素が条件を満たすか(真偽値) | state.grid.every(v => v !== null) | 変えない ✅ |
.includes(v) | 値 v が含まれるか(真偽値) | candidates[idx].includes(num) | 変えない ✅ |
.push(v) | 末尾に追加(長さが変わる) | possibleCells.push(idx) | 変える |
// ❌ これは「元の配列に数字が残ったまま」 state.candidates[relIdx].splice(...); // splice は難しく副作用あり // ✅ filter で「value を含まない新配列」を作って上書き state.candidates[relIdx] = state.candidates[relIdx] .filter(c => c !== value); // 例: [1,2,3,5,7] から value=3 を消す → [1,2,5,7] // 変化があったかどうかを「長さの比較」で検出 const before = state.candidates[relIdx].length; state.candidates[relIdx] = state.candidates[relIdx].filter(...); const after = state.candidates[relIdx].length; if (before !== after) changed = true;
filter は「元を壊さない」ため安全。 実際のコードでは before !== after で「何か変化があったか」を検知するのにも使われている。 これは 変化検知(change detection) の基本パターン。DOM 操作 | Rendering & Events
HTML をプログラムから操作するための API。document.getElementById() などで HTML 要素を取得・変更できる。
renderGrid() の設計:毎回全部描き直す
gridEl.innerHTML = ''
既存の HTML をすべて消す。前回の描画を一掃する。
for (let i = 0; i < 81; i++)
81 マス分ループして、各マスの要素を新しく生成する。
document.createElement('div')
JavaScript で HTML 要素を動的に作成。クラスや属性も JS から設定できる。
cell.appendChild(input)
親要素に子要素を追加する。最終的に gridEl.appendChild(cell) で画面に表示される。
// 要素の取得 const gridEl = document.getElementById('sudokuGrid'); // 要素の作成と属性設定 const cell = document.createElement('div'); cell.className = 'sudoku-cell'; cell.classList.add('initial'); // クラスを追加 // input 要素の作成 const input = document.createElement('input'); input.type = 'text'; input.maxLength = 1; input.value = state.grid[i] || ''; // null なら空文字 input.dataset.index = i; // カスタムデータ属性 // 親子関係の構築 cell.appendChild(input); gridEl.appendChild(cell);
イベント駆動プログラミング
ユーザーの操作(クリック・入力)に反応してコードを実行する設計を イベント駆動 という。
// ボタンのクリックに反応 document.getElementById('solveBtn') .addEventListener('click', () => { solveSudoku(); }); // テキスト入力に反応(アロー関数 + e でイベントオブジェクトを受け取る) input.addEventListener('input', (e) => { const val = e.target.value; // 入力された値 if (/^[1-9]$/.test(val)) { // 正規表現で 1-9 か判定 state.grid[i] = parseInt(val); } else { e.target.value = ''; // 無効な入力をリセット } updateStats(); });
/^[1-9]$/ の読み方 ^ = 先頭、[1-9] = 1から9のいずれか1文字、$ = 末尾。 つまり「1文字で1〜9のいずれか」であることを確認している。 .test() は真偽値を返す。アルゴリズム | Solving Logic
あるマスに数字が確定したら、同じ行・列・ブロックの他のマスからその数字の候補を消す。 この「制約の波及」を繰り返すことで解が求まる。数独ソルバーが使う核心的な手法。
2つの確定テクニック
candidates[i].length === 1 を検査する。for (let row = 0; row < 9; row++) { for (let num = 1; num <= 9; num++) { const possibleCells = []; for (let col = 0; col < 9; col++) { const idx = row * 9 + col; if (state.grid[idx] === null && state.candidates[idx].includes(num)) { possibleCells.push(idx); // num を置けるマスを集める } } if (possibleCells.length === 1) { // ← 唯一のマス → 確定! state.grid[possibleCells[0]] = num; state.candidates[possibleCells[0]] = []; } } }
反復ループで収束させる
1回のパスで全部解けるとは限らない。そのため while ループで「変化がなくなるまで」繰り返す。 これを 不動点反復(Fixed-point iteration) という。
while (iteration < maxIterations) { iteration++; const changed = solveStep(); // 1ステップ実行 if (changed === false) break; // エラー → 即中断 if (!changed) { // 変化なし → 手詰まり log('⚠️ 基本テクニックで解決不可'); break; } if (state.grid.every(v => v !== null)) { // 全マス埋まった? log('🎉 完成!'); break; } } // 3つの終了条件:1エラー 2手詰まり 3完成
changed が常に true を返すケースに備えている。 実用コードではこうした「保険」を入れる習慣が大切。✏️ 確認クイズ Check Your Understanding
Q1. Array(81).fill(null) が作るのは何か?
Array(81) で長さ 81 の空の配列を作り、.fill(null) で全要素を null に初期化する。Q2. インデックス idx = 20 のマスの行番号は?(0始まり)
Math.floor(20 / 9) = Math.floor(2.22…) = 2。0始まりなので画面上は「3行目」に見える。Q3. Set を使う主な理由は?
Set.add() は同じ値を無視するため、手動で重複除去する必要がない。Q4. array.filter(c => c !== 3) が返すのは?
filter は元の配列を変更せず、条件を満たす要素だけの 新しい配列 を返す。これは「非破壊的(immutable)」な操作と呼ばれる。Q5. state.grid.every(v => v !== null) が true を返すのはどんな状態?
every は 全要素 が条件を満たすとき true。1つでも null があれば false。これで数独が解けたか判定している。