五子棋应用开发

创建于:2025年1月10日

使用 O1 回答 Chat01

问题

我想开发一个在线的五子棋应用,你先分析产品PRD,然后编写完整的网站代码

思考

Crafting the PRD

I’m working through an online Gobang app, mapping out requirements with a PRD analysis. Considering front-end tech like HTML and CSS, possibly integrating server-side elements. Progressing through website code development.

Crafting the PRD and tech plan

I’m pulling together a PRD with an overview, features, use cases, flow, and tech stack. Leaning towards a Node.js backend, possibly integrating websockets. Exploring both minimal and advanced multi-lobby systems.

Laying out the game

I’m focusing on PRD details like multiple player support, user actions, connection requirements, and gameplay dynamics. Ensuring a comprehensive understanding of these aspects.

Integrating code

Considering both a minimal and advanced app, I’m mapping out Node.js and socket.io. Figuring out how to span detailed and basic functionalities effectively.

Building and linking

I’m gathering info on the tech stack: HTML, CSS, JavaScript, React, Socket.io, Node.js, and expressing interest in MongoDB. Progressing through the app development and server connection.

Crafting the framework

I’m building the server.js file with Express, HTTP, and Socket.io. The app will handle connections and game logic, serving files from the "public" directory.

Crafting the game

I’m piecing together a minimal Gobang game, focusing on simplicity. The server stores the board state in memory, using a single room logic. Contribution through concise coding and instructions.

Thought about 五子棋在线应用开发,持续 17 秒

回答

以下内容将分为两个部分:
1)产品 PRD 分析
2)完整示例网站代码(基于 Node.js + Socket.IO + HTML/JS)

该示例主要是帮助你快速上手一个可多人在线对战的五子棋雏形项目,功能点还比较基础,后续可根据需求自由扩展。


一、产品 PRD 分析

1. 产品背景及目标

  • 产品名称:在线五子棋(Gobang Online)
  • 目标:提供一个可支持多人对战的在线五子棋平台,用户可在浏览器中实时对弈,系统自动判定胜负。

2. 核心功能

  1. 用户登录/注册
    • 基础版本:本示例中可先不做真实的账号系统,直接使用系统为访客随机生成的昵称或 ID。
    • 完整版本:可通过数据库存储账号信息,允许注册、登录和身份验证等。
  2. 匹配功能
    • 用户可在大厅点击“创建房间”或“加入房间”与他人对战。
    • 如果要简化,可先做“单房间”模式,所有进入网站的两个人即配对成功。
  3. 实时通信
    • 使用 Socket.IO 或其他 WebSocket 框架,实现棋子落下后的实时同步、对局信息广播。
  4. 对弈规则
    • 棋盘默认 15×15(也可自定义尺寸),黑棋先行,先行成五连者胜。
    • 完整版本需处理禁手规则(如长连、四四、三三),本示例暂不实现。
  5. 判定胜负
    • 当任意一方落子后形成五连,即宣布该方胜利,房间可结束或重新开始对战。
  6. 对局记录、房间管理(可选扩展)
    • 对弈后可记录游戏信息,比如玩家对局历史、对局时长、赢家/输家。
    • 管理员可管理多房间并终止异常对局等。

3. 角色 & 使用场景

  • 普通用户
    1. 访问网页后自动分配一个 ID。
    2. 点击“开始游戏”或“加入房间”,进入对弈页面。
    3. 放置黑/白棋子,实时查看对手走棋动态。
    4. 游戏结束后查看结果,并可选择再来一局。

4. 主要流程示例(简化单房间)

  1. 用户 A 进入网站,自动分配 ID = A1,并在大厅等待。
  2. 用户 B 进入网站,自动分配 ID = B1,匹配到用户 A 后一起进入对局页面。
  3. 系统指定黑棋(先行)给用户 A,白棋给用户 B。
  4. A 落子 -> 通过 Socket.IO 广播落子信息 -> B 实时看到 A 的落子。
  5. 双方轮流下棋 -> 检测到 5 个相同颜色连在一起 -> 系统判定胜负 -> 通知所有玩家结束。
  6. 可选择再来一局或者返回大厅。

5. 非功能性需求

  • 性能:单房间并发量不大,Socket.IO 足以支持多人对战;后续可做多房间架构扩展。
  • 安全:基础 Demo 以学习为主,正式环境需完善权限控制、身份校验、数据验证等。
  • 可扩展性:后期可加排行榜、好友系统、观战模式、聊天功能等。

二、完整示例网站代码

下面给出一个最基础的“单房间对战”示例,包含:

  • server.js:Node.js 服务器端代码,负责 Socket 事件处理、静态文件托管等。
  • public/index.html:前端页面,使用 Canvas 绘制棋盘与棋子,通过 Socket.IO 与服务器交互。

