猫 vs 猎物 · 迷你实验版
by EpicCoder88531 lines16.4 KB
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>猫 vs 猎物 · 迷你实验版</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; font-family: system-ui; }
body {
background: #111;
display: flex;
flex-direction: column;
align-items: center;
padding: 10px;
min-height: 100vh;
justify-content: center;
}
canvas {
border: 2px solid #444;
border-radius: 8px;
background: #3d6b37;
max-width: 100%;
height: auto;
touch-action: none;
}
.panel {
margin-top: 12px;
width: 100%;
max-width: 800px;
background: #1a1a1a;
border: 1px solid #444;
border-radius: 6px;
padding: 10px 14px;
color: #ddd;
font-size: 14px;
line-height: 1.6;
}
.panel strong { color: #ffd700; }
.stats {
display: flex;
justify-content: space-between;
flex-wrap: wrap;
}
.stats span {
margin-right: 15px;
}
.controls {
margin-top: 8px;
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.controls button {
background: #333;
color: #fff;
border: 1px solid #555;
border-radius: 4px;
padding: 4px 14px;
cursor: pointer;
font-size: 14px;
}
.controls button.active {
background: #5a7a5a;
border-color: #8aa88a;
}
.log {
margin-top: 8px;
max-height: 100px;
overflow-y: auto;
background: #111;
border-radius: 4px;
padding: 4px 8px;
font-size: 13px;
color: #8f8;
border: 1px solid #333;
}
.log .entry { border-bottom: 1px solid #2a2a2a; padding: 2px 0; }
.log .time { color: #888; }
</style>
</head>
<body>
<canvas id="simCanvas" width="800" height="500"></canvas>
<div class="panel">
<div><strong>🐱 猫 vs 猎物 · 迷你实验</strong></div>
<div class="stats">
<span>🐱 猫: <span id="catStatus">空闲</span></span>
<span>🎯 猎物: <span id="preyStatus">闲逛</span></span>
<span>📏 距离: <span id="distance">0</span></span>
<span>⚡ 速度: <span id="speedDisplay">1×</span></span>
</div>
<div class="controls">
<button id="resetBtn" onclick="resetSim()">🔄 重置</button>
<button id="pauseBtn" onclick="togglePause()">⏸️ 暂停</button>
<button class="active" onclick="setSpeed(1)">1×</button>
<button onclick="setSpeed(2)">2×</button>
<button onclick="setSpeed(5)">5×</button>
</div>
<div class="log" id="logPanel">
<div class="entry">实验启动,观察猫的追击行为...</div>
</div>
</div>
<script>
// ============================================================
// 迷你实验版:1猫 vs 1猎物,关闭饥饿,仅保留核心逻辑
// ============================================================
const canvas = document.getElementById('simCanvas');
const ctx = canvas.getContext('2d');
const catStatusEl = document.getElementById('catStatus');
const preyStatusEl = document.getElementById('preyStatus');
const distanceEl = document.getElementById('distance');
const speedDisplay = document.getElementById('speedDisplay');
const logPanel = document.getElementById('logPanel');
// ---------- 配置 ----------
const CAT_RADIUS = 14;
const PREY_RADIUS = 8;
const CAT_SPEED = 1.8; // 略快于猎物
const PREY_SPEED = 1.2;
const CAT_VIEW_RANGE = 200;
const CAT_VIEW_ANGLE = Math.PI / 2.5; // 约72度
const PREY_VIEW_RANGE = 150;
const PREY_VIEW_ANGLE = Math.PI / 2.5;
const CAT_HUNT_RANGE = 28; // 捕猎距离
const WORLD_BOUND = { left: 0, top: 0, right: 800, bottom: 500 };
// ---------- 状态 ----------
let paused = false;
let speed = 1;
let frameCount = 0;
// 实体
let cat = {
x: 100, y: 250,
angle: 0,
speed: CAT_SPEED,
target: null, // 目标猎物引用
state: 'idle', // idle, chase, attack
stuckTimer: 0
};
let prey = {
x: 600, y: 250,
angle: 0,
speed: PREY_SPEED,
target: null, // 目标植物(无)
state: 'wander', // wander, flee, eating
stuckTimer: 0
};
// 记录日志
let logMessages = [];
// ---------- 工具函数 ----------
function distance(a, b) {
return Math.hypot(a.x - b.x, a.y - b.y);
}
function angleTo(a, b) {
return Math.atan2(b.y - a.y, b.x - a.x);
}
function clamp(v, min, max) { return Math.max(min, Math.min(max, v)); }
function safeMove(entity, moveX, moveY, radius) {
let newX = entity.x + moveX;
let newY = entity.y + moveY;
const left = WORLD_BOUND.left + radius;
const right = WORLD_BOUND.right - radius;
const top = WORLD_BOUND.top + radius;
const bottom = WORLD_BOUND.bottom - radius;
let canX = (newX >= left && newX <= right);
let canY = (newY >= top && newY <= bottom);
if (canX && canY) {
entity.x = newX;
entity.y = newY;
entity.stuckTimer = 0;
} else if (canX && !canY) {
entity.x = newX;
entity.y = clamp(entity.y, top, bottom);
entity.stuckTimer = 0;
} else if (!canX && canY) {
entity.x = clamp(entity.x, left, right);
entity.y = newY;
entity.stuckTimer = 0;
} else {
// 两个方向都卡住,转向
entity.stuckTimer = (entity.stuckTimer || 0) + 1;
if (entity.stuckTimer > 20) {
entity.x = clamp(entity.x + Math.cos(entity.angle)*10, left, right);
entity.y = clamp(entity.y + Math.sin(entity.angle)*10, top, bottom);
entity.angle += (Math.random() > 0.5 ? 1 : -1) * 1.2;
entity.stuckTimer = 0;
} else {
entity.angle += (Math.random() > 0.5 ? 0.3 : -0.3);
}
// 钳制位置
entity.x = clamp(entity.x, left, right);
entity.y = clamp(entity.y, top, bottom);
}
}
// 视线是否被遮挡(这里无遮挡,因为无树/灌木)
function isLineBlocked(x1, y1, x2, y2) {
return false; // 实验版无遮挡
}
// 猎物是否在猫的视野内
function isPreyVisible(catPos, preyPos) {
const d = distance(catPos, preyPos);
if (d > CAT_VIEW_RANGE) return false;
const ang = angleTo(catPos, preyPos);
let delta = ang - catPos.angle;
while (delta > Math.PI) delta -= 2*Math.PI;
while (delta < -Math.PI) delta += 2*Math.PI;
return Math.abs(delta) <= CAT_VIEW_ANGLE/2;
}
// 猫是否在猎物视野内(用于猎物逃跑)
function isCatVisible(preyPos, catPos) {
const d = distance(preyPos, catPos);
if (d > PREY_VIEW_RANGE) return false;
const ang = angleTo(preyPos, catPos);
let delta = ang - preyPos.angle;
while (delta > Math.PI) delta -= 2*Math.PI;
while (delta < -Math.PI) delta += 2*Math.PI;
return Math.abs(delta) <= PREY_VIEW_ANGLE/2;
}
// ---------- 日志 ----------
function addLog(msg) {
const time = new Date().toLocaleTimeString();
logMessages.push({ time, msg });
if (logMessages.length > 30) logMessages.shift();
renderLog();
}
function renderLog() {
logPanel.innerHTML = logMessages.map(e =>
`<div class="entry"><span class="time">[${e.time}]</span> ${e.msg}</div>`
).join('');
logPanel.scrollTop = logPanel.scrollHeight;
}
// ---------- 更新UI ----------
function updateUI() {
const d = distance(cat, prey);
distanceEl.textContent = d.toFixed(1);
catStatusEl.textContent = cat.state;
preyStatusEl.textContent = prey.state;
speedDisplay.textContent = speed + '×';
}
// ---------- 重置 ----------
function resetSim() {
cat.x = 100; cat.y = 250; cat.angle = 0; cat.state = 'idle'; cat.target = null;
prey.x = 600; prey.y = 250; prey.angle = 0; prey.state = 'wander';
logMessages = [];
addLog('场景已重置,猫和猎物分开放置。');
updateUI();
}
// ---------- 暂停/速度 ----------
function togglePause() {
paused = !paused;
document.getElementById('pauseBtn').textContent = paused ? '▶️ 继续' : '⏸️ 暂停';
addLog(paused ? '已暂停' : '已继续');
}
function setSpeed(s) {
speed = s;
document.querySelectorAll('.controls button').forEach(b => b.classList.remove('active'));
// 高亮当前速度按钮(通过文本匹配)
document.querySelectorAll('.controls button').forEach(b => {
if (b.textContent.includes(s+'×')) b.classList.add('active');
});
addLog(`速度切换至 ${s}x`);
}
// ---------- AI更新 ----------
function updateAI() {
if (paused) return;
// ---- 猫的AI ----
const catToPrey = distance(cat, prey);
const catAngleToPrey = angleTo(cat, prey);
// 判断猎物是否在视野内
const visible = isPreyVisible(cat, prey);
if (visible && catToPrey < CAT_VIEW_RANGE) {
// 看到猎物,转向并追击
cat.angle = catAngleToPrey;
if (catToPrey <= CAT_HUNT_RANGE) {
// 捕猎成功
cat.state = 'attack';
// 重置猎物位置(模拟捕获)
addLog('🐱 猫捕获了猎物!重置位置。');
prey.x = 700 + Math.random()*80 - 40;
prey.y = 200 + Math.random()*100 - 50;
prey.angle = Math.random()*2*Math.PI;
prey.state = 'wander';
// 猫重置状态
cat.state = 'idle';
cat.target = null;
// 更新UI
updateUI();
return;
} else {
// 追击
cat.state = 'chase';
const moveX = Math.cos(catAngleToPrey) * cat.speed;
const moveY = Math.sin(catAngleToPrey) * cat.speed;
safeMove(cat, moveX, moveY, CAT_RADIUS);
}
} else {
// 看不到猎物,闲逛
if (cat.state !== 'idle') {
cat.state = 'idle';
addLog('猫丢失猎物,转为闲逛');
}
// 随机转向
if (Math.random() < 0.01) {
cat.angle += (Math.random() - 0.5) * 1.5;
}
const moveX = Math.cos(cat.angle) * cat.speed * 0.4;
const moveY = Math.sin(cat.angle) * cat.speed * 0.4;
safeMove(cat, moveX, moveY, CAT_RADIUS);
}
// ---- 猎物的AI ----
const preyToCat = distance(prey, cat);
const preyAngleToCat = angleTo(prey, cat);
// 判断猫是否在猎物视野内
const catVisible = isCatVisible(prey, cat);
if (catVisible && preyToCat < PREY_VIEW_RANGE) {
// 发现猫,逃跑
prey.state = 'flee';
const fleeAngle = preyAngleToCat + Math.PI; // 背对猫
// 检查逃跑方向是否有效(边界)
const testX = prey.x + Math.cos(fleeAngle)*30;
const testY = prey.y + Math.sin(fleeAngle)*30;
if (testX < WORLD_BOUND.left+20 || testX > WORLD_BOUND.right-20 ||
testY < WORLD_BOUND.top+20 || testY > WORLD_BOUND.bottom-20) {
// 如果逃跑方向出界,随机偏转
fleeAngle += (Math.random() > 0.5 ? 1 : -1) * 1.2;
}
prey.angle = fleeAngle;
const moveX = Math.cos(fleeAngle) * prey.speed * 1.5;
const moveY = Math.sin(fleeAngle) * prey.speed * 1.5;
safeMove(prey, moveX, moveY, PREY_RADIUS);
} else {
// 无威胁,闲逛
if (prey.state !== 'wander') {
prey.state = 'wander';
addLog('猎物安全,转为闲逛');
}
if (Math.random() < 0.01) {
prey.angle += (Math.random() - 0.5) * 1.5;
}
const moveX = Math.cos(prey.angle) * prey.speed * 0.5;
const moveY = Math.sin(prey.angle) * prey.speed * 0.5;
safeMove(prey, moveX, moveY, PREY_RADIUS);
}
// 边界修正(安全钳)
cat.x = clamp(cat.x, WORLD_BOUND.left+CAT_RADIUS, WORLD_BOUND.right-CAT_RADIUS);
cat.y = clamp(cat.y, WORLD_BOUND.top+CAT_RADIUS, WORLD_BOUND.bottom-CAT_RADIUS);
prey.x = clamp(prey.x, WORLD_BOUND.left+PREY_RADIUS, WORLD_BOUND.right-PREY_RADIUS);
prey.y = clamp(prey.y, WORLD_BOUND.top+PREY_RADIUS, WORLD_BOUND.bottom-PREY_RADIUS);
updateUI();
}
// ---------- 绘制 ----------
function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
// 网格
ctx.strokeStyle = 'rgba(255,255,255,0.08)';
ctx.lineWidth = 1;
for (let x=0; x<canvas.width; x+=50) {
ctx.beginPath();
ctx.moveTo(x, 0);
ctx.lineTo(x, canvas.height);
ctx.stroke();
}
for (let y=0; y<canvas.height; y+=50) {
ctx.beginPath();
ctx.moveTo(0, y);
ctx.lineTo(canvas.width, y);
ctx.stroke();
}
// 视野锥(猫)
ctx.save();
ctx.beginPath();
ctx.moveTo(cat.x, cat.y);
const startAngle = cat.angle - CAT_VIEW_ANGLE/2;
const endAngle = cat.angle + CAT_VIEW_ANGLE/2;
ctx.arc(cat.x, cat.y, CAT_VIEW_RANGE, startAngle, endAngle);
ctx.closePath();
ctx.fillStyle = 'rgba(242, 166, 90, 0.08)';
ctx.fill();
ctx.strokeStyle = 'rgba(242, 166, 90, 0.3)';
ctx.lineWidth = 1;
ctx.stroke();
// 猎物视野锥
ctx.beginPath();
ctx.moveTo(prey.x, prey.y);
const preyStart = prey.angle - PREY_VIEW_ANGLE/2;
const preyEnd = prey.angle + PREY_VIEW_ANGLE/2;
ctx.arc(prey.x, prey.y, PREY_VIEW_RANGE, preyStart, preyEnd);
ctx.closePath();
ctx.fillStyle = 'rgba(0, 0, 0, 0.05)';
ctx.fill();
ctx.strokeStyle = 'rgba(0, 0, 0, 0.15)';
ctx.lineWidth = 1;
ctx.stroke();
ctx.restore();
// 猎物
ctx.shadowColor = 'rgba(0,0,0,0.3)';
ctx.shadowBlur = 6;
ctx.beginPath();
ctx.arc(prey.x, prey.y, PREY_RADIUS, 0, 2*Math.PI);
ctx.fillStyle = '#333';
ctx.fill();
ctx.strokeStyle = '#222';
ctx.lineWidth = 2;
ctx.stroke();
// 眼睛
ctx.fillStyle = '#fff';
ctx.beginPath();
ctx.arc(prey.x-3, prey.y-2, 2, 0, 2*Math.PI);
ctx.fill();
ctx.beginPath();
ctx.arc(prey.x+3, prey.y-2, 2, 0, 2*Math.PI);
ctx.fill();
ctx.fillStyle = '#000';
ctx.beginPath();
ctx.arc(prey.x-3, prey.y-2, 0.8, 0, 2*Math.PI);
ctx.fill();
ctx.beginPath();
ctx.arc(prey.x+3, prey.y-2, 0.8, 0, 2*Math.PI);
ctx.fill();
// 状态文字
ctx.shadowBlur = 0;
ctx.fillStyle = '#ccc';
ctx.font = '12px system-ui';
ctx.textAlign = 'center';
ctx.fillText(prey.state, prey.x, prey.y - PREY_RADIUS - 6);
// 猫
ctx.shadowBlur = 6;
ctx.beginPath();
ctx.arc(cat.x, cat.y, CAT_RADIUS, 0, 2*Math.PI);
ctx.fillStyle = '#f2a65a';
ctx.fill();
ctx.strokeStyle = '#222';
ctx.lineWidth = 2;
ctx.stroke();
// 眼睛
ctx.fillStyle = '#000';
const eyeOff = 4;
ctx.beginPath();
ctx.arc(cat.x + Math.cos(cat.angle-0.5)*5 - 2, cat.y + Math.sin(cat.angle-0.5)*5 - 2, 2.5, 0, 2*Math.PI);
ctx.fill();
ctx.beginPath();
ctx.arc(cat.x + Math.cos(cat.angle+0.5)*5 + 2, cat.y + Math.sin(cat.angle+0.5)*5 + 2, 2.5, 0, 2*Math.PI);
ctx.fill();
// 鼻子
ctx.fillStyle = '#d44';
ctx.beginPath();
ctx.arc(cat.x + Math.cos(cat.angle)*4, cat.y + Math.sin(cat.angle)*4, 1.5, 0, 2*Math.PI);
ctx.fill();
// 状态文字
ctx.shadowBlur = 0;
ctx.fillStyle = '#fff';
ctx.font = 'bold 14px system-ui';
ctx.textAlign = 'center';
ctx.fillText(cat.state, cat.x, cat.y - CAT_RADIUS - 8);
// 距离线
ctx.beginPath();
ctx.moveTo(cat.x, cat.y);
ctx.lineTo(prey.x, prey.y);
ctx.strokeStyle = 'rgba(255,255,0,0.2)';
ctx.lineWidth = 1;
ctx.setLineDash([4,4]);
ctx.stroke();
ctx.setLineDash([]);
// 边界
ctx.strokeStyle = '#000';
ctx.lineWidth = 3;
ctx.strokeRect(WORLD_BOUND.left, WORLD_BOUND.top,
WORLD_BOUND.right - WORLD_BOUND.left,
WORLD_BOUND.bottom - WORLD_BOUND.top);
ctx.shadowBlur = 0;
}
// ---------- 主循环 ----------
function gameLoop() {
for (let i=0; i<speed; i++) {
updateAI();
}
draw();
requestAnimationFrame(gameLoop);
}
// ---------- 初始化 ----------
resetSim();
gameLoop();
// 暴露控制函数
window.resetSim = resetSim;
window.togglePause = togglePause;
window.setSpeed = setSpeed;
</script>
</body>
</html>Game Source: 猫 vs 猎物 · 迷你实验版
Creator: EpicCoder88
Libraries: none
Complexity: complex (531 lines, 16.4 KB)
The full source code is displayed above on this page.
Remix Instructions
To remix this game, copy the source code above and modify it. Add a ARCADELAB header at the top with "remix_of: vs-epiccoder88" to link back to the original. Then publish at arcadelab.ai/publish.