<!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>上帝视角 · AI足球冠军杯 | 左右球门+得分</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
user-select: none;
}
body {
background: linear-gradient(145deg, #0a3b1f, #05250e);
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
font-family: 'Segoe UI', 'Poppins', monospace;
padding: 20px;
}
.game-wrapper {
background: #1e5a1e;
border-radius: 56px;
padding: 20px 24px 24px 24px;
box-shadow: 0 20px 35px rgba(0,0,0,0.6), inset 0 1px 4px rgba(255,255,200,0.3);
}
canvas {
display: block;
margin: 0 auto;
border-radius: 32px;
box-shadow: 0 12px 28px black;
cursor: pointer;
background-color: #2c9e2c;
}
.info-board {
margin-top: 20px;
display: flex;
justify-content: space-between;
align-items: center;
gap: 20px;
flex-wrap: wrap;
background: rgba(0, 0, 0, 0.7);
backdrop-filter: blur(8px);
border-radius: 60px;
padding: 8px 28px;
color: #FFF3C9;
}
.score-panel {
display: flex;
gap: 40px;
background: #000000aa;
padding: 6px 28px;
border-radius: 48px;
font-weight: bold;
}
.team-score {
font-size: 1.6rem;
display: flex;
align-items: center;
gap: 12px;
}
.team-score span:first-child {
font-size: 1.9rem;
}
.red-text { color: #ff7b6b; }
.blue-text { color: #6ba5ff; }
.score-number {
font-size: 2.4rem;
font-weight: 800;
color: #FFD966;
margin-left: 8px;
}
.status-area {
background: #1d2f1ad9;
padding: 5px 24px;
border-radius: 32px;
font-weight: bold;
font-size: 0.95rem;
}
button {
background: #ffb347;
border: none;
font-weight: bold;
font-size: 0.9rem;
padding: 6px 20px;
border-radius: 40px;
cursor: pointer;
transition: 0.05s linear;
box-shadow: 0 3px 0 #a1622a;
font-family: inherit;
}
button:active {
transform: translateY(2px);
box-shadow: 0 1px 0 #a1622a;
}
.legend {
display: flex;
gap: 18px;
font-size: 0.7rem;
background: #00000066;
padding: 5px 16px;
border-radius: 32px;
}
.dot {
display: inline-block;
width: 12px;
height: 12px;
border-radius: 12px;
margin-right: 5px;
}
footer {
font-size: 0.7rem;
text-align: center;
margin-top: 12px;
color: #cfefcf;
}
</style>
</head>
<body>
<div>
<div class="game-wrapper">
<canvas id="soccerCanvas" width="1100" height="620"></canvas>
<div class="info-board">
<div class="score-panel">
<div class="team-score"><span class="red-text">🔴 红队</span><span class="score-number" id="redScoreDisplay">0</span></div>
<div class="team-score"><span class="blue-text">🔵 蓝队</span><span class="score-number" id="blueScoreDisplay">0</span></div>
</div>
<div class="status-area" id="gameStatusMsg">⚽ AI 激战中 · 进球自动中圈开球</div>
<button id="resetMatchBtn">🔄 重赛</button>
<div class="legend">
<span><i class="dot" style="background:#e34234;"></i> 红队(攻右门)</span>
<span><i class="dot" style="background:#3b83ff;"></i> 蓝队(攻左门)</span>
<span><i class="dot" style="background:#f5f2b0;"></i> 足球</span>
</div>
</div>
<footer>🎯 AI智能跑位+身体对抗 | 足球碰到球员会踢飞 | 球门区得分自动累计</footer>
</div>
</div>
<script>
(function(){
// ----- CANVAS ------
const canvas = document.getElementById('soccerCanvas');
const ctx = canvas.getContext('2d');
const W = 1100, H = 620;
canvas.width = W; canvas.height = H;
// 球场边界(球员不可逾越,足球也不可出界,但碰到边界会反弹)
const BOUNDS = { left: 28, right: W-28, top: 35, bottom: H-35 };
// ========= 球门定义(左右两侧明显区域)==========
// 左侧球门 (蓝队防守,但红队射此门得分)
const LEFT_GOAL = {
x: 5, y: H/2 - 55, w: 22, h: 110,
teamOwner: 'blue' // 防守方是蓝队,但红队进球
};
// 右侧球门 (红队防守,蓝队射此门得分)
const RIGHT_GOAL = {
x: W-27, y: H/2 - 55, w: 22, h: 110,
teamOwner: 'red'
};
// 物理半径
const BALL_RADIUS = 7;
const PLAYER_RADIUS = 12;
// 运动参数
const MAX_SPEED = 4.6;
const ACC = 0.32;
const AIR_FRICTION = 0.99;
const BALL_FRICTION = 0.993;
// 队伍配置
const PLAYERS_COUNT = 6;
let redTeam = \[\]; // 红队 (左侧半场起始,进攻右侧球门)
let blueTeam = \[\]; // 蓝队 (右侧半场起始,进攻左侧球门)
let ball = { x: W/2, y: H/2, vx: 0, vy: 0 };
// 比分
let redScore = 0, blueScore = 0;
let goalCooldown = false; // 进球后短暂冻结,重置足球位置
let goalTimer = null;
// UI 元素
const redScoreSpan = document.getElementById('redScoreDisplay');
const blueScoreSpan = document.getElementById('blueScoreDisplay');
const statusMsgDiv = document.getElementById('gameStatusMsg');
function updateUI() {
redScoreSpan.innerText = redScore;
blueScoreSpan.innerText = blueScore;
}
function setStatusMsg(msg, isTemp = false) {
statusMsgDiv.innerHTML = msg;
if(isTemp) {
setTimeout(() => {
if(!goalCooldown) statusMsgDiv.innerHTML = "⚽ AI 激战中 · 进球自动中圈开球";
}, 1800);
}
}
// ---------- 重置所有球员到阵型(不重置比分)----------
function resetPositions(centerBall = true) {
// 红队阵型 (左半场 150~400区域)
for(let i=0; i<redTeam.length; i++) {
let angle = (i / redTeam.length) * Math.PI * 2;
let baseX = 160 + Math.sin(angle) * 70;
let baseY = H/2 + Math.cos(angle * 1.2) * 85;
baseX = Math.min(Math.max(baseX, BOUNDS.left + PLAYER_RADIUS), BOUNDS.right - 280);
baseY = Math.min(Math.max(baseY, BOUNDS.top + PLAYER_RADIUS), BOUNDS.bottom - PLAYER_RADIUS);
redTeami.x = baseX;
redTeami.y = baseY;
redTeami.vx = 0;
redTeami.vy = 0;
}
// 蓝队阵型 (右半场 700~1000)
for(let i=0; i<blueTeam.length; i++) {
let angle = (i / blueTeam.length) * Math.PI * 2;
let baseX = W - 180 + Math.sin(angle) * 70;
let baseY = H/2 + Math.cos(angle * 1.1) * 80;
baseX = Math.min(Math.max(baseX, BOUNDS.left + 250), BOUNDS.right - PLAYER_RADIUS);
baseY = Math.min(Math.max(baseY, BOUNDS.top + PLAYER_RADIUS), BOUNDS.bottom - PLAYER_RADIUS);
blueTeami.x = baseX;
blueTeami.y = baseY;
blueTeami.vx = 0;
blueTeami.vy = 0;
}
if(centerBall) {
ball.x = W/2;
ball.y = H/2;
ball.vx = (Math.random() - 0.5) * 1.4;
ball.vy = (Math.random() - 0.5) * 1.2;
}
}
// 完全重置比赛(比分归零,重开)
function fullMatchReset() {
if(goalCooldown) return;
redScore = 0;
blueScore = 0;
updateUI();
resetPositions(true);
for(let p of redTeam) { p.vx = 0; p.vy = 0; }
for(let p of blueTeam) { p.vx = 0; p.vy = 0; }
ball.vx = 0; ball.vy = 0;
goalCooldown = false;
if(goalTimer) clearTimeout(goalTimer);
setStatusMsg("⚡ 比赛已重置,开球!");
}
// 进球处理
function handleGoal(scoringTeam) { // 'red' 或 'blue'
if(goalCooldown) return;
goalCooldown = true;
if(scoringTeam === 'red') {
redScore++;
setStatusMsg("🎉 红队进球! +1分 🎉", true);
} else {
blueScore++;
setStatusMsg("🎉 蓝队进球! +1分 🎉", true);
}
updateUI();
// 进球特效冻结,然后将球放回中圈
if(goalTimer) clearTimeout(goalTimer);
goalTimer = setTimeout(() => {
resetPositions(true);
goalCooldown = false;
setStatusMsg("⚽ 继续比赛!");
goalTimer = null;
}, 1300);
}
// 检测球是否进入球门区域
function checkGoalScored() {
// 左侧球门区域 (红队得分)
if(ball.x + BALL_RADIUS > LEFT_GOAL.x && ball.x - BALL_RADIUS < LEFT_GOAL.x + LEFT_GOAL.w &&
ball.y + BALL_RADIUS > LEFT_GOAL.y && ball.y - BALL_RADIUS < LEFT_GOAL.y + LEFT_GOAL.h) {
if(!goalCooldown) handleGoal('red');
return true;
}
// 右侧球门区域 (蓝队得分)
if(ball.x + BALL_RADIUS > RIGHT_GOAL.x && ball.x - BALL_RADIUS < RIGHT_GOAL.x + RIGHT_GOAL.w &&
ball.y + BALL_RADIUS > RIGHT_GOAL.y && ball.y - BALL_RADIUS < RIGHT_GOAL.y + RIGHT_GOAL.h) {
if(!goalCooldown) handleGoal('blue');
return true;
}
return false;
}
// ========= AI 决策: 球员追逐足球,但带有进攻方向偏移 ==========
function getPlayerTarget(player, teamColor) { // teamColor: 'red' / 'blue'
let dxToBall = ball.x - player.x;
let dyToBall = ball.y - player.y;
let dist = Math.hypot(dxToBall, dyToBall);
let aggression = Math.min(1.0, 170 / (dist + 35));
let targetX = ball.x;
let targetY = ball.y;
if(teamColor === 'red') {
// 红队倾向向右半场进攻 (射右侧球门)
let offensiveShift = 45 * (1 - aggression);
targetX = ball.x + offensiveShift;
// 若球靠近左后场,稍微回防但依然积极
if(ball.x < W/2 - 100 && dist > 130) targetX = Math.min(targetX, W/2 - 40);
targetY = ball.y + (player.y - ball.y) * 0.12;
} else {
// 蓝队倾向向左半场进攻 (射左侧球门)
let offensiveShift = -45 * (1 - aggression);
targetX = ball.x + offensiveShift;
if(ball.x > W/2 + 100 && dist > 130) targetX = Math.max(targetX, W/2 + 40);
targetY = ball.y + (player.y - ball.y) * 0.12;
}
// 边界钳位
targetX = Math.min(Math.max(targetX, BOUNDS.left + 15), BOUNDS.right - 15);
targetY = Math.min(Math.max(targetY, BOUNDS.top + 15), BOUNDS.bottom - 15);
return { x: targetX, y: targetY };
}
function updatePlayersMovement() {
// 红队移动
for(let p of redTeam) {
let target = getPlayerTarget(p, 'red');
let dx = target.x - p.x;
let dy = target.y - p.y;
let len = Math.hypot(dx, dy);
if(len > 0.3) {
let dirX = dx/len, dirY = dy/len;
p.vx += dirX * ACC;
p.vy += dirY * ACC;
let spd = Math.hypot(p.vx, p.vy);
if(spd > MAX_SPEED) { p.vx = (p.vx/spd)*MAX_SPEED; p.vy = (p.vy/spd)*MAX_SPEED; }
}
p.vx *= AIR_FRICTION;
p.vy *= AIR_FRICTION;
p.x += p.vx;
p.y += p.vy;
p.x = Math.min(Math.max(p.x, BOUNDS.left + PLAYER_RADIUS), BOUNDS.right - PLAYER_RADIUS);
p.y = Math.min(Math.max(p.y, BOUNDS.top + PLAYER_RADIUS), BOUNDS.bottom - PLAYER_RADIUS);
}
// 蓝队移动
for(let p of blueTeam) {
let target = getPlayerTarget(p, 'blue');
let dx = target.x - p.x;
let dy = target.y - p.y;
let len = Math.hypot(dx, dy);
if(len > 0.3) {
let dirX = dx/len, dirY = dy/len;
p.vx += dirX * ACC;
p.vy += dirY * ACC;
let spd = Math.hypot(p.vx, p.vy);
if(spd > MAX_SPEED) { p.vx = (p.vx/spd)*MAX_SPEED; p.vy = (p.vy/spd)*MAX_SPEED; }
}
p.vx *= AIR_FRICTION;
p.vy *= AIR_FRICTION;
p.x += p.vx;
p.y += p.vy;
p.x = Math.min(Math.max(p.x, BOUNDS.left + PLAYER_RADIUS), BOUNDS.right - PLAYER_RADIUS);
p.y = Math.min(Math.max(p.y, BOUNDS.top + PLAYER_RADIUS), BOUNDS.bottom - PLAYER_RADIUS);
}
}
// 球员互相推开防粘连
function resolveCollisionPlayers() {
let all = ...redTeam, ...blueTeam;
for(let i=0;i<all.length;i++) {
for(let j=i+1;j<all.length;j++) {
let p1=alli, p2=allj;
let dx = p1.x-p2.x, dy = p1.y-p2.y;
let dist = Math.hypot(dx,dy);
let minDist = PLAYER_RADIUS*2;
if(dist < minDist) {
let angle = Math.atan2(dy,dx);
let overlap = minDist - dist;
let move = overlap * 0.5;
let mx = Math.cos(angle)*move;
let my = Math.sin(angle)*move;
p1.x += mx; p1.y += my;
p2.x -= mx; p2.y -= my;
// 边界校正
p1.x = Math.min(Math.max(p1.x, BOUNDS.left+PLAYER_RADIUS), BOUNDS.right-PLAYER_RADIUS);
p1.y = Math.min(Math.max(p1.y, BOUNDS.top+PLAYER_RADIUS), BOUNDS.bottom-PLAYER_RADIUS);
p2.x = Math.min(Math.max(p2.x, BOUNDS.left+PLAYER_RADIUS), BOUNDS.right-PLAYER_RADIUS);
p2.y = Math.min(Math.max(p2.y, BOUNDS.top+PLAYER_RADIUS), BOUNDS.bottom-PLAYER_RADIUS);
}
}
}
}
// 足球与球员碰撞(踢球力度+射门倾向)
function handleBallPlayerCollision() {
let allPlayers = ...redTeam, ...blueTeam;
for(let player of allPlayers) {
let dx = ball.x - player.x;
let dy = ball.y - player.y;
let dist = Math.hypot(dx, dy);
let minDist = BALL_RADIUS + PLAYER_RADIUS;
if(dist < minDist) {
let angle = Math.atan2(dy, dx);
let overlap = minDist - dist;
// 把球员轻微推开
let push = overlap * 0.35;
player.x -= Math.cos(angle) * push;
player.y -= Math.sin(angle) * push;
player.x = Math.min(Math.max(player.x, BOUNDS.left+PLAYER_RADIUS), BOUNDS.right-PLAYER_RADIUS);
player.y = Math.min(Math.max(player.y, BOUNDS.top+PLAYER_RADIUS), BOUNDS.bottom-PLAYER_RADIUS);
// 重新计算方向
let dx2 = ball.x - player.x;
let dy2 = ball.y - player.y;
let norm = Math.hypot(dx2, dy2);
if(norm < 0.01) norm = 1;
let dirX = dx2/norm, dirY = dy2/norm;
let playerSpd = Math.hypot(player.vx, player.vy);
let basePower = 4.8 + playerSpd * 0.8;
basePower = Math.min(basePower, 9.8);
let team = redTeam.includes(player) ? 'red' : 'blue';
let kickAngle = angle;
if(team === 'red') {
// 倾向右球门方向 (进攻右侧)
let goalAngle = Math.atan2(H/2 - player.y, (W-45) - player.x);
let mix = 0.62;
kickAngle = angle * (1-mix) + goalAngle * mix + (Math.random()-0.5)*0.35;
} else {
// 倾向左球门方向
let goalAngle = Math.atan2(H/2 - player.y, (45) - player.x);
let mix = 0.62;
kickAngle = angle * (1-mix) + goalAngle * mix + (Math.random()-0.5)*0.35;
}
let force = basePower;
ball.vx = Math.cos(kickAngle) * force + player.vx * 0.55;
ball.vy = Math.sin(kickAngle) * force + player.vy * 0.55;
let ballSpeed = Math.hypot(ball.vx, ball.vy);
let maxBall = 11.5;
if(ballSpeed > maxBall) {
ball.vx = ball.vx/ballSpeed * maxBall;
ball.vy = ball.vy/ballSpeed * maxBall;
}
// 推出重叠区
let sep = (minDist + 1.5) / norm;
ball.x = player.x + dirX * sep;
ball.y = player.y + dirY * sep;
break; // 一次帧只处理一次碰撞防止连环瞬移
}
}
}
// 足球物理 + 边界碰撞 (反弹)
function updateBall() {
if(goalCooldown) return;
ball.x += ball.vx;
ball.y += ball.vy;
ball.vx *= BALL_FRICTION;
ball.vy *= BALL_FRICTION;
// 边界碰撞 (保证球不飞出界)
if(ball.x - BALL_RADIUS < BOUNDS.left) {
ball.x = BOUNDS.left + BALL_RADIUS;
ball.vx = -ball.vx * 0.65;
}
if(ball.x + BALL_RADIUS > BOUNDS.right) {
ball.x = BOUNDS.right - BALL_RADIUS;
ball.vx = -ball.vx * 0.65;
}
if(ball.y - BALL_RADIUS < BOUNDS.top) {
ball.y = BOUNDS.top + BALL_RADIUS;
ball.vy = -ball.vy * 0.65;
}
if(ball.y + BALL_RADIUS > BOUNDS.bottom) {
ball.y = BOUNDS.bottom - BALL_RADIUS;
ball.vy = -ball.vy * 0.65;
}
// 进球判定
checkGoalScored();
}
// ---------- 华丽绘图 ----------
function drawField() {
// 草地
ctx.fillStyle = "#2e9e3b";
ctx.fillRect(0,0,W,H);
ctx.strokeStyle = "#e9f5cf";
ctx.lineWidth = 2.5;
ctx.setLineDash(12,16);
ctx.beginPath();
ctx.moveTo(W/2, 10);
ctx.lineTo(W/2, H-10);
ctx.stroke();
ctx.setLineDash(\[\]);
ctx.beginPath();
ctx.arc(W/2, H/2, 50, 0, Math.PI*2);
ctx.stroke();
// 禁区线装饰
ctx.beginPath();
ctx.rect(LEFT_GOAL.x+8, LEFT_GOAL.y-5, 12, LEFT_GOAL.h+10);
ctx.fillStyle = "#488a4877";
ctx.fill();
ctx.beginPath();
ctx.rect(RIGHT_GOAL.x-5, RIGHT_GOAL.y-5, 12, RIGHT_GOAL.h+10);
ctx.fill();
}
function drawGoals() {
// 左侧球门 (带网纹)
ctx.fillStyle = "#D9B382";
ctx.shadowBlur = 0;
ctx.fillRect(LEFT_GOAL.x-2, LEFT_GOAL.y-4, LEFT_GOAL.w+4, LEFT_GOAL.h+8);
ctx.fillStyle = "#B87C3A";
ctx.fillRect(LEFT_GOAL.x, LEFT_GOAL.y, LEFT_GOAL.w, LEFT_GOAL.h);
ctx.fillStyle = "#FFE1A0aa";
for(let i=0;i<6;i++) {
ctx.fillRect(LEFT_GOAL.x+4, LEFT_GOAL.y+8 + i*17, 12, 6);
}
// 右侧球门
ctx.fillStyle = "#D9B382";
ctx.fillRect(RIGHT_GOAL.x-2, RIGHT_GOAL.y-4, RIGHT_GOAL.w+4, RIGHT_GOAL.h+8);
ctx.fillStyle = "#B87C3A";
ctx.fillRect(RIGHT_GOAL.x, RIGHT_GOAL.y, RIGHT_GOAL.w, RIGHT_GOAL.h);
ctx.fillStyle = "#FFE1A0aa";
for(let i=0;i<6;i++) {
ctx.fillRect(RIGHT_GOAL.x+4, RIGHT_GOAL.y+8 + i*17, 12, 6);
}
ctx.fillStyle = "#FFF5CF";
ctx.font = "bold 20px monospace";
ctx.fillText("🚪", LEFT_GOAL.x-5, LEFT_GOAL.y+LEFT_GOAL.h/2+8);
ctx.fillText("🚪", RIGHT_GOAL.x+RIGHT_GOAL.w-9, RIGHT_GOAL.y+RIGHT_GOAL.h/2+8);
}
function drawPlayers() {
for(let p of redTeam) {
ctx.beginPath();
ctx.arc(p.x, p.y, PLAYER_RADIUS-1, 0, Math.PI*2);
ctx.fillStyle = "#E63E2E";
ctx.fill();
ctx.strokeStyle = "#FFA07A";
ctx.lineWidth = 2;
ctx.stroke();
ctx.fillStyle = "white";
ctx.font = "bold 16px 'Segoe UI'";
ctx.fillText("⚡", p.x-7, p.y+6);
}
for(let p of blueTeam) {
ctx.beginPath();
ctx.arc(p.x, p.y, PLAYER_RADIUS-1, 0, Math.PI*2);
ctx.fillStyle = "#3B7BFF";
ctx.fill();
ctx.strokeStyle = "#B8D4FF";
ctx.stroke();
ctx.fillStyle = "white";
ctx.fillText("✦", p.x-6, p.y+6);
}
}
function drawBall() {
ctx.beginPath();
ctx.arc(ball.x, ball.y, BALL_RADIUS, 0, Math.PI*2);
ctx.fillStyle = "#FDF5C9";
ctx.fill();
ctx.strokeStyle = "#4a3b1c";
ctx.lineWidth = 1.8;
ctx.stroke();
ctx.beginPath();
ctx.moveTo(ball.x-4, ball.y-3);
ctx.lineTo(ball.x+4, ball.y+3);
ctx.moveTo(ball.x+4, ball.y-3);
ctx.lineTo(ball.x-4, ball.y+3);
ctx.stroke();
ctx.fillStyle = "#332211";
ctx.beginPath();
ctx.arc(ball.x-2, ball.y-2, 1.6, 0, Math.PI*2);
ctx.fill();
ctx.fillStyle = "white";
ctx.beginPath();
ctx.arc(ball.x-3, ball.y-3, 1, 0, Math.PI*2);
ctx.fill();
}
function drawStatusText() {
if(goalCooldown) {
ctx.font = "bold 28px 'Segoe UI'";
ctx.fillStyle = "#FFEDB5";
ctx.shadowBlur = 8;
ctx.fillText("⚽ 进球! ⚽", W/2-100, 55);
ctx.shadowBlur = 0;
}
}
// 主循环
function updateAndRender() {
if(!goalCooldown) {
updatePlayersMovement();
resolveCollisionPlayers();
updateBall();
handleBallPlayerCollision(); // 踢球核心
// 二次确保边界不溢出
ball.x = Math.min(Math.max(ball.x, BOUNDS.left+BALL_RADIUS), BOUNDS.right-BALL_RADIUS);
ball.y = Math.min(Math.max(ball.y, BOUNDS.top+BALL_RADIUS), BOUNDS.bottom-BALL_RADIUS);
}
drawField();
drawGoals();
drawPlayers();
drawBall();
drawStatusText();
requestAnimationFrame(updateAndRender);
}
// ---------- 初始化队伍 & 启动 ----------
function initTeams() {
redTeam = \[\];
blueTeam = \[\];
for(let i=0; i<PLAYERS_COUNT; i++) {
redTeam.push({ x: 180, y: H/2, vx: 0, vy: 0 });
blueTeam.push({ x: W-180, y: H/2, vx: 0, vy: 0 });
}
resetPositions(true);
}
// 重置按钮监听
document.getElementById('resetMatchBtn').addEventListener('click', () => {
if(goalCooldown) return;
fullMatchReset();
});
initTeams();
updateUI();
updateAndRender();
})();
</script>
</body>
</html>