在数据库中实现复杂的游戏逻辑一直是工程实践中的有趣挑战。国际象棋因其规则的严谨性和逻辑的层次性,成为检验 SQL 编程能力的绝佳载体。本文将深入探讨如何在纯 SQL 环境中完整实现国际象棋的游戏逻辑,重点覆盖棋盘表示、基础走子生成、局面合法性验证以及特殊规则处理。

棋盘数据模型设计

国际象棋棋盘由 64 个方格组成,每个方格可能存放一个棋子或为空。最直观的建模方式是将棋盘存储为一张关系表,每行代表一个方格的状态。这种表示方法充分利用了 SQL 的集合查询能力,便于后续生成候选走法和进行局面分析。

CREATE TABLE chess_board (
    game_id BIGINT NOT NULL,
    square_rank SMALLINT NOT NULL CHECK (square_rank BETWEEN 1 AND 8),
    square_file SMALLINT NOT NULL CHECK (square_file BETWEEN 1 AND 8),
    piece_type CHAR(1) CHECK (piece_type IN ('K', 'Q', 'R', 'B', 'N', 'P', NULL)),
    piece_color CHAR(1) CHECK (piece_color IN ('W', 'B', NULL)),
    PRIMARY KEY (game_id, square_rank, square_file)
);

该表中 rank 代表行号(1-8),file 代表列号(a-h 对应 1-8)。piece_type 使用标准代数记谱法字母:K 代表王、Q 代表后、R 代表车、B 代表象、N 代表马、P 代表兵。同时需要一张游戏状态表来记录王车易位权限、吃过路兵目标格等动态信息。

CREATE TABLE chess_state (
    game_id BIGINT PRIMARY KEY,
    side_to_move CHAR(1) CHECK (side_to_move IN ('W', 'B')),
    w_castle_k BOOLEAN DEFAULT TRUE,
    w_castle_q BOOLEAN DEFAULT TRUE,
    b_castle_k BOOLEAN DEFAULT TRUE,
    b_castle_q BOOLEAN DEFAULT TRUE,
    en_passant_target VARCHAR(2),
    halfmove_clock INTEGER DEFAULT 0,
    fullmove_number INTEGER DEFAULT 1
);

这种双表结构将棋盘静态信息与游戏动态状态分离,便于独立维护和查询。王车易位的四个布尔值分别记录白方和王双方的易位权限,en_passant_target 存储吃过路兵的目标格位置。

基础走子生成策略

在 SQL 中实现走子生成的核心思路是先产生所有伪合法走法,然后通过局面验证筛选出真正合法的走法。伪合法走法是指忽略将军检查、仅遵循棋子本身运动规则的走法集合。

对于不同类型的棋子,需要设计针对性的 SQL 查询逻辑。马(Knight)的走法最为简单,因为其运动不受阻挡影响:

CREATE OR REPLACE FUNCTION generate_knight_moves(
    p_game_id BIGINT,
    p_color CHAR(1)
) RETURNS TABLE (
    from_rank SMALLINT,
    from_file SMALLINT,
    to_rank SMALLINT,
    to_file SMALLINT
) AS $$
BEGIN
    RETURN QUERY
    SELECT 
        b.square_rank AS from_rank,
        b.square_file AS from_file,
        b.square_rank + dr AS to_rank,
        b.square_file + df AS to_file
    FROM chess_board b
    CROSS JOIN (VALUES (2,1), (2,-1), (-2,1), (-2,-1), (1,2), (1,-2), (-1,2), (-1,-2)) AS moves(dr, df)
    WHERE b.game_id = p_game_id 
      AND b.piece_color = p_color 
      AND b.piece_type = 'N'
      AND b.square_rank + dr BETWEEN 1 AND 8
      AND b.square_file + df BETWEEN 1 AND 8
      AND NOT EXISTS (
          SELECT 1 FROM chess_board b2 
          WHERE b2.game_id = p_game_id 
            AND b2.square_rank = b.square_rank + dr 
            AND b2.square_file = b.square_file + df
            AND b2.piece_color = p_color
      );
