DeepSeek生成的网页版小游戏 - 冰壶

冰壶小游戏

🎮 核心玩法功能

  1. 投掷冰壶:在左侧投掷区按住并向右拖动(显示绿色虚线),松开后冰壶按拖动方向力度滑出

  2. 红黄两队对抗:红队先手,每队8个冰壶,轮流投掷

  3. 物理模拟:冰壶具有摩擦力减速、碰撞检测、边界反弹等真实物理效果

  4. AI对手:黄队由电脑AI控制,会根据场上情况智能选择投掷目标

🎯 计分规则

  • 采用冰壶运动标准规则,以冰壶距离大本营(圆心650,200)的距离计分

  • 只有在大本营内的冰壶参与计分

  • 距离圆心更近的一方得分,计分数量为比对方最近壶更近的壶数

🎨 游戏界面

  • 视觉元素:冰面纹理、投掷区(橙色虚线框)、目标区(黄色高亮)、大本营同心圆

  • 状态显示:实时比分、当前回合、已投掷数量

  • 操作提示:绿色拖拽线、瞄准辅助线、方向箭头

🔊 音频系统

  • 背景音乐(可静音切换)

  • 胜利时播放庆祝音乐

  • 静音按钮切换🔊/🔇状态

🏆 游戏流程

  1. 红队玩家在投掷区拖动投掷

  2. 冰壶滑行停止后,黄队AI自动投掷

  3. 所有冰壶投完且静止后,自动计算得分并宣布胜者

  4. 可通过"再来一局"按钮重新开始

📱 移动端适配

  • 支持触摸操作

  • 响应式布局,适配不同屏幕尺寸

  • 防止页面滚动,优化触控体验

