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

引言

轻棋局同时支持中国象棋、五子棋和围棋三种完全不同的棋类。这三种棋的规则、棋盘、棋子、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 实战

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

相关推荐
心之伊始1 小时前
MySQL EXPLAIN 执行计划实战:从 type、Extra 到慢 SQL 定位与优化
java·架构·源码分析·csdn
国科安芯2 小时前
国科安芯推出商业航天级抗辐照全双工 RS485/422 收发器 ASC491S2Y
网络·分布式·单片机·架构·安全性测试
一切皆是因缘际会2 小时前
AI智能新时代
数据结构·人工智能·ai·架构
微三云、小叶3 小时前
新型消费积分商业模式拆解:盈利架构、衰减铸造模型与项目风控要点
架构·软件开发·商业模式·本地生活·商业思维·私域运营
SilentSamsara3 小时前
Python 微服务全链路:gRPC + 链路追踪 + 服务网格接入
开发语言·分布式·python·微服务·架构
candyTong3 小时前
Claude Code 的工具延迟加载机制
架构
葫芦和十三4 小时前
执行拓扑|Agent 不只是会什么,还要怎么跑
架构·agent·ai编程
国科安芯4 小时前
国科安芯推出商业航天级抗辐照半双工 RS485 收发器 ASC485S2Y
前端·单片机·嵌入式硬件·架构·安全性测试
小小龙学IT5 小时前
Go 后端开发实战:从单机千QPS到十万级微服务架构的演进之路
微服务·架构·golang
java_cj5 小时前
Caffeine+Redis两级缓存架构实战:从手动实现到自定义注解的完整方案
缓存·架构