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

引言

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

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

相关推荐
深蓝AI28 分钟前
苹果智能入华拆解:八样首批功能、支持的机型、千问在三个入口的位置
人工智能·架构
海宇大数据2 小时前
零信任架构实战:基于海宇全能小微企业报告构建自动化持牌网络小贷小微商户极速助业贷秒级全维风险决策网关
网络·人工智能·架构·自动化
Nablai6 小时前
LX系列多国语言能力技术解析:ESP32双栈架构下的语种策略设计
人工智能·架构
阿俊-全栈开发7 小时前
LikeShop 多商户 Java 版商城微服务模块拆分与部署架构演进实践
java·微服务·架构·likeshop开源商城·多商户源码
X54先生(人文科技)9 小时前
百度智能对 X54 先生学科底色评估
人工智能·深度学习·百度·架构
HelloWorld0019 小时前
拒绝被大模型算力绑架!基于语义复杂度与意图分流打造多模型混合动态路由网关(全套架构+实战)
架构
电子科技圈9 小时前
芯科科技2026年Tech Talks技术系列讲座再增添全新主题共探无线SoC架构演进与安全合规
科技·嵌入式硬件·mcu·物联网·安全·架构·iot
paopaokaka_luck10 小时前
基于springboot+vue的益暖微光公益服务平台(DeepSeek公益助手、善款信息追溯、志愿服务积分闭环、ECharts运营分析、活动状态自动流转)
java·前端·javascript·spring boot·mysql·架构·echarts
涛声依旧-底层原理研究所10 小时前
拆解 AI Agent 的四个内核:Loop、Tools、Memory 与 Harness
人工智能·架构
傻啦嘿哟10 小时前
分布式爬虫架构:Redis+Scrapy打造企业级爬虫系统
分布式·爬虫·架构