END;
$$ LANGUAGE plpgsql;

该函数首先定位所有指定颜色的马,然后通过交叉连接枚举八个可能的落点,最后过滤掉超出棋盘范围和己方棋子占据的格子。车和象属于滑动棋子,需要沿特定方向检测路径是否通畅。

CREATE OR REPLACE FUNCTION generate_rook_moves(
    p_game_id BIGINT,
    p_color CHAR(1)
) RETURNS TABLE (
    from_rank SMALLINT,
    from_file SMALLINT,
    to_rank SMALLINT,
    to_file SMALLINT
) AS $$
BEGIN
    RETURN QUERY
    WITH pieces AS (
        SELECT square_rank, square_file 
        FROM chess_board 
        WHERE game_id = p_game_id AND piece_color = p_color AND piece_type = 'R'
    ),
    directions AS (
        VALUES (1,0), (-1,0), (0,1), (0,-1)
    )
    SELECT 
        p.square_rank AS from_rank,
        p.square_file AS from_file,
        s.r AS to_rank,
        s.f AS to_file
    FROM pieces p
    CROSS JOIN directions d
    CROSS JOIN LATERAL (
        SELECT p.square_rank + generate_series(1,7) * d.r AS r, 
               p.square_file + generate_series(1,7) * d.f AS f
    ) s
    WHERE s.r BETWEEN 1 AND 8 AND s.f BETWEEN 1 AND 8
      AND NOT EXISTS (
          SELECT 1 FROM chess_board b 
          WHERE b.game_id = p_game_id AND b.square_rank = s.r AND b.square_file = s.f
            AND b.piece_color = p_color
      )
      AND NOT EXISTS (
          SELECT 1 FROM chess_board b 
          WHERE b.game_id = p_game_id AND b.square_rank = s.r AND b.square_file = s.f
            AND b.piece_color IS NOT NULL
            AND NOT b.piece_color = p_color
            AND NOT EXISTS (
                SELECT 1 FROM chess_board b2 
                WHERE b2.game_id = p_game_id 
                  AND b2.square_rank = p.square_rank + sign(d.r) * (
                      SELECT MAX(idx) FROM generate_series(1,7) gs(idx) 
                      WHERE p.square_rank + sign(d.r) * idx <= s.r
                  ) 
                  AND b2.square_file = p.square_file
            )
      );
END;

这段代码展示了使用递归思想生成车走法的方式:沿四个正交方向延伸,直到遇到棋子为止。如果遇到的是对方棋子,该格子也属于可攻击目标但路径到此截止。象和后的实现方式类似,仅需调整方向向量。

将军状态检测与走法合法性

生成候选走法后,必须筛选掉那些会导致己方被将军的走法。这需要实现一个核心函数:判断指定颜色的王是否正处于对方棋子的攻击之下。

