尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

用 Python 字典实现国际象棋:从棋盘建模到走法验证的完整实践

用 Python 字典实现国际象棋:从棋盘建模到走法验证的完整实践 简介这是一份面向Python初学者的国际象棋游戏源码资源源自CSDN作者qq_38334677定位为模块化学习项目适合打算用Python完成课程设计或自学桌面小游戏开发的人阅读参考。作者在编写时有意将棋盘表示、走法验证和界面展示分层既使用基于字典的结构保存棋盘状态也单独实现走法合法性判断并让同一套逻辑同时输出到控制台字符界面和TkInter图形界面便于观察每一步移动如何被驱动。压缩包共20个文件其中6个Python脚本负责棋盘库、棋子规则、游戏入口等核心逻辑12张PNG图片提供黑白双方棋子图标另有说明文档与许可文件整个包仅55KB结构精简而完整。运行环境要求Python 2.5以上并安装TkInter和PIL项目内附说明文档可了解环境配置与启动方式。目前已有433人学习下载读者既能直接运行体验完整的国际象棋对弈也可通过源码学习移动验证、将军判断等规则的实现思路进一步扩展人机对战、悔棋或网络联机功能。1. 一个把棋盘拆成字典的 Python 国际象棋项目坐标即键值即局面见过不少教学用的国际象棋实现大多数第一反应是开一个 8x8 二维数组行列和棋谱坐标之间来回换算。这个项目反过来用 dict 做局面核心键是e4、d5这样的棋谱坐标值直接放棋子对象或空。坐标即键、删子即置空斜线、直线、马腿全都能落成固定偏移去推。没有位棋盘那种性能野心但对于想弄懂“移动验证怎么落地、GUI 怎么接到规则层”的人这套拆分值得拆开看。项目里 board.py、pieces.py、chess.py 分别管局面、棋子与规则gui_tkinter.py 和 gui_console.py 共用同一套棋核。下面按这条链路逐层展开。2. 棋盘表示用 Dict 建模坐标、棋子与局面快照2.1 为什么不用二维数组从棋谱坐标直接落到下标国际象棋的代数坐标天然就是a1到h8这种字符串组合文件和行号在写法上已经绑定。如果拿二维数组存board[0][0] 对应的往往是 a8渲染界面时还要把行号反着换算如果处理来自输入框的走法e2e4又要把字符串拆开再映射到数组下标中间多一层转换出错机会反而多。在这个项目里数据结构和棋谱语法保持了一致。我一般会写两个底层转换函数一个是坐标转到数字索引一个是反向转回供规则层和界面层共用# board.py 里常见的坐标转换函数字符串坐标 - 文件/行索引 def square_to_index(square): e4 - (4, 3)file 用 ord 偏移rank 直接减 1 return ord(square[0]) - ord(a), int(square[1]) - 1 def index_to_square(file_idx, rank_idx): (4, 3) - e4越界时返回 None调用方无需再判边界 if not (0 file_idx 8 and 0 rank_idx 8): return None return chr(ord(a) file_idx) str(rank_idx 1)square_to_index把e4拆成(4, 3)ord(e) - ord(a)得到文件列偏移int(4) - 1得到行号。index_to_square是反向操作越界返回None而不是抛异常这样后面的走法生成函数可以很自然地忽略棋盘外的格子。这里我把“文件列”定义为 07“行号”定义为 07与数学坐标习惯一致。下面这张表是两者的对应关系写规则的时候心里始终要有这张表坐标文件索引行索引实际含义a100左下角格子e443中心偏右一格h877右上角格子2.2 起始局面的构建字典推导与双色对称摆放Board类初始化时要把 32 个棋子摆到指定格子。因为棋谱坐标是字符串初始化代码可以直接按文件列循环一行代码放白方底线、一行放黑方底线对称性一眼能看出来# board.py 里的起始局面构建逻辑核心是枚举 back_rank 再拼坐标 class Board: def __init__(self): self.squares {} self.turn w self.reset() def reset(self): back_rank [r, n, b, q, k, b, n, r] for file_idx, code in enumerate(back_rank): fl chr(ord(a) file_idx) # 白方底线是第 1 行黑方底线是第 8 行 self.squares[fl 1] Piece(w, code) self.squares[fl 2] Piece(w, p) self.squares[fl 8] Piece(b, code) self.squares[fl 7] Piece(b, p)enumerate(back_rank)同时拿到文件列索引和棋子类型fl 1拼出a1这种键。Piece(w, r)表示白车Piece(b, p)表示黑兵颜色和类型是两个独立属性。这样设计的直接好处是GUI 层的图片文件名可以按(颜色, 类型)二元组映射不用在棋子上写死white_rook这种长字符串。值得注意的一点棋子的color和kind在单局中不会变化所以Board.squares这个字典保存的是“哪个格子站着哪个棋子”的映射而不是每个棋子自己的位置属性。移动时只需要改格子映射不需要同步修改棋子对象内部的状态这比把位置存到棋子对象里要不容易出错。提示如果你未来想支持悔棋board.squares.copy()就能拿到一整个局面的浅拷贝。棋子对象不可变浅拷贝足够没必要做深拷贝。2.3 局面快照与 FEN 导出字典到标准棋谱文本的互转棋盘字典还有一层隐藏价值它天然适合做局面快照。保存棋局时一行dict(squares)就够对比局面差异时按坐标逐个比对键值即可。更实用的是把字典转成 FEN 字符串FEN 是国际象棋通用的局面描述格式调试时可以拿真实棋局的 FEN 来喂给你的规则层。# 把 squares 字典转成 FEN 的布局段方便复现真实棋局 def to_fen(board): rows [] for rank in range(8, 0, -1): row_chars [] empty 0 for file_idx in range(8): square chr(ord(a) file_idx) str(rank) piece board.squares.get(square) if piece is None: empty 1 else: if empty: row_chars.append(str(empty)) empty 0 base {r: r, n: n, b: b, q: q, k: k, p: p}[piece.kind] row_chars.append(base.upper() if piece.color w else base) if empty: row_chars.append(str(empty)) rows.append(.join(row_chars)) return /.join(rows)to_fen从第 8 行向第 1 行扫描连续空格合并成数字白棋用大写字母、黑棋用小写字母。empty这个计数器是 FEN 格式的关键4p3就表示该行前四格为空、第五格是黑兵、后面三格为空。有了这个函数你可以从在线棋谱库复制任意中局局面转为内部字典后直接交给移动验证逻辑测试这对后面排错非常有用。3. 移动验证从走法生成到将军判定的规则链3.1 走法表按棋子的方向向量与步长分类移动验证是这个项目的核心也是最容易写乱的部分。如果把每种棋子的走法写成独立函数代码虽直观但要写六份如果全部塞进一个大判断又难调试。常见做法是先把所有棋子的移动方式抽象成“方向向量 是否可连续走”两张表。棋子方向向量文件偏移, 行偏移可连续走特殊规则车(1,0)、(-1,0)、(0,1)、(0,-1)是路径上不能有棋子象(1,1)、(1,-1)、(-1,1)、(-1,-1)是路径上不能有棋子后车与象的八个方向是路径上不能有棋子马(1,2)、(2,1) 等八个组合否可以越子王八个方向各 1 步否不能走进被攻击格兵前进 (0,±1)、斜吃 (±1,±1)否首步可走两格底线升变这里最关键的区别在“可连续走”。车、象、后是长距离棋子需要逐格扫描直到撞到棋子马、王、兵是单步棋子只需要检查目标格本身。我在下面用dir_vectors和一个sliding标志来统一表达避免写多个重复的死循环。3.2 滑动棋子的遮挡检测沿方向扫格到第一个阻挡长距离棋子的核心函数是一个单向扫描输入棋子和方向向量返回该方向上所有可达格包括第一个被占格但不再往后延伸# chess.py 里滑动棋子的单方向扫描 def ray_attack(board, square, direction): targets [] f, r square_to_index(square) df, dr direction f df r dr while 0 f 8 and 0 r 8: target index_to_square(f, r) piece board.squares.get(target) # 空格可走第一个有子的格子可吃再往后全部挡住 targets.append(target) if piece is not None: break f df r dr return targetsray_attack的返回值设计包含“第一颗被撞到的棋子所在的格子”是为了让吃子判断和遮挡判断共用同一份代码。假如白车在 d4d5 是空格、d6 是黑兵ray_attack返回[d5, d6]调用方再根据目标格是否有子来决定是移动还是吃子而 d7 不会出现在列表里因为黑兵已经挡住了视线。direction用(df, dr)二元组表示df是文件列偏移dr是行偏移这样控制台界面和 TkInter 界面在测试同一个函数时结果完全一致。3.3 将军判定从伪合法走法到合法走法的过滤生成走法时很多初学者会忽略“走完不能把自己王暴露在将军下”这条规则。一个最简单的实现方式是分层处理先生成不考虑将军的伪合法走法再逐个尝试走子走完判断己方王是否处于被攻击状态是则过滤掉# chess.py 中伪合法走法 - 合法走法的过滤逻辑 def legal_targets(board, from_square): results [] piece board.get_piece(from_square) for to_square in pseudo_targets(board, from_square): snapshot board.squares.copy() # 模拟走子目标格放己方棋子原格置空 board.squares[to_square] piece board.squares[from_square] None if not is_in_check(board, piece.color): results.append(to_square) board.squares snapshot return resultspseudo_targets负责按 3.1 的走法表生成所有可能目标格不关心将军问题。legal_targets把每一步行子先模拟到棋盘上is_in_check看走完是否仍被攻击。这里最需要注意的是snapshot必须放在循环外面且每次从同一个原始局面恢复否则前一步的模拟结果会污染下一步的判断。is_in_check的实现同样复用伪合法走法生成器# chess.py 将军检测反向检查所有敌方棋子能否吃到王 def is_in_check(board, color): king_square board.find_king(color) enemy b if color w else w for square, piece in board.squares.items(): if piece is None or piece.color ! enemy: continue if king_square in pseudo_targets(board, square): return True return False这里的逻辑是站在“攻击者视角”找王遍历所有敌方棋子如果任意一方的伪合法走法列表包含了王当前所在的格子就说明王被攻击。pseudo_targets里只做几何计算和遮挡判断不递归调用is_in_check所以不会出现无限递归。正是这个分层设计让legal_targets可以在模拟走子后再检查己方王的安全而不会发生循环依赖。注意find_king是将军检测的基础实现为遍历squares.items()找kind k且颜色匹配的棋子。如果出现找不到王的情况多半是之前的走法模拟没有恢复棋盘先回去检查恢复逻辑。4. TkInter 与 Unicode 控制台一套棋核双端界面4.1 gui_console.py在终端用 Unicode 棋子形成可读局面这个项目的一大便利之处在于控制台界面只依赖标准库不启动任何窗口。用一个字典把棋子类型映射到 Unicode 字符循环扫描棋盘即可输出局面# gui_console.py 的核心渲染逻辑 PIECE_CHARS { (w, k): ♔, (w, q): ♕, (w, r): ♖, (w, b): ♗, (w, n): ♘, (w, p): ♙, (b, k): ♚, (b, q): ♛, (b, r): ♜, (b, b): ♝, (b, n): ♞, (b, p): ♟, } def render_board(board): print( a b c d e f g h) # 从第 8 行向下渲染终端输出方向与棋谱一致 for rank in range(8, 0, -1): cells [] for file_idx in range(8): sq chr(ord(a) file_idx) str(rank) piece board.squares.get(sq) cells.append(PIECE_CHARS[(piece.color, piece.kind)] if piece else .) print(f{rank} { .join(cells)})PIECE_CHARS的键是(color, kind)二元组这和第 2.2 节Piece类里的属性一一对应。终端渲染的价值在于不需要任何额外依赖在服务器上排错时尤其方便。Windows 老版本 cmd 里 Unicode 棋子可能显示成方框这时可以临时把PIECE_CHARS的值改成P、N、B、R、Q、K的字母形态虽然不好看但信息不丢。4.2 TkInter 画布与棋子图片加载用文件名反推棋子类型TkInter 界面比控制台多一层像素坐标转换但在窗口里能选子和高亮可读性更强。棋子贴图用的是项目附带的一组 PNG文件名是颜色在前、类型在后whitep.png是白兵blackn.png是黑马。# gui_tkinter.py 中加载棋子图片并缩放到固定尺寸 import tkinter as tk from PIL import Image, ImageTk SQUARE 64 LIGHT_COLOR #f0d9b5 DARK_COLOR #b58863 class ChessGUI: def __init__(self, root, board): self.root root self.board board self.canvas tk.Canvas(root, width8 * SQUARE, height8 * SQUARE) self.canvas.pack() self.piece_images {} self._load_images() self._draw_board() def _load_images(self): name_map { (w, p): whitep.png, (w, n): whiten.png, (w, b): whiteb.png, (w, r): whiter.png, (w, q): whiteq.png, (w, k): whitek.png, (b, p): blackp.png, (b, n): blackn.png, (b, b): blackb.png, (b, r): blackr.png, (b, q): blackq.png, (b, k): blackk.png, } for key, fname in name_map.items(): img Image.open(img/ fname).convert(RGBA) img img.resize((SQUARE, SQUARE), Image.LANCZOS) self.piece_images[key] ImageTk.PhotoImage(img)name_map把(color, kind)映射到具体文件名加载后用resize统一成 64x64。ImageTk.PhotoImage是 PIL 专门提供给 TkInter 的图像包装类TkInter 自身对 PNG 的解码能力有限PIL 在这里负责格式解码和尺寸缩放。镜像文件里白后whiteq.png、黑王blackk.png这些命名并不完全规整所以统一用字典在前端做一次翻译界面代码就不需要关心文件名规则了。4.3 像素坐标与棋格坐标互转点击选子与高亮合法走法TkInter 的鼠标事件返回的是像素坐标窗格左上角是 (0,0)而棋盘上第八行在视觉顶部。转换时用整除取格子索引行号再翻转一次# gui_tkinter.py 中点击事件映射到棋格坐标 def on_click(self, event): file_idx event.x // SQUARE rank_idx 7 - event.y // SQUARE # 窗口顶部是第 8 行 square index_to_square(file_idx, rank_idx) if square is None: return if self.selected is None: piece self.board.get_piece(square) if piece is not None and piece.color self.board.turn: self.selected square self.highlight_targets(square) else: if square in legal_targets(self.board, self.selected): self.board.move(self.selected, square) self.selected None self.redraw() else: self.selected None self.clear_highlights()event.x // SQUARE整除 64 得到 0 到 7 的文件索引7 - event.y // SQUARE把窗口像素行翻转成棋谱行号。整个交互只维护selected一个状态第一次点击选中己方棋子并高亮可走格第二次点击若目标在合法走法里则执行移动否则取消选中。这里调用的legal_targets正是第 3.3 节那个函数GUI 没有复制任何规则逻辑只是把点击结果转成坐标字符串喂给棋核。对比项gui_console.pygui_tkinter.py依赖仅 Python 标准库tkinter PIL渲染方式Unicode 文本字符棋盘格绘制 位图贴图交互方式键盘输入坐标鼠标点击像素坐标适合场景服务器调试、快速看局面桌面演示、新手试玩提示项目标注要求 Python 2.5如果直接跑旧源码Python 3 下要把import Tkinter改成import tkinterPIL 也需要换成pip install pillow。装了 Python 之后记得先用python -c import tkinter验证环境TkInter 在部分 Linux 发行版需要单独装python3-tk。5. 用棋盘快照做规则回归验证不启动 GUI 也能定位走子 bug把棋盘设计成字典带来的一个隐藏红利是你可以不启动任何窗口直接构造一个死局面来验证走子逻辑。国际象棋里很多规则 bug 从界面上看不明显比如“挡住己方王线的子不能动”“兵升变后变成什么棋子”但用字典做测试夹具时就非常直观。# test_helpers.py 中快速构造指定局面的函数 def load_fixture(board, placement): for square, code in placement.items(): # code 形如 wr 表示白车bk 表示黑王 board.squares[square] Piece(code[0], code[1])这个夹具函数接收一个字典键是坐标、值是两位字符码。第一组用例可以验证车的遮挡白车在 d4d6 放一颗黑兵那么 d5 和 d6 应该合法d7 必须不合法。第二个用例验证将军过滤白王放在 e1黑车放在 e8王的所有横向移动都应该被过滤掉因为走完仍会被黑车沿 e 线攻击。# 断言式验证用 pytest 或直接 assert 都可 def verify_targets(board, origin, expected): actual set(legal_targets(board, origin)) assert actual set(expected), ( f{origin} 的实际走法 {sorted(actual)} 与预期 {sorted(expected)} 不一致 ) # 用例车被兵挡住时不能穿过 b1 Board() load_fixture(b1, {d4: wr, d6: bp}) verify_targets(b1, d4, [d5, d6])把这个技巧再往前推一步配合第 2.3 节的to_fen就能做更系统的回归测试从训练棋谱或标准局面库中挑十来个关键局面记录每个局面的合法走法数量每次改动走法生成代码后批量跑一遍。比如起始局面的合法走法总数是 20改完如果变成 24说明某个棋子的走法表被放宽了。我用过的最有效的调试路径是先在 GUI 里复现一个可疑走法然后调出当时的 FEN用load_fixture重建局面最后用一个assert把异常固定下来。比起反复在窗口里点棋子、撤销、再点这种基于字典快照的二分定位能直接告诉你规则链的哪一段出了问题而且每个用例以后还能重复执行。本文还有配套的精品资源点击获取
返回列表