说明

  1. 此示例仅用于演示,未实现登录/注册与持久化存储,仅在内存中保存当前对局的数据。
  2. 仅支持两个玩家同时进行对局,不包含多人观战、切换房间等功能。
  3. 若要扩展多房间、玩家信息等,可在此基础上进行拆分与改造。

1. 目录结构

假设你的项目文件夹名称为 “gobang-online”,可以采用以下结构:

text
gobang-online/ |- server.js |- package.json |- public/ |- index.html

2. server.js

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}`); });

3. public/index.html

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>

4. 运行项目

  1. 在项目目录下(gobang-online)执行 npm init -y 初始化 package.json
  2. 执行 npm install express socket.io 安装依赖。
  3. 运行 node server.js (或使用 nodemon server.js) 启动服务器。
  4. 在浏览器打开 http://localhost:3000,即可进入游戏页面。

可以同时开两个不同的浏览器或在不同电脑/手机上打开同一地址进行测试,两个玩家将匹配在同一个棋盘下对弈。


总结与扩展

  • 上述代码演示了一个非常简易的在线五子棋原型:

    • 利用内存数组 board[][] 保存棋盘状态;
    • 利用 Socket.IO 实现实时落子同步
    • 只实现了单房间、两名玩家对战的最基础功能。
  • 若要扩展

    1. 多房间 / 房间管理:将 playersboard 等数据结构改造为以房间 ID 为维度进行存储。
    2. 用户登录和数据库:增加用户体系(注册/登录),使用数据库(如 MongoDB)保存用户信息、对战记录等。
    3. 观战模式:允许第三人或更多人进入房间观战,只能看不能下棋。
    4. 聊天/其他交互:在房间内增加聊天功能,增强互动。
    5. 安全性 & 稳定性:处理更多异常情况,确保服务器稳定运行。

希望这个 Demo 能帮助你快速搭建在线五子棋的基础框架,你可以据此逐步完善,打造出一个功能更丰富、体验更完善的五子棋在线对战系统。祝开发顺利!

问题

我要的是一个可以作为独立站上线的网站,不是一个简单的demo

思考

Crafting a robust website

I’m thinking through a full-fledged website with multi-lobby capabilities, user registration, login, chat, and admin support. This involves crafting a comprehensive and stable code base, potentially integrating a database like MongoDB or MySQL, and ensuring simplicity and functionality throughout.

Crafting a minimalist approach

I'm working through using Node, Express, and Socket for a minimal yet production-ready site. This involves leveraging JWT for user login, employing either MongoDB or MySQL for data persistence, and considering a design upgrade for better UI.

Setting up the tech stack

I’m piecing together choices for Node 16, MySQL for data storage, and a project structure using folders for server code, public assets, and models, considering either Sequelize or a simpler library.

Piecing together the plan

I’m crafting a production-worthy site with separate code, launch instructions, and a typical Node project setup. This includes package management, environment variables for database credentials, and sessions for user authentication.

Crafting the essentials

I'm building a single code snippet for express-session, user DB setup, and minimal admin interface, ensuring it captures the logic comprehensively without delving into advanced features.

Thought about online GoBang platform,持续 16 秒

回答

下面给你提供一个较为完整、可独立上线部署的“在线五子棋”网站示例。它包含了用户注册与登录多房间对战房间内聊天观战模式等常见功能,并使用 MySQL 数据库进行持久化存储(也可更换为其它数据库,如 MongoDB)。整体结构可直接部署到一台服务器上(如阿里云、腾讯云等),搭配 Nginx + PM2 等常见生产环境方案来上线运行。

以下内容分为四部分:

  1. 产品需求说明(PRD)
  2. 项目结构与功能概览
  3. 示例核心代码
  4. 部署上线指导

提醒:示例中为了演示完整流程,代码量较多,且未进行严谨的错误处理与安全加固,在正式生产环境仍需进行更多的安全与异常处理(如防止 SQL 注入、输入校验、错误日志、限流等)。


一、产品需求说明(PRD)

  1. 产品名称:在线五子棋(可多房间对战、带用户系统)

  2. 主要用户及需求

    • 普通玩家:注册并登录后,可以创建或加入房间,与其他玩家进行在线实时对战;也可进入任意房间观战别人的对局;在对局过程中可以进行文字聊天沟通,游戏结束后可查看结果并重开一局。
    • 管理员/站长(可选角色):拥有管理权限,可查看所有房间情况,踢出违规玩家等(此处示例不做深入实现,仅留扩展接口)。
  3. 核心功能

    1. 用户注册与登录
      • 采用用户名+密码的方式注册,密码以加密形式存储在数据库中。
      • 登录成功后,使用 Session(或 JWT)维持会话状态。
    2. 房间管理
      • 用户登录后可创建房间(可命名),或从房间列表中选择一个房间加入;
      • 每个房间最多允许 2 名玩家正式对战,其他用户只能进入“观战模式”;
      • 房主可设置房间密码(可选),若有设置,其他人加入时需输入正确密码。
    3. 五子棋对局
      • 使用 15×15 棋盘,黑棋先行,先形成五连者胜;
      • 实时通信:玩家落子后所有在房间的成员(包括观战者)都能同步看到棋面变化;
      • 胜负判定:系统在每次落子后自动判断是否达成五连,若达成则宣布胜利并结束此局。
    4. 聊天系统
      • 在房间内部,可使用文字聊天,所有房间内玩家/观众都能看到最新聊天。
    5. 对战记录(可选扩展):
      • 可记录玩家的对战场次、胜负、胜率等(本示例仅在数据库保留对局简单信息,可扩展为详细历史记录)。
  4. 非功能性需求

    • 可扩展性:后续可加好友系统、排行榜、更多五子棋规则(禁手、让子)等。
    • 安全性:示例中简单演示注册登录与房间密码,正式场景下需更完善的验证。
    • 性能:使用 Socket.IO+Express+MySQL,一般中小型应用足够,若玩家量大需后续扩展负载均衡方案。

二、项目结构与功能概览

示例推荐的目录结构如下(可根据自身习惯与框架进行调整):

text
gobang-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

主要功能概览:

  1. 用户注册登录:在 authRoutes.js 中提供注册、登录接口;前端 index.html 中提供登录表单;
  2. 房间管理:在 roomRoutes.js 中提供创建房间、获取房间列表、加入房间等接口;房间信息存储在 RoomModel.js
  3. 实时对战(Socket.IO):server.js 中处理 Socket 事件,每个房间一个命名空间或使用 roomId 加入房间,实现落子广播与聊天;
  4. 数据库:在 db.js 中建立 MySQL 连接;使用 UserModel.js, RoomModel.js 等封装增删改查;
  5. 前端页面
    • index.html:登录/注册跳转;
    • 登录后进入主页面(或 lobby.html),可查看房间列表并创建/加入房间;
    • 进入房间后,加载五子棋对局界面及聊天区域;

三、示例核心代码

以下为了演示核心思想,提供了简化版的完整流程代码。请根据需要自行拆分到 routes/models/public/ 等多个文件,以符合更专业的目录结构。在示例中,我们会写在同一个文件或少量文件里,便于一次阅读。

注意:该示例使用 MySQL 做数据库演示,并采用 express-session 保存登录态;请事先在 MySQL 中创建一个数据库(名称自定,如 gobang_db),并执行简单的建表语句。

1. 数据库准备

在 MySQL 中执行以下脚本(仅示例,实际可自行扩展表字段):

sql
CREATE 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 );

2. 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" } }

3. 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}`); });

