
猫的 AI 才是灵魂
大部分复刻版图省事,猫就往「离边缘最近的方向」跑,这种贪心猫很好围。核心逻辑分两层:
第一层,算距离。 每次轮到猫走,先从棋盘边缘所有格子做一次 BFS 广度优先搜索(BFS 就是"一圈一圈向外扩散"的遍历方式),算出每个格子到边缘的最短步数。这等于猫每一步都重新看一遍全场局势。
第二层,数路。 光找最短路不够——最短的路可能只有一条,你一堵就断。所以它还给每个格子算了一个 routesCount:从这格出发、沿"距离递减"方向走到边缘的路径总条数。最后猫的决策是:在所有能让它离边缘更近的方向里,挑路径条数最多的那个走。
翻译成人话:这只猫不仅想跑出去,还想给自己留最多的退路。这就是为什么它经常不按直觉走位——你以为堵住了它唯一的路,它早就绕去另一条更宽的通道了。
一个独立 HTML
这游戏的核心就是那 30 行寻路算法,渲染完全可以用原生 Canvas 画,猫直接用 emoji 🐱,一个文件搞定,零依赖:
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>圈小猫</title>
<style>
*{margin:0;padding:0;box-sizing:border-box;-webkit-tap-highlight-color:transparent}
body{background:#f7f7f7;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC","Microsoft YaHei",sans-serif;display:flex;flex-direction:column;align-items:center;min-height:100vh;padding:16px}
h1{font-size:20px;color:#333;margin-bottom:4px}
#status{font-size:14px;color:#666;margin-bottom:12px;min-height:20px}
canvas{max-width:96vw;touch-action:manipulation}
#btns{margin-top:14px;display:flex;gap:10px}
button{padding:8px 22px;border:none;border-radius:6px;background:#4a90d9;color:#fff;font-size:14px;cursor:pointer}
button:hover{background:#3a7bc0}
.hint{margin-top:10px;font-size:12px;color:#999}
</style>
</head>
<body>
<h1>🐱 圈小猫</h1>
<div id="status">点击圆点筑墙,围住小猫</div>
<canvas id="c"></canvas>
<div id="btns"><button onclick="game.reset()">重新开局</button></div>
<div class="hint">灵感来自 52pojie 爱盘 404 页彩蛋 · 猫 AI:BFS + 路径计数</div>
<script>
(function(){
"use strict";
// ---------- 参数 ----------
var W = 11, H = 11; // 棋盘列数(奇偶行交错)、行数
var R = 22; // 六边形外接圆半径
var DX = 2 * R, DY = R * Math.sqrt(3);
var WALL_COLOR = "#7a8ba0";
var EMPTY_COLOR = "#d6dee8";
var CAT = "🐱", CAT_CAUGHT = "🙀";
var canvas = document.getElementById("c");
var ctx = canvas.getContext("2d");
var statusBar = document.getElementById("status");
// ---------- 棋盘 ----------
var blocks = []; // blocks[i][j] = true 是墙
var cat = { i: 0, j: 0 };
var state = "playing"; // playing / win / lose
var scale = 1;
function init() {
var dpr = window.devicePixelRatio || 1;
var boardW = (6.5 + 2 * W) * R;
var boardH = (6 + Math.sqrt(3) * H) * R;
canvas.width = boardW * dpr;
canvas.height = boardH * dpr;
canvas.style.width = Math.min(boardW, window.innerWidth * 0.96) + "px";
canvas.style.height = "auto";
scale = dpr;
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
canvas.addEventListener("click", onClick);
}
// 六边形邻居(方向 0-5, 与原版一致):
// 偶数行: [i-1,j] [i-1,j-1] [i,j-1] [i+1,j] [i,j+1] [i-1,j+1]
// 奇数行: [i-1,j] [i,j-1] [i+1,j-1] [i+1,j] [i+1,j+1] [i,j+1]
function neighbours(i, j) {
var s = { i: i - 1, j: j };
var a = { i: i + 1, j: j };
var n, o, r, q;
if ((j & 1) === 0) {
n = { i: i - 1, j: j - 1 };
o = { i: i, j: j - 1 };
r = { i: i - 1, j: j + 1 };
q = { i: i, j: j + 1 };
} else {
n = { i: i, j: j - 1 };
o = { i: i + 1, j: j - 1 };
r = { i: i, j: j + 1 };
q = { i: i + 1, j: j + 1 };
}
return [s, n, o, a, q, r];
}
function getBlock(i, j) {
return i >= 0 && i < W && j >= 0 && j < H ? blocks[i][j] : null;
}
function pos(i, j) {
return {
x: 3 * R + ((j & 1) === 0 ? R : DX) + i * DX,
y: 3 * R + R + j * DY
};
}
// ---------- 猫 AI ----------
// 与原版模块26相同: BFS 算每个格子到边缘的最短距离,
// routesCount = 从该格通往边缘的递减路径条数, 猫选 routesCount 最大的下降方向
function solve(blocksData, ci, cj) {
var dist = [];
var queue = [];
var i, j;
for (i = 0; i < W; i++) {
dist[i] = [];
for (j = 0; j < H; j++) {
dist[i][j] = Infinity;
if (isEdge(i, j) && !blocksData[i][j]) {
dist[i][j] = 0;
queue.push({ i: i, j: j });
}
}
}
while (queue.length > 0) {
var cur = queue.shift();
var nb = neighbours(cur.i, cur.j);
for (var k = 0; k < 6; k++) {
var n = nb[k];
if (n.i < 0 || n.i >= W || n.j < 0 || n.j >= H) continue;
if (blocksData[n.i][n.j]) continue;
if (isEdge(n.i, n.j)) continue;
if (dist[n.i][n.j] > dist[cur.i][cur.j] + 1) {
dist[n.i][n.j] = dist[cur.i][cur.j] + 1;
queue.push(n);
}
}
}
// routesCount 记忆化
var routes = [];
for (i = 0; i < W; i++) routes[i] = [];
function countRoutes(i, j) {
if (routes[i][j] !== undefined) return routes[i][j];
var res;
if (isEdge(i, j)) {
res = 1;
} else {
res = 0;
var nb = neighbours(i, j);
for (var k = 0; k < 6; k++) {
var n = nb[k];
if (n.i < 0 || n.i >= W || n.j < 0 || n.j >= H) continue;
if (blocksData[n.i][n.j]) continue;
if (dist[n.i][n.j] < dist[i][j]) {
res += countRoutes(n.i, n.j);
}
}
}
routes[i][j] = res;
return res;
}
countRoutes(ci, cj);
// 选: 距离递减方向中 routesCount 最大者
var best = -1, bestCount = -1;
var nb = neighbours(ci, cj);
for (var k = 0; k < 6; k++) {
var n = nb[k];
if (n.i < 0 || n.i >= W || n.j < 0 || n.j >= H) continue;
if (blocksData[n.i][n.j]) continue;
if (dist[n.i][n.j] < dist[ci][cj]) {
var c = countRoutes(n.i, n.j);
if (c > bestCount) { bestCount = c; best = k; }
}
}
return best;
}
function isEdge(i, j) {
return i <= 0 || i >= W - 1 || j <= 0 || j >= H - 1;
}
// ---------- 游戏流程 ----------
var anim = null; // 猫移动补间
function reset() {
blocks = [];
for (var i = 0; i < W; i++) {
blocks[i] = [];
for (var j = 0; j < H; j++) blocks[i][j] = false;
}
cat.i = Math.floor(W / 2);
cat.j = Math.floor(H / 2);
state = "playing";
// 随机 8 堵初始墙
var placed = 0;
while (placed < 8) {
var ri = Math.floor(W * Math.random());
var rj = Math.floor(H * Math.random());
if ((ri !== cat.i || rj !== cat.j) && !blocks[ri][rj]) {
blocks[ri][rj] = true;
placed++;
}
}
setStatus("点击圆点筑墙,围住小猫");
anim = null;
draw();
}
function isCaught() {
var nb = neighbours(cat.i, cat.j);
for (var k = 0; k < 6; k++) {
var b = getBlock(nb[k].i, nb[k].j);
if (b !== null && !b) return false;
}
return true;
}
function isEscaped() {
return isEdge(cat.i, cat.j);
}
function onClick(ev) {
if (state !== "playing") { reset(); return; }
var rect = canvas.getBoundingClientRect();
var x = (ev.clientX - rect.left) / rect.width * (canvas.width / scale);
var y = (ev.clientY - rect.top) / rect.height * (canvas.height / scale);
// 找最近的格子
var best = null, bestD = Infinity;
for (var i = 0; i < W; i++) {
for (var j = 0; j < H; j++) {
var p = pos(i, j);
var d = (p.x - x) * (p.x - x) + (p.y - y) * (p.y - y);
if (d < bestD) { bestD = d; best = { i: i, j: j }; }
}
}
if (!best || bestD > R * R * 1.2) return;
if (best.i === cat.i && best.j === cat.j) { setStatus("不能点猫所在的位置"); return; }
if (blocks[best.i][best.j]) { setStatus("这里已经是墙了"); return; }
blocks[best.i][best.j] = true;
draw();
if (isCaught()) {
state = "win";
setStatus("🎉 猫已无路可走,你赢了!点击任意处重开");
draw();
return;
}
catStep();
}
function catStep() {
var dir = solve(blocks, cat.i, cat.j);
if (dir < 0) {
state = "win";
setStatus("🎉 猫认输了,你赢了!点击任意处重开");
draw();
return;
}
var nb = neighbours(cat.i, cat.j);
var target = nb[dir];
var from = pos(cat.i, cat.j);
var to = pos(target.i, target.j);
cat.i = target.i;
cat.j = target.j;
// 移动动画(120ms 补间)
anim = { from: from, to: to, t0: performance.now(), dur: 120 };
requestAnimationFrame(animFrame);
}
function animFrame(now) {
if (!anim) return;
var t = (now - anim.t0) / anim.dur;
if (t >= 1) {
anim = null;
if (isEscaped()) {
state = "lose";
setStatus("😿 猫跑到地图边缘逃走了,你输了!点击任意处重开");
} else if (isCaught()) {
state = "win";
setStatus("🎉 猫被围住了,你赢了!点击任意处重开");
} else {
setStatus("再接再厉,继续围!");
}
draw();
return;
}
// 中间帧: 棋盘 + 猫在插值位置
ctx.clearRect(0, 0, canvas.width / scale, canvas.height / scale);
for (var i = 0; i < W; i++) {
for (var j = 0; j < H; j++) {
var p = pos(i, j);
hexPath(p.x, p.y, 0.9 * R);
ctx.fillStyle = blocks[i][j] ? WALL_COLOR : EMPTY_COLOR;
ctx.fill();
}
}
drawCatGlyph(anim.from.x + (anim.to.x - anim.from.x) * t, anim.from.y + (anim.to.y - anim.from.y) * t);
requestAnimationFrame(animFrame);
}
// ---------- 渲染 ----------
function hexPath(x, y, r) {
ctx.beginPath();
for (var k = 0; k < 6; k++) {
var a = Math.PI / 6 + k * Math.PI / 3;
var px = x + r * Math.cos(a);
var py = y + r * Math.sin(a);
if (k === 0) ctx.moveTo(px, py); else ctx.lineTo(px, py);
}
ctx.closePath();
}
function draw() {
ctx.clearRect(0, 0, canvas.width / scale, canvas.height / scale);
for (var i = 0; i < W; i++) {
for (var j = 0; j < H; j++) {
var p = pos(i, j);
hexPath(p.x, p.y, 0.9 * R);
ctx.fillStyle = blocks[i][j] ? WALL_COLOR : EMPTY_COLOR;
ctx.fill();
}
}
var cp = pos(cat.i, cat.j);
drawCatGlyph(cp.x, cp.y);
}
function drawCatGlyph(x, y) {
ctx.font = (2 * R) + "px serif";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText(state === "win" ? CAT_CAUGHT : CAT, x, y + 2);
}
function setStatus(t) { statusBar.textContent = t; }
// ---------- 启动 ----------
init();
reset();
window.game = { reset: reset };
})();
</script>
</body>
</html>保存成 cat.html 双击就能玩,文件大小不到 6KB。
评论已关闭