局域网下五子棋,html+node.js实现

目录

废话不多说,直接上代码

服务端代码

server.js

javascript 复制代码
const express = require('express');
const app = express();
const http = require('http').createServer(app);
const io = require('socket.io')(http, {
  cors: {
    origin: "*",
    methods: ["GET", "POST"]
  }
});

const PORT = 3000;

// 游戏房间管理
let rooms = {};

io.on('connection', (socket) => {
    console.log('新玩家连接:', socket.id);
    
    // 玩家加入游戏
    socket.on('joinGame', () => {
        // 查找可用房间或创建新房间
        let roomId = null;
        
        // 查找有空位的房间
        for (const id in rooms) {
            if (rooms[id].players.length < 2) {
                roomId = id;
                break;
            }
        }
        
        // 如果没有找到可用房间,创建新房间
        if (!roomId) {
            roomId = Date.now().toString();
            rooms[roomId] = {
                players: [],
                gameActive: false,
                board: Array(15).fill().map(() => Array(15).fill(0)),
                currentPlayer: 1
            };
        }
        
        // 加入房间
        socket.join(roomId);
        rooms[roomId].players.push(socket.id);
        
        console.log(`玩家 ${socket.id} 加入房间 ${roomId}`);
        
        // 分配角色
        const playerRole = rooms[roomId].players.length === 1 ? 1 : 2;
        socket.emit('gameStart', playerRole);
        
        // 如果房间已满,通知两名玩家游戏开始
        if (rooms[roomId].players.length === 2) {
            rooms[roomId].gameActive = true;
            console.log(`房间 ${roomId} 游戏开始`);
            
            // 通知黑棋玩家(第一个加入的玩家)
            io.to(rooms[roomId].players[0]).emit('gameStart', 1);
            
            // 通知白棋玩家(第二个加入的玩家)
            io.to(rooms[roomId].players[1]).emit('gameStart', 2);
        }
    });
    
    // 监听玩家落子
    socket.on('makeMove', (data) => {
        const roomId = getRoomIdByPlayer(socket.id);
        
        if (!roomId || !rooms[roomId] || !rooms[roomId].gameActive) return;
        
        const { row, col, player } = data;
        
        // 验证移动合法性
        if (rooms[roomId].board[row][col] !== 0 || player !== rooms[roomId].currentPlayer) {
            return; // 非法移动,忽略
        }
        
        // 更新游戏状态
        rooms[roomId].board[row][col] = player;
        rooms[roomId].currentPlayer = player === 1 ? 2 : 1;
        
        // 广播给房间内的所有玩家
        io.to(roomId).emit('moveConfirmed', data);
    });
    
    // 监听游戏重置
    socket.on('resetGame', () => {
        const roomId = getRoomIdByPlayer(socket.id);
        
        if (!roomId || !rooms[roomId]) return;
        
        // 重置游戏状态
        rooms[roomId].board = Array(15).fill().map(() => Array(15).fill(0));
        rooms[roomId].currentPlayer = 1;
        rooms[roomId].gameActive = true;
        
        // 广播给房间内的所有玩家
        io.to(roomId).emit('gameReset');
    });
    
    // 监听悔棋请求
    socket.on('undoMove', () => {
        const roomId = getRoomIdByPlayer(socket.id);
        
        if (!roomId || !rooms[roomId] || !rooms[roomId].gameActive) return;
        
        // 简单实现:重置整个游戏
        rooms[roomId].board = Array(15).fill().map(() => Array(15).fill(0));
        rooms[roomId].currentPlayer = 1;
        
        // 广播给房间内的所有玩家
        io.to(roomId).emit('gameReset');
    });
    
    // 监听游戏结束
    socket.on('gameOver', (data) => {
        const roomId = getRoomIdByPlayer(socket.id);
        
        if (!roomId || !rooms[roomId]) return;
        
        rooms[roomId].gameActive = false;
        // 可以记录游戏结果
    });
    
    // 玩家断开连接
    socket.on('disconnect', () => {
        console.log('玩家断开连接:', socket.id);
        
        const roomId = getRoomIdByPlayer(socket.id);
        
        if (!roomId || !rooms[roomId]) return;
        
        // 从房间中移除玩家
        rooms[roomId].players = rooms[roomId].players.filter(id => id !== socket.id);
        
        // 通知其他玩家
        socket.to(roomId).emit('opponentDisconnected');
        
        // 如果房间为空,删除房间
        if (rooms[roomId].players.length === 0) {
            delete rooms[roomId];
            console.log(`房间 ${roomId} 已关闭`);
        }
    });
    
    // 辅助函数:通过玩家ID查找房间ID
    function getRoomIdByPlayer(playerId) {
        for (const roomId in rooms) {
            if (rooms[roomId].players.includes(playerId)) {
                return roomId;
            }
        }
        return null;
    }
});

