
レッサーパンダ配置パズルを作ってみた

最近はやっている パズルを作ってみた
俺でも作れるようになりました
作り方コードを解説してみます
はじめに
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>レッサーパンダ配置パズル</title>
<!-- Google Fonts: 丸みのある読みやすい日本語フォント「Zen Maru Gothic」を読み込み -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Zen+Maru+Gothic:wght@500;700;900&display=swap" rel="stylesheet">
<style>
:root {
/* --- [カラーパレット設定] --- */
/* 全体背景色(温かみのある木目トイ調) */
--bg-color: #f7f2ea ;
/* 盤面外枠と背景のカラー */
--board-bg: #e6cca9 ;
--board-border: #5d4037 ;
/* パネル・カード背景色 */
--panel-bg: #ffffff ;
/* 基本テキストカラー */
--text-color: #3e2723 ;
/* メインアクセントカラー(オレンジ・ボタン用) */
--accent-color: #ff5722 ;
--accent-hover: #e64a19 ;
--accent-shadow: #bf360c ;
/* 識別しやすい鮮やかなポップ8色(エリア背景用) */
--region-0: #ff8a80 ; /* ポップレッド */
--region-1: #ffb74d ; /* オレンジ */
--region-2: #fff176 ; /* イエロー */
--region-3: #81c784 ; /* グリーン */
--region-4: #4dd0e1 ; /* シアン */
--region-5: #64b5f6 ; /* ブルー */
--region-6: #ba68c8 ; /* パープル */
--region-7: #f06292 ; /* ピンク */
}
/* 全要素のリセット設定:サイズ計算をわかりやすくし、範囲外テキスト選択を防止 */
* {
box-sizing: border-box;
user-select: none;
-webkit-user-select: none; }
/* 画面全体のレイアウト:ドットパターンの背景と中央寄せ配置 */
body {
font-family: 'Zen Maru Gothic', sans-serif;
background-color: var(--bg-color);
background-image: radial-gradient(#d7ccc8 1px, transparent 1px);
background-size: 20px 20px;
color: var(--text-color);
margin: 0;
padding: 15px;
display: flex;
flex-direction: column;
align-items: center;
min-height: 100vh;
}
/* --- [ヘッダー&タイトル表示区画] --- */
header {
text-align: center;
margin-bottom: 12px;
}
h1 {
margin: 0;
font-size: 2rem;
font-weight: 900;
color: #d84315 ;
text-shadow: 2px 2px 0 #fff , -2px -2px 0 #fff , 2px -2px 0 #fff , -2px 2px 0 #fff , 0 4px 6px rgba(0,0,0,0.15);
letter-spacing: 1px;
}
.subtitle {
font-size: 0.85rem;
font-weight: 700;
color: #8d6e63 ;
margin-top: 3px;
}
/* --- [メインゲーム枠&ステータスバー] --- */
.game-container {
background: var(--panel-bg);
padding: 18px;
border-radius: 24px;
box-shadow: 0 12px 32px rgba(93, 64, 55, 0.18), inset 0 2px 4px rgba(255,255,255,0.8);
max-width: 480px;
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
border: 4px solid #bcaaa4 ;
}
/* ステータスパネル (LCDデジタルタイマー&スコア表示) */
.status-panel {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
margin-bottom: 15px;
background: #3e2723 ;
padding: 10px 16px;
border-radius: 16px;
color: #fff ;
box-shadow: inset 0 3px 6px rgba(0,0,0,0.4);
border: 2px solid #8d6e63 ;
}
.status-item {
display: flex;
flex-direction: column;
align-items: center;
}
.status-label {
font-size: 0.7rem;
color: #bcaaa4 ;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.5px; }
.status-value {
font-size: 1.3rem;
font-weight: 900;
font-family: monospace, sans-serif;
color: #ffe082 ;
text-shadow: 0 0 4px rgba(255, 224, 130, 0.5);
}
/* --- [パズル盤面レイアウト] --- */
.board-frame {
background: var(--board-bg);
padding: 10px;
border-radius: 16px;
border: 4px solid var(--board-border);
box-shadow: inset 0 3px 8px rgba(0,0,0,0.25), 0 6px 12px rgba(0,0,0,0.1);
margin-bottom: 15px;
width: 100%;
box-sizing: border-box;
display: flex;
justify-content: center;
}
/* 正方形を維持し、CSS Gridで動的分割する盤面本体 */
.board {
display: grid;
border: 3px solid var(--board-border);
border-radius: 6px;
overflow: hidden;
background-color: #fff ;
width: 100%;
max-width: 420px;
aspect-ratio: 1 / 1; /* 正方形比率を維持 */
}
/* 各セル(マス目)の標準設定 */
.cell {
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
box-sizing: border-box;
border: 1px dashed rgba(93, 64, 55, 0.25);
transition: filter 0.1s, transform 0.08s;
position: relative;
}
.cell:hover {
filter: brightness(0.93);
}
.cell:active {
transform: scale(0.95);
}
/* 色領域(8色を循環して設定) */
.r-0 { background-color: var(--region-0); }
.r-1 { background-color: var(--region-1); }
.r-2 { background-color: var(--region-2); }
.r-3 { background-color: var(--region-3); }
.r-4 { background-color: var(--region-4); }
.r-5 { background-color: var(--region-5); }
.r-6 { background-color: var(--region-6); }
.r-7 { background-color: var(--region-7); }
/* エリア同士の境界線を強調する太枠CSSクラス */
.b-top { border-top: 3px solid var(--board-border) !important; }
.b-bottom { border-bottom: 3px solid var(--board-border) !important; }
.b-left { border-left: 3px solid var(--board-border) !important; }
.b-right { border-right: 3px solid var(--board-border) !important; }
/* --- [画像埋め込み&エフェクトアニメーション] --- */
/* 外部画像を使わず、CSS内に埋め込んだベクターSVG(確実に表示されます) */
.cell.panda::after {
content: '';
display: block;
width: 88%;
height: 88%;
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><ellipse cx="22" cy="22" rx="14" ry="18" fill="%23efebe9" stroke="%234e342e" stroke-width="3"/><ellipse cx="78" cy="22" rx="14" ry="18" fill="%23efebe9" stroke="%234e342e" stroke-width="3"/><ellipse cx="22" cy="22" rx="8" ry="12" fill="%23d84315"/><ellipse cx="78" cy="22" rx="8" ry="12" fill="%23d84315"/><ellipse cx="50" cy="55" rx="42" ry="38" fill="%23d84315" stroke="%234e342e" stroke-width="3.5"/><ellipse cx="50" cy="66" rx="22" ry="17" fill="%23fffde7"/><ellipse cx="33" cy="38" rx="7" ry="5" fill="%23fffde7"/><ellipse cx="67" cy="38" rx="7" ry="5" fill="%23fffde7"/><path d="M 20,50 Q 28,55 33,68 Q 23,63 16,56 Z" fill="%23fffde7"/><path d="M 80,50 Q 72,55 67,68 Q 77,63 84,56 Z" fill="%23fffde7"/><ellipse cx="50" cy="59" rx="6" ry="4" fill="%233e2723"/><path d="M 35,46 Q 42,40 48,46 M 52,46 Q 58,40 65,46" fill="none" stroke="%233e2723" stroke-width="3.5" stroke-linecap="round"/><path d="M 38,65 Q 50,78 62,65" fill="%23ff4081" stroke="%233e2723" stroke-width="2.5"/></svg>');
background-size: contain;
background-repeat: no-repeat;
background-position: center;
filter: drop-shadow(0 3px 2px rgba(0,0,0,0.3));
animation: popIn 0.22s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
/* ×マークの表示デザイン */
.cell.cross::after {
content: '×';
color: #4e342e ;
font-weight: 900;
font-size: clamp(1rem, 3.5vw, 2rem);
line-height: 1;
text-shadow: 1px 1px 0 rgba(255,255,255,0.6);
animation: popIn 0.15s ease-out;
}
/* パズルルール違反時の警告アニメーション */
.cell.error {
background-color: #ff8a80 !important;
animation: shake 0.3s ease-in-out;
}
/* 登場ポップインアニメーション */
@keyframes popIn {
0% { transform: scale(0); }
100% { transform: scale(1); }
}
/* エラー揺れアニメーション */
@keyframes shake {
0%, 100% { transform: translateX(0); }
20%, 60% { transform: translateX(-4px); }
40%, 80% { transform: translateX(4px); }
}
/* --- [操作ボタン&ドロップダウン設定] --- */
.btn-group {
display: flex;
gap: 8px;
width: 100%;
justify-content: center;
margin-bottom: 10px;
flex-wrap: wrap;
}
button {
font-family: inherit;
font-weight: 900;
padding: 10px 16px;
border: none;
border-radius: 12px;
background-color: var(--accent-color);
color: white;
cursor: pointer;
transition: all 0.1s ease;
font-size: 0.95rem;
box-shadow: 0 4px 0 var(--accent-shadow);
display: flex;
align-items: center;
gap: 4px;
}
button:hover {
background-color: var(--accent-hover);
}
button:active {
transform: translateY(3px);
box-shadow: 0 1px 0 var(--accent-shadow);
}
button.secondary {
background-color: #795548 ;
box-shadow: 0 4px 0 #4e342e ;
}
button.secondary:hover {
background-color: #6d4c41 ;
}
button.secondary:active {
box-shadow: 0 1px 0 #4e342e ;
}
select {
font-family: inherit;
font-weight: 700;
padding: 10px 14px;
border-radius: 12px;
border: 3px solid #8d6e63 ;
background-color: white;
color: var(--text-color);
outline: none;
font-size: 0.95rem;
cursor: pointer;
}
/* ステージクリア表示用ダイアログ(モーダル) */
.modal-overlay {
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background: rgba(62, 39, 35, 0.7);
backdrop-filter: blur(4px);
display: flex;
justify-content: center;
align-items: center;
opacity: 0;
pointer-events: none;
transition: opacity 0.3s ease;
z-index: 100;
}
.modal-overlay.active {
opacity: 1;
pointer-events: auto;
}
.modal {
background: #fff ;
padding: 28px;
border-radius: 24px;
text-align: center;
max-width: 340px;
width: 90%;
box-shadow: 0 20px 40px rgba(0,0,0,0.3);
border: 5px solid var(--accent-color);
transform: translateY(20px);
transition: transform 0.3s ease;
}
.modal-overlay.active .modal {
transform: translateY(0);
}
.modal h2 {
color: var(--accent-color);
margin: 0 0 10px 0;
font-size: 1.8rem;
}
.stars {
font-size: 2rem;
color: #ffc107 ;
margin: 8px 0;
}
/* スマートフォン向けUI微調整 */
@media (max-width: 440px) {
h1 { font-size: 1.6rem; }
.status-value { font-size: 1.1rem; }
button { padding: 8px 12px; font-size: 0.85rem; }
</style>
</head>
<body>
<!-- ===== [1. 画面タイトル] ===== -->
<header>
<h1>レッサーパンダ配置パズル</h1>
<div class="subtitle">各色・行・列に1匹ずつ!斜め隣もNGだよ!</div>
</header>
<!-- ===== [2. メインゲームエリア] ===== -->
<div class="game-container">
<!-- デジタルタイマー・残り匹数・ベストタイム表示パネル -->
<div class="status-panel">
<div class="status-item">
<span class="status-label">TIME</span>
<span id="timer" class="status-value">00:00</span>
</div>
<div class="status-item">
<span class="status-label">REST</span>
<span id="remaining-count" class="status-value">0</span>
</div>
<div class="status-item">
<span class="status-label">BEST</span>
<span id="best-time" class="status-value">--:--</span>
</div>
</div>
<!-- パズル盤面枠(JavaScriptで可変サイズ描画) -->
<div class="board-frame">
<div id="board" class="board"></div>
</div>
<!-- プレイ操作ボタン群 -->
<div class="btn-group">
<button id="btn-undo" class="secondary">↩ 戻る</button>
<button id="btn-redo" class="secondary">進む ↪</button>
<button id="btn-reset" class="secondary">🔄 リセット</button>
<button id="btn-hint">💡 ヒント</button>
</div>
<!-- 難易度選択(偶数サイズ 4x4, 6x6, 8x8, 10x10, 12x12) & 新ゲームボタン -->
<div class="btn-group">
<select id="difficulty-select">
<option value="4">練習 (4x4)</option>
<option value="6" selected>普通 (6x6)</option>
<option value="8">挑戦 (8x8)</option>
<option value="10">上級 (10x10)</option>
<option value="12">達人 (12x12)</option>
</select>
<button id="btn-new-game">🎮 新しい問題</button>
</div>
</div>
<!-- ===== [3. ステージクリアモーダルダイアログ] ===== -->
<div id="clear-modal" class="modal-overlay">
<div class="modal">
<h2>🎉 STAGE CLEAR!</h2>
<div id="stars-display" class="stars">⭐⭐⭐</div>
<p style="font-size: 1.1rem; font-weight: bold; margin: 10px 0;">
タイム: <span id="clear-time" style="color: var(--accent-color);">00:00</span>
</p> <button id="btn-modal-next" style="width: 100%; justify-content: center; font-size: 1.1rem; padding: 12px; margin-top: 15px;">
次のステージへ ➔
</button>
</div>
</div>
<script>
/**
* 【SoundEngine】音声合成Web Audio APIクラス
* 音声ファイルを読み込む必要がなく、リアルタイムで効果音を生成・発声します。
*/
class SoundEngine {
constructor() {
this.ctx = null;
}
// ユーザーの初回クリック時にオーディオコンテキストを初期化(ブラウザ制限対策)
init() {
if (!this.ctx) {
this.ctx = new (window.AudioContext || window.webkitAudioContext)();
}
if (this.ctx.state === 'suspended') {
this.ctx.resume();
}
}
// 周波数(Hz), 音形(sine/triangle), 再生時間, 音量を指定して発振
playTone(freq, type, duration, vol = 0.15) {
try {
this.init();
const osc = this.ctx.createOscillator();
const gain = this.ctx.createGain();
osc.type = type;
osc.frequency.setValueAtTime(freq, this.ctx.currentTime);
gain.gain.setValueAtTime(vol, this.ctx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.0001, this.ctx.currentTime + duration);
osc.connect(gain);
gain.connect(this.ctx.destination);
osc.start();
osc.stop(this.ctx.currentTime + duration);
} catch (e) {
// オーディオの制限で再生されない場合のフォールバック
}
}
/* 各種操作音のトーン設定 */
playPanda() { this.playTone(587.33, 'sine', 0.15, 0.25); } // パンダ配置音 (D5)
playCross() { this.playTone(329.63, 'triangle', 0.08, 0.15); } // ×印配置音 (E4)
playClearCell() { this.playTone(293.66, 'sine', 0.08, 0.1); } // 消去音 (D4)
// クリアファンファーレ音
playStageClear() {
const sequence = [
{ f: 523.25, d: 0.12 }, { f: 659.25, d: 0.12 },
{ f: 783.99, d: 0.12 }, { f: 1046.50, d: 0.4 }
];
sequence.forEach((note, idx) => {
setTimeout(() => this.playTone(note.f, 'sine', note.d, 0.25), idx * 110);
}); }
}
// 効果音エンジンのインスタンス生成
const sound = new SoundEngine();
/**
* 【PandaPuzzleGame】パズルの核心ロジッククラス
*/
class PandaPuzzleGame {
constructor() {
this.size = 6; // 現在の盤面サイズ(初期: 6x6)
this.boardState = []; // 各マスの状態(0:空, 1:パンダ, 2:×)
this.regions = []; // 各マスの色領域ID
this.solution = []; // 正解の配置座標リスト
this.history = []; // Undo(戻る)履歴
this.redoStack = []; // Redo(進む)履歴
this.timer = 0; // 経過時間(秒)
this.timerInterval = null; // タイマーのsetInterval参照
this.isCompleted = false; // クリア判定フラグ
this.initDOM();
this.bindEvents();
this.newGame();
}
/* DOM要素の参照を取得 */
initDOM() {
this.boardEl = document.getElementById('board');
this.timerEl = document.getElementById('timer');
this.remainingEl = document.getElementById('remaining-count');
this.bestTimeEl = document.getElementById('best-time');
this.modalEl = document.getElementById('clear-modal');
this.clearTimeEl = document.getElementById('clear-time');
this.starsDisplayEl = document.getElementById('stars-display');
this.diffSelect = document.getElementById('difficulty-select'); }
/* ボタンやクリックイベントの紐付け */
bindEvents() {
document.getElementById('btn-undo').addEventListener('click', () => this.undo());
document.getElementById('btn-redo').addEventListener('click', () => this.redo());
document.getElementById('btn-reset').addEventListener('click', () => this.resetBoard());
document.getElementById('btn-hint').addEventListener('click', () => this.giveHint());
document.getElementById('btn-new-game').addEventListener('click', () => {
this.size = parseInt(this.diffSelect.value, 10);
this.newGame();
});
document.getElementById('btn-modal-next').addEventListener('click', () => {
this.modalEl.classList.remove('active');
this.newGame();
});
// 初回タッチ/クリックでサウンドをアクティブ化
window.addEventListener('click', () => sound.init(), { once: true });
window.addEventListener('touchstart', () => sound.init(), { once: true }); }
/* 新しいゲームを開始 */
newGame() {
this.isCompleted = false;
this.boardState = Array(this.size * this.size).fill(0);
this.history = [];
this.redoStack = [];
this.generatePuzzle(); // アルゴリズムで解とマップを生成 this.renderBoard(); // DOM描画
this.updateStatus(); // 表示クリア&ベストタイム読み込み this.startTimer(); // リアルタイムタイマー開始
)
/**
* 【問題自動生成エンジン】
* バックトラック法(深さ優先探索)により、偶数サイズ(最大12x12まで)でも一瞬で
* 「各行・列・斜め隣接NG」の解を生成し、シード成長法で色領域を割り当てます。
*/
generatePuzzle() {
const N = this.size;
let solutionCols = null;
// 解が見つかるまで探査
while (!solutionCols) {
solutionCols = this.solveNQueensVariant(N);
}
// 解の座標 [row, col] リストを保存
this.solution = solutionCols.map((c, r) => [r, c]);
// シード拡張法(領域マルチシード成長)にる色エリアの生成
this.regions = Array(N * N).fill(-1);
const queue = [];
// 解の位置を各色領域の「タネ(シード)」として割り当てる
this.solution.forEach(([r, c], regionId) => {
const idx = r * N + c;
this.regions[idx] = regionId;
queue.push({r, c, regionId});
});
// シードから周囲へランダムに領域を広げて盤面全体を埋める
const dirs = [[-1,0],[1,0],[0,-1],[0,1]];
while (queue.length > 0) {
const idx = Math.floor(Math.random() * queue.length);
const {r, c, regionId} = queue.splice(idx, 1)[0];
for (const [dr, dc] of dirs) {
const nr = r + dr, nc = c + dc;
if (nr >= 0 && nr < N && nc >= 0 && nc < N) {
const nIdx = nr * N + nc;
if (this.regions[nIdx] === -1) {
this.regions[nIdx] = regionId;
queue.push({r: nr, c: nc, regionId});
}
}
}
}
}
/* エイト・クイーン変形解法(バックトラック法) */
solveNQueensVariant(N) {
const cols = [];
const usedCols = new Set();
const backtrack = (row) => {
if (row === N) return true; // 全行配置完了
// 列候補をシャッフル(毎回ランダムな盤面を生成するため)
const candidates = Array.from({length: N}, (_, i) => i);
candidates.sort(() => Math.random() - 0.5)
for (const c of candidates) {
if (usedCols.has(c)) continue; // 同じ列はNG
if (row > 0 && Math.abs(cols[row - 1] - c) <= 1) continue; // 斜め隣接NG
cols.push(c);
usedCols.add(c);
if (backtrack(row + 1)) return true;
// 元に戻す(バックトラック)
cols.pop();
usedCols.delete(c);
}
return false;
};
return backtrack(0) ? cols : null;
}
/**
* 盤面のHTMLエレメント作成と境界線の計算
*/
renderBoard() {
const N = this.size;
this.boardEl.style.gridTemplateColumns = `repeat(${N}, 1fr)`;
this.boardEl.innerHTML = '';
for (let r = 0; r < N; r++) {
for (let c = 0; c < N; c++) {
const idx = r * N + c;
const cell = document.createElement('div');
const regionId = this.regions[idx];
// 8色を循環割り当て (.r-0 ~ .r-7)
cell.className = `cell r-${regionId % 8}`;
// 異なる色領域の境界に太枠を追加する計算
if (r === 0 || this.regions[(r-1)*N + c] !== regionId) cell.classList.add('b-top');
if (r === N-1 || this.regions[(r+1)*N + c] !== regionId) cell.classList.add('b-bottom');
if (c === 0 || this.regions[r*N + (c-1)] !== regionId) cell.classList.add('b-left');
if (c === N-1 || this.regions[r*N + (c+1)] !== regionId) cell.classList.add('b-right');
// 左クリック(タップ):空 → パンダ → × → 空
cell.addEventListener('click', () => this.handleCellClick(r, c))
// 右クリック(長押し):空 → × → 空
cell.addEventListener('contextmenu', (e) => {
e.preventDefault();
this.handleCellRightClick(r, c);
});
this.boardEl.appendChild(cell);
} }
this.updateBoardUI();
}
/* マス目クリック操作ハンドラ */
handleCellClick(r, c) {
if (this.isCompleted) return;
const idx = r * this.size + c;
const current = this.boardState[idx];
const next = (current + 1) % 3; // 0:空 -> 1:パンダ -> 2:× -> 0
this.pushHistory(idx, current, next);
this.boardState[idx] = next;
// 効果音再生
if (next === 1) sound.playPanda();
else if (next === 2) sound.playCross();
else sound.playClearCell();
this.updateBoardUI();
this.checkCompletion();
}
/* マス目右クリック操作ハンドラ */
handleCellRightClick(r, c) {
if (this.isCompleted) return;
const idx = r * this.size + c;
const current = this.boardState[idx];
const next = current === 2 ? 0 : 2;
this.pushHistory(idx, current, next);
this.boardState[idx] = next;
if (next === 2) sound.playCross();
else sound.playClearCell();
this.updateBoardUI();
}
/* 履歴スタックへの記録 */
pushHistory(idx, prev, next) {
this.history.push({ idx, prev, next });
this.redoStack = []; // 新規操作があったらRedoスタックをクリア
}
/* 元に戻す(Undo) */
undo() {
if (this.history.length === 0 || this.isCompleted) return;
const action = this.history.pop();
this.boardState[action.idx] = action.prev;
this.redoStack.push(action);
sound.playClearCell();
this.updateBoardUI();
}
/* やり直す(Redo) */
redo() {
if (this.redoStack.length === 0 || this.isCompleted) return;
const action = this.redoStack.pop();
this.boardState[action.idx] = action.next;
this.history.push(action);
sound.playClearCell();
this.updateBoardUI();
}
/* リセットボタン */
resetBoard() {
if (this.isCompleted) return;
this.boardState.fill(0);
this.history = [];
this.redoStack = [];
sound.playClearCell();
this.updateBoardUI(); }
/* ヒント機能(正しい位置にパンダを1匹配置) */
giveHint() {
if (this.isCompleted) return;
for (const [r, c] of this.solution) {
const idx = r * this.size + c;
if (this.boardState[idx] !== 1) {
this.pushHistory(idx, this.boardState[idx], 1);
this.boardState[idx] = 1;
sound.playPanda();
this.updateBoardUI();
this.checkCompletion();
break; }
}
/**
* UI画面上のクラス(パンダ/×/エラー赤い枠)をリアルタイム更新
*/
updateBoardUI() {
const cells = this.boardEl.querySelectorAll('.cell');
let pandaCount = 0;
cells.forEach((cell, idx) => {
cell.classList.remove('panda', 'cross', 'error');
const val = this.boardState[idx];
if (val === 1) {
cell.classList.add('panda');
pandaCount++;
} else if (val === 2) {
cell.classList.add('cross');
}
});
// ルール違反の判定と赤色エラー表示処理
this.highlightErrors(cells);
this.remainingEl.textContent = Math.max(0, this.size - pandaCount); }
/* ルールチェック(同じ行・列・色エリア・斜め隣接にパンダが複数ないか) */
highlightErrors(cells) {
const N = this.size;
const pandas = [];
// 配置されているパンダの位置を全取得
for (let i = 0; i < this.boardState.length; i++) {
if (this.boardState[i] === 1) {
pandas.push({ r: Math.floor(i / N), c: i % N, idx: i, region: this.regions[i] }); } }
// 全パンダのペアについて衝突を検知
for (let i = 0; i < pandas.length; i++) {
for (let j = i + 1; j < pandas.length; j++) {
const p1 = pandas[i], p2 = pandas[j];
const sameRow = p1.r === p2.r;
const sameCol = p1.c === p2.c;
const sameRegion = p1.region === p2.region;
const isAdjacent = Math.abs(p1.r - p2.r) <= 1 && Math.abs(p1.c - p2.c) <= 1;
if (sameRow || sameCol || sameRegion || isAdjacent) {
cells[p1.idx].classList.add('error');
cells[p2.idx].classList.add('error'); }
} } }
/* ゲームクリア判定 */
checkCompletion() {
const N = this.size;
const pandaIndices = [];
this.boardState.forEach((val, idx) => {
if (val === 1) pandaIndices.push(idx);
});
// パンダの数が各サイズと同一でない場合は不完全
if (pandaIndices.length !== N) return;
// エラー(赤枠)が1つも無ければクリア
const errorCells = this.boardEl.querySelectorAll('.cell.error');
if (errorCells.length === 0) {
this.onComplete();
} }
/* クリア達成時の演出・スコア計算 */
onComplete() {
this.isCompleted = true;
this.stopTimer();
sound.playStageClear();
const formattedTime = this.formatTime(this.timer);
this.clearTimeEl.textContent = formattedTime;
this.saveBestTime(this.timer);
// クリアタイムに応じた星評価(サイズ別動的しきい値)
let stars = "⭐⭐⭐";
const thresholdMedium = this.size * 15;
const thresholdHard = this.size * 30;
if (this.timer > thresholdHard) stars = "⭐☆☆";
else if (this.timer > thresholdMedium) stars = "⭐⭐☆";
this.starsDisplayEl.textContent = stars;
setTimeout(() => {
this.modalEl.classList.add('active');
}, 300);
}
/* 毎秒1刻みで正確に動くタイマー管理 */
startTimer() {
this.stopTimer();
this.timer = 0;
this.timerEl.textContent = '00:00';
this.timerInterval = setInterval(() => {
this.timer++;
this.timerEl.textContent = this.formatTime(this.timer);
}, 1000);
}
stopTimer() {
if (this.timerInterval) {
clearInterval(this.timerInterval);
this.timerInterval = null;
} }
/* 秒数を 00:00 形式にフォーマット */
formatTime(sec)
const m = Math.floor(sec / 60).toString().padStart(2, '0');
const s = (sec % 60).toString().padStart(2, '0');
return `${m}:${s}`;
}
/* ローカルストレージからのハイスコア読み込み */
loadBestTime() {
const key = `panda_puzzle_best_${this.size}`;
const best = localStorage.getItem(key);
if (best) {
this.bestTimeEl.textContent = this.formatTime(parseInt(best, 10));
このコードで動きます
これは誰でもAIで作れます