轻棋局(五):多棋种统一架构

引言

轻棋局同时支持中国象棋、五子棋和围棋三种完全不同的棋类。这三种棋的规则、棋盘、棋子、AI 算法都截然不同,但它们共享同一套用户系统、房间系统、对局管理和前端界面。

本篇讲解如何设计一个统一架构来支持多种棋类。


1. 设计挑战

三种棋的差异:

维度 中国象棋 五子棋 围棋
棋盘 10×9 15×15 19×19
棋子 32枚(红黑各16) 黑白各无限 黑白各181/180
走法 吃子/移动 放子 放子/提子
胜负 将杀/困毙 五连 领地/数目
AI Negamax + Alpha-Beta Alpha-Beta + 启发式 外部引擎(KataGo)
特殊规则 塞象眼、蹩马腿 禁手(长连、四四、三三) 打劫、打二还一

设计目标:

  1. 统一的房间系统 --- 创建/加入房间的 API 通用
  2. 统一的对局管理 --- 走棋、认输、和棋的 API 通用
  3. 统一的前端界面 --- 同一个 SPA 支持三种棋
  4. 独立的游戏逻辑 --- 每种棋的规则和 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 不继承同一个抽象类,而是各自独立实现。原因是:

  1. 规则差异太大,强制统一会导致大量空方法或条件判断
  2. Java 的类型系统不需要基类也能实现多态(通过接口)
  3. 独立实现更容易理解和维护

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) { ... }
}

共享组件

虽然对局模型独立,但共享以下组件:

  1. MatchPlayer --- 玩家状态(ID、用户名、剩余时间、准备状态)
  2. GameClock --- 计时器(支持不同时间控制)
  3. GameType --- 棋种枚举
  4. 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. 扩展新棋种

如果要添加新的棋种(如国际象棋),需要:

后端

  1. 在 GameType 枚举中添加新棋种
  2. 创建新的 Board 类(如 ChessBoard.java)
  3. 创建新的 Engine 接口和实现
  4. 创建新的 Match 类(如 ChessMatch.java)
  5. 在 RoomService 中添加创建对局的逻辑

前端

  1. 在 board.js 中添加新的渲染函数
  2. 在事件处理中添加新的分支
  3. 添加棋种特定的 UI 元素

数据库

无需修改 Schema --- 现有表结构已经支持任意棋种。


小结

多棋种统一架构的关键设计:

  1. 枚举驱动 --- 用 GameType 枚举区分棋种,而非继承层次
  2. 接口隔离 --- 每种棋的引擎接口独立,不强制统一
  3. 共享基础设施 --- 房间、用户、对局管理通用
  4. 按需分支 --- 在关键节点(走棋、渲染)根据棋种分发
  5. 数据格式统一 --- board_json 用 JSON 序列化,格式灵活

这种设计既保证了代码复用,又避免了过度抽象导致的复杂性。


上一篇:(四)前端 SPA 实战

下一篇:(六)部署上线与运维

相关推荐
qq_452396237 小时前
第十四篇:《Codex 源码拆解:137 个 crate 构成的智能体架构》
架构
MobotStone7 小时前
做了几个 Agent 项目后,我发现:真正拉开 AI 产品经理差距的,不是技术
人工智能·架构
IT大白鼠9 小时前
DeepSeek Harness 详解开源插件化 AI Agent 运行时:架构、模式、安装与生态
人工智能·架构·开源
这个DBA有点耶10 小时前
数据库双轨并行实战:全量并行策略、增量延迟控制、双向回切与一致性校验
数据库·架构·dba
Gl�ria10 小时前
MySQL 单机版 vs 高可用版:宕机排查 + 故障处理
mysql·adb·架构
Devlive 开源社区11 小时前
KnowForge 2026.0.8 发布:协作写作、团队空间、AI 朗读,这次更新有点大
架构
53488736abcdefg12 小时前
Hive 入门&架构原理
hive·hadoop·架构
一条大祥脚12 小时前
【CS336】lecture5 GPU|算力缩放|架构|内存模型|执行模型|TPU
架构
吴建旭 智宅焕12 小时前
智能家居B端交付能力解耦架构:从全链路自持到全国交付基础设施接入
架构·智能家居