五子棋应用开发
创建于:2025年1月10日
创建于:2025年1月10日
我想开发一个在线的五子棋应用,你先分析产品PRD,然后编写完整的网站代码
以下内容将分为两个部分:
1)产品 PRD 分析
2)完整示例网站代码(基于 Node.js + Socket.IO + HTML/JS)
该示例主要是帮助你快速上手一个可多人在线对战的五子棋雏形项目,功能点还比较基础,后续可根据需求自由扩展。
下面给出一个最基础的“单房间对战”示例,包含:
说明:
- 此示例仅用于演示,未实现登录/注册与持久化存储,仅在内存中保存当前对局的数据。
- 仅支持两个玩家同时进行对局,不包含多人观战、切换房间等功能。
- 若要扩展多房间、玩家信息等,可在此基础上进行拆分与改造。
假设你的项目文件夹名称为 “gobang-online”,可以采用以下结构:
textgobang-online/ |- server.js |- package.json |- public/ |- index.html
javascript// server.js const express = require('express'); const http = require('http'); const socketIO = require('socket.io'); // 创建 Express 应用 const app = express(); const server = http.createServer(app); const io = socketIO(server); // 使用 Express 托管静态资源 app.use(express.static('public')); // 存储游戏状态(仅单房间示例) let players = []; // 存储已连接玩家的 socket.id let currentPlayerIndex = 0; // 当前执棋玩家在 players 中的索引(0 or 1) let boardSize = 15; let board = []; // 记录棋盘状态, board[row][col] = 'black' / 'white' / null let isGameOver = false; // 初始化棋盘 function initBoard() { board = []; for (let i = 0; i < boardSize; i++) { let row = []; for (let j = 0; j < boardSize; j++) { row.push(null); } board.push(row); } } // 轮到某位玩家下棋,返回 'black' 或 'white' function getChessColorByIndex(index) { return index === 0 ? 'black' : 'white'; } // 胜负判定:检查坐标 (row, col) 落子后是否连成 5 子 function checkWin(row, col, color) { // 需要判断四个方向:横、竖、左斜、右斜 // 这里只写一个通用的检查函数 function countStones(dRow, dCol) { let count = 1; // 当前落子已计入 // 往正方向 let r = row + dRow, c = col + dCol; while ( r >= 0 && r < boardSize && c >= 0 && c < boardSize && board[r][c] === color ) { count++; r += dRow; c += dCol; } // 往反方向 r = row - dRow; c = col - dCol; while ( r >= 0 && r < boardSize && c >= 0 && c < boardSize && board[r][c] === color ) { count++; r -= dRow; c -= dCol; } return count; } // 四个方向:水平、垂直、左斜、右斜 const directions = [ [0, 1], // 水平 [1, 0], // 垂直 [1, 1], // 左上-右下斜 [1, -1], // 右上-左下斜 ]; for (let [dr, dc] of directions) { if (countStones(dr, dc) >= 5) { return true; } } return false; } // Socket.io 事件 io.on('connection', (socket) => { console.log('A user connected:', socket.id); // 如果玩家人数少于 2,则加入 players if (players.length < 2) { players.push(socket.id); // 如果这是第一个玩家,则初始化棋盘 if (players.length === 1) { initBoard(); currentPlayerIndex = 0; isGameOver = false; } } // 通知前端当前玩家身份(黑棋/白棋) let playerIndex = players.indexOf(socket.id); let playerColor = getChessColorByIndex(playerIndex); socket.emit('playerInfo', { color: playerColor }); // 广播当前连接玩家数量 io.emit('playersCount', { count: players.length }); // 监听落子事件 socket.on('placeChess', (data) => { // data 包含 row, col if (isGameOver) return; // 判断是否轮到自己 if (socket.id !== players[currentPlayerIndex]) { // 不是你的回合 return; } // 落子 let color = getChessColorByIndex(currentPlayerIndex); let { row, col } = data; // 防止重复落子 if (board[row][col]) { return; } board[row][col] = color; // 广播给所有人 io.emit('chessPlaced', { row, col, color }); // 检测胜负 if (checkWin(row, col, color)) { io.emit('gameOver', { winner: color }); isGameOver = true; return; } // 轮换当前玩家 currentPlayerIndex = (currentPlayerIndex + 1) % 2; }); // 监听断开连接 socket.on('disconnect', () => { console.log('User disconnected:', socket.id); // 从 players 中移除 players = players.filter(id => id !== socket.id); // 广播当前玩家数量 io.emit('playersCount', { count: players.length }); }); }); // 启动服务器 const PORT = 3000; server.listen(PORT, () => { console.log(`Server is running at http://localhost:${PORT}`); });
html<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8" /> <title>在线五子棋 Demo</title> <style> body { margin: 0; padding: 0; background: #fafafa; font-family: sans-serif; display: flex; flex-direction: column; align-items: center; justify-content: center; } h1 { margin-top: 40px; text-align: center; } #info { margin: 10px; text-align: center; } #board { border: 1px solid #666; background-color: #f2c68d; /* 棋盘背景色 */ cursor: pointer; } </style> </head> <body> <h1>在线五子棋 Demo</h1> <div id="info"> <p id="status">正在连接服务器...</p> <p>当前在线玩家数:<span id="players-count">0</span></p> </div> <!-- 棋盘 --> <canvas id="board" width="450" height="450"></canvas> <!-- Socket.IO --> <script src="/socket.io/socket.io.js"></script> <script> const socket = io(); // 画布配置 const boardSize = 15; // 15 x 15 const cellSize = 30; // 每个格子 30 像素 const canvas = document.getElementById('board'); const ctx = canvas.getContext('2d'); // 玩家信息 let myColor = null; // 'black' 或 'white' let isGameOver = false; // 初始化棋盘绘制 function drawBoard() { ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.strokeStyle = '#333'; // 画 15 条线(横 + 纵) for (let i = 0; i < boardSize; i++) { // 画水平线 ctx.beginPath(); ctx.moveTo(cellSize / 2, cellSize / 2 + i * cellSize); ctx.lineTo(cellSize * (boardSize - 0.5), cellSize / 2 + i * cellSize); ctx.stroke(); ctx.closePath(); // 画垂直线 ctx.beginPath(); ctx.moveTo(cellSize / 2 + i * cellSize, cellSize / 2); ctx.lineTo(cellSize / 2 + i * cellSize, cellSize * (boardSize - 0.5)); ctx.stroke(); ctx.closePath(); } } // 在棋盘上画棋子 function drawChess(row, col, color) { const x = cellSize / 2 + col * cellSize; const y = cellSize / 2 + row * cellSize; ctx.beginPath(); ctx.arc(x, y, 12, 0, 2 * Math.PI); if (color === 'black') { ctx.fillStyle = '#000'; } else { ctx.fillStyle = '#fff'; } ctx.fill(); ctx.strokeStyle = '#000'; ctx.stroke(); ctx.closePath(); } // 处理点击落子 canvas.addEventListener('click', (e) => { if (isGameOver) return; // 计算点击位置行列 const rect = canvas.getBoundingClientRect(); const x = e.clientX - rect.left; const y = e.clientY - rect.top; const col = Math.floor(x / cellSize); const row = Math.floor(y / cellSize); // 发送落子消息 socket.emit('placeChess', { row, col }); }); // 连接成功后,接收玩家信息 socket.on('playerInfo', (data) => { myColor = data.color; document.getElementById('status').innerText = `你是 ${myColor === 'black' ? '黑棋(先手)' : '白棋(后手)'},请等待对方进入游戏。`; }); // 更新玩家数量 socket.on('playersCount', (data) => { document.getElementById('players-count').innerText = data.count; }); // 棋子已落下 socket.on('chessPlaced', (data) => { const { row, col, color } = data; drawChess(row, col, color); }); // 游戏结束 socket.on('gameOver', (data) => { isGameOver = true; const winner = data.winner; if (winner === myColor) { alert('恭喜,你赢了!'); } else { alert('很遗憾,你输了!'); } }); // 页面初始化 drawBoard(); </script> </body> </html>
gobang-online
)执行 npm init -y
初始化 package.json
。npm install express socket.io
安装依赖。node server.js
(或使用 nodemon server.js
) 启动服务器。可以同时开两个不同的浏览器或在不同电脑/手机上打开同一地址进行测试,两个玩家将匹配在同一个棋盘下对弈。
上述代码演示了一个非常简易的在线五子棋原型:
board[][]
保存棋盘状态;若要扩展:
players
、board
等数据结构改造为以房间 ID 为维度进行存储。希望这个 Demo 能帮助你快速搭建在线五子棋的基础框架,你可以据此逐步完善,打造出一个功能更丰富、体验更完善的五子棋在线对战系统。祝开发顺利!
我要的是一个可以作为独立站上线的网站,不是一个简单的demo
下面给你提供一个较为完整、可独立上线部署的“在线五子棋”网站示例。它包含了用户注册与登录、多房间对战、房间内聊天、观战模式等常见功能,并使用 MySQL 数据库进行持久化存储(也可更换为其它数据库,如 MongoDB)。整体结构可直接部署到一台服务器上(如阿里云、腾讯云等),搭配 Nginx + PM2 等常见生产环境方案来上线运行。
以下内容分为四部分:
提醒:示例中为了演示完整流程,代码量较多,且未进行严谨的错误处理与安全加固,在正式生产环境仍需进行更多的安全与异常处理(如防止 SQL 注入、输入校验、错误日志、限流等)。
产品名称:在线五子棋(可多房间对战、带用户系统)
主要用户及需求
核心功能
非功能性需求
示例推荐的目录结构如下(可根据自身习惯与框架进行调整):
textgobang-online/ ├─ config/ │ └─ db.js # 数据库连接配置 ├─ models/ │ ├─ UserModel.js # 用户表 │ ├─ RoomModel.js # 房间表 │ └─ GameRecordModel.js # 对局记录表(可选) ├─ routes/ │ ├─ authRoutes.js # 用户注册登录等 │ ├─ roomRoutes.js # 创建房间、获取房间列表等 │ └─ ... ├─ public/ │ ├─ css/ │ │ └─ style.css │ ├─ js/ │ │ └─ main.js # 前端主逻辑 │ ├─ img/ │ └─ index.html # 主页 ├─ server.js # 主入口,启动 Express + Socket.IO ├─ package.json └─ README.md
主要功能概览:
authRoutes.js
中提供注册、登录接口;前端 index.html
中提供登录表单;roomRoutes.js
中提供创建房间、获取房间列表、加入房间等接口;房间信息存储在 RoomModel.js
;server.js
中处理 Socket 事件,每个房间一个命名空间或使用 roomId
加入房间,实现落子广播与聊天;db.js
中建立 MySQL 连接;使用 UserModel.js
, RoomModel.js
等封装增删改查;index.html
:登录/注册跳转;lobby.html
),可查看房间列表并创建/加入房间;以下为了演示核心思想,提供了简化版的完整流程代码。请根据需要自行拆分到 routes/
、models/
、public/
等多个文件,以符合更专业的目录结构。在示例中,我们会写在同一个文件或少量文件里,便于一次阅读。
注意:该示例使用 MySQL 做数据库演示,并采用
express-session
保存登录态;请事先在 MySQL 中创建一个数据库(名称自定,如gobang_db
),并执行简单的建表语句。
在 MySQL 中执行以下脚本(仅示例,实际可自行扩展表字段):
sqlCREATE DATABASE IF NOT EXISTS gobang_db; USE gobang_db; -- 用户表 CREATE TABLE IF NOT EXISTS users ( id INT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(100) NOT NULL, -- 存加密后的密码 created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); -- 房间表 CREATE TABLE IF NOT EXISTS rooms ( id INT AUTO_INCREMENT PRIMARY KEY, room_name VARCHAR(100) NOT NULL, room_password VARCHAR(100) DEFAULT NULL, -- 如果非空,则加入房间需要密码 created_by INT NOT NULL, -- 房主(对应用户id) created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); -- 可选:对局记录表 CREATE TABLE IF NOT EXISTS game_records ( id INT AUTO_INCREMENT PRIMARY KEY, room_id INT NOT NULL, black_user_id INT, white_user_id INT, winner_color VARCHAR(10), -- 'black' / 'white' finished_at DATETIME DEFAULT CURRENT_TIMESTAMP );
package.json
json{ "name": "gobang-online", "version": "1.0.0", "description": "Online Gobang (five-in-a-row) with multi-room & user login", "main": "server.js", "scripts": { "start": "node server.js", "dev": "nodemon server.js" }, "dependencies": { "bcrypt": "^5.1.0", "body-parser": "^1.20.2", "express": "^4.18.2", "express-session": "^1.17.3", "mysql2": "^3.3.0", "socket.io": "^4.6.1" } }
server.js
(核心示例)说明:此文件包含了所有关键逻辑:数据库连接、用户注册登录路由、房间路由、Socket.IO 实时对战等。
在实际项目中应拆分为多个文件来保持代码整洁。
js/******************************** * server.js ********************************/ const express = require('express'); const path = require('path'); const bcrypt = require('bcrypt'); const bodyParser = require('body-parser'); const session = require('express-session'); const mysql = require('mysql2/promise'); const http = require('http'); const { Server } = require('socket.io'); // ============ 1. 数据库连接配置 ============ const dbConfig = { host: 'localhost', user: 'root', // 请按实际情况修改 password: '123456',// 请按实际情况修改 database: 'gobang_db' }; let pool; // MySQL 连接池 (async () => { try { pool = await mysql.createPool({ ...dbConfig, connectionLimit: 10 }); console.log('[DB] MySQL pool created successfully.'); } catch (err) { console.error('[DB] Error creating MySQL pool:', err); process.exit(1); } })(); // ============ 2. 初始化 Express 应用 ============ const app = express(); const server = http.createServer(app); const io = new Server(server); // 设置 session 中间件 app.use(session({ secret: 'some_secret_key', // 换成更安全的随机字符串 resave: false, saveUninitialized: false, })); // 解析 POST 数据 app.use(bodyParser.urlencoded({ extended: false })); app.use(bodyParser.json()); // 静态文件托管 app.use(express.static(path.join(__dirname, 'public'))); // ============ 3. 用户注册 & 登录接口 ============ // [注册] POST /api/register app.post('/api/register', async (req, res) => { try { const { username, password } = req.body; if (!username || !password) { return res.status(400).json({ error: '用户名和密码均不能为空' }); } // 检查用户是否已存在 const [rows] = await pool.query('SELECT id FROM users WHERE username=?', [username]); if (rows.length > 0) { return res.status(400).json({ error: '用户名已被占用' }); } // 加密密码 const hashedPwd = await bcrypt.hash(password, 10); // 插入数据库 await pool.query('INSERT INTO users (username, password) VALUES (?, ?)', [username, hashedPwd]); return res.json({ success: true }); } catch (error) { console.error('[Register Error]', error); res.status(500).json({ error: '服务器错误' }); } }); // [登录] POST /api/login app.post('/api/login', async (req, res) => { try { const { username, password } = req.body; // 查询用户 const [rows] = await pool.query('SELECT * FROM users WHERE username=?', [username]); if (rows.length === 0) { return res.status(400).json({ error: '用户名或密码错误' }); } const user = rows[0]; // 验证密码 const match = await bcrypt.compare(password, user.password); if (!match) { return res.status(400).json({ error: '用户名或密码错误' }); } // 登录成功,写入 session req.session.user = { id: user.id, username: user.username }; return res.json({ success: true, user: { id: user.id, username: user.username } }); } catch (error) { console.error('[Login Error]', error); res.status(500).json({ error: '服务器错误' }); } }); // [登出] GET /api/logout app.get('/api/logout', (req, res) => { req.session.destroy(() => { res.clearCookie('connect.sid'); res.json({ success: true }); }); }); // ============ 4. 房间管理接口 ============ // [创建房间] POST /api/room/create app.post('/api/room/create', async (req, res) => { try { const user = req.session.user; if (!user) { return res.status(401).json({ error: '请先登录' }); } const { roomName, roomPassword } = req.body; if (!roomName) { return res.status(400).json({ error: '房间名不能为空' }); } // 插入到 rooms 表 const [result] = await pool.query( 'INSERT INTO rooms (room_name, room_password, created_by) VALUES (?, ?, ?)', [roomName, roomPassword || null, user.id] ); const roomId = result.insertId; res.json({ success: true, roomId }); } catch (error) { console.error('[Create Room Error]', error); res.status(500).json({ error: '服务器错误' }); } }); // [获取房间列表] GET /api/room/list app.get('/api/room/list', async (req, res) => { try { const [rows] = await pool.query( 'SELECT r.id, r.room_name, r.room_password, u.username as owner FROM rooms r JOIN users u ON r.created_by = u.id ORDER BY r.id DESC' ); // 返回房间信息列表 const data = rows.map(r => ({ id: r.id, roomName: r.room_name, hasPassword: !!r.room_password, owner: r.owner })); res.json({ success: true, rooms: data }); } catch (error) { console.error('[Room List Error]', error); res.status(500).json({ error: '服务器错误' }); } }); // [加入房间] POST /api/room/join app.post('/api/room/join', async (req, res) => { try { const user = req.session.user; if (!user) { return res.status(401).json({ error: '请先登录' }); } const { roomId, roomPassword } = req.body; // 查房间 const [rows] = await pool.query('SELECT * FROM rooms WHERE id=?', [roomId]); if (rows.length === 0) { return res.status(404).json({ error: '房间不存在' }); } const room = rows[0]; // 若房间有密码,校验 if (room.room_password && room.room_password !== roomPassword) { return res.status(403).json({ error: '房间密码错误' }); } res.json({ success: true, roomId: room.id }); } catch (error) { console.error('[Join Room Error]', error); res.status(500).json({ error: '服务器错误' }); } }); // ============ 5. Socket.IO 实时对战逻辑 ============ // 数据结构:存储每个房间的实时状态 let roomStates = { /* [roomId]: { board: 15x15数组, players: [ userIdBlack, userIdWhite ], // 两个对弈玩家 currentColor: 'black' or 'white', isGameOver: false, ... } */ }; function initRoomState(roomId) { roomStates[roomId] = { board: Array.from({ length: 15 }, () => Array(15).fill(null)), players: [], currentColor: 'black', isGameOver: false }; } // 检查 5 连 function checkWin(board, row, col, color) { function countSame(dirRow, dirCol) { let count = 1; // 当前落子已算1 // 正方向 let r = row + dirRow, c = col + dirCol; while (r >= 0 && r < 15 && c >= 0 && c < 15 && board[r][c] === color) { count++; r += dirRow; c += dirCol; } // 反方向 r = row - dirRow; c = col - dirCol; while (r >= 0 && r < 15 && c >= 0 && c < 15 && board[r][c] === color) { count++; r -= dirRow; c -= dirCol; } return count; } const directions = [ [1, 0], // 垂直 [0, 1], // 水平 [1, 1], // 左上-右下 [1, -1], // 右上-左下 ]; for (let [dr, dc] of directions) { if (countSame(dr, dc) >= 5) return true; } return false; } // Socket 连接 io.on('connection', (socket) => { console.log('[Socket] new connection:', socket.id); // 每个 socket 需要知道自己是谁、在哪个房间 let currentRoomId = null; let currentUserId = null; // 登录后在 session 中的用户id // 从 cookie-session 中取用户信息 // 注意:默认 Socket.IO 不会自动带上 Express 的 session,需要额外中间件或在前端设置 // 在此处为了示例简单,我们通过前端主动 emit ‘joinWithSession’ 带上 userId/roomId // 监听:用户请求加入房间的 Socket 频道 socket.on('joinWithSession', async (data) => { const { userId, roomId } = data; currentUserId = userId; currentRoomId = roomId; // 房间状态初始化 if (!roomStates[roomId]) { initRoomState(roomId); } // 将 socket 加入房间的命名空间 socket.join(`room_${roomId}`); console.log(`[Socket] user ${userId} joined room ${roomId}`); // 如果房间内玩家 < 2 且该用户尚未在 players 中,则把他放进去 const roomState = roomStates[roomId]; if (!roomState.players.includes(userId) && roomState.players.length < 2) { roomState.players.push(userId); } // 告知房间内所有用户,当前的在线玩家列表 const playersInfo = await getPlayersInfo(roomState.players); io.to(`room_${roomId}`).emit('roomPlayers', playersInfo); // 若当前玩家数满 2 人且游戏未开始,可以进行游戏 // 也可能已有棋盘进度,直接让新来的用户(观众)拿到当前棋盘 socket.emit('initBoard', { board: roomState.board }); }); // 监听:落子事件 socket.on('placeChess', async (data) => { const { row, col } = data; const roomState = roomStates[currentRoomId]; if (!roomState || roomState.isGameOver) return; // 判断是否轮到该用户 // players[0] => 黑棋, players[1] => 白棋 // currentColor => 'black' or 'white' const userIndex = roomState.players.indexOf(currentUserId); if (userIndex === -1) { return; // 该用户不是对局玩家 } const expectedColor = userIndex === 0 ? 'black' : 'white'; if (expectedColor !== roomState.currentColor) { return; // 未轮到你 } // 棋盘落子 if (roomState.board[row][col] === null) { roomState.board[row][col] = roomState.currentColor; // 广播给房间内所有人 io.to(`room_${currentRoomId}`).emit('chessPlaced', { row, col, color: roomState.currentColor }); // 判断胜负 if (checkWin(roomState.board, row, col, roomState.currentColor)) { roomState.isGameOver = true; io.to(`room_${currentRoomId}`).emit('gameOver', { winnerColor: roomState.currentColor }); // 记录到数据库(可选) try { await pool.query( `INSERT INTO game_records (room_id, black_user_id, white_user_id, winner_color) VALUES (?, ?, ?, ?)`, [ currentRoomId, roomState.players[0] || null, roomState.players[1] || null, roomState.currentColor ] ); } catch (err) { console.error('[Save Record Error]', err); } return; } // 切换下一个颜色 roomState.currentColor = (roomState.currentColor === 'black') ? 'white' : 'black'; } }); // 监听:房间聊天 socket.on('sendMessage', (message) => { // 广播消息给该房间内所有人 io.to(`room_${currentRoomId}`).emit('chatMessage', { userId: currentUserId, message }); }); // 断开连接 socket.on('disconnect', () => { console.log('[Socket] disconnected:', socket.id); // 目前示例中不做复杂的玩家离场处理,可自行在此处理玩家退出逻辑 }); }); // 获取玩家信息(从数据库查询 username 等) async function getPlayersInfo(userIds) { if (!userIds || userIds.length === 0) return []; const [rows] = await pool.query( `SELECT id, username FROM users WHERE id IN (?)`, [userIds] ); return rows; } // ============ 6. 启动服务器 ============ const PORT = 3000; server.listen(PORT, () => { console.log(`Server is running on http://localhost:${PORT}`); });
public/index.html
)这里演示一个最简单的前端页面,包含:
实际项目中,你可以使用更复杂的前端框架(Vue/React/Angular),或拆分为多页面。
注意:要让 Socket.IO 在登录后也能获取到对应 Session,需要做额外配置。示例中为了简化,前端拿到用户 ID 后手动 emit 给服务器
joinWithSession
。
html<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8" /> <title>在线五子棋</title> <style> body { font-family: sans-serif; margin: 0; padding: 20px; } .hidden { display: none; } #login-section, #register-section, #lobby-section, #room-section { border: 1px solid #ccc; padding: 15px; margin-bottom: 20px; } #board { border: 1px solid #333; cursor: pointer; } .chat-box { height: 200px; border: 1px solid #666; overflow-y: auto; margin-top: 10px; padding: 5px; } .chat-input { width: 80%; } </style> </head> <body> <h1>在线五子棋</h1> <!-- 登录表单 --> <div id="login-section"> <h3>登录</h3> <p><input id="login-username" placeholder="用户名" /></p> <p><input id="login-password" type="password" placeholder="密码" /></p> <p><button id="btn-login">登录</button></p> <p><a href="#" id="show-register">没有账号?点此注册</a></p> </div> <!-- 注册表单 --> <div id="register-section" class="hidden"> <h3>注册</h3> <p><input id="reg-username" placeholder="用户名" /></p> <p><input id="reg-password" type="password" placeholder="密码" /></p> <p><button id="btn-register">注册</button></p> <p><a href="#" id="show-login">已有账号?返回登录</a></p> </div> <!-- 大厅(房间列表 & 创建房间) --> <div id="lobby-section" class="hidden"> <h3>房间列表</h3> <div id="room-list"></div> <h4>创建房间</h4> <p> <input id="create-room-name" placeholder="房间名称" /> <input id="create-room-password" type="password" placeholder="可选密码" /> <button id="btn-create-room">创建</button> </p> <p><button id="btn-logout">退出登录</button></p> </div> <!-- 房间对战区 --> <div id="room-section" class="hidden"> <h3>房间对战</h3> <p id="room-info"></p> <canvas id="board" width="450" height="450"></canvas> <h4>聊天</h4> <div class="chat-box" id="chat-box"></div> <p> <input id="chat-input" class="chat-input" placeholder="说点什么..."/> <button id="chat-send">发送</button> </p> <p><button id="btn-exit-room">返回大厅</button></p> </div> <script src="/socket.io/socket.io.js"></script> <script> const socket = io(); // 全局状态 let currentUser = null; let currentRoomId = null; // DOM const loginSection = document.getElementById('login-section'); const registerSection = document.getElementById('register-section'); const lobbySection = document.getElementById('lobby-section'); const roomSection = document.getElementById('room-section'); const showRegister = document.getElementById('show-register'); const showLogin = document.getElementById('show-login'); const loginUsername = document.getElementById('login-username'); const loginPassword = document.getElementById('login-password'); const regUsername = document.getElementById('reg-username'); const regPassword = document.getElementById('reg-password'); const btnLogin = document.getElementById('btn-login'); const btnRegister = document.getElementById('btn-register'); const btnLogout = document.getElementById('btn-logout'); const roomListDiv = document.getElementById('room-list'); const createRoomName = document.getElementById('create-room-name'); const createRoomPassword = document.getElementById('create-room-password'); const btnCreateRoom = document.getElementById('btn-create-room'); const roomInfo = document.getElementById('room-info'); const boardCanvas = document.getElementById('board'); const chatBox = document.getElementById('chat-box'); const chatInput = document.getElementById('chat-input'); const chatSend = document.getElementById('chat-send'); const btnExitRoom = document.getElementById('btn-exit-room'); // 切换UI显示 function showSection(section) { loginSection.classList.add('hidden'); registerSection.classList.add('hidden'); lobbySection.classList.add('hidden'); roomSection.classList.add('hidden'); section.classList.remove('hidden'); } showRegister.addEventListener('click', () => { showSection(registerSection); }); showLogin.addEventListener('click', () => { showSection(loginSection); }); // ========== 登录/注册 ========== btnLogin.addEventListener('click', async () => { const username = loginUsername.value.trim(); const password = loginPassword.value.trim(); if (!username || !password) { alert('请输入用户名和密码'); return; } const resp = await fetch('/api/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username, password }) }); const data = await resp.json(); if (data.success) { currentUser = data.user; alert('登录成功'); showSection(lobbySection); fetchRoomList(); } else { alert(data.error || '登录失败'); } }); btnRegister.addEventListener('click', async () => { const username = regUsername.value.trim(); const password = regPassword.value.trim(); if (!username || !password) { alert('请输入用户名和密码'); return; } const resp = await fetch('/api/register', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username, password }) }); const data = await resp.json(); if (data.success) { alert('注册成功,请登录'); showSection(loginSection); } else { alert(data.error || '注册失败'); } }); btnLogout.addEventListener('click', async () => { const resp = await fetch('/api/logout'); const data = await resp.json(); if (data.success) { currentUser = null; showSection(loginSection); } }); // ========== 大厅:房间相关 ========== async function fetchRoomList() { const resp = await fetch('/api/room/list'); const data = await resp.json(); if (!data.success) return; roomListDiv.innerHTML = ''; data.rooms.forEach(r => { const div = document.createElement('div'); div.innerHTML = ` <b>房间${r.id}:</b> ${r.roomName} - 房主:${r.owner} ${r.hasPassword ? '(有密码)' : ''} <button onclick="joinRoom(${r.id}, ${r.hasPassword})">进入</button> `; roomListDiv.appendChild(div); }); } window.joinRoom = async function(roomId, hasPassword) { let inputPwd = null; if (hasPassword) { inputPwd = prompt('请输入房间密码:'); if (inputPwd === null) return; } const resp = await fetch('/api/room/join', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ roomId, roomPassword: inputPwd }) }); const data = await resp.json(); if (data.success) { currentRoomId = roomId; showSection(roomSection); roomInfo.innerText = `你正在房间 ${roomId}`; // 通知服务器Socket层 socket.emit('joinWithSession', { userId: currentUser.id, roomId }); } else { alert(data.error || '加入房间失败'); } }; btnCreateRoom.addEventListener('click', async () => { const roomName = createRoomName.value.trim(); const roomPassword = createRoomPassword.value.trim(); if (!roomName) { alert('房间名称不能为空'); return; } const resp = await fetch('/api/room/create', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ roomName, roomPassword }) }); const data = await resp.json(); if (data.success) { alert('创建成功'); fetchRoomList(); } else { alert(data.error || '创建房间失败'); } }); btnExitRoom.addEventListener('click', () => { currentRoomId = null; showSection(lobbySection); fetchRoomList(); }); // ========== 房间:五子棋对战 ========== const ctx = boardCanvas.getContext('2d'); const boardSize = 15; const cellSize = 30; // 先画空棋盘 drawBoard(); function drawBoard() { ctx.clearRect(0, 0, boardCanvas.width, boardCanvas.height); ctx.strokeStyle = '#333'; for (let i = 0; i < boardSize; i++) { // 水平线 ctx.beginPath(); ctx.moveTo(cellSize / 2, cellSize / 2 + i * cellSize); ctx.lineTo(cellSize * (boardSize - 0.5), cellSize / 2 + i * cellSize); ctx.stroke(); // 垂直线 ctx.beginPath(); ctx.moveTo(cellSize / 2 + i * cellSize, cellSize / 2); ctx.lineTo(cellSize / 2 + i * cellSize, cellSize * (boardSize - 0.5)); ctx.stroke(); } } function drawChess(row, col, color) { const x = cellSize / 2 + col * cellSize; const y = cellSize / 2 + row * cellSize; ctx.beginPath(); ctx.arc(x, y, 12, 0, 2 * Math.PI); ctx.fillStyle = (color === 'black') ? '#000' : '#fff'; ctx.fill(); ctx.stroke(); } // 点击落子 boardCanvas.addEventListener('click', (e) => { if (!currentRoomId) return; const rect = boardCanvas.getBoundingClientRect(); const x = e.clientX - rect.left; const y = e.clientY - rect.top; const col = Math.floor(x / cellSize); const row = Math.floor(y / cellSize); socket.emit('placeChess', { row, col }); }); // 监听服务器下发的初始化棋盘 socket.on('initBoard', (data) => { const { board } = data; drawBoard(); for (let r = 0; r < 15; r++) { for (let c = 0; c < 15; c++) { if (board[r][c]) { drawChess(r, c, board[r][c]); } } } }); // 监听落子 socket.on('chessPlaced', (data) => { const { row, col, color } = data; drawChess(row, col, color); }); // 监听游戏结束 socket.on('gameOver', (data) => { alert(`游戏结束!${data.winnerColor === 'black' ? '黑棋' : '白棋'}获胜!`); }); // 监听房间玩家列表 socket.on('roomPlayers', (players) => { console.log('房间玩家信息', players); // 可在房间信息区域显示玩家名字 }); // ========== 房间:聊天 ========== chatSend.addEventListener('click', () => { const msg = chatInput.value.trim(); if (!msg) return; socket.emit('sendMessage', msg); chatInput.value = ''; }); socket.on('chatMessage', (data) => { const { userId, message } = data; const p = document.createElement('p'); p.textContent = `玩家${userId}: ${message}`; chatBox.appendChild(p); chatBox.scrollTop = chatBox.scrollHeight; }); </script> </body> </html>
准备服务器环境
上传代码 & 安装依赖
gobang-online/
)上传到服务器;npm install
,安装依赖。修改数据库配置
server.js
(或拆分的 db.js
)中修改 MySQL 的连接信息(host、user、password、database)。启动项目
npm run dev
或 node server.js
,检查日志无报错;http://服务器IP:3000
若能打开并正常操作,即基本可用。使用 Nginx 反向代理(可选)
localhost:3000
;使用 PM2 守护进程(推荐)
npm install pm2 -g
;pm2 start server.js --name gobang
;pm2 logs gobang
查看日志,并用 pm2 restart gobang
重启应用。至此,你就可以在浏览器输入 https://yourdomain.com
(或 IP:端口)来访问这套在线五子棋网站,完成注册、登录、创建房间、对战、观战、聊天等基本功能了。
以上是一套可独立站上线的“在线五子棋”示例方案:
希望能帮助你快速搭建并上线一款功能较完整的在线五子棋网站。祝开发顺利、项目成功上线!