http.listen(PORT, () => {
    console.log(`服务器运行在端口 ${PORT}`);
});

客户端代码

index.html

javascript 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>五子棋游戏 - 局域网联机版</title>
    <script src="https://cdn.tailwindcss.com"></script>
    <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/css/all.min.css" rel="stylesheet">
   <script src="https://cdn.jsdelivr.net/npm/socket.io-client@4.4.1/dist/socket.io.min.js"></script>
    <script>
        tailwind.config = {
            theme: {
                extend: {
                    colors: {
                        primary: '#8B5A2B',
                        secondary: '#D2B48C',
                        board: '#DEB887',
                        black: '#000000',
                        white: '#FFFFFF',
                        online: '#10B981',
                        offline: '#EF4444',
                    },
                    fontFamily: {
                        sans: ['Inter', 'system-ui', 'sans-serif'],
                    },
                }
            }
        }
    </script>
    <style type="text/tailwindcss">
        @layer utilities {
            .content-auto {
                content-visibility: auto;
            }
            .board-grid {
                background-size: 100% 100%;
                background-image: linear-gradient(to right, rgba(0,0,0,0.6) 1px, transparent 1px),
                                  linear-gradient(to bottom, rgba(0,0,0,0.6) 1px, transparent 1px);
            }
            .piece-shadow {
                filter: drop-shadow(0 4px 3px rgb(0 0 0 / 0.07)) drop-shadow(0 2px 2px rgb(0 0 0 / 0.06));
            }
            .piece-transition {
                transition: all 0.2s ease-out;
            }
            .btn-hover {
                transition: all 0.2s ease;
            }
            .btn-hover:hover {
                transform: translateY(-2px);
                box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
            }
            .online-dot {
                width: 10px;
                height: 10px;
                border-radius: 50%;
                display: inline-block;
            }
        }
    </style>
