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

引言

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

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

相关推荐
Anastasiozzzz2 小时前
万字深度实战!AI Agent 接入万物的底层密码:MCP 协议传输机制与开发指南(下篇)
java·开发语言·数据库·人工智能·ai·架构
Anastasiozzzz2 小时前
深度解析 AI 时代的“TCP/IP协议”:Agent-to-Agent (A2A) 通信架构与多智能体协同底层逻辑
大数据·开发语言·网络·数据库·网络协议·tcp/ip·架构
AI自动化工坊2 小时前
OpenHuman爆火GitHub:AI桌面助手技术架构深度解析
人工智能·架构·github·ai agent·openhuman
想不明白的过度思考者2 小时前
Unity全局事件中心与新版输入架构实现练习——上帝模式与英雄模式的输入系统映射切换
java·unity·架构
踩着两条虫11 小时前
「AI + 低代码」的可视化设计器
开发语言·前端·低代码·设计模式·架构
耕烟煮云13 小时前
本文深入解析AI Native产品设计的核心范式——Linear三层架构模型
人工智能·架构
阿洛学长15 小时前
CSDN、掘金、简书博客文章如何转为Markdown?
运维·数据库·架构·php·持续部署
一切皆是因缘际会15 小时前
AI技术新风口:边缘计算与智能体协同,解锁产业落地新范式
大数据·人工智能·安全·ai·架构·语音识别
轻刀快马16 小时前
AI Agent 架构里的隐形杀手:MCP 协议下 ProcessBuilder 的 64KB 死锁陷阱
架构