メインコンテンツへスキップ
見出し画像

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


    画像

    最近はやっている パズルを作ってみた
    俺でも作れるようになりました 
    作り方コードを解説してみます

    はじめに


    <!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で作れます

    #:ゲーム #AI #fパズル


     
     

    MK

     
     
    フリーでいろいろチャレンジしています これからはAIを使いこなしたいと思います 使いこなせない方も一緒にステップアップしていきましょう

    あなたへのおすすめ