五子棋游戏开发教程
五子棋是非常适合做成网页小游戏的项目:规则清晰、交互简单,但又能覆盖棋盘建模、状态管理、胜负判断、历史记录、样式布局等完整开发环节。
这次在站点的游戏模块里新增了一个本地双人五子棋页面:/game/puzzle/gomoku。它不依赖外部服务,打开页面后黑白双方轮流落子,任意横向、纵向或斜向连成五子即可获胜。
更新日期:2026-07-06。
一、游戏规则
本地五子棋的规则设计得比较直接:
- 棋盘大小为
15 x 15。 - 黑棋先手。
- 玩家轮流在空交叉点落子。
- 横向、纵向、左斜、右斜任意方向连成 5 子即胜。
- 如果棋盘下满仍无人五连,则判定平局。
- 支持悔棋和重开。
这个版本没有加入禁手、AI 对战和计时器,重点放在基础玩法和核心算法上,适合作为网页棋类游戏的入门模板。
二、页面结构
五子棋页面复用了现有游戏模块的窗口样式:
GameWindowPage
└─ GomokuGame
├─ 状态栏
├─ 操作按钮:悔棋、重开
├─ 15 x 15 棋盘
└─ 最近落子记录
游戏列表数据放在 src/data/game/index.ts,新增一项:
{
slug: 'gomoku',
name: '五子棋',
desc: '本地双人五子棋,黑白轮流落子,支持五连胜负判断、悔棋和重开。',
shortName: 'GOM',
color: '#8b5a2b',
tags: ['棋类', '双人'],
}
有了 slug 后,站点会把它映射到 /game/puzzle/gomoku,游戏列表和左侧导航也会自动出现入口。
三、棋盘数据模型
棋盘用二维数组表示,每个位置有三种状态:
type Stone = 'black' | 'white'
type Board = Array<Array<Stone | null>>
空棋盘生成函数:
const boardSize = 15
const emptyBoard = () =>
Array.from({ length: boardSize }, () =>
Array<Stone | null>(boardSize).fill(null),
)
每次落子记录为:
type Move = {
r: number
c: number
stone: Stone
}
其中 r 是行,c 是列,stone 是当前棋子颜色。
四、落子流程
一次落子的流程是:
- 检查游戏是否还在进行。
- 检查目标位置是否为空。
- 克隆棋盘,写入当前棋子。
- 基于最后一步检查是否形成五连。
- 如果胜利,保存胜者和连线位置。
- 如果棋盘已满,判定平局。
- 否则切换下一手棋子。
核心思路是:只需要检查“刚刚落下的这颗棋子”附近是否形成五连,不需要每次扫描整个棋盘。
const move = { r, c, stone: currentStone }
const nextBoard = cloneBoard(board)
nextBoard[r][c] = currentStone
const line = getWinningLine(nextBoard, move)
if (line.length >= 5) {
setStatus('won')
setWinner(currentStone)
setWinningLine(line)
return
}
这样判断范围小,逻辑也更容易维护。
五、胜负判断算法
五子棋只需要判断 4 个方向:
const directions = [
[1, 0], // 竖向
[0, 1], // 横向
[1, 1], // 左上到右下
[1, -1], // 右上到左下
]
为什么是 4 个方向,而不是 8 个?
因为每条直线都有正反两个方向。例如横向只需要 [0, 1],算法内部会同时向左和向右扩展,所以不用再写 [0, -1]。
核心函数是 collectLine:
function collectLine(board, move, rowStep, colStep) {
const line = [{ r: move.r, c: move.c }]
for (const direction of [-1, 1]) {
let point = {
r: move.r + rowStep * direction,
c: move.c + colStep * direction,
}
while (isInside(point) && board[point.r][point.c] === move.stone) {
if (direction === -1) {
line.unshift(point)
} else {
line.push(point)
}
point = {
r: point.r + rowStep * direction,
c: point.c + colStep * direction,
}
}
}
return line
}
这个函数会从最后落子点出发,沿某个方向的正反两边扩展,把连续同色棋子收集起来。
如果某个方向收集到的棋子数量大于等于 5,就说明获胜:
function getWinningLine(board, move) {
for (const [rowStep, colStep] of directions) {
const line = collectLine(board, move, rowStep, colStep)
if (line.length >= 5) {
return line
}
}
return []
}
返回整条连线的好处是,页面可以把获胜的棋子高亮出来,而不只是显示“胜利”两个字。
六、悔棋实现
悔棋可以有两种做法:
- 保存每一步棋盘快照。
- 只保存落子历史,悔棋时重新生成棋盘。
这里使用第二种方式。原因是五子棋棋盘只有 15 x 15,重新回放历史成本很低,代码更简单,也不容易出现状态不一致。
const nextHistory = history.slice(0, -1)
const nextBoard = emptyBoard()
nextHistory.forEach((move) => {
nextBoard[move.r][move.c] = move.stone
})
悔棋后还要重置胜负状态:
setStatus('playing')
setWinner(null)
setWinningLine([])
然后把当前手切回被撤销的棋子颜色。
七、界面细节
棋盘使用 CSS Grid:
.gomokuBoard {
display: grid;
grid-template-columns: repeat(15, var(--gomoku-cell-size));
}
棋子用圆形 span 表示:
.gomokuStone {
border-radius: 999px;
height: 78%;
width: 78%;
}
页面还做了几个视觉提示:
- 鼠标悬停空位时显示淡色落点。
- 最后一步棋有描边和中心点。
- 获胜连线位置有高亮背景。
- 右侧显示最近 8 步落子记录。
这些小细节能让棋局读起来更清楚。
八、后续可以继续优化
这个版本是基础双人模式,后面可以继续扩展:
- 增加人机对战。
- 增加简单 AI:优先堵四、补四、堵三。
- 增加禁手规则。
- 增加计时器和回合倒计时。
- 增加胜负统计和本地存储。
- 增加导出棋谱或复盘模式。
如果做 AI,对入门版本来说,推荐先做启发式评分,而不是直接上复杂搜索:
自己能赢 -> 立即落子
对方能赢 -> 立即拦截
自己有活四/冲四 -> 优先补强
对方有活四/冲四 -> 优先阻断
否则选择中心附近评分最高的位置
这套思路足够做出一个能陪练的初级 AI。
总结
五子棋的核心不在 UI,而在状态和算法边界:
- 用二维数组描述棋盘。
- 用历史数组记录落子。
- 每次只围绕最后一步检查 4 条线。
- 胜利时返回整条连线用于高亮。
- 悔棋时用历史记录重建棋盘,避免状态混乱。
掌握这些之后,很多棋盘类小游戏都可以用类似结构实现,比如黑白棋、连珠、简化围棋、井字棋和棋谱复盘工具。