</head>
<body class="bg-gray-100 min-h-screen flex flex-col items-center justify-center p-4 font-sans">
    <div class="max-w-4xl w-full bg-white rounded-2xl shadow-xl overflow-hidden">
        <div class="bg-primary text-white p-6 text-center">
            <h1 class="text-[clamp(1.5rem,3vw,2.5rem)] font-bold">五子棋 - 局域网联机版</h1>
            <p class="text-secondary mt-2">与局域网内的朋友对战</p>
        </div>
        
        <div class="p-6 md:p-8 flex flex-col md:flex-row gap-6">
            <!-- 游戏区域 -->
            <div class="flex-1 relative">
                <div class="aspect-square bg-board rounded-lg shadow-lg overflow-hidden board-grid" style="background-size: calc(100% / 14) calc(100% / 14);">
                    <canvas id="gameCanvas" class="w-full h-full cursor-pointer"></canvas>
                </div>
                
                <div id="gameStatus" class="mt-4 p-3 bg-secondary/20 rounded-lg text-center">
                    <p id="statusText" class="font-medium">等待连接...</p>
                </div>
            </div>
            
            <!-- 游戏控制和信息 -->
            <div class="w-full md:w-80 flex flex-col gap-6">
                <div class="bg-gray-50 rounded-lg p-5 shadow-sm">
                    <h2 class="text-lg font-semibold mb-3 flex items-center">
                        <i class="fa-solid fa-info-circle mr-2 text-primary"></i>游戏信息
                    </h2>
                    <div class="space-y-3">
                        <div class="flex items-center justify-between">
                            <span class="text-gray-600">游戏状态</span>
                            <div class="flex items-center">
                                <span id="connectionStatus" class="online-dot bg-offline mr-2"></span>
                                <span id="connectionText">未连接</span>
                            </div>
                        </div>
                        <div class="flex items-center justify-between">
                            <span class="text-gray-600">你的角色</span>
                            <div class="flex items-center">
                                <div id="playerRole" class="w-6 h-6 rounded-full mr-2 piece-shadow"></div>
                                <span id="roleText">未分配</span>
                            </div>
                        </div>
                        <div class="flex items-center justify-between">
                            <span class="text-gray-600">当前回合</span>
                            <div class="flex items-center">
                                <div id="currentPlayer" class="w-6 h-6 rounded-full bg-black mr-2 piece-shadow"></div>
                                <span id="playerText">黑棋</span>
                            </div>
                        </div>
                        <div class="flex items-center justify-between">
                            <span class="text-gray-600">游戏时间</span>
                            <span id="gameTime" class="font-mono">00:00</span>
                        </div>
                        <div class="flex items-center justify-between">
                            <span class="text-gray-600">步数</span>
                            <span id="moveCount">0</span>
                        </div>
                    </div>
                </div>
                
                <div class="bg-gray-50 rounded-lg p-5 shadow-sm">
                    <h2 class="text-lg font-semibold mb-3 flex items-center">
                        <i class="fa-solid fa-crown mr-2 text-primary"></i>游戏规则
                    </h2>
                    <ul class="text-sm text-gray-600 space-y-2">
                        <li class="flex items-start">
                            <i class="fa-solid fa-circle text-xs mt-1.5 mr-2 text-primary"></i>
                            <span>黑棋和白棋轮流在棋盘上落子</span>
                        </li>
                        <li class="flex items-start">
                            <i class="fa-solid fa-circle text-xs mt-1.5 mr-2 text-primary"></i>
                            <span>先在横、竖或斜方向形成五子连线者获胜</span>
                        </li>
                        <li class="flex items-start">
                            <i class="fa-solid fa-circle text-xs mt-1.5 mr-2 text-primary"></i>
                            <span>点击棋盘上的交叉点放置棋子</span>
                        </li>
                    </ul>
                </div>
                
                <div class="bg-gray-50 rounded-lg p-5 shadow-sm">
                    <h2 class="text-lg font-semibold mb-3 flex items-center">
                        <i class="fa-solid fa-server mr-2 text-primary"></i>服务器连接
                    </h2>
                    <div class="space-y-3">
                        <div>
                            <label for="serverIp" class="block text-sm font-medium text-gray-700 mb-1">服务器IP地址</label>
                            <input type="text" id="serverIp" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent" placeholder="例如: 192.168.1.100" value="localhost">
                        </div>
                        <div>
                            <label for="serverPort" class="block text-sm font-medium text-gray-700 mb-1">端口号</label>
                            <input type="number" id="serverPort" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent" placeholder="默认: 3000" value="3000">
                        </div>
                        <button id="connectBtn" class="w-full bg-primary hover:bg-primary/90 text-white py-2 px-4 rounded-lg font-medium btn-hover flex items-center justify-center">
                            <i class="fa-solid fa-wifi mr-2"></i>连接服务器
                        </button>
                    </div>
                </div>
                
                <div class="flex gap-3">
                    <button id="restartBtn" class="flex-1 bg-primary hover:bg-primary/90 text-white py-3 px-4 rounded-lg font-medium btn-hover flex items-center justify-center" disabled>
                        <i class="fa-solid fa-refresh mr-2"></i>重新开始
                    </button>
                    <button id="undoBtn" class="flex-1 bg-gray-200 hover:bg-gray-300 text-gray-700 py-3 px-4 rounded-lg font-medium btn-hover flex items-center justify-center" disabled>
                        <i class="fa-solid fa-undo mr-2"></i>悔棋
                    </button>
                </div>
            </div>
        </div>
        
        <div class="bg-gray-50 p-4 text-center text-sm text-gray-500">
            <p>© 2025 五子棋联机版 | 局域网对战游戏</p>
        </div>
    </div>

    <!-- 胜利提示模态框 -->
    <div id="winModal" class="fixed inset-0 bg-black/50 flex items-center justify-center z-50 hidden opacity-0 transition-opacity duration-300">
        <div class="bg-white rounded-xl p-8 max-w-md w-full mx-4 transform transition-transform duration-300 scale-95">
            <div class="text-center">
                <div class="w-16 h-16 bg-green-100 rounded-full flex items-center justify-center mx-auto mb-4">
                    <i class="fa-solid fa-trophy text-3xl text-yellow-500"></i>
                </div>
                <h2 class="text-2xl font-bold mb-2" id="winnerText">黑棋获胜!</h2>
                <p class="text-gray-600 mb-6">恭喜您赢得了这场精彩的比赛!</p>
                <button id="newGameBtn" class="bg-primary hover:bg-primary/90 text-white py-3 px-8 rounded-lg font-medium btn-hover">
                    开始新游戏
                </button>
            </div>
        </div>
    </div>

    <!-- 等待对手模态框 -->
    <div id="waitingModal" class="fixed inset-0 bg-black/50 flex items-center justify-center z-50 hidden opacity-0 transition-opacity duration-300">
        <div class="bg-white rounded-xl p-8 max-w-md w-full mx-4 transform transition-transform duration-300 scale-95">
            <div class="text-center">
                <div class="w-16 h-16 bg-blue-100 rounded-full flex items-center justify-center mx-auto mb-4 animate-spin">
                    <i class="fa-solid fa-circle-notch text-3xl text-blue-500"></i>
                </div>
                <h2 class="text-2xl font-bold mb-2">等待对手连接</h2>
                <p class="text-gray-600 mb-6">请等待其他玩家加入游戏...</p>
                <button id="cancelWaitingBtn" class="bg-gray-200 hover:bg-gray-300 text-gray-700 py-2 px-6 rounded-lg font-medium btn-hover">
                    取消
                </button>
            </div>
        </div>
    </div>

    <script>
        document.addEventListener('DOMContentLoaded', () => {
            // 游戏常量
            const BOARD_SIZE = 15; // 15x15的棋盘
            const CELL_SIZE = Math.min(window.innerWidth * 0.8 / BOARD_SIZE, window.innerHeight * 0.6 / BOARD_SIZE);
            const PIECE_SIZE = CELL_SIZE * 0.8;
            
            // 游戏状态
            let gameBoard = Array(BOARD_SIZE).fill().map(() => Array(BOARD_SIZE).fill(0));
            let currentPlayer = 1; // 1: 黑棋, 2: 白棋
            let playerRole = 0;    // 0: 未分配, 1: 黑棋, 2: 白棋
            let gameActive = false;
            let moveHistory = [];
            let gameTime = 0;
            let timerInterval;
            let isWaitingForServer = false; // 新增状态变量,用于防止重复点击
            
            // DOM元素
            const canvas = document.getElementById('gameCanvas');
            const ctx = canvas.getContext('2d');
            const statusText = document.getElementById('statusText');
            const currentPlayerEl = document.getElementById('currentPlayer');
            const playerText = document.getElementById('playerText');
            const moveCountEl = document.getElementById('moveCount');
            const gameTimeEl = document.getElementById('gameTime');
            const restartBtn = document.getElementById('restartBtn');
            const undoBtn = document.getElementById('undoBtn');
            const winModal = document.getElementById('winModal');
            const winnerText = document.getElementById('winnerText');
            const newGameBtn = document.getElementById('newGameBtn');
            const waitingModal = document.getElementById('waitingModal');
            const cancelWaitingBtn = document.getElementById('cancelWaitingBtn');
            const connectBtn = document.getElementById('connectBtn');
            const serverIpInput = document.getElementById('serverIp');
            const serverPortInput = document.getElementById('serverPort');
            const connectionStatus = document.getElementById('connectionStatus');
            const connectionText = document.getElementById('connectionText');
            const playerRoleEl = document.getElementById('playerRole');
            const roleText = document.getElementById('roleText');
            
            // Socket.IO连接
            let socket;
            
            // 设置Canvas尺寸
            canvas.width = CELL_SIZE * (BOARD_SIZE - 1);
            canvas.height = CELL_SIZE * (BOARD_SIZE - 1);
            
            // 绘制棋盘
            function drawBoard() {
                ctx.clearRect(0, 0, canvas.width, canvas.height);
                
                // 绘制网格线
                ctx.strokeStyle = '#8B4513';
                ctx.lineWidth = 1.5;
                
                for (let i = 0; i < BOARD_SIZE; i++) {
                    // 水平线
                    ctx.beginPath();
                    ctx.moveTo(0, i * CELL_SIZE);
                    ctx.lineTo(canvas.width, i * CELL_SIZE);
                    ctx.stroke();
                    
                    // 垂直线
                    ctx.beginPath();
                    ctx.moveTo(i * CELL_SIZE, 0);
                    ctx.lineTo(i * CELL_SIZE, canvas.height);
                    ctx.stroke();
                }
                
                // 绘制天元和星位
                const starPoints = [
                    {x: 3, y: 3}, {x: 3, y: 11}, {x: 7, y: 7}, 
                    {x: 11, y: 3}, {x: 11, y: 11}
                ];
                
                starPoints.forEach(point => {
                    ctx.beginPath();
                    ctx.arc(point.x * CELL_SIZE, point.y * CELL_SIZE, 4, 0, Math.PI * 2);
                    ctx.fillStyle = '#8B4513';
                    ctx.fill();
                });
                
                // 绘制棋子
                for (let i = 0; i < BOARD_SIZE; i++) {
                    for (let j = 0; j < BOARD_SIZE; j++) {
                        if (gameBoard[i][j] !== 0) {
                            drawPiece(i, j, gameBoard[i][j]);
                        }
                    }
                }
            }
            
            // 绘制棋子
            function drawPiece(row, col, player) {
                const x = col * CELL_SIZE;
                const y = row * CELL_SIZE;
                
                // 棋子阴影
                ctx.beginPath();
                ctx.arc(x, y, PIECE_SIZE / 2 + 2, 0, Math.PI * 2);
                ctx.fillStyle = 'rgba(0, 0, 0, 0.2)';
                ctx.fill();
                
                // 棋子本体
                ctx.beginPath();
                ctx.arc(x, y, PIECE_SIZE / 2, 0, Math.PI * 2);
                
                if (player === 1) {
                    // 黑棋 - 渐变效果
                    const gradient = ctx.createRadialGradient(
                        x - PIECE_SIZE / 6, y - PIECE_SIZE / 6, PIECE_SIZE / 10,
                        x, y, PIECE_SIZE / 2
                    );
                    gradient.addColorStop(0, '#555');
                    gradient.addColorStop(1, '#000');
                    ctx.fillStyle = gradient;
                } else {
                    // 白棋 - 渐变效果
                    const gradient = ctx.createRadialGradient(
                        x - PIECE_SIZE / 6, y - PIECE_SIZE / 6, PIECE_SIZE / 10,
                        x, y, PIECE_SIZE / 2
                    );
                    gradient.addColorStop(0, '#fff');
                    gradient.addColorStop(1, '#ddd');
                    ctx.fillStyle = gradient;
                }
                
                ctx.fill();
                
                // 棋子边缘
                ctx.strokeStyle = player === 1 ? '#333' : '#ccc';
                ctx.lineWidth = 1;
                ctx.stroke();
            }
            
            // 检查胜利条件
            function checkWin(row, col, player) {
                const directions = [
                    [1, 0],   // 水平
                    [0, 1],   // 垂直
                    [1, 1],   // 对角线
                    [1, -1]   // 反对角线
                ];
                
                for (const [dx, dy] of directions) {
                    let count = 1;  // 当前位置已经有一个棋子
                    
                    // 正向检查
                    for (let i = 1; i < 5; i++) {
                        const newRow = row + i * dy;
                        const newCol = col + i * dx;
                        
                        if (newRow < 0 || newRow >= BOARD_SIZE || newCol < 0 || newCol >= BOARD_SIZE) {
                            break;
                        }
                        
                        if (gameBoard[newRow][newCol] === player) {
                            count++;
                        } else {
                            break;
                        }
                    }
                    
                    // 反向检查
                    for (let i = 1; i < 5; i++) {
                        const newRow = row - i * dy;
                        const newCol = col - i * dx;
                        
                        if (newRow < 0 || newRow >= BOARD_SIZE || newCol < 0 || newCol >= BOARD_SIZE) {
                            break;
                        }
                        
                        if (gameBoard[newRow][newCol] === player) {
                            count++;
                        } else {
                            break;
                        }
                    }
                    
                    if (count >= 5) {
                        return true;
                    }
                }
                
                return false;
            }
            
            // 检查平局
            function checkDraw() {
                for (let i = 0; i < BOARD_SIZE; i++) {
                    for (let j = 0; j < BOARD_SIZE; j++) {
                        if (gameBoard[i][j] === 0) {
                            return false; // 还有空位,不是平局
                        }
                    }
                }
                return true; // 棋盘已满,平局
            }
            
            // 更新游戏状态显示
            function updateGameStatus() {
                if (gameActive) {
                    statusText.textContent = `游戏进行中 - ${currentPlayer === 1 ? '黑棋' : '白棋'}回合`;
                    currentPlayerEl.className = `w-6 h-6 rounded-full ${currentPlayer === 1 ? 'bg-black' : 'bg-white border border-gray-300'} mr-2 piece-shadow`;
                    playerText.textContent = currentPlayer === 1 ? '黑棋' : '白棋';
                    
                    // 启用/禁用棋盘点击
                    canvas.style.cursor = (playerRole === currentPlayer && !isWaitingForServer) ? 'pointer' : 'not-allowed';
                } else if (socket && socket.connected && playerRole !== 0) {
                    statusText.textContent = playerRole === currentPlayer ? 
                        `等待对手确认...` : `等待对手行动...`;
                    canvas.style.cursor = 'not-allowed';
                } else {
                    statusText.textContent = `等待连接...`;
                    canvas.style.cursor = 'not-allowed';
                }
                
                moveCountEl.textContent = moveHistory.length;
            }
            
            // 更新游戏时间
            function updateGameTime() {
                gameTime++;
                const minutes = Math.floor(gameTime / 60).toString().padStart(2, '0');
                const seconds = (gameTime % 60).toString().padStart(2, '0');
                gameTimeEl.textContent = `${minutes}:${seconds}`;
            }
            
            // 开始计时
            function startTimer() {
                clearInterval(timerInterval);
                timerInterval = setInterval(updateGameTime, 1000);
            }
            
            // 停止计时
            function stopTimer() {
                clearInterval(timerInterval);
            }
            
            // 显示胜利模态框
            function showWinModal(winner) {
                gameActive = false;
                stopTimer();
                
                winnerText.textContent = `${winner === 1 ? '黑棋' : '白棋'}获胜!`;
                winModal.classList.remove('hidden');
                
                // 添加动画效果
                setTimeout(() => {
                    winModal.classList.add('opacity-100');
                    winModal.querySelector('div').classList.remove('scale-95');
                    winModal.querySelector('div').classList.add('scale-100');
                }, 10);
            }
            
            // 隐藏胜利模态框
            function hideWinModal() {
                winModal.classList.remove('opacity-100');
                winModal.querySelector('div').classList.remove('scale-100');
                winModal.querySelector('div').classList.add('scale-95');
                
                setTimeout(() => {
                    winModal.classList.add('hidden');
                }, 300);
            }
            
            // 显示等待模态框
            function showWaitingModal() {
                waitingModal.classList.remove('hidden');
                
                // 添加动画效果
                setTimeout(() => {
                    waitingModal.classList.add('opacity-100');
                    waitingModal.querySelector('div').classList.remove('scale-95');
                    waitingModal.querySelector('div').classList.add('scale-100');
                }, 10);
            }
            
            // 隐藏等待模态框
            function hideWaitingModal() {
                waitingModal.classList.remove('opacity-100');
                waitingModal.querySelector('div').classList.remove('scale-100');
                waitingModal.querySelector('div').classList.add('scale-95');
                
                setTimeout(() => {
                    waitingModal.classList.add('hidden');
                }, 300);
            }
            
            // 重置游戏
            function resetGame(sendToServer = true) {
                gameBoard = Array(BOARD_SIZE).fill().map(() => Array(BOARD_SIZE).fill(0));
                currentPlayer = 1;
                gameActive = true;
                moveHistory = [];
                gameTime = 0;
                isWaitingForServer = false;
                
                drawBoard();
                updateGameStatus();
                gameTimeEl.textContent = '00:00';
                
                stopTimer();
                startTimer();
                
                hideWinModal();
                
                // 发送重置游戏请求
                if (sendToServer && socket && socket.connected) {
                    socket.emit('resetGame');
                }
            }
            
            // 悔棋
            function undoMove() {
                if (moveHistory.length === 0 || !gameActive) {
                    return;
                }
                
                const lastMove = moveHistory.pop();
                gameBoard[lastMove.row][lastMove.col] = 0;
                currentPlayer = lastMove.player; // 回到上一个玩家
                
                drawBoard();
                updateGameStatus();
                
                // 发送悔棋请求
                if (socket && socket.connected) {
                    socket.emit('undoMove');
                }
            }
            
            // 连接到服务器
            function connectToServer() {
                const serverIp = serverIpInput.value.trim();
                const serverPort = serverPortInput.value.trim();
                
                if (!serverIp || !serverPort) {
                    alert('请输入服务器IP地址和端口号');
                    return;
                }
                
                // 断开现有连接
                if (socket) {
                    socket.disconnect();
                }
                
                // 创建新连接
                socket = io(`http://${serverIp}:${serverPort}`);
                
                // 更新连接状态
                connectionStatus.className = 'online-dot bg-yellow-500 mr-2';
                connectionText.textContent = '连接中...';
                connectBtn.disabled = true;
                connectBtn.innerHTML = '<i class="fa-solid fa-spinner fa-spin mr-2"></i>连接中...';
                
                // 监听连接成功事件
                socket.on('connect', () => {
                    connectionStatus.className = 'online-dot bg-online mr-2';
                    connectionText.textContent = '已连接';
                    connectBtn.disabled = true;
                    connectBtn.innerHTML = '<i class="fa-solid fa-check mr-2"></i>已连接';
                    
                    // 发送加入游戏请求
                    socket.emit('joinGame');
                    showWaitingModal();
                });
                
                // 监听连接断开事件
                socket.on('disconnect', () => {
                    connectionStatus.className = 'online-dot bg-offline mr-2';
                    connectionText.textContent = '未连接';
                    playerRole = 0;
                    gameActive = false;
                    updateGameStatus();
                    connectBtn.disabled = false;
                    connectBtn.innerHTML = '<i class="fa-solid fa-wifi mr-2"></i>连接服务器';
                    hideWaitingModal();
                    
                    // 更新角色显示
                    playerRoleEl.className = 'w-6 h-6 rounded-full mr-2 piece-shadow';
                    roleText.textContent = '未分配';
                    
                    // 禁用游戏按钮
                    restartBtn.disabled = true;
                    undoBtn.disabled = true;
                    
                    statusText.textContent = '连接已断开';
                });
                
                // 监听错误事件
                socket.on('connect_error', (error) => {
                    connectionStatus.className = 'online-dot bg-offline mr-2';
                    connectionText.textContent = '连接失败';
                    connectBtn.disabled = false;
                    connectBtn.innerHTML = '<i class="fa-solid fa-wifi mr-2"></i>连接服务器';
                    
                    alert(`连接失败: ${error.message}`);
                });
                
                // 监听游戏开始事件
                socket.on('gameStart', (role) => {
                    playerRole = role;
                    gameActive = true;
                    hideWaitingModal();
                    
                    // 更新角色显示
                    playerRoleEl.className = `w-6 h-6 rounded-full ${playerRole === 1 ? 'bg-black' : 'bg-white border border-gray-300'} mr-2 piece-shadow`;
                    roleText.textContent = playerRole === 1 ? '黑棋' : '白棋';
                    
                    // 启用游戏按钮
                    restartBtn.disabled = false;
                    undoBtn.disabled = false;
                    
                    // 只在首次进入时重绘棋盘和状态,不要调用 resetGame()
                    drawBoard();
                    updateGameStatus();
                });
                
                // 监听对手落子事件
                socket.on('moveConfirmed', (data) => {
                    console.log('收到服务器落子确认:', data);
                    isWaitingForServer = false;
                    
                    const { row, col, player } = data;
                    
                    // 更新棋盘
                    gameBoard[row][col] = player;
                    
                    // 确保历史记录同步
                    const lastMove = moveHistory[moveHistory.length - 1];
                    if (!lastMove || lastMove.row !== row || lastMove.col !== col || lastMove.player !== player) {
                        moveHistory.push({ row, col, player });
                    }
                    
                    // 重绘棋盘
                    drawBoard();
                    
                    // 检查是否胜利
                    if (checkWin(row, col, player)) {
                        showWinModal(player);
                        return;
                    }
                    
                    // 检查是否平局
                    if (checkDraw()) {
                        gameActive = false;
                        stopTimer();
                        statusText.textContent = '游戏结束 - 平局!';
                        return;
                    }
                    
                    // 切换当前玩家
                    currentPlayer = player === 1 ? 2 : 1;
                    
                    // 更新游戏状态
                    updateGameStatus();
                });
                
                // 监听游戏重置事件
                socket.on('gameReset', () => resetGame(false));
                
                // 监听对手断开连接事件
                socket.on('opponentDisconnected', () => {
                    gameActive = false;
                    stopTimer();
                    statusText.textContent = '对手已断开连接';
                    
                    // 禁用游戏按钮
                    restartBtn.disabled = true;
                    undoBtn.disabled = true;
                    
                    // 显示提示
                    alert('对手已断开连接');
                });
            }
            
            // 处理棋盘点击事件
            canvas.addEventListener('click', (event) => {
                if (!gameActive || playerRole !== currentPlayer || isWaitingForServer) {
                    console.log('点击事件触发 - 游戏状态:', gameActive, '当前玩家:', currentPlayer, '我的角色:', playerRole);
                    return;
                }
                
                // 计算点击位置对应的棋盘坐标
                const rect = canvas.getBoundingClientRect();
                const scaleX = canvas.width / rect.width;
                const scaleY = canvas.height / rect.height;
                
                const clickX = (event.clientX - rect.left) * scaleX;
                const clickY = (event.clientY - rect.top) * scaleY;
                
                const col = Math.round(clickX / CELL_SIZE);
                const row = Math.round(clickY / CELL_SIZE);
                
                // 检查坐标是否在棋盘范围内
                if (row < 0 || row >= BOARD_SIZE || col < 0 || col >= BOARD_SIZE) {
                    return;
                }
                
                // 检查该位置是否已经有棋子
                if (gameBoard[row][col] !== 0) {
                    return;
                }
                
                // 设置状态为等待服务器确认
                isWaitingForServer = true;
                updateGameStatus();
                
                // 向服务器发送落子请求
                socket.emit('makeMove', { row, col, player: playerRole });
                
                // 这里不再直接更新棋盘,等待服务器确认后再更新
            });
            
            // 窗口大小调整时重绘棋盘
            window.addEventListener('resize', () => {
                const newCellSize = Math.min(window.innerWidth * 0.8 / BOARD_SIZE, window.innerHeight * 0.6 / BOARD_SIZE);
                
                if (newCellSize !== CELL_SIZE) {
                    // 由于CELL_SIZE是常量,无法直接修改,这里只重绘不调整大小
                    drawBoard();
                }
            });
            
            // 绑定按钮事件
            connectBtn.addEventListener('click', connectToServer);
            restartBtn.addEventListener('click', () => resetGame(true));
            undoBtn.addEventListener('click', undoMove);
            newGameBtn.addEventListener('click', () => resetGame(true));
            cancelWaitingBtn.addEventListener('click', () => {
                if (socket) {
                    socket.disconnect();
                }
                hideWaitingModal();
            });
            
            // 初始化棋盘
            drawBoard();
        });
    </script>