4. 前端示例(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>

四、部署上线指导

  1. 准备服务器环境

    • 购买或使用已有云服务器(Linux),安装 Node.js(建议 14+ 或 16+ 版本)与 MySQL;
    • 创建一个数据库并执行上述建表脚本;
    • 安装 Nginx 用于做反向代理和静态资源处理。
  2. 上传代码 & 安装依赖

    • 将本项目文件夹(gobang-online/)上传到服务器;
    • 在该文件夹下执行 npm install,安装依赖。
  3. 修改数据库配置

    • server.js(或拆分的 db.js)中修改 MySQL 的连接信息(host、user、password、database)。
  4. 启动项目

    • 测试启动:npm run devnode server.js,检查日志无报错;
    • 访问 http://服务器IP:3000 若能打开并正常操作,即基本可用。
  5. 使用 Nginx 反向代理(可选)

    • 配置一个域名指向该服务器;
    • 在 Nginx 配置文件中,把域名或 80/443 端口的请求转发到本地 localhost:3000
    • 同时可以在 Nginx 配合 SSL 证书实现 HTTPS。
  6. 使用 PM2 守护进程(推荐)

    • 全局安装 pm2:npm install pm2 -g
    • 进入项目目录:pm2 start server.js --name gobang
    • 可通过 pm2 logs gobang 查看日志,并用 pm2 restart gobang 重启应用。

至此,你就可以在浏览器输入 https://yourdomain.com(或 IP:端口)来访问这套在线五子棋网站,完成注册、登录、创建房间、对战、观战、聊天等基本功能了。


结语

以上是一套可独立站上线的“在线五子棋”示例方案:

  • 具备 用户系统多房间管理实时对弈聊天对局记录等核心功能;
  • 采用 Node.js + Express + Socket.IO + MySQL 技术栈;
  • 前后端仅做了基础演示,生产环境下应考虑更多安全措施与异常处理;
  • 你可以在此基础上进一步完善 UI 界面、添加更多功能(如好友系统、排行榜、私聊、禁手规则等)。

希望能帮助你快速搭建并上线一款功能较完整的在线五子棋网站。祝开发顺利、项目成功上线!

分享这个问答