「プログラミングを始めたけど、複雑なゲーム作りはちょっと難しそう…」
「手軽に作れて、友達に見せても“すごい!”と言われるような作品を作りたい!」
もしあなたがそう感じているなら、この記事がその悩みを解決します。
ここでは、Web開発の基本となる
HTML・CSS・JavaScript の3つだけを使って、
誰もが知る定番の脳トレゲーム、スライドパズル(15パズル)をゼロから作っていきます。
難しい環境構築は不要です。
1つのHTMLファイルにコードを貼り付けるだけで、
完成したスライドパズルがすぐに動くようになります。
このスライドパズルを完成させることで、
あなたはWebアプリの仕組み・配列処理・イベント操作を自然に理解できるようになります。
1つのゲームを通して、
「作って楽しい」「遊んで学べる」両方の経験を積みながら、
あなたのプログラミングスキルを確実にレベルアップさせましょう。
完成イメージと仕様を確認しよう
まずは、今回作るスライドパズル(15パズル)がどんなゲームなのかを見てみましょう。
完成形をイメージしておくと、これから学ぶコードの内容がぐっと理解しやすくなります。
完成版を体験してみよう
ゲームの仕様まとめ
画面には、4×4のマス目が表示されます。
そこに1〜15までの数字タイルが並び、右下の1マスだけが空白になっています。
タイルはクリックまたは矢印キーで移動でき、
空白の位置にスライドしていきます。
最終的に「1〜15が順番に並んで、右下が空白」になればクリアです!
スマホでも快適に動くデザイン
このパズルは、レスポンシブデザインを採用しています。
つまり、PCでもスマホでもタイルの大きさが自動で調整される仕組みです。
- PC:広い画面で見やすく、キーボード操作にも対応
- スマホ:タップだけでサクサク遊べる直感操作
ハイスコア機能(最少手数を記録)
プレイヤーがタイルを動かすたびに、
「手数」が1ずつカウントされます。
クリア時の最小手数は自動的に保存され、
次回以降も「ベストスコア」として表示されます。
解ける配置のみ生成
スライドパズルでは、絶対に解けない配置が存在します。
しかしこのプログラムでは、独自の関数を使い、
必ず解ける初期配置のみを生成するようにしています。
まずは動かしてみよう(完成コードをコピペ)
この記事では、最も手軽にWeb開発を体験できるよう、1つのHTMLファイルにすべてのコードを書く「単一ファイル方式」で進めます。
環境構築は不要。コピー&ペーストだけでOKです。
新規ファイルを用意する
- デスクトップなど分かりやすい場所に、任意のフォルダを作成(例:
slide-puzzle)。 - メモ帳(Windows)やテキストエディット(Mac)などのテキストエディタを開く。
- ファイル名を
puzzle.htmlとして保存。
完成コードをまるごと貼り付ける
下の「完成コード」をすべてコピーして、puzzle.html に貼り付け、上書き保存します。
完成コード
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 |
<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>15パズル(スマホ対応&ハイスコア)</title> <style> /* --- 1. 全体デザイン & リセット --- */ body { font-family: 'Arial', sans-serif; display: flex; flex-direction: column; align-items: center; justify-content: center; min-height: 90vh; margin: 0; background-color: #e6e6e6; padding: 10px; /* スマホで上下に余裕を持たせる */ } .container { width: 95%; max-width: 450px; /* スマホの縦幅を意識した最大幅 */ padding: 20px 15px; border-radius: 10px; background-color: #ffffff; box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1); text-align: center; } h1 { color: #333; font-size: clamp(1.5em, 5vw, 1.8em); /* フォントもレスポンシブに */ margin-bottom: 15px; } /* --- 2. 情報表示エリア --- */ .score-box { /* ハイスコア表示を追加するためのフレックスコンテナ */ display: flex; flex-direction: column; gap: 5px; } .info { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; padding: 10px 15px; border: 1px solid #ddd; border-radius: 8px; background-color: #f8f8f8; } #movesDisplay, #bestScoreDisplay { font-size: 1.1em; font-weight: bold; color: #007bff; white-space: nowrap; /* 折り返しを防ぐ */ } #message { font-size: 1.1em; font-weight: bold; color: #333; } /* --- 3. パズルボード(4x4 グリッド) --- */ #puzzleBoard { display: grid; grid-template-columns: repeat(4, 1fr); grid-template-rows: repeat(4, 1fr); gap: 5px; width: 100%; aspect-ratio: 1 / 1; background-color: #ccc; border-radius: 5px; padding: 5px; touch-action: manipulation; /* スマホでの操作遅延防止 */ } /* --- 4. パズルタイル --- */ .tile { display: flex; align-items: center; justify-content: center; background-color: #007bff; color: white; font-size: clamp(1.5em, 6vw, 2em); /* タイル内のフォントもレスポンシブに */ font-weight: bold; border-radius: 5px; cursor: pointer; box-shadow: 0 3px 6px rgba(0, 0, 0, 0.2); transition: transform 0.2s ease-out, background-color 0.4s; user-select: none; position: relative; } .tile.empty { background-color: #ccc !important; box-shadow: none; cursor: default; color: transparent; } .tile.correct { background-color: #28a745; } /* --- 5. ボタン --- */ #resetButton { padding: 10px 20px; font-size: 1.1em; cursor: pointer; background-color: #dc3545; color: white; border: none; border-radius: 25px; margin-top: 20px; } </style> </head> <body> <div class="container"> <h1>15パズル</h1> <div class="info"> <div id="message">スタート!</div> <div class="score-box"> <div id="bestScoreDisplay">ベスト: -</div> <div id="movesDisplay">手数: 0</div> </div> </div> <div id="puzzleBoard"> </div> <button id="resetButton">リセット (シャッフル)</button> </div> <script> // --- 1. 定数とDOM要素の定義 --- const BOARD_SIZE = 4; const TILE_COUNT = BOARD_SIZE * BOARD_SIZE; const puzzleBoard = document.getElementById('puzzleBoard'); const movesDisplay = document.getElementById('movesDisplay'); const messageDisplay = document.getElementById('message'); const bestScoreDisplay = document.getElementById('bestScoreDisplay'); const resetButton = document.getElementById('resetButton'); const HIGHSCORE_KEY = 'puzzleHighScore'; // ハイスコア保存用のキー // --- 2. ゲームの状態変数 --- let tiles = []; let boardState = []; let moves = 0; let isGameActive = false; // --- 3. ユーティリティ関数 --- /** * ハイスコア(最少手数)をロードし、表示する */ function loadHighScore() { const savedScore = localStorage.getItem(HIGHSCORE_KEY); // 保存されていなければ Infinity を返す return savedScore ? parseInt(savedScore, 10) : Infinity; } /** * 解が存在するかどうかを判定する (インバージョン数チェック) */ function isSolvable(state) { let inversions = 0; const tempState = state.filter(n => n !== TILE_COUNT); for (let i = 0; i < tempState.length; i++) { for (let j = i + 1; j < tempState.length; j++) { if (tempState[i] > tempState[j]) { inversions++; } } } const emptyTileIndex = state.indexOf(TILE_COUNT); const emptyTileRowFromBottom = BOARD_SIZE - Math.floor(emptyTileIndex / BOARD_SIZE); const isRowOdd = emptyTileRowFromBottom % 2 !== 0; const isInversionsOdd = inversions % 2 !== 0; // 偶数サイズパズルの解ける条件は「奇偶が異なる」こと return isRowOdd !== isInversionsOdd; } /** * 配列をシャッフルし、解けるまで繰り返す */ function generateSolvableState() { let state; do { state = Array.from({length: TILE_COUNT}, (_, i) => i + 1); // Fisher-Yatesシャッフル for (let i = state.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [state[i], state[j]] = [state[j], state[i]]; } } while (!isSolvable(state)); return state; } // --- 4. ゲームボードの構築と初期化 --- function updateHighScoreDisplay() { const currentHighScore = loadHighScore(); bestScoreDisplay.textContent = `ベスト: ${currentHighScore === Infinity ? '-' : currentHighScore}`; } function createBoard() { tiles = []; puzzleBoard.innerHTML = ''; boardState = generateSolvableState(); boardState.forEach((value, index) => { const tile = document.createElement('div'); tile.classList.add('tile'); tile.textContent = value === TILE_COUNT ? '' : value; tile.dataset.value = value; tile.dataset.correctPos = value; tile.dataset.currentPos = index + 1; if (value === TILE_COUNT) { tile.classList.add('empty'); } else { tile.addEventListener('click', handleTileClick); } puzzleBoard.appendChild(tile); tiles.push(tile); }); isGameActive = true; updateTilePositions(); updateCorrectTiles(); } // --- 5. 移動ロジック --- function updateTilePositions() { tiles.forEach((tile, index) => { const value = boardState[index]; const actualTile = tiles.find(t => parseInt(t.dataset.value) === value); const row = Math.floor(index / BOARD_SIZE) + 1; const col = (index % BOARD_SIZE) + 1; actualTile.style.gridRowStart = row; actualTile.style.gridColumnStart = col; actualTile.dataset.currentPos = index + 1; }); } function updateCorrectTiles() { tiles.forEach(tile => { const currentValue = parseInt(tile.dataset.value); const currentPos = parseInt(tile.dataset.currentPos); const correctPos = parseInt(tile.dataset.correctPos); if (currentValue !== TILE_COUNT && currentValue === currentPos) { tile.classList.add('correct'); } else { tile.classList.remove('correct'); } }); } function tryMoveTile(tileIndex) { if (!isGameActive) return false; const emptyIndex = boardState.indexOf(TILE_COUNT); const tileRow = Math.floor(tileIndex / BOARD_SIZE); const tileCol = tileIndex % BOARD_SIZE; const emptyRow = Math.floor(emptyIndex / BOARD_SIZE); const emptyCol = emptyIndex % BOARD_SIZE; const isAdjacent = Math.abs(tileRow - emptyRow) + Math.abs(tileCol - emptyCol) === 1; if (isAdjacent) { [boardState[tileIndex], boardState[emptyIndex]] = [boardState[emptyIndex], boardState[tileIndex]]; moves++; movesDisplay.textContent = `手数: ${moves}`; updateTilePositions(); updateCorrectTiles(); checkForWin(); return true; } return false; } // --- 6. イベントハンドラ --- function handleTileClick(event) { const tile = event.currentTarget; const tileValue = parseInt(tile.dataset.value); const tileIndex = boardState.indexOf(tileValue); tryMoveTile(tileIndex); } function handleKeydown(event) { const emptyIndex = boardState.indexOf(TILE_COUNT); let targetIndex = -1; switch (event.key) { case 'ArrowUp': targetIndex = emptyIndex + BOARD_SIZE; break; case 'ArrowDown': targetIndex = emptyIndex - BOARD_SIZE; break; case 'ArrowLeft': targetIndex = emptyIndex + 1; if ((emptyIndex % BOARD_SIZE) === BOARD_SIZE - 1) targetIndex = -1; break; case 'ArrowRight': targetIndex = emptyIndex - 1; if ((emptyIndex % BOARD_SIZE) === 0) targetIndex = -1; break; default: return; } if (targetIndex >= 0 && targetIndex < TILE_COUNT) { if (tryMoveTile(targetIndex)) { event.preventDefault(); } } } // --- 7. ゲーム終了と初期化 --- function checkForWin() { const isWin = boardState.every((value, index) => value === index + 1); if (isWin) { isGameActive = false; messageDisplay.textContent = `クリア!${moves}手で達成!`; // ハイスコア更新 const currentHighScore = loadHighScore(); if (moves < currentHighScore) { localStorage.setItem(HIGHSCORE_KEY, moves); updateHighScoreDisplay(); messageDisplay.textContent = `新記録!${moves}手で達成!`; } const emptyTile = tiles.find(t => t.dataset.value === String(TILE_COUNT)); emptyTile.classList.remove('empty'); emptyTile.textContent = TILE_COUNT; emptyTile.style.backgroundColor = '#28a745'; } } function resetGame() { moves = 0; movesDisplay.textContent = `手数: ${moves}`; messageDisplay.textContent = 'スタート!'; createBoard(); } // --- 8. 実行 --- window.onload = () => { updateHighScoreDisplay(); // 初期表示 createBoard(); resetButton.addEventListener('click', resetGame); document.addEventListener('keydown', handleKeydown); }; </script> </body> </html> |
ブラウザで開いて動作確認する
puzzle.htmlをダブルクリックして開く(既定のブラウザで起動)。- すぐにプレイできます。
- スマホ表示の確認は、PCのブラウザでデベロッパーツールのデバイス表示を使うと便利です。
表示がおかしい/動かないときのチェック
- 文字コードがUTF-8になっているか
- 途中で一文字でも抜けがないか(特に
{}();) - キャッシュを消して再読み込み(Windows:
Ctrl + F5/ Mac:Cmd + Shift + R)
コードブロックの役割をざっくり把握しよう
HTML(構造)
- ページの設計図です。
見出し(<h1>)、情報表示エリア(<div class="info">)、
パズル盤面(<div id="puzzleBoard">)、ボタン類など骨組みを定義します。 - DOM用のIDを付与(例:
id="puzzleBoard")して、後述のJavaScriptから要素を取得・操作できるようにしています。
【用語解説:DOM】
DOM(Document Object Model)は、HTMLを木構造(ツリー)として表現し、JavaScriptから要素を探す・書き換えるための仕組みです。document.getElementById() はその代表的な入口です。
CSS(見た目)
- 装飾とレイアウトを担当します。
背景色やタイルの色、フォント、グリッド配置、アニメーション(スライドのなめらかさ)、レスポンシブ(スマホ対応)などを定義します。
JavaScript(ロジック)
- このゲームの頭脳です。タイルのシャッフル、移動判定、手数カウント、ハイスコア保存など、ゲームのルールすべてが記述されています。
HTMLの役割と構造を理解しよう(ゲームの骨組みを作る)
HTMLは、Webページの骨組み(設計図)を作る言語です。
今回のスライドパズルでは、ゲームのタイトルやスコア表示、パズル盤面、ボタンなど、
すべての要素をHTMLで定義しています。
HTMLの主要な構造
以下が、ゲームの中心となるHTMLの構成です
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
<body> <div class="container"> <h1>15パズル</h1> <div class="info"> <div id="message">スタート!</div> <div class="score-box"> <div id="bestScoreDisplay">ベスト: -</div> <div id="movesDisplay">手数: 0</div> </div> </div> <div id="puzzleBoard"> </div> <button id="resetButton">リセット (シャッフル)</button> </div> </body> |
各ブロックの役割を見てみよう
container:全体を包む外枠
- ページ全体の要素をまとめる“箱”のような存在です。
- CSSで幅を指定し、画面中央に配置するために使用します。
- この中に、タイトル・スコア表示・パズル・ボタンを入れていきます。
info:スコアやメッセージを表示するエリア
- ゲーム中の動的な情報をまとめる領域です。
- ここには、「現在の手数」や「ベストスコア」、「メッセージ表示」などが入ります。
- JavaScriptから更新される部分なので、要素ごとにIDが付いています。
ポイント:
IDがあると、JavaScriptでdocument.getElementById()を使って直接操作できるようになります。
puzzleBoard:パズルのタイルを配置するグリッド
- ここが4×4の盤面になります。
- CSSのGridレイアウトを使って、16マスのタイルをきれいに配置します。
- JavaScriptで動的にタイルを生成し、この要素の中に並べていきます。
タイルを最初から書いてはいません。
JavaScriptで自動生成する仕組みなので、HTMLでは“受け皿”だけ用意します。
resetButton:ゲームをリセット(再シャッフル)
- 押すと盤面が新しくシャッフルされ、新しいゲームが始まります。
- JavaScriptでクリックイベントを登録し、シャッフル処理を呼び出します。
CSSで見た目と配置を整えよう(デザインとレスポンシブ対応)
HTMLで骨組みを作ったら、次は見た目を整える番です。
CSS(スタイルシート)の役割は大きく分けて2つあります。
- ページの見た目を整える(色・フォント・背景など)
- 要素のレイアウトを定義する(配置や大きさを決める)
スライドパズルでは、この2つの力をフル活用して、
きれいな4×4の盤面とスマホでも崩れないデザインを実現しています。
CSS Gridでパズル盤面を作る
今回のパズルボード(#puzzleBoard)は、CSS Gridという強力なレイアウト機能で作ります。
Gridを使うと、タイルを“きれいな格子状”に自動整列させることができます。
|
1 2 3 4 5 6 7 8 9 |
#puzzleBoard { display: grid; /* これでグリッドコンテナになります */ grid-template-columns: repeat(4, 1fr); /* 4列を均等な幅 (1fr) で作成 */ grid-template-rows: repeat(4, 1fr); /* 4行を均等な高さ (1fr) で作成 */ gap: 5px; /* タイル間の隙間 */ width: 100%; aspect-ratio: 1 / 1; /* ★重要★ 幅と高さを常に1:1に保ち、正方形を維持します */ /* ... その他の装飾 ... */ } |
コード解説
display: grid;
→ この要素を「グリッドコンテナ」に変えます。
内部の子要素(ここではタイル)が格子状に自動配置されるようになります。grid-template-columns: repeat(4, 1fr);
→ 横方向を4等分します。1frは「残りのスペースを1単位に分割」という意味。grid-template-rows: repeat(4, 1fr);
→ 縦方向も4等分。これで4×4=16マスの格子が完成します。aspect-ratio: 1 / 1;
→ 幅と高さの比率を常に1:1に保ちます。
画面サイズが変わってもパズルの形が崩れず、常に正方形を維持できる便利な指定です。
Gridとaspect-ratioを組み合わせると、CSSだけで“柔軟な正方形レイアウト”が作れます。
スマホ対応(レスポンシブデザイン)
次は、画面サイズに合わせて自動で見た目を調整する
レスポンシブデザインの考え方を見ていきましょう。
このスライドパズルでは、@mediaクエリ(条件分岐)を一切使わずに、
clamp()関数などのCSSの計算機能で柔軟に対応しています。
clamp()関数でフォントサイズを自動調整
font-size: clamp(1.5em, 6vw, 2em); のように使います。
これは「最小値 (1.5em)、画面幅に基づく推奨値 (6vw)、最大値 (2em)」の間でフォントサイズを自動調整する機能です。
これにより、小さな画面ではフォントが小さくなりすぎず、大きな画面では大きくなりすぎないように制御できます。
containerの幅を調整(max-width と width)
containerに width: 95%; と max-width: 450px; を設定することで、画面幅が狭い場合は画面の95%を使い、広すぎる場合は最大450pxまでに制限します。
この設定で、スマホでもPCでも“ちょうどいいサイズ感”が保たれます。
CSSは“見た目をきれいにする”だけでなく、
ゲームの形そのものを決める重要なパートです。
JavaScriptでゲームを動かそう(ロジックと仕組み)
HTMLが骨組み、CSSが見た目だとすると、
JavaScript(JS)はゲームの頭脳(ロジック)を担います。
このスライドパズルでは、
- 配列による盤面管理
- タイルの移動判定と入れ替え(スワップ)
- 勝利判定
など、すべてのルールと動作がJavaScriptで制御されています。
初期化と状態管理:配列で盤面を表現する
まず、ゲームの「状態」をJavaScriptに覚えさせる必要があります。
このために配列(Array)を使って盤面を管理します。
状態変数の定義(抜粋)
|
1 2 3 4 |
let tiles = []; // 画面上のHTML要素(タイル)を格納する配列 let boardState = [];// 実際のパズルの並び順(ロジックの核) let moves = 0; // 手数カウンター const TILE_COUNT = 16; // 4x4なので16マス |
boardStateの役割:
- この配列には、1から16(空白マス)までの数字が、現在のマス目順に格納されます。
- 例:
[1, 5, 2, 6, ...]は「1番目のマスに1、2番目のマスに5…」という状態を意味します。 - タイルを動かす=この配列の要素を入れ替える(スワップする)こと。
つまり、見た目の動きは、配列の変化を反映しているだけなんです。
盤面の生成:HTML要素を動的に作る
初期化時、createBoard() 関数で boardState の中身をもとに#puzzleBoard の中に16個のタイル(div要素)を自動生成します。
タイルの移動処理:隣接判定とスワップ
タイルをクリックしたとき(または矢印キーを押したとき)、JSは以下の2つの重要な判定を行います。
空白マスを見つける
タイルが移動できるのは、隣に空白マスがあるときだけです。
|
1 2 |
// boardStateの中で「16」(空白マス)がどこにあるかを探す const emptyIndex = boardState.indexOf(TILE_COUNT); |
移動可能か(隣接しているか)を判定する
クリックされたタイルの位置(tileIndex)と空白マスの位置(emptyIndex)を比較し、隣り合っているかを判断します。
|
1 2 3 4 5 6 7 |
// 行と列の差を計算 const tileRow = Math.floor(tileIndex / BOARD_SIZE); const emptyRow = Math.floor(emptyIndex / BOARD_SIZE); // ... 中略 ... // 距離が1、かつ、同じ行か同じ列であるか? const isAdjacent = Math.abs(tileRow - emptyRow) + Math.abs(tileCol - emptyCol) === 1; |
配列内の要素の入れ替え(スワップ)
移動が許可されたら、最も重要な配列の操作(スワップ)を行います。
|
1 2 3 4 5 6 7 8 9 |
if (isAdjacent) { // ES6の分割代入を使ったエレガントな入れ替え [boardState[tileIndex], boardState[emptyIndex]] = [boardState[emptyIndex], boardState[tileIndex]]; moves++; // 画面上のタイルの位置をCSSで更新する処理を呼び出す updateTilePositions(); } |
この操作により、データ(boardState)が更新され、そのデータに基づいて見た目(タイルのCSS位置)が更新される、という流れでパズルの移動が実現します。
勝利判定の仕組み
プレイヤーがタイルを動かすたびに、
JSは「完成状態(1〜16が順番通りか)」をチェックします。
|
1 2 3 4 5 |
const isWin = boardState.every((value, index) => value === index + 1); if (isWin) { // ... ゲームクリア処理 ... } |
ロジックのポイント
every():配列のすべての要素が条件を満たすか調べる関数value === index + 1:
「1番目に1、2番目に2…16番目に16」がそろっているかを確認
クリア時の処理とハイスコア保存
クリアしたら、現在の手数(moves)が
過去の最少手数より少ない場合、ハイスコアを更新します。
|
1 2 3 4 5 6 7 |
// ハイスコア更新 const currentHighScore = loadHighScore(); if (moves < currentHighScore) { localStorage.setItem(HIGHSCORE_KEY, moves); updateHighScoreDisplay(); messageDisplay.textContent = `新記録!${moves}手で達成!`; } |
「解けない配置」を防ぐ仕組み(インバージョン数チェック)
解けないスライドパズルとは?
スライドパズルには、どんなに頑張っても完成できない配置が存在します。
ランダムに並べただけでは、「絶対に解けない状態」が約半数の確率で発生するのです。
こうした“詰み状態”を防ぐのが、数学的な仕組み
「インバージョン数(反転数)チェック」です。
インバージョン数(反転数)とは?
インバージョン数とは、
「ある数字の後ろに、自分より小さい数字がいくつあるか」を数える値です。
すべてのタイルについてこれを数え、合計したものがインバージョン数です。
例でみてみよう
配列 [3, 1, 2] の場合:
| 数字 | 後ろにある小さい数 | カウント |
|---|---|---|
| 3 | 1, 2 → 2個 | +2 |
| 1 | (なし) | +0 |
| 2 | (なし) | +0 |
| 合計 | 2(偶数) |
この「2」がインバージョン数です。
これをパズル全体で計算することで、「解ける/解けない」を判断できます。
4×4パズルが「解ける」条件
4×4のように横幅が偶数のパズルでは、解ける条件が少し複雑です。
ポイントは「空白マスの位置」と「インバージョン数」の奇数・偶数(パリティ)です。
条件一覧
| 空白マスの位置(下から数えて) | インバージョン数 | 解ける? |
|---|---|---|
| 奇数行(1, 3行目など) | 偶数 | 解ける |
| 偶数行(2, 4行目など) | 奇数 | 解ける |
| それ以外の組み合わせ | 解けない |
つまり、「空白行の奇偶」と「インバージョン数の奇偶」が異なるときにだけ解ける。
JavaScriptでの実装例
このゲームの中では、次の2つの関数が「解ける配置」を保証しています。
isSolvable関数:解の存在を判定
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
function isSolvable(state) { let inversions = 0; const tempState = state.filter(n => n !== TILE_COUNT); // インバージョン数を計算 for (let i = 0; i < tempState.length; i++) { for (let j = i + 1; j < tempState.length; j++) { if (tempState[i] > tempState[j]) { inversions++; } } } const emptyTileIndex = state.indexOf(TILE_COUNT); const emptyTileRowFromBottom = BOARD_SIZE - Math.floor(emptyTileIndex / BOARD_SIZE); const isRowOdd = emptyTileRowFromBottom % 2 !== 0; const isInversionsOdd = inversions % 2 !== 0; // 偶数サイズパズルの解ける条件は「奇偶が異なる」こと return isRowOdd !== isInversionsOdd; } |
generateSolvableState関数:解けるまでシャッフル
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
function generateSolvableState() { let state; do { state = Array.from({length: TILE_COUNT}, (_, i) => i + 1); // Fisher-Yatesシャッフル for (let i = state.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [state[i], state[j]] = [state[j], state[i]]; } } while (!isSolvable(state)); return state; } |
このように、
「作る → 判定 → 解けないなら作り直す」を自動的に繰り返すことで、
プレイヤーは必ず解ける配置からスタートできます。
アレンジしてカスタマイズしよう
スライドパズルの基本構造を理解したら、
少し手を加えて自分だけのオリジナル版に進化させてみましょう。
次のような発展機能にも挑戦できます
| 機能 | 内容 | 難易度 |
|---|---|---|
| タイマー機能 | 経過時間を計測してスコアに反映 | ★★★ |
| 難易度選択 | 3×3 / 4×4 / 5×5 を選べるメニューを追加 | ★★★ |
| 画像パズル | 1枚の画像を16分割してタイル化 | ★★★★ |
いきなり全部は難しいですが、1つずつ試すことで確実にスキルが上がります。
おすすめの学習リソース|もっとスキルを伸ばしたいあなたへ
「スライドパズル」を完成させたことで、あなたはすでに HTML・CSS・JavaScriptの基礎 に触れました。
ここからさらにステップアップするために役立つ学習リソースを紹介します。
おすすめ書籍
『ゲームで学ぶJavaScript入門 増補改訂版 ~ブラウザゲームづくりでHTML&CSSも身につく!』
- ゲームを作りながら学べるので、退屈せずにJavaScriptの基礎が身につく
- HTML・CSS・JavaScriptの基本を一冊で網羅、Web制作の土台が自然に理解できる
- 13本のサンプルゲームを実際に動かせるから「作れる喜び」が味わえる
- 初心者・中高生にもやさしい解説で、初めてのゲームプログラミングに最適
『スラスラわかるHTML&CSSのきほん』(SBクリエイティブ)
- Web制作初心者に最適な入門書。HTMLとCSSの仕組みをやさしく解説。
- これからWebページを作ってみたい方にぴったり。
- レイアウトの基本やスタイルの調整方法など、実践的に学べます。
オンライン講座編
Udemy|世界最大級のオンライン学習プラットフォーム
世界中で利用されるオンライン学習サイト。
HTML、CSS、JavaScriptの入門から応用まで、高評価の講座が数百種類揃っています。
初心者でも動画を見ながら手を動かせるので、挫折しにくいのが魅力です。
まとめ|スライドパズルで学ぶ、動くWebアプリの基本
ここまで、HTML・CSS・JavaScriptを使って
スライドパズル(15パズル)をゼロから完成させてきました。
この1つのプロジェクトの中に、Web開発の大切な要素がすべて詰まっています。
ポイント
| 学習テーマ | 内容 | キーワード |
|---|---|---|
| HTML | ゲームの骨組み(構造)を作る | 要素・ID・DOM |
| CSS | 見た目と配置を整える | Grid・aspect-ratio・clamp |
| JavaScript | ゲームを動かすロジックを制御 | 配列・スワップ・イベント処理 |
| 解けない配置対策 | 数学的に「解ける状態」だけ生成 | インバージョン数 |
スライドパズルは、遊びながら学べる最高の教材です。
自分で“動くもの”を作る体験が、次の一歩につながります。
完成したスライドパズルをベースに、
新しいアイデアをどんどん形にしていきましょう!
関連記事
【実際に遊べるスライドパズルはこちら。ベストスコア更新を目指して挑戦してみよう!】▼
スライドパズルを作れたあなたは、
他のゲームも作れる実力があります。
次のような作品にも挑戦してみましょう。
【作り方記事】神経衰弱ゲームの作り方|記憶力を鍛えるカードゲームを作ろう▼
【作り方記事】タイピング練習ゲームの作り方|60秒でスコアを競うゲームを作ろう▼