</body>
</html>

服务端启动命令

bash 复制代码
// 1、初始化项目
npm init -y

// 2、安装所需的依赖项
npm install express socket.io

// 3、运行服务
node server.js
相关推荐
xiaofeichaichai6 小时前
Webpack
前端·webpack·node.js
Python私教9 小时前
把开源 Agent 打包成"解压双击即用"的 Windows 便携包:一条命令的完整实现
node.js
没事别瞎琢磨11 小时前
十一、审计与 Run Session——每一步操作都被记录
人工智能·node.js
没事别瞎琢磨11 小时前
十六、AgentSandbox——把所有模块串起来的编排类
人工智能·node.js
没事别瞎琢磨11 小时前
十二、网络代理与白名单规则引擎
人工智能·node.js
没事别瞎琢磨11 小时前
十四、Git Worktree 隔离执行
人工智能·node.js
没事别瞎琢磨13 小时前
十、统一 Runner 入口——能力检测与模式回退
人工智能·node.js
没事别瞎琢磨13 小时前
八、环境隔离——构建安全的子进程环境
人工智能·node.js
没事别瞎琢磨14 小时前
六、输出捕获与截断
人工智能·node.js
没事别瞎琢磨14 小时前
七、敏感路径预检——Protected Paths
人工智能·node.js