引言

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

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 类,不强制继承同一个基类:

象棋棋盘

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

五子棋棋盘

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

围棋棋盘

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. 引擎抽象

引擎接口

每种棋定义自己的引擎接口:

// 象棋引擎
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)

引擎选择

用户可以选择使用哪个引擎:

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 分别管理各自的对局:

// 象棋对局
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 字段区分:

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;
}

创建房间

public class CreateRoomRequest {
    String gameType;        // 必填
    String visibility;      // PUBLIC 或 PRIVATE
    int initialTimeSeconds; // 时间控制(秒)
}

房间到对局的转换

当双方准备就绪后,RoomService 创建对应的 Match:

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 路由

所有棋种共享同一套路由:

// 路由表(不分棋种)
const routes = ['home', 'play', 'room', 'game', 'practice', 
                'learn', 'watch', 'community', 'me'];

// 通过 state.game.gameType 判断当前棋种
function currentRoute() {
    const raw = location.hash.replace(/^#\/?/, '');
    // ... 解析路由
}

棋盘渲染

board.js 根据棋种选择不同的渲染函数:

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;
    }
}

事件处理

走棋事件根据棋种分发:

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

通用表结构

-- 对局表(支持所有棋种)
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 格式不同:

// 象棋
{
    "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. 学习系统

统一的学习内容

学习内容(教程、残局、练习)按棋种分类:

{
    "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"
        }
    ]
}

学习进度跟踪

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 实战
下一篇:(六)部署上线与运维

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