跳到主要内容

五子棋游戏开发教程

· 阅读需 6 分钟
ahKevinXy
作者

五子棋是非常适合做成网页小游戏的项目:规则清晰、交互简单,但又能覆盖棋盘建模、状态管理、胜负判断、历史记录、样式布局等完整开发环节。

这次在站点的游戏模块里新增了一个本地双人五子棋页面:/game/puzzle/gomoku。它不依赖外部服务,打开页面后黑白双方轮流落子,任意横向、纵向或斜向连成五子即可获胜。

更新日期:2026-07-06。

一、游戏规则

本地五子棋的规则设计得比较直接:

  1. 棋盘大小为 15 x 15
  2. 黑棋先手。
  3. 玩家轮流在空交叉点落子。
  4. 横向、纵向、左斜、右斜任意方向连成 5 子即胜。
  5. 如果棋盘下满仍无人五连,则判定平局。
  6. 支持悔棋和重开。

这个版本没有加入禁手、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 是当前棋子颜色。

四、落子流程

一次落子的流程是:

  1. 检查游戏是否还在进行。
  2. 检查目标位置是否为空。
  3. 克隆棋盘,写入当前棋子。
  4. 基于最后一步检查是否形成五连。
  5. 如果胜利,保存胜者和连线位置。
  6. 如果棋盘已满,判定平局。
  7. 否则切换下一手棋子。

核心思路是:只需要检查“刚刚落下的这颗棋子”附近是否形成五连,不需要每次扫描整个棋盘。

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 []
}

返回整条连线的好处是,页面可以把获胜的棋子高亮出来,而不只是显示“胜利”两个字。

六、悔棋实现

悔棋可以有两种做法:

  1. 保存每一步棋盘快照。
  2. 只保存落子历史,悔棋时重新生成棋盘。

这里使用第二种方式。原因是五子棋棋盘只有 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%;
}

页面还做了几个视觉提示:

  1. 鼠标悬停空位时显示淡色落点。
  2. 最后一步棋有描边和中心点。
  3. 获胜连线位置有高亮背景。
  4. 右侧显示最近 8 步落子记录。

这些小细节能让棋局读起来更清楚。

八、后续可以继续优化

这个版本是基础双人模式,后面可以继续扩展:

  1. 增加人机对战。
  2. 增加简单 AI:优先堵四、补四、堵三。
  3. 增加禁手规则。
  4. 增加计时器和回合倒计时。
  5. 增加胜负统计和本地存储。
  6. 增加导出棋谱或复盘模式。

如果做 AI,对入门版本来说,推荐先做启发式评分,而不是直接上复杂搜索:

自己能赢 -> 立即落子
对方能赢 -> 立即拦截
自己有活四/冲四 -> 优先补强
对方有活四/冲四 -> 优先阻断
否则选择中心附近评分最高的位置

这套思路足够做出一个能陪练的初级 AI。

总结

五子棋的核心不在 UI,而在状态和算法边界:

  1. 用二维数组描述棋盘。
  2. 用历史数组记录落子。
  3. 每次只围绕最后一步检查 4 条线。
  4. 胜利时返回整条连线用于高亮。
  5. 悔棋时用历史记录重建棋盘,避免状态混乱。

掌握这些之后,很多棋盘类小游戏都可以用类似结构实现,比如黑白棋、连珠、简化围棋、井字棋和棋谱复盘工具。