html 复制代码
<!DOCTYPE html>
<html lang="zh">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
    <title>🥌 冰壶小游戏</title>
    <style>
        * {
            box-sizing: border-box;
            user-select: none;
            -webkit-tap-highlight-color: transparent;
        }
        body {
            margin: 0;
            min-height: 100vh;
            background: linear-gradient(145deg, #0b2b44 0%, #1c4e72 100%);
            display: flex;
            align-items: center;
            justify-content: center;
            font-family: 'Segoe UI', Roboto, system-ui, -apple-system, sans-serif;
            padding: 10px;
        }
        .game-wrapper {
            width: 100%;
            max-width: 850px;
            margin: 0 auto;
            position: relative;
        }
        .game-container {
            background: #aac8e0;
            padding: 1.2rem 1rem 1.5rem 1rem;
            border-radius: 2rem;
            box-shadow: 0 25px 35px rgba(0, 0, 0, 0.5), inset 0 0 0 2px #eef9ff, inset 0 0 15px #7fa3c0;
            position: relative;
        }
        .game-title {
            text-align: center;
            margin-bottom: 1rem;
            font-size: 2.2rem;
            font-weight: 800;
            color: #ffd966;
            text-shadow: 4px 4px 0 #2d5269, 6px 6px 0 #0a3142;
            letter-spacing: 4px;
            word-break: keep-all;
            display: flex;
            align-items: center;
            justify-content: center;
            gap: 15px;
        }
        .game-title span {
            display: inline-block;
            transform: rotate(-2deg);
            background: rgba(255,255,240,0.2);
            padding: 0 15px;
            border-radius: 60px 20px 60px 20px;
        }
        /* 静音按钮 */
        .mute-btn {
            background: #406e89;
            border: none;
            border-radius: 50%;
            width: 50px;
            height: 50px;
            font-size: 1.8rem;
            cursor: pointer;
            box-shadow: 0 5px 0 #1d3f52, 0 8px 12px #0000006e;
            transition: 0.08s linear;
            display: flex;
            align-items: center;
            justify-content: center;
            color: white;
            touch-action: manipulation;
        }
        .mute-btn:active {
            transform: translateY(5px);
            box-shadow: 0 2px 0 #1d3f52, 0 8px 12px #0000006e;
        }
        canvas {
            display: block;
            margin: 0 auto;
            width: 100%;
            height: auto;
            aspect-ratio: 800 / 400;
            border-radius: 10px;
            background: #edf5fc;
            box-shadow: inset 0 8px 12px rgba(0,20,40,0.3), inset 0 -4px 8px #ffffffcc, 0 18px 25px #0a1e2e;
            cursor: grab;
            touch-action: none;
            -webkit-touch-callout: none;
        }
        canvas:active {
            cursor: grabbing;
        }
        .ice-panel {
            display: flex;
            flex-wrap: wrap;
            justify-content: center;
            align-items: center;
            gap: 1rem;
            margin-top: 1.2rem;
            padding: 0 0.2rem;
            color: #052c3f;
            text-shadow: 2px 2px 0 #c7e3ff;
            font-weight: bold;
            font-size: 1.2rem;
        }
        .scoreboard {
            background: #183d57;
            border-radius: 100px;
            padding: 0.3rem 1.5rem;
            color: white;
            text-shadow: 2px 2px 0 #02141e;
            box-shadow: inset 0 2px 7px #9fc8f0, 0 5px 0 #06212e;
            font-size: 1.5rem;
            letter-spacing: 2px;
            min-width: 200px;
            text-align: center;
            white-space: nowrap;
        }
        .turn-indicator {
            background: #ffd966;
            border-radius: 3rem;
            padding: 0.3rem 1.5rem;
            font-size: 1.3rem;
            box-shadow: inset 0 -3px 0 #bb8f39, 0 5px 0 #6b4f20;
            color: #2f220c;
            min-width: 280px;
            text-align: center;
            font-weight: 600;
        }
        .footer-tip {
            margin-top: 1rem;
            color: #d2ebff;
            text-align: center;
            font-weight: 500;
            text-shadow: 1px 1px 0 #0a3142;
            font-size: 1rem;
            padding: 0 5px;
        }
        .victory-overlay {
            position: absolute;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background: rgba(255, 215, 0, 0.3);
            display: flex;
            flex-direction: column;
            align-items: center;
            justify-content: center;
            z-index: 20;
            backdrop-filter: blur(3px);
            border-radius: 2rem;
            animation: glowPulse 0.5s infinite alternate;
            padding: 20px;
            box-sizing: border-box;
        }
        .victory-text {
            font-size: clamp(2rem, 12vw, 4rem);
            font-weight: 900;
            color: gold;
            text-shadow: 0 0 30px white, 0 0 60px orange;
            animation: bounce 0.6s infinite alternate;
            margin-bottom: 30px;
            text-align: center;
            line-height: 1.2;
            word-break: break-word;
        }
        .rematch-button {
            background: #ffd966;
            border: none;
            border-radius: 3rem;
            padding: 1rem 2.5rem;
            font-size: clamp(1.5rem, 6vw, 2rem);
            font-weight: bold;
            color: #023047;
            box-shadow: 0 10px 0 #b38b2a, 0 15px 20px #0000006e;
            cursor: pointer;
            transition: 0.08s linear;
            border: 2px solid white;
            animation: fadeIn 0.5s;
            touch-action: manipulation;
            min-width: 200px;
        }
        .rematch-button:active {
            transform: translateY(10px);
            box-shadow: 0 5px 0 #b38b2a, 0 15px 20px #0000006e;
        }
        @keyframes glowPulse {
            from { box-shadow: 0 0 30px gold; }
            to { box-shadow: 0 0 100px orange; }
        }
        @keyframes bounce {
            from { transform: scale(1); }
            to { transform: scale(1.1); }
        }
        @keyframes fadeIn {
            from { opacity: 0; transform: scale(0.8); }
            to { opacity: 1; transform: scale(1); }
        }
        .audio-hidden {
            display: none;
        }
        @supports (padding-bottom: env(safe-area-inset-bottom)) {
            .game-container {
                padding-bottom: calc(1.5rem + env(safe-area-inset-bottom));
            }
        }
        @media (max-width: 500px) {
            .game-title {
                font-size: 1.8rem;
            }
            .mute-btn {
                width: 40px;
                height: 40px;
                font-size: 1.4rem;
            }
            .scoreboard {
                font-size: 1.2rem;
                padding: 0.2rem 1rem;
                min-width: 160px;
            }
            .turn-indicator {
                font-size: 1rem;
                padding: 0.2rem 1rem;
                min-width: 240px;
            }
            .footer-tip {
                font-size: 0.9rem;
            }
        }
    </style>
</head>
<body>
    <div class="game-wrapper">
        <div class="game-container" id="gameContainer">
            <div class="game-title">
                <span>🥌 冰壶小游戏 🥌</span>
                <button class="mute-btn" id="muteBtn">🔊</button>
            </div>
            
            <canvas id="gameCanvas" width="800" height="400"></canvas>

            <div class="ice-panel">
                <div class="scoreboard" id="scoreDisplay">
                    红方 0 : 0 黄方
                </div>
                <div class="turn-indicator" id="turnDisplay">
                    🔴 红队回合 (0/8)
                </div>
            </div>
            
            <div class="footer-tip">
                👆 在左侧地毯区按住·向右拉(绿色虚线)松手投掷 · 红队先手 · 静止后计分
            </div>

            <div id="victoryOverlay" class="victory-overlay" style="display: none;">
                <div class="victory-text" id="victoryMessage">🏆 胜利 🏆</div>
                <button class="rematch-button" id="rematchBtn">⚡ 再来一局 ⚡</button>
            </div>
        </div>
    </div>

    <!-- 背景音乐 -->
    <audio id="bgMusic" class="audio-hidden" loop preload="auto">
        <source src="https://amitofoicu.github.io/home/beijing.ogg" type="audio/ogg">
        <source src="https://amitofoicu.github.io/home/beijing.mp3" type="audio/mpeg">
    </audio>
    
    <!-- 胜利音乐 -->
    <audio id="victoryMusic" class="audio-hidden" preload="auto">
        <source src="https://amitofoicu.github.io/home/xiaochu.mp3" type="audio/mpeg">
        <source src="https://amitofoicu.github.io/home/xiaochu.ogg" type="audio/ogg">
    </audio>

    <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 ? '🔇' : '🔊';
            });

            // 设置背景图片
            const backgroundImage = new Image();
            backgroundImage.src = 'https://amitofoicu.github.io/home/lianchi.jpg';
            backgroundImage.crossOrigin = 'anonymous';
            
            // 显示元素
            const scoreDisplay = document.getElementById('scoreDisplay');
            const turnDisplay = document.getElementById('turnDisplay');

            // 物理参数
            const FRICTION = 0.985;
            const MIN_SPEED = 0.03;
            const MAX_DRAG_DIST = 160;
            const PULL_FACTOR = 0.2;
            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;

            let isDragging = false;
            let dragStart = { x: 0, y: 0 };
            let currentMousePos = { x: 0, y: 0 };

            let redScore = 0;
            let yellowScore = 0;

            let activeTouchId = null;

            // 背景音乐播放尝试
            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;
                }
                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;
            }

            function aiTakeTurn() {
                if (!(yellowThrowsLeft > 0 && !anyStoneMoving() && currentTurn === 'yellow' && gameActive)) return;

                waitingForAI = true;

                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.12 : 0.10;
                    let speed = targetDistance * speedFactor;
                    
                    const MAX_SPEED = 10;
                    if (speed > MAX_SPEED) speed = MAX_SPEED;
                    
                    if (yellowThrown > 4) speed = speed * 0.9;
                    
                    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;
                    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
                };
            }

            function startDrag(clientX, clientY) {
                if (isDragging || !(redThrowsLeft > 0 && !anyStoneMoving() && currentTurn === 'red' && gameActive)) 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;
                    currentMousePos.x = canvasX;
                    currentMousePos.y = canvasY;
                }
            }

            function onDragMove(clientX, clientY) {
                if (!isDragging) return;
                
                const { x: canvasX, y: canvasY } = getCanvasCoords(clientX, clientY);
                currentMousePos.x = canvasX;
                currentMousePos.y = canvasY;
            }

            function endDrag(clientX, clientY) {
                if (!isDragging || !(redThrowsLeft > 0 && !anyStoneMoving() && currentTurn === 'red' && gameActive)) {
                    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;

                if (yellowThrowsLeft > 0) {
                    currentTurn = 'yellow';
                    updateTurnDisplay();
                    aiTakeTurn();
                } else {
                    currentTurn = null;
                    updateTurnDisplay();
                }
                
                checkGameCompletion();
            }

            // 物理更新函数
            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();
                }
            }

            // 绘制函数
            function draw() {
                ctx.clearRect(0, 0, 800, 400);
                
                // 背景
                if (backgroundImage.complete && backgroundImage.naturalHeight > 0) {
                    ctx.drawImage(backgroundImage, 0, 0, 800, 400);
                } else {
                    ctx.fillStyle = '#edf5fc';
                    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 = currentMousePos.x - dragStart.x;
                    let dy = currentMousePos.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() {
                initGame();
                updateScoreDisplay();
                updateTurnDisplay();
            }

            rematchBtn.addEventListener('click', resetGame);

            // 事件处理
            function handlePointerStart(e) {
                e.preventDefault();
                const clientX = e.clientX ?? (e.touches?.[0]?.clientX);
                const clientY = e.clientY ?? (e.touches?.[0]?.clientY);
                if (clientX !== undefined) {
                    if (e.touches) {
                        if (activeTouchId === null) {
                            activeTouchId = e.touches[0].identifier;
                            startDrag(clientX, clientY);
                        }
                    } else {
                        startDrag(clientX, clientY);
                    }
                }
            }

            function handlePointerMove(e) {
                e.preventDefault();
                if (!isDragging) return;
                
                let clientX, clientY;
                if (e.touches) {
                    for (let i = 0; i < e.touches.length; i++) {
                        if (e.touches[i].identifier === activeTouchId) {
                            clientX = e.touches[i].clientX;
                            clientY = e.touches[i].clientY;
                            break;
                        }
                    }
                    if (clientX === undefined) return;
                } else {
                    clientX = e.clientX;
                    clientY = e.clientY;
                }
                onDragMove(clientX, clientY);
            }

            function handlePointerEnd(e) {
                e.preventDefault();
                if (!isDragging) return;
                
                let clientX, clientY;
                if (e.changedTouches) {
                    for (let i = 0; i < e.changedTouches.length; i++) {
                        if (e.changedTouches[i].identifier === activeTouchId) {
                            clientX = e.changedTouches[i].clientX;
                            clientY = e.changedTouches[i].clientY;
                            break;
                        }
                    }
                    if (clientX === undefined) return;
                } else {
                    clientX = e.clientX;
                    clientY = e.clientY;
                }
                endDrag(clientX, clientY);
            }

            function handlePointerCancel(e) {
                isDragging = false;
                activeTouchId = null;
            }

            canvas.addEventListener('mousedown', handlePointerStart);
            canvas.addEventListener('mousemove', handlePointerMove);
            canvas.addEventListener('mouseup', handlePointerEnd);
            canvas.addEventListener('mouseleave', () => {
                if (isDragging) {
                    isDragging = false;
                    activeTouchId = null;
                }
            });
            
            canvas.addEventListener('touchstart', handlePointerStart, { passive: false });
            canvas.addEventListener('touchmove', handlePointerMove, { passive: false });
            canvas.addEventListener('touchend', handlePointerEnd, { passive: false });
            canvas.addEventListener('touchcancel', handlePointerCancel, { passive: false });

            initGame();

            function gameLoop() {
                updatePhysics();
                draw();
                requestAnimationFrame(gameLoop);
            }
            gameLoop();
        })();
    </script>
