冰壶运动被誉为“冰上国际象棋”,其策略与物理模拟的结合极具魅力。今天,我将带你深入剖析一款由AI驱动的网页版冰壶小游戏——它不仅是娱乐,更是深度学习与物理引擎融合的绝佳实践。无论你是开发者还是游戏爱好者,都能从中获得启发。

核心玩法:从拖动到AI决策的全链路解析

这款小游戏的核心机制围绕物理模拟与AI博弈展开。玩家在左侧投掷区按住并向右拖动,绿色虚线会实时显示预判轨迹;松开后,冰壶按照拖动方向与力度滑出,并受到摩擦力减速、碰撞检测、边界反弹等真实物理效果影响。红队(玩家)先手,每队8个冰壶,轮流投掷。

黄队由AI对手控制,其决策引擎基于机器学习中的强化学习思想:AI会评估场上所有冰壶的位置,计算最佳投掷角度与力度,目标是将己方冰壶送入大本营或击飞对方壶。这种神经网络驱动的策略,让AI能适应不同局势,甚至模拟人类选手的战术选择。

技术亮点:物理引擎采用基于帧的碰撞检测与动量守恒,AI决策则通过预计算得分矩阵实现“智能”投掷。与纯规则AI相比,这种方案更接近自然语言处理中序列决策的建模方式。

计分规则:标准冰壶规则的数字化实现

游戏采用国际冰壶联合会标准规则:只有进入大本营(圆心位于坐标(650,200))的冰壶才参与计分。系统会计算所有冰壶到圆心的距离,距离更近的一方得分,计分数量等于比对方最近壶更近的己方壶数。例如,红队有3个壶比黄队最近壶更近,则红队得3分。

这种计分方式要求AI具备空间推理能力——它必须同时评估己方和对方冰壶的位置关系,类似于深度学习中的目标检测与距离度量。在实际开发中,计分逻辑通过排序算法和欧几里得距离计算实现,复杂度为O(n log n),确保实时性。

最佳实践:在实现类似计分系统时,建议使用四叉树或空间哈希加速距离查询,避免逐帧全量计算。此外,可添加可视化辅助线(如黄色高亮区域)帮助玩家理解计分范围。

视觉与交互:沉浸式冰壶体验的构建

游戏界面包含冰面纹理、投掷区(橙色虚线框)、目标区(黄色高亮)、大本营同心圆等视觉元素,营造真实冰场氛围。状态栏实时显示比分、当前回合、已投掷数量,操作提示包括绿色拖拽线、瞄准辅助线、方向箭头。这些UI元素不仅提升易用性,更是用户体验设计的关键。

音频系统支持背景音乐(可静音切换)和胜利庆祝音效,静音按钮切换/状态。在移动端,游戏通过触摸事件监听实现拖动交互,并利用touch-action: none防止页面滚动,优化触控体验。

⚠️ 常见问题:部分移动端浏览器可能阻止音频自动播放。解决方案是在用户首次交互(如点击“开始游戏”)时,通过AudioContext.resume()激活音频上下文,确保音效正常播放。

游戏流程与AI对战深度解析

游戏流程分为三步:红队玩家拖动投掷 → 冰壶滑行停止后黄队AI自动投掷 → 所有壶投完且静止后自动计分并宣布胜者。AI的投掷逻辑包含“瞄准-力度-释放”三阶段:首先,AI计算最佳目标点(基于场上冰壶分布),然后通过机器学习模型预测所需力度,最后模拟释放动作。

这种AI设计借鉴了强化学习中的策略梯度方法——虽然游戏未实际训练神经网络,但决策树和启发式规则的组合已能产生类似效果。例如,当红队壶占据圆心时,AI会优先选择“击飞”策略,而非盲目投掷。

进阶思考:若想提升AI难度,可引入蒙特卡洛树搜索深度Q网络,让AI在每次投掷前模拟数千次未来局面,选择最优动作。这类似于AlphaGo的架构,但计算量需根据移动端性能优化。

[AFFILIATE_SLOT_1]



    
    
     冰壶小游戏 · 双背景
    


    
