バニラJavaScriptで完全完結するナンプレ(ナンバープレイス)自動生成&解答&問題共有Webアプリを作ってみた
1. はじめに
ライブラリや外部フレームワーク(ReactやVueなど)を一切使わず、完全なバニラJavaScript(単一のHTMLファイルのみ)で動作する軽量な「ナンプレ(ナンバープレイス)自動生成&解答&問題共有ツール」を作成しました。
二十数年くらい昔私がC++コンソールアプリで実装していたナンプレの解法アルゴリズム(ビットマスク演算を活用した高速探索)をJavaScriptに移植し、以下の機能をブラウザ上だけで完結するように実装しています。
- 一意解(解が1つだけ存在する)を保証した問題自動生成
- ビット演算による高速なソルバー&検証エンジン
- 自作問題の妥当性チェック(解が存在するか/一意解か)
- URLパラメータ(Base64エンコード)による問題のSNS/WEB共有機能
- 初めてマスを入力した瞬間にスタートする自動プレイ時間計測
2. 主な機能と工夫したポイント
2-1. C++から移植したビットマスク探索ソルバー
ナンプレ(ナンバープレイス)の自動生成と検証において、最もコアとなるのが「高速な解法探索(ソルバー)」と「解が一意(1つだけ)かどうかの判定」です。
ここでは、C++で書かれたビット演算ベースの高速ソルバーをJavaScriptに移植したコードの仕組みを、4つのポイントに分けて詳しく解説します。
2-1-1. ビットマスクによる高速な候補数字の特定 (searchNumPl)
ナンプレにおいて最も処理が重くなるのが、「あるマスにどの数字が入るか(重複チェック)」の判定です。愚直に配列やループを回すと計算量が膨らむため、ビット演算(ビットマスク)を用いて高速化しています。
-
ビットフラグの定義:
1〜9の数字をそれぞれ9ビットのフラグ(0x001から0x100)に対応させています。
const NUM = [0x001, 0x002, 0x004, 0x008, 0x010, 0x020, 0x040,
0x080, 0x100];
let currentQuestion = Array(81).fill(0);
let currentSolution = Array(81).fill(0);
let isCustomMode = false;
function getNum(k, a) {
let ret = 1, j = 0;
for (let i = 1; i < 0x200; i *= 2, ret++) {
if (k & i) j++;
if (a && j === a) return ret;
}
return 0;
}
function searchNumPl(np, idx) {
if (np[idx] !== 0) return { n: 0, k: 0, valid: false };
const r = Math.floor(idx / 9);
const c = idx % 9;
const br = Math.floor(r / 3) * 3;
const bc = Math.floor(c / 3) * 3;
let used = 0;
for (let a = 0; a < 9; a++) {
const nr = np[r * 9 + a];
if (nr) used |= NUM[nr - 1];
const nc = np[a * 9 + c];
if (nc) used |= NUM[nc - 1];
const nb = np[(br + Math.floor(a / 3)) * 9 + (bc + (a % 3))];
if (nb) used |= NUM[nb - 1];
}
let count = 0, mask = 0;
for (let a = 0; a < 9; a++) {
if (!(used & NUM[a])) {
count++;
mask |= NUM[a];
}
}
return { n: count, k: mask, valid: true };
}
-
重複チェックのビットOR演算:
対象のマスと同じ行・列・3×3ブロックに存在する数字を走査し、すでに使われている数字のビットをused |= NUM[nr - 1]のようにビットOR(論理和)でまとめていきます。 -
空き候補の抽出:
usedの反転を取ることで、「そのマスに置ける数字」をビットマスク(mask)と候補数(count)として一撃で算出します。これにより、条件分岐を最小限に抑えた高速な判定を実現しています。
2-1-2. 再帰とバックトラックによる全域探索 (calcNumPl)
function calcNumPl(np, bans = false) {
let work = [...np];
function solve() {
let m = 10, j = -1, l = 0, emptyCount = 0;
for (let i = 0; i < 81; i++) {
const res = searchNumPl(work, i);
if (res.valid) {
if (res.n < m) {
m = res.n;
j = i;
l = res.k;
}
} else {
emptyCount++;
}
}
if (emptyCount === 81) return true;
if (m < 1 || m > 9) return false;
if (m === 1) {
work[j] = getNum(l, 1);
return solve();
} else {
const loopRange = bans
? Array.from({length: m}, (_, i) => m - i)
: Array.from({length: m}, (_, i) => i + 1);
for (let a of loopRange) {
const backup = [...work];
work[j] = getNum(l, a);
if (solve()) return true;
work = backup;
}
return false;
}
}
return solve() ? work : null;
}
候補数が少ないマス(定員が少ないマス)を優先的に埋めながら、再帰的に解を探索するバックトラック法(深さ優先探索)のコアエンジンです。
-
候補数が最も少ないマス(Minimum Remaining Value法)を探索:
盤面全体を見渡し、置ける数字の選択肢(res.n)が最も少ない空きマス(j)を特定します。 -
候補が1つの場合の確定処理 (
m === 1):
選択肢が1つしかないマスは迷わず確定させ、再帰的に次のマスを解き進めます(数独における「唯一の候補」の確定)。 -
分岐とバックトラック (
m > 1):
複数の選択肢がある場合は、実際に1つ仮置きして再帰呼び出し(solve())を行います。もし途中で破綻(解なし)に行き当たった場合は、バックアップから盤面を復元(バックトラック)して別の数字を試します。 -
bans(逆順探索)フラグの役割:
引数bansをtrueまたはfalseに切り替えることで、ループの走査順序(正順または逆順)を反転させることができます。これは次の「一意解の検証」で極めて重要な役割を持ちます。
2-1-3. 盤面のルール違反チェック (hasRuleViolation)
function hasRuleViolation(board) {
for (let i = 0; i < 9; i++) {
let rowSet = new Set(), colSet = new Set(), blockSet = new Set();
for (let j = 0; j < 9; j++) {
let rv = board[i * 9 + j];
if (rv !== 0) {
if (rowSet.has(rv)) return true;
rowSet.add(rv);
}
let cv = board[j * 9 + i];
if (cv !== 0) {
if (colSet.has(cv)) return true;
colSet.add(cv);
}
let br = Math.floor(i / 3) * 3 + Math.floor(j / 3);
let bc = (i % 3) * 3 + (j % 3);
let bv = board[br * 9 + bc];
if (bv !== 0) {
if (blockSet.has(bv)) return true;
blockSet.add(bv);
}
}
}
return false;
}
ユーザーが自作問題を入力した際や、不正なデータを受け取った際に、ナンプレの基本ルール(行・列・3×3ブロック内に同じ数字が重複していないか)に違反していないかを事前にスキャンする関数です。
- JavaScriptの
Setオブジェクトを活用し、各行・各列・各ブロックを走査して重複値(Set.has(rv))が見つかった時点で即座にtrue(違反あり)を返します。 - これにより、無駄なソルバーの実行を防ぎ、エラーメッセージを高速に返すことができます。
2-1-4. 一意解の保証(唯一解チェック)のカラクリ (solveAndVerifyUnique)
「ナンプレとして成立するか」の最大の条件は、「答えが世界にただ1つだけ存在する(一意解)」ことです(複数答えがある問題はクイズとして欠陥品になります)。これを完璧に判定しているのがこの関数です。
function solveAndVerifyUnique(np) {
if (hasRuleViolation(np)) {
return { solvable: false, unique: false, solution: null, reason: 'rule_violation' };
}
// 1回目:正順(1から順に候補を試す)で解を探索
const ansForward = calcNumPl(np, false);
if (!ansForward) return { solvable: false, unique: false, solution: null, reason: 'unsolvable' };
// 2回目:逆順(大きい数字や逆から候補を試す)で解を探索
const ansBackward = calcNumPl(np, true);
// 両者の解が完全に一致するか比較
const isUnique = ansForward.every((val, i) => val === ansBackward[i]);
return {
solvable: true,
unique: isUnique,
solution: ansForward,
reason: isUnique ? 'valid' : 'multiple_solutions'
};
}
-
なぜ正順・逆順で一意解がわかるのか?:
もしパズルに複数の解(マルチプル・ソリューション)が存在する場合、バックトラックの探索順序(正から試すか、逆から試すか)によって最初にヒットする答えが変わる性質を利用しています。 -
ansForward(正順の解)とansBackward(逆順の解)の全81マスを比較し、完全一致していれば「答えは1つだけ(一意解)」、不一致であれば「複数の答えが存在する」と数学的に厳密に判定できます。
2-1-5. ビットマスク探索ソルバーまとめ
このC++移植ソルバーによって、ブラウザのJavaScript上であっても、わずか数十ミリ秒で以下の高度な処理が可能になっています。
- ルール違反の即座検知 (
rule_violation) - 解が存在しない破綻した問題の検知 (
unsolvable) - 複数解の検知 (
multiple_solutions) - 高精度な一意解の保証&自動生成・自作問題の妥当性チェック (
valid)
2-2. URLパラメータ(Base64)による軽量な問題共有
作成した問題や自作した問題は、81個の配列(0〜9の数値列)を文字列化してBase64エンコードし、URLセーフな形式に変換してクエリパラメータ(?data=...)として埋め込んでいます。
タイトルや出題者名(title, author)もパラメータに載せることができ、サーバーやデータベースを用意しなくてもURL1本で問題の共有が可能です。
function encodeBoard(board) {
const str = board.join('');
return btoa(str).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}
function decodeBoard(encodedStr) {
try {
let base64 = encodedStr.replace(/-/g, '+').replace(/_/g, '/');
while (base64.length % 4) base64 += '=';
const str = atob(base64);
if (str.length !== 81) return null;
return str.split('').map(Number);
} catch (e) {
return null;
}
}
2-3. ユーザー体験(UX)を意識したタイマー自動スタート
ゲーム画面を開いてすぐにタイマーが走るのではなく、「プレイヤーが最初の数字をマスに入力した瞬間」にタイマーが動き出します。
-
inputイベントで初回の数字入力を検知してstartTimer()を実行。 - 「入力チェック」ボタンを押して全マス正解(クリア)になった瞬間に
stopTimer()が走り、クリアタイムを表示。 - 新規問題生成やリセット時にはタイマーも自動クリア。
3. まとめ・おわりに
フレームワークを使用しない「生(バニラ)のJavaScript」だけでも、ビット操作と効率的な探索アルゴリズムを組み合わせることで、非常に高速かつ機能豊富なパズルWebツールを作成することができます。
作者
GitHub: https://github.com/NAS6mixfoolv
X(旧Twitter): https://x.com/NAS6_oxo
作者HP: https://nas6.net
気に入っていただけたら GitHub に ⭐ をいただけると嬉しいです!
