引言
轻棋局同时支持中国象棋、五子棋和围棋三种完全不同的棋类。这三种棋的规则、棋盘、棋子、AI 算法都截然不同,但它们共享同一套用户系统、房间系统、对局管理和前端界面。
本篇讲解如何设计一个统一架构来支持多种棋类。
1. 设计挑战
三种棋的差异:
| 维度 | 中国象棋 | 五子棋 | 围棋 |
|---|---|---|---|
| 棋盘 | 10×9 | 15×15 | 19×19 |
| 棋子 | 32枚(红黑各16) | 黑白各无限 | 黑白各181/180 |
| 走法 | 吃子/移动 | 放子 | 放子/提子 |
| 胜负 | 将杀/困毙 | 五连 | 领地/数目 |
| AI | Negamax + Alpha-Beta | Alpha-Beta + 启发式 | 外部引擎(KataGo) |
| 特殊规则 | 塞象眼、蹩马腿 | 禁手(长连、四四、三三) | 打劫、打二还一 |
设计目标:
- 统一的房间系统 --- 创建/加入房间的 API 通用
- 统一的对局管理 --- 走棋、认输、和棋的 API 通用
- 统一的前端界面 --- 同一个 SPA 支持三种棋
- 独立的游戏逻辑 --- 每种棋的规则和 AI 独立实现
2. 棋种枚举
java
public enum GameType {
XIANGQI("象棋", "Chinese Chess", 10, 9),
GOMOKU("五子棋", "Gomoku", 15, 15),
GO("围棋", "Go", 19, 19);
final String displayName;
final String englishName;
final int rows;
final int cols;
}
3. 棋盘抽象
每种棋有自己的 Board 类,不强制继承同一个基类:
象棋棋盘
java
public class Board {
public static final int ROWS = 10;
public static final int COLS = 9;
private Piece[][] board;
private PieceColor currentTurn;
private long zobristHash;
// 走法生成
public List<Move> generateMoves(PieceColor color) { ... }
// 走棋
public void makeMove(Move move) { ... }
// 撤销
public void undoMove(Move move) { ... }
// 胜负判断
public boolean isGameOver() { ... }
public PieceColor getWinner() { ... }
}
五子棋棋盘
java
public class GomokuBoard {
public static final int SIZE = 15;
private GomokuStone[][] board;
private GomokuStone currentPlayer;
// 落子
public GomokuPlaceResult place(int row, int col, GomokuStone stone) { ... }
// 五连检测
public boolean hasFive(int row, int col) { ... }
// 禁手检测
public boolean isForbidden(int row, int col) { ... }
}
围棋棋盘
java
public class GoBoard {
public static final int SIZE = 19;
private GoStone[][] board;
private GoStone currentPlayer;
private boolean[][] koPoint; // 打劫点
// 落子
public GoMoveResult place(int row, int col, GoStone stone) { ... }
// 提子
public List<int[]> captureDeadStones(int row, int col) { ... }
// 领地计算
public GoScoreSummary calculateScore() { ... }
}
关键设计决策:三种 Board 不继承同一个抽象类,而是各自独立实现。原因是:
- 规则差异太大,强制统一会导致大量空方法或条件判断
- Java 的类型系统不需要基类也能实现多态(通过接口)
- 独立实现更容易理解和维护
4. 引擎抽象
引擎接口
每种棋定义自己的引擎接口:
java
// 象棋引擎
public interface XiangqiEngine {
Move findBestMove(Board board, PieceColor aiColor, MinimaxAI.Difficulty difficulty);
String getEngineId();
String getEngineText();
}
// 五子棋引擎
public interface GomokuEngine {
GomokuMove findBestMove(GomokuBoard board, GomokuStone aiColor);
String getEngineId();
String getEngineText();
}
// 围棋引擎
public interface GoEngine {
GoEngineMove findBestMove(GoBoard board, GoStone aiColor);
String getEngineId();
String getEngineText();
}
引擎实现层次
XiangqiEngine (接口)
├── BuiltinXiangqiEngine --- 内置 Minimax AI
├── ConfigurableXiangqiEngine --- 可配置外部引擎
└── PikafishUciEngine --- Pikafish UCI 适配器
GomokuEngine (接口)
├── BuiltinGomokuEngine --- 内置 Alpha-Beta AI
├── ConfigurableGomokuEngine --- 可配置外部引擎
└── PiskvorkGomokuEngine --- Piskvork 适配器
GoEngine (接口)
└── ConfigurableGoEngine --- 外部引擎(KataGo)
引擎选择
用户可以选择使用哪个引擎:
java
public class OnlineMatchEngine {
private final Map<String, XiangqiEngine> xiangqiEngines;
private final Map<String, GomokuEngine> gomokuEngines;
private final Map<String, GoEngine> goEngines;
public Move getAIMove(String gameType, String engineId, ...) {
switch (gameType) {
case "XIANGQI":
return xiangqiEngines.get(engineId).findBestMove(...);
case "GOMOKU":
return gomokuEngines.get(engineId).findBestMove(...);
case "GO":
return goEngines.get(engineId).findBestMove(...);
}
}
}
5. 对局管理
统一的对局模型
XiangqiMatch 和 GomokuMatch 分别管理各自的对局:
java
// 象棋对局
public class XiangqiMatch {
private final String gameId;
private final Board board;
private final MatchPlayer first;
private final MatchPlayer second;
private GameClock clock;
private PieceColor currentTurn;
private String status; // PLAYING, CHECKMATE, STALEMATE, RESIGNED
public MoveResult makeMove(String userId, Move move) { ... }
public void resign(String userId) { ... }
public void offerDraw(String userId) { ... }
}
// 五子棋对局
public class GomokuMatch {
private final String gameId;
private final GomokuBoard board;
private final MatchPlayer first;
private final MatchPlayer second;
private GameClock clock;
private GomokuStone currentTurn;
private String status;
public GomokuMoveResult makeMove(String userId, GomokuMove move) { ... }
}
共享组件
虽然对局模型独立,但共享以下组件:
- MatchPlayer --- 玩家状态(ID、用户名、剩余时间、准备状态)
- GameClock --- 计时器(支持不同时间控制)
- GameType --- 棋种枚举
- PlayerSide --- 玩家方(象棋用 RED/BLACK,五子棋/围棋用 BLACK/WHITE)
6. 房间系统
通用房间模型
房间不绑定特定棋种,通过 gameType 字段区分:
java
public class RoomSnapshot {
String roomId;
String gameType; // XIANGQI, GOMOKU, GO
String status; // WAITING, READY, PLAYING, FINISHED
String visibility; // PUBLIC, PRIVATE
String inviteCode;
String creatorId;
String creatorUsername;
String joinerId;
String joinerUsername;
Instant createdAt;
}
创建房间
java
public class CreateRoomRequest {
String gameType; // 必填
String visibility; // PUBLIC 或 PRIVATE
int initialTimeSeconds; // 时间控制(秒)
}
房间到对局的转换
当双方准备就绪后,RoomService 创建对应的 Match:
java
public class RoomService {
public GameSnapshot startGame(String roomId) {
Room room = repository.get(roomId);
switch (room.getGameType()) {
case "XIANGQI":
return createXiangqiGame(room);
case "GOMOKU":
return createGomokuGame(room);
case "GO":
return createGoGame(room);
}
}
private GameSnapshot createXiangqiGame(Room room) {
Board board = new Board();
XiangqiMatch match = new XiangqiMatch(
generateGameId(), board,
new MatchPlayer(room.getCreatorId(), room.getCreatorUsername()),
new MatchPlayer(room.getJoinerId(), room.getJoinerUsername()),
new GameClock(room.getInitialTimeSeconds())
);
return match.start();
}
}
7. 前端统一
SPA 路由
所有棋种共享同一套路由:
javascript
// 路由表(不分棋种)
const routes = ['home', 'play', 'room', 'game', 'practice',
'learn', 'watch', 'community', 'me'];
// 通过 state.game.gameType 判断当前棋种
function currentRoute() {
const raw = location.hash.replace(/^#\/?/, '');
// ... 解析路由
}
棋盘渲染
board.js 根据棋种选择不同的渲染函数:
javascript
function drawBoard(canvas, game) {
const ctx = canvas.getContext('2d');
switch (game.gameType) {
case 'XIANGQI':
drawXiangqiBoard(ctx, game.board, game.selectedFrom);
break;
case 'GOMOKU':
drawGomokuBoard(ctx, game.board, game.selectedFrom);
break;
case 'GO':
drawGoBoard(ctx, game.board, game.selectedFrom);
break;
}
}
事件处理
走棋事件根据棋种分发:
javascript
function handleBoardClick(event) {
const { row, col } = getClickPosition(event);
switch (state.game.gameType) {
case 'XIANGQI':
handleXiangqiClick(row, col);
break;
case 'GOMOKU':
handleGomokuClick(row, col);
break;
case 'GO':
handleGoClick(row, col);
break;
}
}
8. 数据库 Schema
通用表结构
sql
-- 对局表(支持所有棋种)
create table if not exists games (
id varchar(64) primary key,
room_id varchar(64) not null,
game_type varchar(16) not null, -- XIANGQI, GOMOKU, GO
status varchar(16) not null,
first_user_id varchar(64) not null,
second_user_id varchar(64) not null,
current_turn varchar(16),
winner_side varchar(16),
board_json text not null, -- 棋盘状态 JSON
move_count int not null default 0,
-- ... 其他字段
);
-- 着法表(支持所有棋种)
create table if not exists game_moves (
id varchar(96) primary key,
game_id varchar(64) not null,
move_index int not null,
side varchar(16) not null,
notation varchar(120), -- 走法记号
payload_json text not null, -- 走法数据 JSON
created_at timestamp not null
);
board_json 的序列化
不同棋种的 board_json 格式不同:
json
// 象棋
{
"type": "XIANGQI",
"board": [
["CHE_BLACK", null, "MA_BLACK", ...],
[null, null, null, ...],
...
],
"currentTurn": "RED"
}
// 五子棋
{
"type": "GOMOKU",
"board": [
[null, null, "BLACK", ...],
[null, "WHITE", null, ...],
...
],
"currentTurn": "WHITE"
}
// 围棋
{
"type": "GO",
"board": [
[null, null, "BLACK", ...],
[null, "WHITE", null, ...],
...
],
"currentTurn": "WHITE",
"koPoint": [4, 4]
}
9. 学习系统
统一的学习内容
学习内容(教程、残局、练习)按棋种分类:
json
{
"tutorials": [
{
"id": "tut-xq-001",
"gameType": "XIANGQI",
"title": "象棋基本规则",
"difficulty": "BEGINNER",
"steps": [...]
},
{
"id": "tut-gm-001",
"gameType": "GOMOKU",
"title": "五子棋基本策略",
"difficulty": "BEGINNER",
"steps": [...]
}
],
"puzzles": [
{
"id": "puz-xq-001",
"gameType": "XIANGQI",
"title": "马后炮杀法",
"fen": "...",
"solution": [...]
}
],
"recommendedPractice": [
{
"gameType": "XIANGQI",
"difficulty": "MEDIUM",
"engine": "BUILTIN"
}
]
}
学习进度跟踪
sql
create table if not exists learn_progress (
user_id varchar(64) not null,
content_type varchar(16) not null, -- tutorial, puzzle, practice
content_id varchar(96) not null,
completed_at timestamp not null,
primary key (user_id, content_type, content_id)
);
10. 扩展新棋种
如果要添加新的棋种(如国际象棋),需要:
后端
- 在
GameType枚举中添加新棋种 - 创建新的 Board 类(如
ChessBoard.java) - 创建新的 Engine 接口和实现
- 创建新的 Match 类(如
ChessMatch.java) - 在
RoomService中添加创建对局的逻辑
前端
- 在
board.js中添加新的渲染函数 - 在事件处理中添加新的分支
- 添加棋种特定的 UI 元素
数据库
无需修改 Schema --- 现有表结构已经支持任意棋种。
小结
多棋种统一架构的关键设计:
- 枚举驱动 --- 用
GameType枚举区分棋种,而非继承层次 - 接口隔离 --- 每种棋的引擎接口独立,不强制统一
- 共享基础设施 --- 房间、用户、对局管理通用
- 按需分支 --- 在关键节点(走棋、渲染)根据棋种分发
- 数据格式统一 --- board_json 用 JSON 序列化,格式灵活
这种设计既保证了代码复用,又避免了过度抽象导致的复杂性。
上一篇:(四)前端 SPA 实战
下一篇:(六)部署上线与运维