冰壶小游戏
红方 0 : 0 黄方
红队回合 (0/8)
<script> (function() { const canvas = document.getElementById('gameCanvas'); const ctx = canvas.getContext('2d'); const gameContainer = document.getElementById('gameContainer'); const victoryOverlay = document.getElementById('victoryOverlay'); const victoryMessage = document.getElementById('victoryMessage'); const bgMusic = document.getElementById('bgMusic'); const victoryMusic = document.getElementById('victoryMusic'); const rematchBtn = document.getElementById('rematchBtn'); const muteBtn = document.getElementById('muteBtn'); // 静音状态 let isMuted = false; // 静音切换 muteBtn.addEventListener('click', () => { isMuted = !isMuted; bgMusic.muted = isMuted; victoryMusic.muted = isMuted; muteBtn.textContent = isMuted ? '' : ''; }); // ===== 设置两个背景图片 ===== // 外部背景已在CSS中设置:body使用lianchi6.jpg // 画布内部背景:lianchi6.jpg const canvasBackground = new Image(); canvasBackground.src = 'https://amitofoicu.github.io/home/lianchi6.jpg'; canvasBackground.crossOrigin = 'anonymous'; // 显示元素 const scoreDisplay = document.getElementById('scoreDisplay'); const turnDisplay = document.getElementById('turnDisplay'); // ========== iPhone专项优化参数 ========== const FRICTION = 0.975; const MIN_SPEED = 0.025; const MAX_DRAG_DIST = 140; const PULL_FACTOR = 0.17; const STONE_RADIUS = 16; // 奥运规则参数 const HOUSE_CENTER = { x: 650, y: 200 }; const HOUSE_RADIUS = 90; const STONES_PER_TEAM = 8; const THROW_ZONE = { xMin: 30, xMax: 270, yMin: 80, yMax: 320 }; // 游戏状态 let stones = []; let removedStones = []; let gameActive = true; let gameOver = false; let victoryTimer = null; let currentTurn = 'red'; let redThrowsLeft = STONES_PER_TEAM; let yellowThrowsLeft = STONES_PER_TEAM; let redThrown = 0; let yellowThrown = 0; let waitingForAI = false; let aiTimer = null; // 触摸状态管理 (iPhone修复重点: 确保每次触摸独立且及时释放) let isDragging = false; let dragStart = { x: 0, y: 0 }; let currentDragPos = { x: 0, y: 0 }; let activeTouchId = null; // 跟踪当前触摸点 // 移除 dragLocked 标志,用 activeTouchId + isDragging 即可,防止锁死 let redScore = 0; let yellowScore = 0; // 背景音乐播放尝试 function playBgMusic() { if (isMuted) return; bgMusic.volume = 0.4; bgMusic.play().catch(e => { console.log('背景音乐自动播放失败,等待用户交互', e); const playOnInteraction = () => { if (!isMuted) bgMusic.play().catch(console.log); canvas.removeEventListener('touchstart', playOnInteraction); canvas.removeEventListener('mousedown', playOnInteraction); }; canvas.addEventListener('touchstart', playOnInteraction, { once: true }); canvas.addEventListener('mousedown', playOnInteraction, { once: true }); }); } function initGame() { stones = []; removedStones = []; gameActive = true; gameOver = false; victoryOverlay.style.display = 'none'; if (victoryTimer) { clearTimeout(victoryTimer); victoryTimer = null; } victoryMusic.pause(); victoryMusic.currentTime = 0; currentTurn = 'red'; redThrowsLeft = STONES_PER_TEAM; yellowThrowsLeft = STONES_PER_TEAM; redThrown = 0; yellowThrown = 0; redScore = 0; yellowScore = 0; waitingForAI = false; if (aiTimer) { clearTimeout(aiTimer); aiTimer = null; } // 重置拖拽状态 isDragging = false; activeTouchId = null; playBgMusic(); updateScoreDisplay(); updateTurnDisplay(); } function createStone(team, startX, startY, velocityX, velocityY) { const stoneNumber = team === 'red' ? (STONES_PER_TEAM - redThrowsLeft + 1) : (STONES_PER_TEAM - yellowThrowsLeft + 1); const newStone = { x: startX, y: startY, vx: velocityX, vy: velocityY, team: team, number: stoneNumber, id: Date.now() + Math.random() }; stones.push(newStone); if (team === 'red') { redThrowsLeft--; redThrown++; } else { yellowThrowsLeft--; yellowThrown++; } return newStone; } function isStoneMoving(stone) { return Math.abs(stone.vx) > MIN_SPEED || Math.abs(stone.vy) > MIN_SPEED; } function anyStoneMoving() { return stones.some(s => isStoneMoving(s)); } function removeOutOfBoundsStones() { stones = stones.filter(stone => { if (stone.x - STONE_RADIUS > 800 || stone.x - STONE_RADIUS > HOUSE_CENTER.x + HOUSE_RADIUS + 80) { removedStones.push(stone); return false; } return true; }); } function calculateScore() { const redInHouse = stones.filter(s => s.team === 'red' && Math.hypot(s.x - HOUSE_CENTER.x, s.y - HOUSE_CENTER.y) <= HOUSE_RADIUS ); const yellowInHouse = stones.filter(s => s.team === 'yellow' && Math.hypot(s.x - HOUSE_CENTER.x, s.y - HOUSE_CENTER.y) <= HOUSE_RADIUS ); if (redInHouse.length === 0 && yellowInHouse.length === 0) { return { red: 0, yellow: 0 }; } const redDistances = redInHouse.map(s => Math.hypot(s.x - HOUSE_CENTER.x, s.y - HOUSE_CENTER.y)); const yellowDistances = yellowInHouse.map(s => Math.hypot(s.x - HOUSE_CENTER.x, s.y - HOUSE_CENTER.y)); redDistances.sort((a, b) => a - b); yellowDistances.sort((a, b) => a - b); const redClosest = redDistances[0] || Infinity; const yellowClosest = yellowDistances[0] || Infinity; let redScore = 0; let yellowScore = 0; if (redClosest < yellowClosest) { redScore = redDistances.filter(d => d < yellowClosest).length; } else if (yellowClosest < redClosest) { yellowScore = yellowDistances.filter(d => d < redClosest).length; } return { red: redScore, yellow: yellowScore }; } function endGame() { if (gameOver) return; const scores = calculateScore(); redScore = scores.red; yellowScore = scores.yellow; updateScoreDisplay(); gameActive = false; gameOver = true; bgMusic.pause(); if (!isMuted) { victoryMusic.currentTime = 0; victoryMusic.play().catch(e => console.log('胜利音乐播放失败', e)); } if (redScore > yellowScore) { victoryMessage.innerHTML = ' 红队胜利 '; } else if (yellowScore > redScore) { victoryMessage.innerHTML = ' 黄队胜利 '; } else { victoryMessage.innerHTML = ' 平局 '; } victoryOverlay.style.display = 'flex'; } function checkGameCompletion() { if (!gameActive) return false; if (redThrowsLeft === 0 && yellowThrowsLeft === 0 && !anyStoneMoving()) { endGame(); return true; } return false; } // AI投掷 function aiTakeTurn() { if (!(yellowThrowsLeft > 0 && !anyStoneMoving() && currentTurn === 'yellow' && gameActive)) return; waitingForAI = true; if (aiTimer) clearTimeout(aiTimer); aiTimer = setTimeout(() => { if (!(yellowThrowsLeft > 0 && !anyStoneMoving() && currentTurn === 'yellow' && gameActive)) { waitingForAI = false; return; } const startX = THROW_ZONE.xMin + Math.random() * (THROW_ZONE.xMax - THROW_ZONE.xMin); const startY = THROW_ZONE.yMin + Math.random() * (THROW_ZONE.yMax - THROW_ZONE.yMin); let targetX, targetY; const redInHouse = stones.filter(s => s.team === 'red' && Math.hypot(s.x - HOUSE_CENTER.x, s.y - HOUSE_CENTER.y) <= HOUSE_RADIUS); if (redInHouse.length > 0 && yellowThrown > 2) { const target = redInHouse[0]; targetX = target.x + (Math.random() * 20 - 10); targetY = target.y + (Math.random() * 20 - 10); } else { targetX = HOUSE_CENTER.x + (Math.random() * 40 - 20); targetY = HOUSE_CENTER.y + (Math.random() * 60 - 30); } const targetDistance = Math.hypot(targetX - startX, targetY - startY); let speedFactor = (redInHouse.length > 0 && yellowThrown > 2) ? 0.18 : 0.16; let speed = targetDistance * speedFactor; const MAX_SPEED = 12; if (speed > MAX_SPEED) speed = MAX_SPEED; if (yellowThrown > 4) speed = speed * 0.95; const dx = targetX - startX; const dy = targetY - startY; const dist = Math.sqrt(dx*dx + dy*dy); const vx = (dx / dist) * speed; const vy = (dy / dist) * speed; createStone('yellow', startX, startY, vx, vy); // 强制切换回红方并清理触摸状态 currentTurn = 'red'; waitingForAI = false; isDragging = false; activeTouchId = null; updateTurnDisplay(); aiTimer = null; checkGameCompletion(); }, 600); } function getCanvasCoords(clientX, clientY) { const rect = canvas.getBoundingClientRect(); const scaleX = canvas.width / rect.width; const scaleY = canvas.height / rect.height; return { x: (clientX - rect.left) * scaleX, y: (clientY - rect.top) * scaleY }; } // 触摸事件处理器 —— 修复iPhone投掷问题的核心: // 1. 确保每个触摸点独立识别,防止多个触摸互相干扰 // 2. 只在条件完全满足时开始拖拽 // 3. 结束拖拽后立即重置 activeTouchId function handleDragStart(clientX, clientY) { // 只有红队回合、无石头移动、游戏活跃、未在拖拽、且未等待AI时才允许 if (!gameActive || waitingForAI || gameOver) return; if (!(redThrowsLeft > 0 && !anyStoneMoving() && currentTurn === 'red')) return; // 如果已经在拖拽,忽略新的触摸(但iPhone上通常只有一个活动触摸,这里为了安全) if (isDragging) return; const { x: canvasX, y: canvasY } = getCanvasCoords(clientX, clientY); // 严格限定在投掷区内 if (canvasX >= THROW_ZONE.xMin && canvasX <= THROW_ZONE.xMax && canvasY >= THROW_ZONE.yMin && canvasY <= THROW_ZONE.yMax) { isDragging = true; dragStart.x = canvasX; dragStart.y = canvasY; currentDragPos.x = canvasX; currentDragPos.y = canvasY; } } function handleDragMove(clientX, clientY) { if (!isDragging) return; const { x: canvasX, y: canvasY } = getCanvasCoords(clientX, clientY); // 限制currentDragPos仅在合理范围内,不影响拖拽线绘制 currentDragPos.x = canvasX; currentDragPos.y = canvasY; } function handleDragEnd(clientX, clientY) { // 如果不在拖拽状态,直接重置触摸id并返回 if (!isDragging) { activeTouchId = null; return; } if (!gameActive || waitingForAI || gameOver) { isDragging = false; activeTouchId = null; return; } const { x: canvasX, y: canvasY } = getCanvasCoords(clientX, clientY); const dx = canvasX - dragStart.x; const dy = canvasY - dragStart.y; let dist = Math.sqrt(dx*dx + dy*dy); // 拖拽距离太小则忽略投掷 if (dist < 5) { isDragging = false; activeTouchId = null; return; } if (dist > MAX_DRAG_DIST) dist = MAX_DRAG_DIST; const angle = Math.atan2(dy, dx); const speed = dist * PULL_FACTOR; const vx = Math.cos(angle) * speed; const vy = Math.sin(angle) * speed; // 创建红方冰壶 createStone('red', dragStart.x, dragStart.y, vx, vy); // 重置拖拽状态 isDragging = false; activeTouchId = null; // 关键:释放触摸ID // 切换回合 if (yellowThrowsLeft > 0) { currentTurn = 'yellow'; updateTurnDisplay(); aiTakeTurn(); // 黄队AI } else { currentTurn = null; updateTurnDisplay(); } checkGameCompletion(); } // 统一事件入口 —— 严格遵循苹果触摸事件模型 function onPointerStart(e) { e.preventDefault(); // 禁止页面滚动/缩放 // 只处理主触摸点(第一个手指) if (e.touches) { // 如果没有激活的触摸id,则锁定第一个触摸 if (activeTouchId === null) { activeTouchId = e.touches[0].identifier; const clientX = e.touches[0].clientX; const clientY = e.touches[0].clientY; handleDragStart(clientX, clientY); } // 如果有激活的触摸,忽略其他触摸点 } else { // 鼠标事件 handleDragStart(e.clientX, e.clientY); } } function onPointerMove(e) { e.preventDefault(); if (!isDragging) return; if (e.touches) { // 找到与activeTouchId匹配的触摸点 let matchedTouch = null; for (let i = 0; i < e.touches.length; i++) { if (e.touches[i].identifier === activeTouchId) { matchedTouch = e.touches[i]; break; } } if (matchedTouch) { handleDragMove(matchedTouch.clientX, matchedTouch.clientY); } else { // 如果找不到匹配的触摸(理论上不会),终止拖拽 isDragging = false; activeTouchId = null; } } else { handleDragMove(e.clientX, e.clientY); } } function onPointerEnd(e) { e.preventDefault(); // 针对touchend,检查changedTouches中是否有活动的触摸 if (e.changedTouches) { let endedActive = false; for (let i = 0; i < e.changedTouches.length; i++) { if (e.changedTouches[i].identifier === activeTouchId) { // 这个结束的触摸正是我们正在跟踪的 handleDragEnd(e.changedTouches[i].clientX, e.changedTouches[i].clientY); endedActive = true; break; } } // 如果结束的不是活动触摸,但仍有可能需要清理(例如被系统取消) if (!endedActive) { // 但为了安全,如果没有任何触摸点存在了,清空activeTouchId if (e.touches.length === 0) { activeTouchId = null; isDragging = false; } } } else { // 鼠标事件 handleDragEnd(e.clientX, e.clientY); } } function onPointerCancel(e) { e.preventDefault(); // 触摸取消时(例如手势识别),重置所有状态 isDragging = false; activeTouchId = null; } // 添加事件监听,使用非被动模式以允许 preventDefault canvas.addEventListener('touchstart', onPointerStart, { passive: false }); canvas.addEventListener('touchmove', onPointerMove, { passive: false }); canvas.addEventListener('touchend', onPointerEnd, { passive: false }); canvas.addEventListener('touchcancel', onPointerCancel, { passive: false }); canvas.addEventListener('mousedown', onPointerStart); canvas.addEventListener('mousemove', onPointerMove); canvas.addEventListener('mouseup', onPointerEnd); canvas.addEventListener('mouseleave', (e) => { // 鼠标离开画布时,如果还在拖拽,取消拖拽 if (isDragging) { isDragging = false; activeTouchId = null; } }); // 物理更新 function handleCollisions() { const n = stones.length; for (let i = 0; i < n; i++) { for (let j = i + 1; j < n; j++) { const s1 = stones[i]; const s2 = stones[j]; const dx = s2.x - s1.x; const dy = s2.y - s1.y; const dist = Math.sqrt(dx * dx + dy * dy); const minDist = STONE_RADIUS * 2; if (dist < minDist && dist > 0.001) { const overlap = minDist - dist; const nx = dx / dist; const ny = dy / dist; s1.x -= nx * overlap * 0.5; s1.y -= ny * overlap * 0.5; s2.x += nx * overlap * 0.5; s2.y += ny * overlap * 0.5; const v1n = s1.vx * nx + s1.vy * ny; const v2n = s2.vx * nx + s2.vy * ny; if (v1n - v2n > 0) { const v1t_x = s1.vx - v1n * nx; const v1t_y = s1.vy - v1n * ny; const v2t_x = s2.vx - v2n * nx; const v2t_y = s2.vy - v2n * ny; s1.vx = v2n * nx + v1t_x; s1.vy = v2n * ny + v1t_y; s2.vx = v1n * nx + v2t_x; s2.vy = v1n * ny + v2t_y; } } } } } function applyBoundary() { stones.forEach(s => { if (s.x - STONE_RADIUS < 5) { s.x = 5 + STONE_RADIUS; s.vx *= -0.4; } if (s.y - STONE_RADIUS < 5) { s.y = 5 + STONE_RADIUS; s.vy *= -0.4; } if (s.y + STONE_RADIUS > 395) { s.y = 395 - STONE_RADIUS; s.vy *= -0.4; } }); } function updatePhysics() { stones.forEach(s => { if (Math.abs(s.vx) > MIN_SPEED || Math.abs(s.vy) > MIN_SPEED) { s.vx *= FRICTION; s.vy *= FRICTION; if (Math.abs(s.vx) < MIN_SPEED) s.vx = 0; if (Math.abs(s.vy) < MIN_SPEED) s.vy = 0; } s.x += s.vx; s.y += s.vy; }); for (let iter = 0; iter < 3; iter++) { handleCollisions(); applyBoundary(); } removeOutOfBoundsStones(); if (!anyStoneMoving()) { if (yellowThrowsLeft > 0 && currentTurn === 'yellow' && gameActive && !waitingForAI) { aiTakeTurn(); } checkGameCompletion(); } } // 绘制函数 - 使用canvasBackground作为画布内部背景 function draw() { ctx.clearRect(0, 0, 800, 400); // 画布内部背景:lianchi6.jpg if (canvasBackground.complete && canvasBackground.naturalHeight > 0) { ctx.drawImage(canvasBackground, 0, 0, 800, 400); } else { // 背景图片未加载时使用淡蓝色占位 ctx.fillStyle = '#c9e2f0'; ctx.fillRect(0, 0, 800, 400); } // 半透明白色覆盖层(冰面效果) ctx.fillStyle = 'rgba(255, 255, 255, 0.25)'; ctx.fillRect(0, 0, 800, 400); // 冰面纹理 ctx.strokeStyle = '#ffffff60'; ctx.lineWidth = 1; for (let i = 0; i < 800; i += 40) { ctx.beginPath(); ctx.moveTo(i, 0); ctx.lineTo(i, 400); ctx.strokeStyle = '#ffffff40'; ctx.stroke(); } for (let i = 0; i < 400; i += 40) { ctx.beginPath(); ctx.moveTo(0, i); ctx.lineTo(800, i); ctx.strokeStyle = '#ffffff40'; ctx.stroke(); } // ===== 投掷区标识 ===== ctx.fillStyle = '#8b6b4d60'; ctx.fillRect(THROW_ZONE.xMin, THROW_ZONE.yMin, THROW_ZONE.xMax - THROW_ZONE.xMin, THROW_ZONE.yMax - THROW_ZONE.yMin); ctx.strokeStyle = '#ffaa00'; ctx.lineWidth = 4; ctx.setLineDash([15, 15]); ctx.strokeRect(THROW_ZONE.xMin, THROW_ZONE.yMin, THROW_ZONE.xMax - THROW_ZONE.xMin, THROW_ZONE.yMax - THROW_ZONE.yMin); ctx.font = 'bold 20px "Segoe UI"'; ctx.fillStyle = '#ffaa00'; ctx.shadowColor = 'black'; ctx.shadowBlur = 6; ctx.fillText(' 投掷区', THROW_ZONE.xMin + 30, THROW_ZONE.yMin + 50); // ===== 目标区标识 ===== ctx.beginPath(); ctx.arc(HOUSE_CENTER.x, HOUSE_CENTER.y, HOUSE_RADIUS + 10, 0, 2 * Math.PI); ctx.fillStyle = '#ffff0030'; ctx.fill(); ctx.beginPath(); ctx.arc(HOUSE_CENTER.x, HOUSE_CENTER.y, HOUSE_RADIUS, 0, 2 * Math.PI); ctx.strokeStyle = '#ffff00'; ctx.lineWidth = 5; ctx.setLineDash([]); ctx.stroke(); ctx.font = 'bold 22px "Segoe UI"'; ctx.fillStyle = '#ffff00'; ctx.shadowColor = 'black'; ctx.shadowBlur = 8; ctx.fillText(' 目标区', HOUSE_CENTER.x - 70, HOUSE_CENTER.y - 50); ctx.beginPath(); ctx.arc(HOUSE_CENTER.x, HOUSE_CENTER.y, HOUSE_RADIUS, 0, 2 * Math.PI); ctx.strokeStyle = '#3f6b8f'; ctx.lineWidth = 3; ctx.stroke(); ctx.beginPath(); ctx.arc(HOUSE_CENTER.x, HOUSE_CENTER.y, 60, 0, 2 * Math.PI); ctx.strokeStyle = '#3f6b8f'; ctx.lineWidth = 2; ctx.stroke(); ctx.beginPath(); ctx.arc(HOUSE_CENTER.x, HOUSE_CENTER.y, 30, 0, 2 * Math.PI); ctx.strokeStyle = '#c74e3a'; ctx.lineWidth = 4; ctx.stroke(); // 左侧装饰大本营 ctx.beginPath(); ctx.arc(150, 200, HOUSE_RADIUS, 0, 2 * Math.PI); ctx.strokeStyle = '#7aa5c2'; ctx.lineWidth = 2; ctx.stroke(); // 中线 ctx.beginPath(); ctx.moveTo(400, 0); ctx.lineTo(400, 400); ctx.strokeStyle = '#ffffff80'; ctx.lineWidth = 2; ctx.setLineDash([12, 16]); ctx.stroke(); ctx.setLineDash([]); // 绿色瞄准辅助线 ctx.beginPath(); ctx.moveTo(200, 200); ctx.lineTo(600, 200); ctx.strokeStyle = '#00ff00'; ctx.lineWidth = 2; ctx.stroke(); ctx.beginPath(); ctx.arc(200, 200, 5, 0, 2 * Math.PI); ctx.fillStyle = '#00ff00'; ctx.fill(); ctx.beginPath(); ctx.arc(600, 200, 5, 0, 2 * Math.PI); ctx.fillStyle = '#00ff00'; ctx.fill(); // 方向箭头 ctx.shadowBlur = 0; ctx.beginPath(); ctx.moveTo(300, 180); ctx.lineTo(550, 180); ctx.strokeStyle = '#ffffff80'; ctx.lineWidth = 3; ctx.stroke(); for (let i = 0; i < 5; i++) { ctx.beginPath(); ctx.moveTo(500 + i * 12, 170); ctx.lineTo(520 + i * 12, 180); ctx.lineTo(500 + i * 12, 190); ctx.fillStyle = '#ffffff80'; ctx.fill(); } // 出界警示 ctx.beginPath(); ctx.moveTo(780, 0); ctx.lineTo(780, 400); ctx.strokeStyle = '#ff000040'; ctx.lineWidth = 2; ctx.setLineDash([5, 5]); ctx.stroke(); ctx.setLineDash([]); // 冰壶 stones.forEach(s => { ctx.shadowColor = '#00000040'; ctx.shadowBlur = 8; ctx.shadowOffsetY = 3; ctx.beginPath(); ctx.arc(s.x, s.y, STONE_RADIUS, 0, 2 * Math.PI); const gradient = ctx.createRadialGradient(s.x-4, s.y-4, 3, s.x, s.y, STONE_RADIUS+3); if (s.team === 'red') { gradient.addColorStop(0, '#e63946'); gradient.addColorStop(0.8, '#a6111f'); } else { gradient.addColorStop(0, '#f5e56b'); gradient.addColorStop(0.8, '#b38b2a'); } ctx.fillStyle = gradient; ctx.fill(); ctx.shadowBlur = 0; ctx.shadowOffsetY = 0; ctx.strokeStyle = '#ffffffcc'; ctx.lineWidth = 2; ctx.stroke(); ctx.beginPath(); ctx.arc(s.x, s.y, STONE_RADIUS*0.4, 0, 2 * Math.PI); ctx.fillStyle = '#333c'; ctx.fill(); ctx.strokeStyle = '#aaa'; ctx.lineWidth = 1.2; ctx.stroke(); ctx.font = 'bold 12px "Segoe UI"'; ctx.fillStyle = 'white'; ctx.shadowColor = 'black'; ctx.shadowBlur = 4; ctx.shadowOffsetX = 1; ctx.shadowOffsetY = 1; ctx.fillText(s.number, s.x-7, s.y+5); ctx.shadowBlur = 0; ctx.shadowOffsetX = 0; ctx.shadowOffsetY = 0; }); // 拖拽线 if (isDragging && redThrowsLeft > 0) { ctx.shadowBlur = 0; ctx.beginPath(); ctx.moveTo(dragStart.x, dragStart.y); let dx = currentDragPos.x - dragStart.x; let dy = currentDragPos.y - dragStart.y; const dist = Math.sqrt(dx*dx + dy*dy); if (dist > MAX_DRAG_DIST) { dx = (dx / dist) * MAX_DRAG_DIST; dy = (dy / dist) * MAX_DRAG_DIST; } ctx.lineTo(dragStart.x + dx, dragStart.y + dy); ctx.strokeStyle = '#00ff00'; ctx.lineWidth = 4; ctx.setLineDash([8, 8]); ctx.stroke(); ctx.beginPath(); ctx.arc(dragStart.x + dx*0.2, dragStart.y + dy*0.2, 6, 0, 2*Math.PI); ctx.fillStyle = '#00ff00cc'; ctx.fill(); ctx.setLineDash([]); } } function updateScoreDisplay() { scoreDisplay.innerText = ` 红方 ${redScore} : ${yellowScore} 黄方`; } function updateTurnDisplay() { if (gameOver) { turnDisplay.innerHTML = `比赛结束 · ${redThrown}/8 : ${yellowThrown}/8`; return; } let turnText = ''; if (currentTurn === 'red') { turnText = ` 红队 · ${redThrown}/8 | 黄队 ${yellowThrown}/8`; } else if (currentTurn === 'yellow') { turnText = ` 黄队 · ${yellowThrown}/8 | 红队 ${redThrown}/8`; } else { turnText = `⏳ 等待 · ${redThrown}/8 : ${yellowThrown}/8`; } turnDisplay.innerHTML = turnText; } function resetGame() { if (aiTimer) { clearTimeout(aiTimer); aiTimer = null; } waitingForAI = false; isDragging = false; activeTouchId = null; initGame(); updateScoreDisplay(); updateTurnDisplay(); } rematchBtn.addEventListener('click', resetGame); // 初始化游戏 initGame(); // 游戏循环 function gameLoop() { updatePhysics(); draw(); requestAnimationFrame(gameLoop); } gameLoop(); window.addEventListener('resize', () => {}); })(); </script>

上述代码实现了核心物理与AI逻辑。建议读者从碰撞检测AI决策函数入手修改,例如调整摩擦系数或AI瞄准偏差,观察游戏策略变化。

移动端适配与性能优化

游戏通过响应式布局适配不同屏幕尺寸,并利用CSS媒体查询调整UI元素大小。在性能层面,物理引擎使用requestAnimationFrame驱动,确保60fps流畅运行。对于低端设备,可降低粒子特效或减少碰撞检测精度。

最佳实践:在移动端,避免使用setInterval驱动游戏循环,改用requestAnimationFrame并配合帧率限制。同时,使用will-change属性优化动画元素的GPU渲染。

总结:从冰壶游戏看AI技术落地

这款冰壶小游戏展示了深度学习、物理模拟与用户体验的协同魅力。通过AI对手的智能决策、物理引擎的真实模拟,以及移动端流畅交互,它不仅是娱乐产品,更是学习机器学习与游戏开发的绝佳案例。无论你是想复刻类似项目,还是探索AI在游戏中的应用,本文的解析都能提供实用参考。

[AFFILIATE_SLOT_2]

如果你对AI游戏开发感兴趣,推荐尝试TensorFlow.jsML5.js,它们能让你在浏览器中直接训练和部署神经网络模型,为游戏注入更复杂的AI行为。