CREATE OR REPLACE FUNCTION is_square_attacked(
    p_game_id BIGINT,
    p_rank SMALLINT,
    p_file SMALLINT,
    p_attacker_color CHAR(1)
) RETURNS BOOLEAN AS $$
BEGIN
    -- 检测马的攻击
    IF EXISTS (
        SELECT 1 FROM chess_board b
        WHERE b.game_id = p_game_id AND b.piece_type = 'N'
          AND b.piece_color = p_attacker_color
          AND (abs(b.square_rank - p_rank) = 2 AND abs(b.square_file - p_file) = 1
            OR abs(b.square_rank - p_rank) = 1 AND abs(b.square_file - p_file) = 2)
    ) THEN RETURN TRUE; END IF;

    -- 检测车的直线攻击
    IF EXISTS (
        SELECT 1 FROM chess_board b
        WHERE b.game_id = p_game_id AND b.piece_type IN ('R', 'Q')
          AND b.piece_color = p_attacker_color
          AND (b.square_rank = p_rank OR b.square_file = p_file)
          AND NOT EXISTS (
              SELECT 1 FROM chess_board b2
              WHERE b2.game_id = p_game_id
                AND b2.square_rank BETWEEN LEAST(b.square_rank, p_rank) 
                    AND GREATEST(b.square_rank, p_rank)
                AND b2.square_file BETWEEN LEAST(b.square_file, p_file)
                    AND GREATEST(b.square_file, p_file)
                AND (b2.square_rank != p_rank OR b2.square_file != p_file)
                AND (b2.square_rank != b.square_rank OR b2.square_file != b.square_file)
                AND b2.piece_color IS NOT NULL
          )
    ) THEN RETURN TRUE; END IF;

    -- 检测象的斜线攻击
    IF EXISTS (
        SELECT 1 FROM chess_board b
        WHERE b.game_id = p_game_id AND b.piece_type IN ('B', 'Q')
          AND b.piece_color = p_attacker_color
          AND abs(b.square_rank - p_rank) = abs(b.square_file - p_file)
          AND NOT EXISTS (
              SELECT 1 FROM chess_board b2
              WHERE b2.game_id = p_game_id
                AND sign(b2.square_rank - b.square_rank) = sign(p_rank - b.square_rank)
                AND sign(b2.square_file - b.square_file) = sign(p_file - b.square_file)
                AND abs(b2.square_rank - b.square_rank) < abs(p_rank - b.square_rank)
                AND b2.piece_color IS NOT NULL
          )
    ) THEN RETURN TRUE; END IF;

    -- 检测兵的斜吃
    IF EXISTS (
        SELECT 1 FROM chess_board b
        WHERE b.game_id = p_game_id AND b.piece_type = 'P'
          AND b.piece_color = p_attacker_color
          AND (b.piece_color = 'W' AND b.square_rank = p_rank - 1
            OR b.piece_color = 'B' AND b.square_rank = p_rank + 1)
          AND abs(b.square_file - p_file) = 1
    ) THEN RETURN TRUE; END IF;

    -- 检测王的直接攻击
    IF EXISTS (
        SELECT 1 FROM chess_board b
        WHERE b.game_id = p_game_id AND b.piece_type = 'K'
          AND b.piece_color = p_attacker_color
          AND abs(b.square_rank - p_rank) <= 1
          AND abs(b.square_file - p_file) <= 1
    ) THEN RETURN TRUE; END IF;

    RETURN FALSE;
END;

该函数逐一检测六种棋子的攻击模式。对于滑动棋子(车、象、后),需要额外查询路径上是否有其他棋子阻挡。实现中假设王不会被对方王直接攻击,因为王车易位规则会处理这种情况。

验证一步走法是否合法的完整逻辑是:先执行该走法创建临时局面,然后检测己方王是否被攻击。若未被攻击,则该走法合法。

王车易位与吃过路兵的特殊处理

王车易位是国际象棋中最复杂的走法之一,涉及多个前置条件的验证。易位分为长易位(后翼)和短易位(王翼),各自有不同的约束条件。

短易位的基本验证流程如下:首先确认王和目标位置的车均未移动过;其次检查王到目标位置之间的路径是否全部为空;第三验证王当前格、途经格和目标格均未被对方攻击;最后在执行层面同时移动王和车。

CREATE OR REPLACE FUNCTION validate_castling(
    p_game_id BIGINT,
    p_color CHAR(1),
    p_side CHAR(1)  -- 'K' for kingside, 'Q' for queenside
) RETURNS BOOLEAN AS $$
DECLARE
    v_king_rank SMALLINT;
    v_castle_k BOOLEAN;
    v_castle_q BOOLEAN;
    v_path_ranks SMALLINT[];
    v_attack_squares SMALLINT[];
