返回 2026-06-12
🤖 AI / ML

用 Claude 和 Prolog 解决国际象棋谜题Solving a chess puzzle with Claude and Prolog

johndcook.com·2026-06-11

作者尝试用 Claude 配合 Prolog 来解决国际象棋谜题。Prolog 作为最早的逻辑编程语言,其核心优势在于能直接表达逻辑问题,但学习曲线陡峭。实验验证了 LLM 与 Prolog 结合的可行性:Claude 负责将自然语言描述的棋局转化为 Prolog 逻辑规则,Prolog 引擎则负责精确推理求解。这种组合为逻辑推理类问题提供了一种 LLM 单独难以胜任的可靠方案。

John

Prolog 是最早的逻辑编程语言。其名称源于“programming in logic”(逻辑编程)。更具体地说,这个名字来自“programmation en logique”,因为该语言的发明者 Philippe Roussel 是法国人。

Prolog 有其优缺点。优点之一是该语言能够直接表达逻辑问题。缺点之一是它的语法可能有些古怪。但如果是由 LLM 编写代码,或者至少在辅助编写代码,语法问题就没那么重要了。

我想测试一下 Claude(Sonnet 4.6,中等推理力度)解决 Martin Gardner 国际象棋谜题的能力如何,我在一年多前曾写过关于这个谜题的文章。我选择了一个相对冷门的问题,而不是像八皇后问题那样的经典题目,因为对于后者,LLM 可能会直接照搬无数现成文章中的答案。

谜题

正如我在去年的文章中所述,任务是将两个车、两个象和两个马放置在一个 4x4 的国际象棋棋盘上,使得任何棋子都不会攻击到其他棋子。

它有两个基本解;如果把旋转和镜像反射算作不同的解,则有十二个。

Prolog 运行结果

Claude 编写了一个 SWI-Prolog 程序,我使用以下命令运行了它:

swipl -g "run, halt" chess_placement.pl

它给出了如下输出。

Total solutions (raw): 12
Solutions up to D4 symmetry: 2
Solutions up to D4 symmetry: 2

Solution 1:
B . . N
N . . B
. R . .
. . R .

Solution 2:
. B N .
. . . R
R . . .
. N B .

Claude 生成了一张展示这些解的精美图片。

需要注意的是,通过旋转和翻转的组合,第一个解可以产生八种变体。而第二个解只有四种变体,因为旋转两次就会恢复到初始状态。

Prolog 源代码

以下是 Claude 生成的 Prolog 代码。

% ============================================================
%  Non-attacking placement of 2 Rooks, 2 Bishops, 2 Knights
%  on a 4x4 chessboard
%
%  Run with SWI-Prolog:
%    swipl -g "run, halt" chess_placement.pl
%
%  Or interactively:
%    swipl chess_placement.pl
%    ?- run.
%    ?- count_solutions(N).
%    ?- count_unique(N).
% ============================================================

:- use_module(library(lists)).

% ---- Board squares: (Row, Col) with Row, Col in 0..3 ----------------

square(R, C) :- member(R, [0,1,2,3]), member(C, [0,1,2,3]).

% ---- Attack predicates ----------------------------------------------

rook_attacks(R,_,R,_).          % same row
rook_attacks(_,C,_,C).          % same column

bishop_attacks(R1,C1,R2,C2) :-
    DR is abs(R1-R2), DC is abs(C1-C2),
    DR =:= DC, DR > 0.

knight_attacks(R1,C1,R2,C2) :-
    DR is abs(R1-R2), DC is abs(C1-C2),
    ( DR =:= 1, DC =:= 2 ; DR =:= 2, DC =:= 1 ).

% attacks(+Type1, +R1, +C1, +Type2, +R2, +C2)
% True if either piece threatens the other's square.
attacks(rook,   R1,C1, _,     R2,C2) :- rook_attacks(R1,C1,R2,C2).
attacks(_,      R1,C1, rook,  R2,C2) :- rook_attacks(R1,C1,R2,C2).
attacks(bishop, R1,C1, _,     R2,C2) :- bishop_attacks(R1,C1,R2,C2).
attacks(_,      R1,C1, bishop,R2,C2) :- bishop_attacks(R1,C1,R2,C2).
attacks(knight, R1,C1, _,     R2,C2) :- knight_attacks(R1,C1,R2,C2).
attacks(_,      R1,C1, knight,R2,C2) :- knight_attacks(R1,C1,R2,C2).

% ---- All pairs safe -------------------------------------------------

% Check all unordered pairs in a list.
no_attack_pair(_, []).
no_attack_pair(T1-(R1,C1), [T2-(R2,C2)|Rest]) :-
    \+ attacks(T1,R1,C1,T2,R2,C2),
    no_attack_pair(T1-(R1,C1), Rest).

all_pairs_safe([]).
all_pairs_safe([P|Rest]) :-
    no_attack_pair(P, Rest),
    all_pairs_safe(Rest).

