[HTML] 纯文本查看 复制代码
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>🐱 圈小猫 · 单方案快速求解 (修复作弊显示)</title>
<style>
* {
box-sizing: border-box;
font-family: system-ui, 'Segoe UI', Roboto, 'Helvetica Neue', sans-serif;
}
body {
background: #f5f7fb;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
margin: 20px;
}
.game-container {
max-width: 1100px;
width: 100%;
background: white;
border-radius: 28px;
box-shadow: 0 20px 40px rgba(0,0,0,0.08);
padding: 24px;
}
.main-panel {
display: flex;
gap: 24px;
flex-wrap: wrap;
}
.play-area {
flex: 2;
min-width: 500px;
}
.solution-panel {
flex: 1.2;
min-width: 280px;
background: #fafbfc;
border-radius: 20px;
padding: 18px 16px;
box-shadow: inset 0 1px 3px rgba(0,0,0,0.02), 0 4px 8px rgba(0,0,0,0.02);
}
.canvas-wrapper {
background: #ffffff;
border-radius: 24px;
padding: 16px;
box-shadow: 0 6px 14px rgba(0,0,0,0.03);
border: 1px solid #e9ecf0;
}
canvas {
display: block;
margin: 0 auto;
border-radius: 16px;
background: #fefefe;
cursor: pointer;
touch-action: none;
width: 100%;
height: auto;
border: 1px solid #dee2e6;
}
.button-bar {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin: 16px 0 8px;
}
.btn {
background: white;
border: 1px solid #d0d7de;
border-radius: 40px;
padding: 8px 16px;
font-size: 14px;
font-weight: 500;
color: #1f2a3a;
cursor: pointer;
box-shadow: 0 2px 4px rgba(0,0,0,0.02);
transition: all 0.15s;
flex: 1 0 auto;
}
.btn-primary {
background: #2c3e50;
border-color: #1e2b37;
color: white;
}
.btn-warning {
background: #e67e22;
border-color: #d35400;
color: white;
}
.btn:disabled {
opacity: 0.5;
cursor: not-allowed;
pointer-events: none;
}
.btn:hover:not(:disabled) {
background: #f1f5f9;
border-color: #9aa6b2;
}
.btn-primary:hover:not(:disabled) {
background: #1e2b37;
}
.progress-row {
display: flex;
align-items: center;
gap: 12px;
margin: 10px 0;
}
.progress-bar-bg {
flex: 1;
height: 8px;
background: #e2e8f0;
border-radius: 20px;
overflow: hidden;
}
.progress-bar-fill {
height: 100%;
width: 0%;
background: #3498db;
border-radius: 20px;
transition: width 0.2s;
}
.info-text {
font-size: 13px;
color: #475569;
margin: 8px 0;
}
.solution-header {
display: flex;
justify-content: space-between;
align-items: baseline;
margin-bottom: 12px;
}
.tree-container {
background: white;
border-radius: 16px;
padding: 6px;
max-height: 280px;
overflow-y: auto;
border: 1px solid #e2e8f0;
}
.solution-item {
padding: 8px 12px;
border-radius: 12px;
margin: 4px 0;
background: #ffffff;
border: 1px solid #edf2f7;
cursor: pointer;
display: flex;
justify-content: space-between;
transition: all 0.1s;
}
.solution-item.active {
background: #dbeafe;
border-color: #3b82f6;
}
.solution-stats {
font-size: 13px;
color: #64748b;
}
.detail-box {
background: white;
border-radius: 16px;
padding: 14px;
margin-top: 16px;
border: 1px solid #e2e8f0;
font-size: 13px;
max-height: 180px;
overflow-y: auto;
white-space: pre-wrap;
font-family: 'SF Mono', monospace;
}
.cheat-row {
display: flex;
align-items: center;
gap: 8px;
margin-top: 10px;
}
.status-badge {
background: #ecfdf3;
color: #067647;
padding: 4px 12px;
border-radius: 30px;
font-size: 13px;
}
.flex-row {
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
}
select, .sort-select {
padding: 6px 12px;
border-radius: 30px;
border: 1px solid #d0d7de;
background: white;
}
.footer-note {
margin-top: 16px;
color: #64748b;
font-size: 13px;
text-align: center;
}
</style>
</head>
<body>
<div class="game-container">
<div class="main-panel">
<!-- 左侧游戏区 -->
<div class="play-area">
<div class="canvas-wrapper">
<canvas id="gameCanvas" width="580" height="540"></canvas>
</div>
<div class="button-bar">
<button class="btn" id="resetBtn">🔄 重置</button>
<button class="btn btn-primary" id="computeBtn">🧠 计算方案 (单解)</button>
<button class="btn" id="stopBtn" disabled>⏹️ 停止</button>
<button class="btn" id="randomWallsBtn">🎲 生成墙壁</button>
<button class="btn btn-warning" id="startGameBtn">🎮 开始游戏</button>
</div>
<div class="progress-row">
<span id="progressLabel" style="min-width: 120px;">就绪</span>
<div class="progress-bar-bg">
<div id="progressFill" class="progress-bar-fill" style="width:0%"></div>
</div>
</div>
<div class="flex-row">
<span class="info-text" id="infoMessage">✨ 点击圆圈设置墙壁 (最多8个)</span>
<div class="cheat-row">
<input type="checkbox" id="cheatCheckbox">
<label for="cheatCheckbox" style="font-size:13px;">👁️ 显示方案步骤</label>
</div>
</div>
</div>
<!-- 右侧方案面板 -->
<div class="solution-panel">
<div class="solution-header">
<strong>📋 解决方案</strong>
<span id="solutionCount" class="status-badge">0 个</span>
</div>
<div class="flex-row" style="margin-bottom: 10px;">
<span style="font-size:13px;">排序:</span>
<select id="sortSelect" class="sort-select" disabled>
<option value="steps" selected>步数 ↑</option>
</select>
</div>
<div class="tree-container" id="solutionListContainer">
<div style="padding:12px; text-align:center; color:#94a3b8;">暂无方案,点击“计算方案”</div>
</div>
<div class="detail-box" id="detailText">
选择一个方案查看详情…
</div>
<div class="footer-note">
⚡ Alpha-Beta + 置换表 · 单解快速模式
</div>
</div>
</div>
</div>
<script>
(function(){
"use strict";
// ---------- 配置参数 ----------
const WIDTH = 11;
const HEIGHT = 11;
const CELL_RADIUS = 18;
const CELL_SPACING = 4;
const DX = 2 * CELL_RADIUS + CELL_SPACING;
const DY = (2 * CELL_RADIUS + CELL_SPACING) * 0.866;
const CAT_START = [Math.floor(WIDTH/2), Math.floor(HEIGHT/2)];
// 求解限制
const MAX_DEPTH = 20; // 最大搜索深度
const MAX_CANDIDATES = 8; // 每层候选墙壁数
const MAX_TOTAL_TIME = 45; // 秒
// 置换表常量
const EXACT = 0;
const LOWERBOUND = 1;
const UPPERBOUND = 2;
// ---------- 全局状态 ----------
let canvas = document.getElementById('gameCanvas');
let ctx = canvas.getContext('2d');
// 墙壁与模式
let initialWalls = new Set(); // 字符串 "i,j"
let wallClickOrder = [];
let gameMode = false;
let gameWalls = new Set();
let gameCatPos = null;
let gameStep = 0;
// 方案相关 (只保留一个方案)
let currentSolution = null; // 当前方案数组
let currentSolutionWalls = new Set();
let showSolutionSteps = true; // 默认显示步骤序号
let solutionOnBoard = [];
// 计算控制
let computing = false;
let stopRequested = false;
let computeStartTime = 0;
// AI 缓存与表
const moveCache = new Map();
const evalCache = new Map();
const escapePathCache = new Map();
const neighborsMap = new Map();
// Alpha-Beta 专用表
const transpositionTable = new Map();
const historyTable = new Map();
// DOM 元素
const progressLabel = document.getElementById('progressLabel');
const progressFill = document.getElementById('progressFill');
const infoMessage = document.getElementById('infoMessage');
const solutionCountSpan = document.getElementById('solutionCount');
const solutionListDiv = document.getElementById('solutionListContainer');
const detailText = document.getElementById('detailText');
const resetBtn = document.getElementById('resetBtn');
const computeBtn = document.getElementById('computeBtn');
const stopBtn = document.getElementById('stopBtn');
const randomWallsBtn = document.getElementById('randomWallsBtn');
const startGameBtn = document.getElementById('startGameBtn');
const cheatCheck = document.getElementById('cheatCheckbox');
const sortSelect = document.getElementById('sortSelect');
// 初始化邻居
function computeNeighbors(i, j) {
let neighbors;
if (j % 2 === 0) {
neighbors = [[i-1, j], [i-1, j-1], [i, j-1], [i+1, j], [i, j+1], [i-1, j+1]];
} else {
neighbors = [[i-1, j], [i, j-1], [i+1, j-1], [i+1, j], [i+1, j+1], [i, j+1]];
}
return neighbors.filter(([ni, nj]) => ni>=0 && ni<WIDTH && nj>=0 && nj<HEIGHT);
}
for (let j=0; j<HEIGHT; j++) {
for (let i=0; i<WIDTH; i++) {
neighborsMap.set(`${i},${j}`, computeNeighbors(i, j));
}
}
function getNeighbors(i, j) {
return neighborsMap.get(`${i},${j}`) || [];
}
// 工具函数
function posKey(i,j) { return `${i},${j}`; }
function parseKey(key) { return key.split(',').map(Number); }
function isCatEscaped(pos) { return pos[0]===0 || pos[0]===WIDTH-1 || pos[1]===0 || pos[1]===HEIGHT-1; }
function isCatCompletelyTrapped(wallsSet, catPos) {
for (let [ni,nj] of getNeighbors(catPos[0], catPos[1]))
if (!wallsSet.has(posKey(ni,nj))) return false;
return true;
}
// 绘图
function getCellCenter(i, j) {
const offset = (j % 2 === 1) ? CELL_RADIUS : 0;
const x = CELL_RADIUS + CELL_SPACING + i * DX + offset;
const y = CELL_RADIUS + CELL_SPACING + j * DY;
return [x, y];
}
function drawBoard() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
for (let j=0; j<HEIGHT; j++) {
for (let i=0; i<WIDTH; i++) {
const [x, y] = getCellCenter(i, j);
let color = '#E0E0E0';
const key = `${i},${j}`;
if (gameMode) {
if (gameCatPos && i===gameCatPos[0] && j===gameCatPos[1]) color = '#FFB6C1';
else if (gameWalls.has(key)) color = '#333333';
// 游戏模式下也支持显示方案墙壁(作弊模式)
else if (showSolutionSteps && solutionOnBoard.some(p => p[0]===i && p[1]===j)) color = '#90EE90';
} else {
if (i===CAT_START[0] && j===CAT_START[1]) color = '#FFB6C1';
else if (initialWalls.has(key)) color = '#333333';
else if (currentSolutionWalls.has(key)) color = '#FF6B6B';
else if (showSolutionSteps && solutionOnBoard.some(p => p[0]===i && p[1]===j)) color = '#90EE90';
}
ctx.beginPath();
ctx.arc(x, y, CELL_RADIUS*0.85, 0, 2*Math.PI);
ctx.fillStyle = color;
ctx.fill();
ctx.strokeStyle = '#555';
ctx.lineWidth = 1.5;
ctx.stroke();
ctx.font = 'bold 16px "Segoe UI Emoji", "Apple Color Emoji", system-ui, sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
if (gameMode) {
if (gameCatPos && i===gameCatPos[0] && j===gameCatPos[1]) {
ctx.fillStyle = '#b45309';
ctx.fillText('🐱', x, y-1);
}
} else {
if (i===CAT_START[0] && j===CAT_START[1]) {
ctx.fillStyle = '#b45309';
ctx.fillText('🐱', x, y-1);
}
}
// 显示步骤序号(游戏模式和非游戏模式均支持)
if (showSolutionSteps && solutionOnBoard.length) {
const idx = solutionOnBoard.findIndex(p => p[0]===i && p[1]===j);
if (idx !== -1) {
ctx.font = 'bold 12px system-ui';
ctx.fillStyle = '#1d4ed8';
ctx.fillText(idx+1, x, y-2);
}
}
}
}
}
// ---------- AI 核心函数 (保持不变) ----------
function calcDistances(wallsSet) {
const dist = Array(WIDTH).fill().map(() => Array(HEIGHT).fill(Infinity));
const q = [];
for (let i=0; i<WIDTH; i++) {
for (let j=0; j<HEIGHT; j++) {
if ((i===0 || i===WIDTH-1 || j===0 || j===HEIGHT-1) && !wallsSet.has(posKey(i,j))) {
dist[i][j] = 0;
q.push([i,j]);
}
}
}
while (q.length) {
const [i,j] = q.shift();
const cur = dist[i][j];
for (let [ni,nj] of getNeighbors(i,j)) {
if (!wallsSet.has(posKey(ni,nj)) && dist[ni][nj] > cur+1) {
dist[ni][nj] = cur+1;
q.push([ni,nj]);
}
}
}
return dist;
}
function getRoutesCount(wallsSet, i, j, dist, cache=new Map()) {
const key = `${i},${j}`;
if (cache.has(key)) return cache.get(key);
if (wallsSet.has(key)) { cache.set(key,0); return 0; }
if (i===0 || i===WIDTH-1 || j===0 || j===HEIGHT-1) { cache.set(key,1); return 1; }
let count = 0;
const curDist = dist[i][j];
for (let [ni,nj] of getNeighbors(i,j)) {
if (!wallsSet.has(posKey(ni,nj)) && dist[ni][nj] < curDist) {
count += getRoutesCount(wallsSet, ni, nj, dist, cache);
}
}
cache.set(key, count);
return count;
}
function getCatNextMove(wallsSet, catPos) {
const cacheKey = JSON.stringify([...wallsSet].sort()) + '|' + catPos;
if (moveCache.has(cacheKey)) return moveCache.get(cacheKey);
if (isCatEscaped(catPos)) { moveCache.set(cacheKey, null); return null; }
const dist = calcDistances(wallsSet);
const [ci,cj] = catPos;
if (dist[ci][cj] === Infinity) { moveCache.set(cacheKey, null); return null; }
let bestMove = null, bestRoutes = -1;
const currentDist = dist[ci][cj];
const routesCache = new Map();
for (let [ni,nj] of getNeighbors(ci,cj)) {
if (!wallsSet.has(posKey(ni,nj)) && dist[ni][nj] < currentDist) {
const routes = getRoutesCount(wallsSet, ni, nj, dist, routesCache);
if (routes > bestRoutes) {
bestRoutes = routes;
bestMove = [ni,nj];
}
}
}
moveCache.set(cacheKey, bestMove);
return bestMove;
}
function heuristic(pos) {
return Math.min(pos[0], WIDTH-1-pos[0], pos[1], HEIGHT-1-pos[1]);
}
function getEscapePath(wallsSet, catPos) {
const key = JSON.stringify([...wallsSet])+'|'+catPos;
if (escapePathCache.has(key)) return escapePathCache.get(key);
const openSet = [[heuristic(catPos), catPos]];
const cameFrom = new Map();
const gScore = new Map(); gScore.set(catPos.toString(), 0);
while (openSet.length) {
openSet.sort((a,b) => a[0]-b[0]);
const [f, cur] = openSet.shift();
const [i,j] = cur;
if (isCatEscaped(cur)) {
const path = [];
let p = cur;
while (cameFrom.has(p.toString())) {
path.push(p);
p = cameFrom.get(p.toString());
}
path.reverse();
escapePathCache.set(key, path);
return path;
}
for (let nb of getNeighbors(i,j)) {
if (wallsSet.has(posKey(...nb))) continue;
const tg = (gScore.get(cur.toString())||0) + 1;
const nbKey = nb.toString();
if (!gScore.has(nbKey) || tg < gScore.get(nbKey)) {
cameFrom.set(nbKey, cur);
gScore.set(nbKey, tg);
openSet.push([tg+heuristic(nb), nb]);
}
}
}
escapePathCache.set(key, []);
return [];
}
function quickEvaluate(wallsSet, catPos, wallPos) {
const cacheKey = JSON.stringify([...wallsSet])+'|'+catPos+'|'+wallPos;
if (evalCache.has(cacheKey)) return evalCache.get(cacheKey);
let score = 0;
const dist = Math.abs(wallPos[0]-catPos[0]) + Math.abs(wallPos[1]-catPos[1]);
if (dist===1) score+=30; else if(dist===2) score+=20; else if(dist<=4) score+=15-dist*2;
const path = getEscapePath(wallsSet, catPos);
if (path.slice(0,3).some(p=>p[0]===wallPos[0]&&p[1]===wallPos[1])) score+=25;
const testWalls = new Set(wallsSet); testWalls.add(posKey(...wallPos));
const newPath = getEscapePath(testWalls, catPos);
if (!newPath.length) score+=50;
else if (path.length && newPath.length>path.length) score+=(newPath.length-path.length)*5;
evalCache.set(cacheKey, score);
return score;
}
function getCandidates(wallsSet, catPos, limit=8) {
const strategic = new Set();
for (let nb of getNeighbors(catPos[0],catPos[1])) if(!wallsSet.has(posKey(...nb))) strategic.add(posKey(...nb));
const path = getEscapePath(wallsSet, catPos);
path.slice(0,3).forEach(p=> strategic.add(posKey(...p)));
for (let i=Math.max(0,catPos[0]-2); i<Math.min(WIDTH,catPos[0]+3); i++)
for (let j=Math.max(0,catPos[1]-2); j<Math.min(HEIGHT,catPos[1]+3); j++)
if (!wallsSet.has(posKey(i,j)) && (i!==catPos[0]||j!==catPos[1])) strategic.add(posKey(i,j));
const candidates = [];
for (let key of strategic) {
const pos = parseKey(key);
candidates.push({pos, score: quickEvaluate(wallsSet, catPos, pos)});
}
candidates.sort((a,b)=>b.score-a.score);
return candidates.slice(0, limit).map(c=>c.pos);
}
function hashState(wallsSet, catPos) {
const wallsKey = Array.from(wallsSet).sort().join(';');
return `${wallsKey}|${catPos[0]},${catPos[1]}`;
}
function evaluate(wallsSet, catPos, depthFromRoot) {
if (isCatEscaped(catPos)) return -100000 + depthFromRoot;
if (isCatCompletelyTrapped(wallsSet, catPos)) return 100000 - depthFromRoot;
const dist = calcDistances(wallsSet);
const [ci, cj] = catPos;
const minDist = dist[ci][cj];
if (minDist === Infinity) return 100000 - depthFromRoot;
return -minDist * 10;
}
function alphaBeta(wallsSet, catPos, depth, alpha, beta, maxDepth, startTime) {
if (Date.now() - startTime > MAX_TOTAL_TIME * 1000) {
stopRequested = true;
return 0;
}
if (stopRequested) return 0;
const stateKey = hashState(wallsSet, catPos);
const ttEntry = transpositionTable.get(stateKey);
if (ttEntry && ttEntry.depth >= depth) {
if (ttEntry.flag === EXACT) return ttEntry.score;
if (ttEntry.flag === LOWERBOUND && ttEntry.score >= beta) return ttEntry.score;
if (ttEntry.flag === UPPERBOUND && ttEntry.score <= alpha) return ttEntry.score;
}
if (isCatCompletelyTrapped(wallsSet, catPos)) return 100000 - (maxDepth - depth);
if (isCatEscaped(catPos)) return -100000 + (maxDepth - depth);
if (depth <= 0) return evaluate(wallsSet, catPos, maxDepth - depth);
let candidates = getCandidates(wallsSet, catPos, MAX_CANDIDATES * 2);
const ttBest = ttEntry ? ttEntry.bestMove : null;
candidates.sort((a, b) => {
if (ttBest && a[0]===ttBest[0] && a[1]===ttBest[1]) return -1;
if (ttBest && b[0]===ttBest[0] && b[1]===ttBest[1]) return 1;
const ha = historyTable.get(posKey(...a)) || 0;
const hb = historyTable.get(posKey(...b)) || 0;
return hb - ha;
});
candidates = candidates.slice(0, MAX_CANDIDATES);
let bestScore = -Infinity;
let bestMove = null;
const originalAlpha = alpha;
for (const wallPos of candidates) {
const wallKey = posKey(...wallPos);
if (wallsSet.has(wallKey)) continue;
const newWalls = new Set(wallsSet);
newWalls.add(wallKey);
let nextCatPos = getCatNextMove(newWalls, catPos);
let score;
if (!nextCatPos) {
score = 100000 - (maxDepth - depth + 1);
} else {
score = alphaBeta(newWalls, nextCatPos, depth - 1, alpha, beta, maxDepth, startTime);
}
if (stopRequested) return 0;
if (score > bestScore) {
bestScore = score;
bestMove = wallPos;
}
if (bestScore > alpha) alpha = bestScore;
if (alpha >= beta) {
const hist = historyTable.get(wallKey) || 0;
historyTable.set(wallKey, hist + depth * depth);
break;
}
}
let flag;
if (bestScore <= originalAlpha) flag = UPPERBOUND;
else if (bestScore >= beta) flag = LOWERBOUND;
else flag = EXACT;
transpositionTable.set(stateKey, {
score: bestScore,
depth: depth,
flag: flag,
bestMove: bestMove
});
return bestScore;
}
function collectBestSequence(initialWalls, startCatPos, maxDepth, startTime) {
const solution = [];
let walls = new Set(initialWalls);
let catPos = [...startCatPos];
let depthLeft = maxDepth;
while (depthLeft > 0 && !stopRequested) {
if (isCatCompletelyTrapped(walls, catPos)) break;
if (isCatEscaped(catPos)) return null;
alphaBeta(walls, catPos, depthLeft, -Infinity, Infinity, depthLeft, startTime);
const stateKey = hashState(walls, catPos);
const ttEntry = transpositionTable.get(stateKey);
let bestMove = ttEntry ? ttEntry.bestMove : null;
if (!bestMove) {
let bestScore = -Infinity;
for (let i=0; i<WIDTH; i++) {
for (let j=0; j<HEIGHT; j++) {
if (walls.has(posKey(i,j)) || (i===catPos[0]&&j===catPos[1])) continue;
const score = quickEvaluate(walls, catPos, [i,j]);
if (score > bestScore) {
bestScore = score;
bestMove = [i,j];
}
}
}
}
if (!bestMove) break;
solution.push(bestMove);
walls.add(posKey(...bestMove));
const nextCat = getCatNextMove(walls, catPos);
if (!nextCat) break;
catPos = nextCat;
depthLeft--;
}
return solution;
}
function validateSolution(solution) {
let walls = new Set(initialWalls);
let cat = [...CAT_START];
for (const wallPos of solution) {
walls.add(posKey(...wallPos));
if (isCatCompletelyTrapped(walls, cat)) return true;
const next = getCatNextMove(walls, cat);
if (!next) return true;
if (isCatEscaped(next)) return false;
cat = next;
}
return isCatCompletelyTrapped(walls, cat);
}
// ---------- 迭代加深搜索 (只找第一个有效解) ----------
async function findFirstSolution() {
stopRequested = false;
const startTime = Date.now();
transpositionTable.clear();
historyTable.clear();
for (let depth = 6; depth <= MAX_DEPTH; depth += 2) {
if (stopRequested || (Date.now() - startTime) > MAX_TOTAL_TIME * 1000) break;
updateProgress(`🔍 搜索深度 ${depth}...`, (depth - 6) / (MAX_DEPTH - 6));
const solution = collectBestSequence(initialWalls, CAT_START, depth, startTime);
if (stopRequested) break;
if (solution && solution.length > 0 && validateSolution(solution)) {
return solution;
}
await new Promise(r => setTimeout(r, 0));
}
return null;
}
// ---------- UI 更新 (单方案显示) ----------
function updateProgress(text, percent=null) {
progressLabel.textContent = text;
if (percent!==null) progressFill.style.width = Math.min(100, percent*100)+'%';
}
function displaySingleSolution(solution) {
currentSolution = solution;
currentSolutionWalls = new Set(solution.map(p => posKey(...p)));
solutionOnBoard = [...solution];
showSolutionSteps = true;
cheatCheck.checked = true;
// 更新列表区
const eff = (1/solution.length).toFixed(3);
solutionListDiv.innerHTML = `
<div class="solution-item active" data-index="0">
<span>📌 当前方案</span>
<span class="solution-stats">${solution.length}步 · ${eff}</span>
</div>
`;
solutionCountSpan.textContent = '1 个';
// 显示详情
let details = `📘 唯一方案 (${solution.length}步)\n${'═'.repeat(25)}\n`;
let walls = new Set(initialWalls), cat = [...CAT_START];
for (let step=0; step<solution.length; step++) {
const w = solution[step];
walls.add(posKey(...w));
details += `第${step+1}步: 放墙 ${w[0]},${w[1]}`;
if (isCatCompletelyTrapped(walls, cat)) { details += ` → 🎉 猫被困!`; break; }
const next = getCatNextMove(walls, cat);
if (!next) { details += ` → 🎉 猫无法移动`; break; }
cat = next;
details += ` → 猫移至 ${cat[0]},${cat[1]}`;
if (isCatEscaped(cat)) { details += ` ❌ 逃脱`; break; }
details += '\n';
}
detailText.textContent = details;
drawBoard();
}
function clearSolution() {
currentSolution = null;
currentSolutionWalls.clear();
solutionOnBoard = [];
solutionListDiv.innerHTML = '<div style="padding:12px; text-align:center; color:#94a3b8;">暂无方案</div>';
solutionCountSpan.textContent = '0 个';
detailText.textContent = '选择一个方案查看详情…';
drawBoard();
}
// ---------- 事件处理 ----------
function handleCanvasClick(e) {
if (computing) { alert('计算中,请稍候'); return; }
const rect = canvas.getBoundingClientRect();
const scaleX = canvas.width / rect.width;
const scaleY = canvas.height / rect.height;
const clickX = (e.clientX - rect.left) * scaleX;
const clickY = (e.clientY - rect.top) * scaleY;
let minDist = Infinity, closest = null;
for (let j=0; j<HEIGHT; j++) for (let i=0; i<WIDTH; i++) {
const [cx, cy] = getCellCenter(i,j);
const d = Math.hypot(clickX-cx, clickY-cy);
if (d < minDist && d < CELL_RADIUS*1.2) { minDist = d; closest = [i,j]; }
}
if (!closest) return;
const [i,j] = closest;
if (gameMode) {
if (gameCatPos && i===gameCatPos[0] && j===gameCatPos[1]) return;
const key = posKey(i,j);
if (!gameWalls.has(key)) {
gameWalls.add(key);
gameStep++;
drawBoard();
if (isCatCompletelyTrapped(gameWalls, gameCatPos)) { alert(`🎉 胜利!${gameStep}步困住小猫`); exitGameMode(); return; }
const nextCat = getCatNextMove(gameWalls, gameCatPos);
if (!nextCat) { alert(`🎉 胜利!小猫无法移动`); exitGameMode(); }
else if (isCatEscaped(nextCat)) { alert(`😿 小猫逃脱了…`); exitGameMode(); }
else { gameCatPos = nextCat; drawBoard(); }
}
return;
}
if (i===CAT_START[0] && j===CAT_START[1]) return;
const key = posKey(i,j);
if (initialWalls.has(key)) {
initialWalls.delete(key);
wallClickOrder = wallClickOrder.filter(k => k!==key);
} else {
if (initialWalls.size >= 8) { alert('最多8个墙壁'); return; }
initialWalls.add(key);
wallClickOrder.push(key);
}
clearSolution();
drawBoard();
}
function resetAll() {
if (computing) return;
initialWalls.clear(); wallClickOrder = [];
clearSolution();
gameMode = false; startGameBtn.textContent = '🎮 开始游戏';
enableControls(true);
drawBoard();
infoMessage.textContent = '✨ 点击圆圈设置墙壁';
cheatCheck.checked = true;
showSolutionSteps = true;
updateProgress('就绪', 0);
}
function generateRandomWalls() {
if (computing||gameMode) return;
if (wallClickOrder.length>=8) {
const keep = wallClickOrder.slice(0,8);
initialWalls = new Set(keep);
wallClickOrder = keep;
} else {
const need = 8 - wallClickOrder.length;
const available = [];
for (let j=0;j<HEIGHT;j++) for(let i=0;i<WIDTH;i++)
if(!(i===CAT_START[0]&&j===CAT_START[1]) && !initialWalls.has(posKey(i,j))) available.push(posKey(i,j));
const shuffled = available.sort(()=>Math.random()-0.5);
for (let i=0; i<need && i<shuffled.length; i++) wallClickOrder.push(shuffled[i]);
initialWalls = new Set(wallClickOrder);
}
clearSolution();
drawBoard();
}
async function startComputation() {
if (!initialWalls.size) { alert('请先设置墙壁'); return; }
if (computing) return;
computing = true; stopRequested = false;
computeStartTime = Date.now();
stopBtn.disabled = false; computeBtn.disabled = true;
updateProgress('Alpha-Beta 搜索中...', 0.05);
moveCache.clear(); evalCache.clear(); escapePathCache.clear();
try {
const solution = await findFirstSolution();
if (!stopRequested) {
if (solution) {
displaySingleSolution(solution);
updateProgress(`完成! 找到 ${solution.length} 步方案`, 1);
infoMessage.textContent = `✅ 已找到方案,共 ${solution.length} 步`;
} else {
updateProgress('未找到方案', 1);
infoMessage.textContent = '❌ 当前布局无解';
clearSolution();
}
} else {
updateProgress('已停止');
}
} catch(e) {
alert('错误: '+e);
updateProgress('计算出错');
}
computing = false; stopBtn.disabled = true; computeBtn.disabled = false;
}
function stopComputation() { stopRequested = true; }
function startGame() {
if (computing) return;
if (!initialWalls.size) { alert('请先设置墙壁'); return; }
gameMode = true;
gameWalls = new Set(initialWalls);
gameCatPos = [...CAT_START];
gameStep = 0;
startGameBtn.textContent = '游戏中...';
infoMessage.textContent = '🎯 点击放置墙壁,困住小猫!';
enableControls(false);
drawBoard();
}
function exitGameMode() {
gameMode = false; startGameBtn.textContent = '🎮 开始游戏';
enableControls(true);
drawBoard();
infoMessage.textContent = '✨ 点击圆圈设置墙壁';
}
function enableControls(enabled) {
const btns = [resetBtn, computeBtn, randomWallsBtn];
btns.forEach(b=> b.disabled = !enabled);
stopBtn.disabled = true;
startGameBtn.disabled = !enabled;
}
function toggleCheat() {
showSolutionSteps = cheatCheck.checked;
drawBoard();
}
// 绑定事件
canvas.addEventListener('click', handleCanvasClick);
resetBtn.addEventListener('click', resetAll);
computeBtn.addEventListener('click', startComputation);
stopBtn.addEventListener('click', stopComputation);
randomWallsBtn.addEventListener('click', generateRandomWalls);
startGameBtn.addEventListener('click', startGame);
cheatCheck.addEventListener('change', toggleCheat);
// 初始化
cheatCheck.checked = true;
showSolutionSteps = true;
drawBoard();
clearSolution();
})();
</script>
</body>
</html>