在数据库中实现复杂的游戏逻辑一直是工程实践中的有趣挑战。国际象棋因其规则的严谨性和逻辑的层次性,成为检验 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 中关于王车易位验证与攻击检测的技术细节。