% ---- Generate a placement -------------------------------------------
%
%  Placement = [rook-(R1,C1), rook-(R2,C2),
%               bishop-(R3,C3), bishop-(R4,C4),
%               knight-(R5,C5), knight-(R6,C6)]
%
%  We enforce an ordering within each pair to avoid counting
%  same-type duplicates (rook1 vs rook2 swapped).
%  Squares are represented canonically: (R,C) with R*4+C as index.

sq_index(R, C, I) :- I is R*4 + C.

placement(Placement) :-
    % --- Rooks ---
    square(R1r, C1r), square(R2r, C2r),
    sq_index(R1r,C1r,I1r), sq_index(R2r,C2r,I2r),
    I1r < I2r,          % canonical order for the two rooks

    % --- Bishops ---
    square(R1b, C1b), square(R2b, C2b),
    sq_index(R1b,C1b,I1b), sq_index(R2b,C2b,I2b),
    I1b < I2b,          % canonical order for the two bishops

    % --- Knights ---
    square(R1n, C1n), square(R2n, C2n),
    sq_index(R1n,C1n,I1n), sq_index(R2n,C2n,I2n),
    I1n < I2n,          % canonical order for the two knights

    % --- All six squares distinct ---
    Squares = [(R1r,C1r),(R2r,C2r),(R1b,C1b),(R2b,C2b),(R1n,C1n),(R2n,C2n)],
    all_distinct_squares(Squares),

    % --- Build placement list ---
    Placement = [ rook-(R1r,C1r),   rook-(R2r,C2r),
                  bishop-(R1b,C1b), bishop-(R2b,C2b),
                  knight-(R1n,C1n), knight-(R2n,C2n) ],

    % --- No piece attacks any other ---
    all_pairs_safe(Placement).

all_distinct_squares([]).
all_distinct_squares([S|Rest]) :-
    \+ member(S, Rest),
    all_distinct_squares(Rest).

% ---- Count all solutions --------------------------------------------

count_solutions(N) :-
    findall(P, placement(P), Ps),
    length(Ps, N),
    format("Total solutions (raw): ~w~n", [N]).

% ---- D4 symmetry transformations on a 4x4 board ---------------------
%  Squares (R,C) in 0..3

transform(identity, R, C, R,  C).
transform(rot90,    R, C, C,  Nr) :- Nr is 3-R.
transform(rot180,   R, C, Nr, Nc) :- Nr is 3-R, Nc is 3-C.
transform(rot270,   R, C, Nc, R)  :- Nc is 3-C.
transform(flipH,    R, C, R,  Nc) :- Nc is 3-C.
transform(flipV,    R, C, Nr, C)  :- Nr is 3-R.
transform(flipD1,   R, C, C,  R).
transform(flipD2,   R, C, Nr, Nc) :- Nr is 3-C, Nc is 3-R.

apply_transform(_, [], []).
apply_transform(T, [Type-(R,C)|Rest], [Type-(NR,NC)|TRest]) :-
    transform(T, R, C, NR, NC),
    apply_transform(T, Rest, TRest).

% Canonical form: sort pieces within same-type pairs, then sort the
% whole list to get a unique representative.
canonical_placement(Placement, Canonical) :-
    findall(T, member(T,[identity,rot90,rot180,rot270,
                          flipH,flipV,flipD1,flipD2]), Ts),
    maplist(transform_and_sort(Placement), Ts, AllForms),
    msort(AllForms, Sorted),
    Sorted = [Canonical|_].

transform_and_sort(Placement, T, Sorted) :-
    apply_transform(T, Placement, TPl),
    msort(TPl, Sorted).

% ---- Count solutions up to D4 symmetry ------------------------------

count_unique(N) :-
    findall(P, placement(P), Ps),
    maplist(canonical_placement, Ps, Canonicals),
    list_to_set(Canonicals, Unique),
    length(Unique, N),
    format("Solutions up to D4 symmetry: ~w~n", [N]).

% ---- Pretty-print a board -------------------------------------------

print_board(Placement) :-
    forall(member(R, [0,1,2,3]),
           print_row(R, Placement)),
    nl.

print_row(R, Placement) :-
    forall(member(C, [0,1,2,3]),
           print_cell(R, C, Placement)),
    nl.

print_cell(R, C, Placement) :-
    (   member(rook-(R,C),   Placement) -> write('R ')
    ;   member(bishop-(R,C), Placement) -> write('B ')
    ;   member(knight-(R,C), Placement) -> write('N ')
    ;   write('. ')
    ).

% ---- Print all unique solutions -------------------------------------

print_unique_solutions :-
    findall(P, placement(P), Ps),
    maplist(canonical_placement, Ps, Canonicals),
    list_to_set(Canonicals, Unique),
    length(Unique, N),
    format("~nSolutions up to D4 symmetry: ~w~n~n", [N]),
    forall(nth1(I, Unique, Sol),
           ( format("Solution ~w:~n", [I]),
             print_board(Sol) )).

% ---- Top-level entry point ------------------------------------------

run :-
    count_solutions(Raw),
    count_unique(Sym),
    format("~n"),
    print_unique_solutions,
    format("Summary: ~w raw solutions, ~w up to D4 symmetry.~n",
           [Raw, Sym]).

需要完整排版与评论请前往来源站点阅读。