BEGIN
    v_king_rank := CASE WHEN p_color = 'W' THEN 1 ELSE 8 END;
    
    IF p_color = 'W' THEN
        SELECT w_castle_k, w_castle_q INTO v_castle_k, v_castle_q FROM chess_state WHERE game_id = p_game_id;
    ELSE
        SELECT b_castle_k, b_castle_q INTO v_castle_k, v_castle_q FROM chess_state WHERE game_id = p_game_id;
    END IF;

    IF p_side = 'K' THEN
        IF NOT v_castle_k THEN RETURN FALSE; END IF;
        v_path_ranks := ARRAY[5, 6];  -- f, g files for white
        v_attack_squares := ARRAY[5, 6, 7];
    ELSE
        IF NOT v_castle_q THEN RETURN FALSE; END IF;
        v_path_ranks := ARRAY[3, 4];  -- d, c files for white
        v_attack_squares := ARRAY[3, 4, 5];
    END IF;

    -- 检查路径是否为空
    IF EXISTS (
        SELECT 1 FROM chess_board 
        WHERE game_id = p_game_id 
          AND square_rank = v_king_rank 
          AND square_file = ANY(v_path_ranks)
          AND piece_type IS NOT NULL
    ) THEN RETURN FALSE; END IF;

    -- 检查途经格是否被攻击
    FOR i IN 1..array_length(v_attack_squares, 1) LOOP
        IF is_square_attacked(p_game_id, v_king_rank, v_attack_squares[i], 
            CASE WHEN p_color = 'W' THEN 'B' ELSE 'W' END) THEN
            RETURN FALSE;
        END IF;
    END LOOP;

    RETURN TRUE;
END;

吃过路兵的处理相对简洁,但需要额外的状态追踪。当兵第一步推进两格时,需要记录目标格到游戏状态表中。后续一步内,对方兵可以斜吃该格,而被吃的兵实际上位于推进后的兵的身后一格。

CREATE OR REPLACE FUNCTION handle_en_passant(
    p_game_id BIGINT,
    p_from_rank SMALLINT,
    p_from_file SMALLINT,
    p_to_rank SMALLINT,
    p_to_file SMALLINT,
    p_color CHAR(1
) RETURNS BOOLEAN AS $$
DECLARE
    v_en_passant VARCHAR(2);
    v_captured_rank SMALLINT;
BEGIN
    SELECT en_passant_target INTO v_en_passant FROM chess_state WHERE game_id = p_game_id;
    
    IF v_en_passant IS NULL THEN
        RETURN FALSE;
    END IF;

    -- 验证目标格是否为吃过路兵目标
    IF NOT (
        (p_to_rank = 6 AND p_color = 'W' AND p_from_rank = 5) OR
        (p_to_rank = 3 AND p_color = 'B' AND p_from_rank = 4)
    ) THEN
        RETURN FALSE;
    END IF;

    -- 删除被吃掉的兵(位于目标格后方)
    v_captured_rank := CASE WHEN p_color = 'W' THEN p_to_rank + 1 ELSE p_to_rank - 1 END;
    DELETE FROM chess_board 
    WHERE game_id = p_game_id 
      AND square_rank = v_captured_rank 
      AND square_file = p_to_file
      AND piece_type = 'P'
      AND piece_color = CASE WHEN p_color = 'W' THEN 'B' ELSE 'W' END;

    RETURN TRUE;
END;

实际实现时还需要处理兵升变、双方不能送王等完整规则。限于篇幅,本文聚焦于核心架构与关键验证逻辑。

工程实践要点

在生产环境中使用纯 SQL 实现游戏逻辑时,需要注意几个关键工程问题。首先是性能优化,对于每一步走法验证都创建临时局面会造成较大开销,建议使用事务和保存点来隔离测试状态。其次是递归查询深度限制,PostgreSQL 默认的递归层数可能不足以处理复杂局面分析,可通过调整 max_recursive_iterations 参数扩展。第三是考虑使用物化视图缓存经常查询的局面信息,如某方所有可攻击格等。

本文介绍的实现方案基于 PostgreSQL 的 PL/pgSQL 过程语言,利用其对集合操作和事务的原生支持。核心思想是将游戏规则分解为可独立验证的谓词,再通过 SQL 查询组合这些条件。这种方法的优势在于充分利用数据库的查询优化器,同时保持代码的声明式可读性。

资料来源:本文参考了 SQLChess 集合思维教程中关于棋盘表示与走法生成的内容,以及 Chess Programming Wiki 中关于王车易位验证与攻击检测的技术细节。