</body>
</html>
相关推荐
用户5757303346241 小时前
深入理解 Promise:从单线程到异步流程控制的终极指南
javascript
我是伪码农1 小时前
Vue 大事件管理系统
前端·javascript·vue.js
无巧不成书02181 小时前
KMP适配鸿蒙开发实战|从0到1搭建可运行工程
javascript·华为·harmonyos·kmp
木斯佳2 小时前
前端八股文面经大全:腾讯云前端实习一面(2025-12-26)·面经深度解析
前端·状态模式·腾讯云
哆啦A梦15882 小时前
Vue3魔法手册 作者 张天禹 012_路由_(二)
前端·vue.js·typescript
木斯佳2 小时前
前端八股文面经大全:2026-01-29 字节-AIDP前端实习一面面经深度解析
前端·状态模式
We་ct2 小时前
LeetCode 100. 相同的树:两种解法(递归+迭代)详解
前端·算法·leetcode·链表·typescript
键盘鼓手苏苏2 小时前
Flutter for OpenHarmony:injector 轻量级依赖注入库(比 GetIt 更简单的选择) 深度解析与鸿蒙适配指南
css·网络·flutter·华为·rust·harmonyos
哆啦A梦15882 小时前
Vue3魔法手册 作者 张天禹 08_回顾TS中的-接口-泛型-自定义事件
前端·vue.js·typescript