// programming study material

数独ソルバーで学ぶ
JavaScript の基礎

実際に動くアプリのコードを題材に、プログラミングの核心概念を体系的に学ぶ。 状態管理・関数設計・配列操作・DOM・アルゴリズムまで一通りカバー。

State Management Array Methods DOM Manipulation Algorithm Design Event-Driven Set / Data Structures
§ 1

状態管理 | State Management

プログラムが「今どんな状況か」を記憶するのが 状態(State)。 数独ソルバーはゲームの盤面を記憶し続ける必要があるため、状態管理が設計の核心になる。

state オブジェクト Single Source of Truth

アプリの状態をひとつのオブジェクトにまとめる設計パターン。 バラバラな変数より管理しやすく、バグを減らせる。

state management
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 の表示モード
};
💡
なぜ1つにまとめるのか? let grid = … let initial = … と別々に書くこともできるが、 オブジェクトにまとめると「状態のリセット」や「渡し回し」が簡単になる。 React・Vue など現代のフレームワークも同じ発想を持つ。

9×9 をフラット配列で表す

2次元グリッドを 1次元配列(長さ 81) で表現するのはよくあるテクニック。 行と列は計算で求める。

index conversion
// 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
% (余り) と Math.floor の組み合わせ 2次元→1次元変換は index = row * 9 + col、逆変換は上の式。 画像処理・ゲーム開発など多くの場面で登場する基本パターン。
§ 2

関数設計 | Function Design

純粋関数 Pure Function

同じ入力を与えれば必ず同じ出力を返し、外部の状態を変更しない関数。 テストしやすく、バグが見つけやすい。

純粋関数の例
getRow() getCol() getBlock()
引数だけを使い、state を読み書きしない。
⚠️
副作用がある関数
eliminateCandidates()
外部の state.candidates を書き換える。意図的だが、そのことを意識する必要がある。
🔁
戻り値の設計
eliminateCandidates()true/false/changed を返す。呼び出し側がその結果で分岐できる設計。

Set で重複なしの集合を作る

getRelatedIndices() は「同じ行・列・ブロックのマス」をまとめて返す関数。 行・列・ブロックで重複するインデックスが出るため Set を使う。

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 や配列メソッドが使える。
§ 3

配列メソッド | 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)変える
filter の活用 | 候補数字の削除
// ❌ これは「元の配列に数字が残ったまま」
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 が好まれるか filter は「元を壊さない」ため安全。 実際のコードでは before !== after で「何か変化があったか」を検知するのにも使われている。 これは 変化検知(change detection) の基本パターン。
§ 4

DOM 操作 | Rendering & Events

DOM Document Object Model

HTML をプログラムから操作するための API。document.getElementById() などで HTML 要素を取得・変更できる。

renderGrid() の設計:毎回全部描き直す

1

gridEl.innerHTML = ''

既存の HTML をすべて消す。前回の描画を一掃する。

2

for (let i = 0; i < 81; i++)

81 マス分ループして、各マスの要素を新しく生成する。

3

document.createElement('div')

JavaScript で HTML 要素を動的に作成。クラスや属性も JS から設定できる。

4

cell.appendChild(input)

親要素に子要素を追加する。最終的に gridEl.appendChild(cell) で画面に表示される。

DOM 操作パターン
// 要素の取得
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);

イベント駆動プログラミング

ユーザーの操作(クリック・入力)に反応してコードを実行する設計を イベント駆動 という。

addEventListener | イベントリスナー
// ボタンのクリックに反応
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() は真偽値を返す。
§ 5

アルゴリズム | Solving Logic

制約伝播 Constraint Propagation

あるマスに数字が確定したら、同じ行・列・ブロックの他のマスからその数字の候補を消す。 この「制約の波及」を繰り返すことで解が求まる。数独ソルバーが使う核心的な手法。

2つの確定テクニック

1️⃣
Naked Single(裸の単一候補)
あるマスの候補が 1つだけ になった → その数字に確定。 最もシンプルな推論。

candidates[i].length === 1 を検査する。
🔍
Hidden Single(隠れた単一候補)
ある数字が特定の行・列・ブロックの中で 1つのマスにしか入れない → 確定。 候補数は複数あっても使える。
Hidden Single | 行のチェック
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完成
⚠️
maxIterations = 100 の意味 無限ループを防ぐための安全弁。理論上、数独の基本テクニックなら数十回で収束するが、 バグで 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。これで数独が解けたか判定している。