なぜ作ったか
マインスイーパは誰もがプレイしたことのあるゲームですが、自分で実装すると「初手安全保証」「フラッドフィルの停止条件」「フラグと残り地雷のカウンタ」など、意外と設計判断が必要。純粋関数として切り出すと、テストも書きやすく構造がきれいになります。
作ったもの
Minesweeper — https://sen.ltd/portfolio/minesweeper/
- 4 段階の難易度(初級 9×9 / 中級 16×16 / 上級 30×16 / カスタム)
- 初手安全保証(クリック位置 + 周囲 8 セルに地雷なし)
- BFS フラッドフィル(空セルの自動展開)
- キーボード操作(矢印 / Space / F)
- タイマー、ベストタイム記録(localStorage)
- クラシック Win95 風 3D ボーダー
- 日本語 / 英語 UI
vanilla JS、ゼロ依存、ビルド不要。node --test で 24 ケース。
初手安全保証
初手で地雷を踏まないために、ボードは最初のクリック後に生成。クリック位置 + 周囲 8 セルを「安全ゾーン」として除外:
export function createBoard(rows, cols, mineCount, safeRow, safeCol) {
const safeSet = new Set();
for (let dr = -1; dr <= 1; dr++) {
for (let dc = -1; dc <= 1; dc++) {
safeSet.add((safeRow + dr) * cols + (safeCol + dc));
}
}
// 安全ゾーン外からランダムに地雷配置
let eligible = positions.filter(([r, c]) => !safeSet.has(r * cols + c));
}
安全ゾーンが 9 セルなので、初手は必ず領域が開く。1 セルだけ開くよりずっと良い UX。
BFS フラッドフィル
隣接地雷数 0 のセルを開くと、連続する空セルを自動展開:
while (queue.length > 0) {
const [r, c] = queue.shift();
cur.revealed = true;
if (cur.neighbors === 0) {
// 周囲 8 セルをキューに追加
queue.push([nr, nc]);
}
}
ポイント: neighbors === 0 のセルだけ展開を継続。数字セル(1-8)は開示するが、そこでフロンティアを止める。これで「数字で縁取られた空き領域」が自然にできます。
イミュータブルな更新
revealCell と toggleFlag は新しいボードを返す:
const newBoard = board.map(r => r.map(c => ({ ...c })));
前のボード参照を保持するだけで undo が可能。純粋関数なのでテストも容易。
勝利判定
export function checkWin(board) {
for (const row of board) {
for (const cell of row) {
if (!cell.mine && !cell.revealed) return false;
}
}
return true;
}
地雷でないセルがすべて開示されたら勝利。フラグは関係ない(全地雷にフラグを立てる必要はない)。Windows 版と同じ仕様。
テスト
node --test で 24 ケース:
- ボード生成(サイズ、地雷数、安全ゾーン)
- フラッドフィル(地雷なしボードで全セル展開)
- 地雷踏み判定
- フラグトグル
- 勝利判定
- 隣接カウントの正確性
シリーズ
100+ 公開ポートフォリオ シリーズの #35 です。
- 📦 リポジトリ: https://github.com/sen-ltd/minesweeper
- 🌐 デモ: https://sen.ltd/portfolio/minesweeper/
- 🏢 会社: https://sen.ltd/
