一个基于 EdgeOne Pages + 云函数 + Blob Storage 的实时五子棋在线对战小游戏。支持创建/加入房间、随机匹配、观战、实时聊天、胜负判定、战绩排行榜与第三方登录,开箱即部署。
- 房间制对战:创建房间获得 6 位房间号,分享给好友加入;也可在大厅点击"等待中的房间"直接加入。
- 快速匹配:一键匹配,自动加入无密码的等待中房间,没有则自动创建新房间。
- 私密房:创建房间时可设置密码,加入需输入正确密码;大厅列表仅显示"私密"标识不暴露密码。
- 棋盘大小选择:支持 9×9 / 13×13 / 15×15 三种规格,星位根据棋盘大小动态计算。
- 禁手规则(默认关闭):黑方长连、四四、三三禁手判定,可手动开启;白方不受限。
- 计时模式(默认关闭):每步 2 分钟、总时长不限,超时判负;娱乐模式默认不限时。
- 实时同步:房间页 500ms 轮询拉取最新棋局状态,落子有音效与最后一手高亮。
- 获胜连线高亮:五子连成时用金色发光线贯穿,获胜棋子脉动闪烁。
- 悔棋 / 求和 / 认输 / 再来一局:悔棋撤回发起方自己的棋子,求和与再来一局需对手同意(双向确认),认输直接判负。
- 观战模式:大厅"对战中"的房间可进入观战,只读不参与。
- 局内聊天:支持文字与表情消息,最近 50 条。
- 断线重进:本地记录"我的房间",刷新/重进后可一键回到未结束的对局。
- 游客持久化:游客昵称存入 localStorage,刷新页面不丢失。
- 战绩排行榜:仅记录登录用户,以 socialUid 为唯一键防止刷榜;积分制排序(胜 3 分 / 平 1 分 / 负 0 分),展示胜率。
- 防自刷榜:同一登录用户不能同时占黑白双方。
- 第三方登录:聚合登录(QQ / 微信),也支持游客模式直接输入昵称游玩。
- 房间自动回收:等待、对战、已结束三种状态分别有 TTL,空闲自动清理。
| 层 | 技术 |
|---|---|
| 前端 | React 19 + TypeScript + Vite |
| 样式 | Tailwind CSS v4(@tailwindcss/vite 插件) |
| 后端 | EdgeOne Pages 云函数(onRequest 边缘函数) |
| 存储 | EdgeOne Blob Storage(@edgeone/pages-blob) |
| 登录 | 聚合登录 juhedenglu.cn(QQ / 微信 OAuth) |
| 代码规范 | oxlint |
gomoku-game/
├── cloud-functions/ # 边缘云函数(后端 API)
│ └── api/
│ ├── auth/ # 登录相关
│ │ ├── login.ts # 获取第三方登录跳转地址
│ │ ├── callback.ts # code 换取用户信息
│ │ └── return.ts # OAuth 回调落地页
│ ├── room/ # 房间与对局逻辑
│ │ ├── _game.ts # 棋盘创建 / 胜负判定 / 禁手判定
│ │ ├── _timer.ts # 计时工具
│ │ ├── _utils.ts # Blob 封装 + 排行榜读写(版本化)
│ │ ├── create.ts # 创建房间
│ │ ├── match.ts # 快速匹配
│ │ ├── join.ts # 加入房间
│ │ ├── kick.ts # 房主解散房间
│ │ ├── leave.ts # 玩家离开
│ │ ├── list.ts # 大厅房间列表
│ │ ├── state.ts # 拉取房间状态
│ │ ├── move.ts # 落子 + 胜负判定 + 禁手 + 排行榜
│ │ ├── request.ts # 悔棋/求和/再来一局(双向确认)
│ │ ├── action.ts # 认输
│ │ ├── rejoin.ts # 断线重进
│ │ └── chat.ts # 局内聊天
│ └── leaderboard.ts # 排行榜读取(带版本校验自动清理旧数据)
├── src/ # 前端源码
│ ├── components/
│ │ ├── Board.tsx # 棋盘组件(获胜连线高亮)
│ │ └── ChatPanel.tsx # 聊天面板
│ ├── pages/
│ │ ├── Login.tsx # 登录页(含游客持久化)
│ │ ├── Callback.tsx # 登录回调页
│ │ ├── Home.tsx # 大厅首页
│ │ ├── Room.tsx # 对局页
│ │ ├── Leaderboard.tsx # 排行榜(积分制)
│ │ └── UserCenter.tsx # 个人中心
│ ├── api.ts # API 封装 + 本地存储
│ ├── types.ts # 类型定义
│ ├── sound.ts # 落子音效
│ ├── App.tsx # 路由
│ ├── main.tsx # 入口
│ └── index.css # 全局样式
├── index.html
├── vite.config.ts
├── edgeone.json # EdgeOne 部署配置
└── package.json
┌──────────────┐ fetch /api/* ┌────────────────────────┐ Blob KV ┌──────────────┐
│ 浏览器前端 │ ───────────────▶ │ EdgeOne 边缘云函数 │ ──────────▶ │ Blob Storage │
│ React SPA │ ◀─────────────── │ (cloud-functions/api) │ ◀────────── │ GAME_BLOB │
└──────────────┘ JSON 响应 └────────────────────────┘ └──────────────┘
│ ▲
│ OAuth 跳转 │ 读写房间/排行榜
▼ │
┌──────────────────┐ code 换信息 ┌──────────────────┐ │
│ 聚合登录 juhedenglu│ ◀──────────────▶ │ auth/login 等 │ ─────────────────┘
└──────────────────┘ └──────────────────┘
- 前端为单页应用,所有数据通过
fetch调用同源的/api/*边缘函数。 - 后端为无状态云函数,所有持久化都落在名为
GAME_BLOB的 Blob Store 中:房间以房间号为 key,排行榜以_leaderboard为 key。 - 实时性由前端轮询实现:大厅 4s、对局 500ms、聊天 2s。
interface Room {
id: string // 6 位房间号
players: {
black: { nickname: string; socialUid?: string } // 房主(黑方)
white: { nickname: string; socialUid?: string } | null // 加入者(白方)
}
board: ('black' | 'white' | null)[][] // 棋盘(9/13/15)
boardSize: number // 棋盘大小(9/13/15,默认 15)
currentTurn: 'black' | 'white'
winner: 'black' | 'white' | 'draw' | null
winLine?: [number, number][] | null // 获胜的 5 连位置(用于高亮)
status: 'waiting' | 'playing' | 'finished'
createdAt: number
lastActiveAt: number // 心跳时间戳
moves?: MoveRecord[] // 棋谱
request?: ConsentRequest | null // 当前未决请求
forbid: boolean // 是否启用禁手(默认 false)
timer: { perMoveMs: number; totalMs: number } // 计时配置(0 = 不限)
password: string | null // 私密房密码
turnStartAt: number
blackUsedMs: number
whiteUsedMs: number
timeLoser: 'black' | 'white' | null
messages?: ChatMessage[]
}// 版本化存储,旧格式数据自动清理
interface LeaderboardData {
version: number // 当前版本号 = 2
entries: Record<socialUid, {
nickname: string
wins: number
losses: number
draws: number
}>
}仅记录有 socialUid 的登录用户,以 socialUid 为唯一键,游客不记入排行榜。数据带版本号,旧格式数据读取时自动清空重置。
可手动开启。黑方落子若形成长连(≥6 连)、四四、三三,判黑方负、白方胜。白方不受禁手限制。
可手动开启,每步 2 分钟、总时长不限。仅单步超时判负。后端在落子和轮询两处检查超时。
悔棋撤回发起方自己刚下的最后一步棋(从棋谱末尾向前找到发起方的最后一手),同时撤回之后对方可能已下的棋子,回合回到发起方。需对手同意。
- 求和:直接判平局,需对手同意。
- 认输:发起方直接判负,无需确认。
- 再来一局:清空棋盘重新开始,需对手同意。
判定胜负时记录 winLine(5 连坐标),前端用金色发光线贯穿五子,获胜棋子脉动闪烁。
- 仅登录用户:以
socialUid为唯一键,游客不记入。 - 防自刷榜:同一
socialUid不能同时占黑白双方(join 和 match 双重校验)。 - 积分制排序:胜 +3 分、平 +1 分、负 0 分;积分相同比胜场,再比负场少。
- 版本化存储:数据带
version字段,旧格式数据自动清理,无需手动操作。 - 条目校验:读取时校验每条数据格式,过滤无效条目。
| 机制 | 说明 |
|---|---|
| 仅登录用户记录 | 游客无 socialUid,不记入排行榜 |
| socialUid 唯一键 | 同名昵称不会合并,每个账号独立统计 |
| 禁止自对战 | 同一 socialUid 不能同时占黑白双方 |
| 强一致读写 | 排行榜读写均用强一致读,避免并发覆盖 |
EdgeOne Blob Storage 的 get(key) 默认是最终一致读取,get(key, { consistency: "strong" }) 才是强一致读取。
- 判定与变更路径(join / move / request / state / rejoin)一律使用强一致读。
- 大厅列表
list.ts对每个房间用强一致读。 - 排行榜的读与写均使用强一致读。
- 弱一致读仅用于
create.ts生成房间号时的查重(碰撞概率极低)。 - 密码安全:所有返回给前端的接口均通过解构剔除密码明文。
# 安装依赖
npm install
# 本地前端开发
npm run dev
# 类型检查 + 构建
npm run build
# 预览构建产物
npm run preview
# 代码检查
npm run lint注意:云函数依赖 EdgeOne 运行时与 Blob Storage,本地
npm run dev仅能调试前端;完整联调请在 EdgeOne Pages 环境中进行。
- 将本仓库导入 EdgeOne Pages。
edgeone.json已配置:构建命令npm run build,输出目录dist。- 确认云函数存储依赖已配置:
edgeone.json中"node-functions": { "external_node_modules": ["@edgeone/pages-blob"] }(本项目已配好)。 - 在 EdgeOne 控制台为站点配置环境变量。
- 部署后,云函数挂载在
/api/*,前端与 API 同源,无需额外配置跨域。
| 变量名 | 说明 |
|---|---|
JHDL_APPID |
聚合登录 juhedenglu.cn 的 AppID |
JHDL_APPKEY |
聚合登录 juhedenglu.cn 的 AppKey |
未配置时登录接口返回提示,游客模式仍可使用。
- 实时性依赖轮询,未使用 WebSocket / 长连接,落子有最高约 500ms 的可见延迟。
- Blob Storage 不提供原子的条件写(CAS),极端竞态理论上仍可能发生;强一致读 + 双重校验已覆盖实际场景。
- 游客昵称持久化在 localStorage,清除浏览器数据后会丢失。
Gomoku Online · EdgeOne Makers · GitHub