🤖 AI / ML
在 6x5 棋盘上放置所有棋子All pieces on a 6 by 5 board
探讨如何利用大语言模型(LLM)生成代码来解决复杂的国际象棋谜题。文章记录了使用 Claude 生成 Z3/Python 代码的实践过程,以解决在 6x5 棋盘上放置所有国际象棋棋子(国王、王后、双车等)的特定排布问题。这是继之前使用 Claude 和 ChatGPT 生成 Prolog 代码之后的进一步尝试。实验展示了 LLM 在结合形式化验证工具(如 Z3 约束求解器)解决复杂逻辑问题方面的应用潜力。
John
我最近写了几篇关于让 LLM 生成代码来解决国际象棋问题的文章。第一篇使用 Claude 生成 Prolog,第二篇使用 ChatGPT 生成 Prolog。这篇文章将使用 Claude 生成 Z3/Python 代码。
这个谜题是我以前写过的:
将所有棋子——王、后、两个象、两个马和两个车——放置在 6 × 5 的棋盘上,要求两个象位于颜色相反的格子上,且没有任何棋子互相攻击。
顺便提一下,通常“piece”是不包括兵的,就像上面那样。但你要怎么称呼棋盘上的所有物件呢?你可能会叫它们“chess pieces”,在这种情况下,兵是“chess piece”而不是“piece”。一种惯例是使用“chessmen”或简称为“men”来统称主棋子和兵。
这是我使用的提示词。
编写 Z3/Python 代码,找出以下国际象棋谜题的所有解。将所有棋子——王、后、两个象、两个马和两个车——放置在 6 × 5 的棋盘上,要求两个象位于颜色相反的格子上,且没有任何棋子互相攻击。
代码找到了 192 = 8 × 24 个解。系数 8 来自于交换成对象、马和车的 2^3 种方式。脚本输出如下
Total raw solutions: 192
Unique solutions (deduplicating piece-pair swaps): 24
── Solution 1 ──
0 1 2 3 4
0 . K . . N
1 . . . . B
2 . . R . .
3 Q . . . .
4 . . . R .
5 . B . . N
King: (0,1)
Queen: (3,0)
Bishop1: (5,1) [light]
Bishop2: (1,4) [dark]
Knight1: (5,4)
Knight2: (0,4)
Rook1: (4,3)
Rook2: (2,2)
── Solution 2 ──
0 1 2 3 4
0 N . . K .
1 B . . . .
2 . . R . .
3 . . . . Q
4 . R . . .
5 N . . B .
King: (0,3)
Queen: (3,4)
Bishop1: (5,3) [light]
Bishop2: (1,0) [dark]
Knight1: (5,0)
Knight2: (0,0)
Rook1: (4,1)
Rook2: (2,2)
...
── Solution 24 ──
0 1 2 3 4
0 B . . B .
1 N . . . .
2 . . . . Q
3 . . R . .
4 . R . . .
5 N . . K .
King: (5,3)
Queen: (2,4)
Bishop1: (0,0) [light]
Bishop2: (0,3) [dark]
Knight1: (5,0)
Knight2: (1,0)
Rook1: (3,2)
Rook2: (4,1)以下是代码。
"""
Chess Puzzle Solver using Z3
Place king, queen, 2 bishops, 2 knights, 2 rooks on a 6×5 board
such that:
- The two bishops are on opposite-colored squares
- No piece attacks any other piece
"""
from z3 import (
Int, Solver, Or, And, Not, Distinct, If,
sat, ForAll, Implies
)
import itertools
ROWS = 6
COLS = 5
TOTAL_CELLS = ROWS * COLS # 30
PIECES = ["king", "queen", "bishop1", "bishop2", "knight1", "knight2", "rook1", "rook2"]
N = len(PIECES) # 8 pieces
def cell(r, c):
return r * COLS + c
def pos_to_rc(pos):
return pos // COLS, pos % COLS
def color(r, c):
"""Square color: 0=light, 1=dark"""
return (r + c) % 2
def attacks_king(r1, c1, r2, c2):
return max(abs(r1 - r2), abs(c1 - c2)) == 1
def attacks_queen(r1, c1, r2, c2):
return (r1 == r2 or c1 == c2 or
abs(r1 - r2) == abs(c1 - c2))
def attacks_bishop(r1, c1, r2, c2):
return abs(r1 - r2) == abs(c1 - c2)
def attacks_rook(r1, c1, r2, c2):
return r1 == r2 or c1 == c2
def attacks_knight(r1, c1, r2, c2):
dr, dc = abs(r1 - r2), abs(c1 - c2)
return (dr == 1 and dc == 2) or (dr == 2 and dc == 1)
# Map piece index to its attack function
ATTACK_FN = {
0: attacks_king,
1: attacks_queen,
2: attacks_bishop,
3: attacks_bishop,
4: attacks_knight,
5: attacks_knight,
6: attacks_rook,
7: attacks_rook,
}
def solve_all():
solutions = []
# Each piece gets an integer position in [0, TOTAL_CELLS)
pos = [Int(f"pos_{p}") for p in PIECES]
s = Solver()
# All positions in valid range
for p in pos:
s.add(p >= 0, p < TOTAL_CELLS)
# All pieces on distinct squares
s.add(Distinct(*pos))
# Bishops on opposite colors
# bishop1 = pos[2], bishop2 = pos[3]
# color of pos = ((pos // COLS) + (pos % COLS)) % 2
b1_color = (pos[2] / COLS + pos[2] % COLS) % 2 # Z3 integer arithmetic
b2_color = (pos[3] / COLS + pos[3] % COLS) % 2
# Z3 doesn't do Python //; use integer division carefully
# We'll encode opposite colors: sum of colors == 1
# color(pos) = (row + col) % 2 = (pos//COLS + pos%COLS) % 2
# For Z3 int vars, use: (pos / COLS + pos % COLS) % 2
s.add((pos[2] / COLS + pos[2] % COLS) % 2 != (pos[3] / COLS + pos[3] % COLS) % 2)
# No piece attacks another
# We enumerate all (i,j) pairs and for each possible (pos_i, pos_j) assignment,
# assert that those pieces don't attack each other.
# Since positions are Z3 vars, we use a constraint table approach:
# For each pair (i,j), add constraints over all concrete (r1,c1,r2,c2) combos.
# Pre-build attack lookup tables for each piece-type pair
# This avoids slow Z3 symbolic reasoning over large disjunctions.
# We'll encode: for all concrete assignments, if pos[i]==cell(r1,c1) and pos[j]==cell(r2,c2),
# then piece i must not attack piece j.
# Equivalently: NOT (pos[i]==cell(r1,c1) AND pos[j]==cell(r2,c2)) for all attacking pairs.
# For efficiency, we build a set of "forbidden (cell_i, cell_j)" pairs per piece pair.
for i, j in itertools.combinations(range(N), 2):
fn_i = ATTACK_FN[i]
fn_j = ATTACK_FN[j]
forbidden = []
for r1, c1 in itertools.product(range(ROWS), range(COLS)):
for r2, c2 in itertools.product(range(ROWS), range(COLS)):
if (r1, c1) == (r2, c2):
continue
# i attacks j or j attacks i
if fn_i(r1, c1, r2, c2) or fn_j(r2, c2, r1, c1):
forbidden.append((cell(r1, c1), cell(r2, c2)))
if forbidden:
# Add: NOT (pos[i] == ci AND pos[j] == cj) for each forbidden pair
s.add(And([
Not(And(pos[i] == ci, pos[j] == cj))
for ci, cj in forbidden
]))
# Enumerate all solutions
solution_count = 0
while s.check() == sat:
m = s.model()
sol = [m[pos[k]].as_long() for k in range(N)]
solutions.append(sol)
solution_count += 1
# Block this solution (and symmetric duplicates via blocking exact assignment)
s.add(Or([pos[k] != sol[k] for k in range(N)]))
if solution_count % 10 == 0:
print(f" Found {solution_count} solutions so far...")
return solutions
def print_board(sol):
board = [["." for _ in range(COLS)] for _ in range(ROWS)]
symbols = ["K", "Q", "B", "B", "N", "N", "R", "R"]
for k, p in enumerate(sol):
r, c = pos_to_rc(p)
board[r][c] = symbols[k]
print(" " + " ".join(str(c) for c in range(COLS)))
for r in range(ROWS):
print(f"{r} " + " ".join(board[r]))
def canonicalize(sol):
"""
Produce a canonical form to deduplicate solutions where
bishop1/bishop2, knight1/knight2, rook1/rook2 are interchangeable.
Returns a frozenset-based key.
"""
king_pos = sol[0]
queen_pos = sol[1]
bishops = tuple(sorted([sol[2], sol[3]]))
knights = tuple(sorted([sol[4], sol[5]]))
rooks = tuple(sorted([sol[6], sol[7]]))
return (king_pos, queen_pos, bishops, knights, rooks)
def main():
print("Solving chess puzzle on 6×5 board...")
print("Pieces: King, Queen, 2 Bishops (opposite colors), 2 Knights, 2 Rooks")
print("Constraint: No piece attacks another\n")
solutions = solve_all()
# Deduplicate by canonical form
seen = {}
for sol in solutions:
key = canonicalize(sol)
if key not in seen:
seen[key] = sol
unique = list(seen.values())
print(f"\nTotal raw solutions: {len(solutions)}")
print(f"Unique solutions (deduplicating piece-pair swaps): {len(unique)}\n")
for idx, sol in enumerate(unique, 1):
print(f"── Solution {idx} ──")
print_board(sol)
labels = ["King", "Queen", "Bishop1", "Bishop2", "Knight1", "Knight2", "Rook1", "Rook2"]
for k, p in enumerate(sol):
r, c = pos_to_rc(p)
col_name = "light" if color(r, c) == 0 else "dark"
sq = f"({r},{c})"
extra = f" [{col_name}]" if "Bishop" in labels[k] else ""
print(f" {labels[k]}: {sq}{extra}")
print()
if __name__ == "__main__":
main()相关文章:使用 Z3 SAT/SMT 求解器的经验教训
需要完整排版与评论请前往来源站点阅读。