ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

用 minimax 算法构建带 AI 玩家的 Python 井字棋游戏引擎

用 minimax 算法构建带 AI 玩家的 Python 井字棋游戏引擎
  • 示例工程
  • 教程

【免费下载链接】materials

Bonus materials, exercises, and example projects for our Python tutorials

项目地址:https://gitcode.com/gh_mirrors/ma/materials
点击查看免费下载

本文是井字棋(Tic-Tac-Toe)AI 项目实战指南。它以可复用的游戏引擎库为骨架,把棋局逻辑与前端表现彻底解耦,并提供了终端、桌面 GUI(Tkinter)与浏览器(PyScript)三套可直接运行的前端,其中电脑玩家基于 minimax 算法实现"不可战胜"。读完本文,你将掌握从零搭建井字棋引擎库、实现 minimax 与 alpha-beta 剪枝、预计算棋局评分表,以及通过python -m方式启动不同前端、自由搭配人类/随机/minimax 玩家的完整实战方案。

该项目的完整源码位于 tic-tac-toe-ai-python 目录,包含source_code_bonus(完整版,含三个前端)与source_code_final、source_code_step_1~3(分步版本)等快照,便于对照学习演进过程。

项目整体构成:一个引擎库,三种前端

README 明确指出本项目由两大部分组成:

  1. 游戏引擎库(Game Engine Library):封装全部棋局逻辑,可被多个前端复用而无需重复编写;
  2. 游戏前端(Game Front Ends):分别实现于独立的包中,负责界面与交互。

这种"引擎库 + 多前端"的架构使你可以轻松为同一套规则逻辑挂接任意交互载体。在完整版源码中,引擎库位于 source_code_bonus/tic-tac-toe/library,三个前端则位于 source_code_bonus/tic-tac-toe/frontends 下的console、window、browser三个子包中。

游戏引擎库:安装与打包

创建虚拟环境并安装引擎库

在运行任何前端之前,需要先把引擎库安装进虚拟环境(源码命令):

$ cd tic-tac-toe/ $ python -m venv venv/ $ source venv/bin/activate (venv) $ python -m pip install library/

其中library/即引擎库目录,其构建配置见 source_code_bonus/tic-tac-toe/library/pyproject.toml:包名为tic-tac-toe、版本1.0.0、requires-python = ">=3.10",且不依赖任何第三方库(dependencies = []),是地道的纯 Python 实现。安装成功后即可运行本项目提供的各个前端。

打包成 wheel 供浏览器前端使用

浏览器前端不通过虚拟环境加载引擎库,而是依赖一个可分发的 Python wheel 文件。因此需要先构建并打包库:

$ cd tic-tac-toe/ $ python -m pip wheel library/ $ mv tic_tac_toe-1.0.0-py3-none-any.whl frontends/browser/

README 特别注明:这些命令不需要虚拟环境即可工作,但在虚拟环境中执行也完全没问题。生成并移动后的 wheel(tic_tac_toe-1.0.0-py3-none-any.whl)正是浏览器前端通过 PyScript 动态加载的对象,在 source_code_bonus/tic-tac-toe/frontends/browser/ 中可以看到该文件已就位。

引擎库内部结构:logic 层与 game 层

从源码结构看,引擎库按"纯逻辑"与"游戏运行"分成了两个子包,边界清晰:

library/src/tic_tac_toe/ ├── logic/ # 纯棋局逻辑:与界面完全无关 │ ├── models.py # Mark、Grid、Move、GameState 数据模型 │ ├── minimax.py # minimax 算法与评分表序列化 │ ├── validators.py │ └── exceptions.py └── game/ # 游戏运行层:串联玩家与渲染器 ├── engine.py # TicTacToe 同步引擎 ├── engine_async.py # AsyncTicTacToe 异步引擎(浏览器前端使用) ├── players.py # 玩家抽象与实现 ├── players_async.py # 异步玩家实现 └── renderers.py # 渲染器抽象基类

数据模型:用 9 字符字符串表示棋盘

在 logic/models.py 中,整个棋局被建模为三类不可变数据类:

  • Mark:继承自str的枚举,只有CROSS = "X"与NAUGHT = "O"两种子,并通过other属性快速取对方的棋子;
  • Grid:棋盘本体,内部就是 9 个字符的字符串(" " * 9表示空盘),X/O/空格分别代表两方棋子与空位;x_count、o_count、empty_count由cached_property缓存;
  • GameState:一局棋的完整状态(棋盘 + 先手方),通过属性推算出current_mark(轮到谁走)、game_over、tie、winner、winning_cells以及possible_moves(所有合法走法列表)。

胜负判定非常巧妙:代码里预定义了 8 个WINNING_PATTERNS(3 行 + 3 列 + 2 条对角线,用?通配位置),再用正则pattern.replace("?", mark)去匹配当前棋盘字符串,命中的子即胜者。possible_moves则用re.finditer(r"\s", ...)扫描空位,并为每个空位调用make_move_to(index)生成一手Move。

校验器:保证棋局状态永远合法

logic/validators.py 在每次构造Grid与GameState时自动执行校验:

  • validate_grid:棋盘必须恰好由 9 个X/O/空格组成;
  • validate_number_of_marks:两方棋子数之差不能超过 1;
  • validate_starting_mark:根据先手方与当前棋子数判断先手是否合理;
  • validate_winner:结合先手方检查胜者对应的落子数关系;
  • validate_players:两个玩家不能使用同一种棋子。

这些校验保证了引擎在任意状态下都不会进入矛盾局面,也是GameState.__post_init__自动触发的第一道防线。

minimax 算法:不可战胜的 AI 核心

AI 的灵魂在 logic/minimax.py 中,共提供了三档实现,性能与复杂度逐级优化。

评分规则

GameState.evaluate_score(mark)(见 models.py#L135)定义了游戏终局评分:

  • 平局:返回0;
  • 指定棋子获胜:返回1;
  • 指定棋子落败:返回-1。

标准 minimax(V1)

核心函数minimax()对每一步的后续局面递归求值,max 层(当前方)取子局面评分最大值,min 层(对手方)取最小值:

def minimax(move, maximizer, choose_highest_score=False) -> int: if move.after_state.game_over: return move.after_state.evaluate_score(maximizer) return (max if choose_highest_score else min)( minimax(next_move, maximizer, not choose_highest_score) for next_move in move.after_state.possible_moves )

对外入口find_best_move(game_state)以当前玩家为maximizer,对possible_moves按 minimax 评分取最大者。

alpha-beta 剪枝优化(V2/V3)

find_best_move_optimized()实现了 alpha-beta 剪枝:在递归中维护alpha(当前 max 层已获得的下界)与beta(当前 min 层已获得的上界),一旦beta <= alpha立即剪掉剩余分支,大幅减少探索节点。源码中用-2/2作为评分界(合法评分只有-1/0/1),保证剪枝不会误伤。

V3 版本进一步做了一手开局优化:当game_not_started(空盘)时先随机走一步,以引入对局多样性。

预计算评分表(V4,默认方案)

MinimaxSerializer 把"以 X 先手"和"以 O 先手"两种初始状态下、所有可达走法的评分一次性遍历计算出来,序列化为minimax.json(见 logic/minimax.json,采用@cache惰性加载)。find_best_move_precomputed()直接查表即可在常数时间内返回最优走法——运行期零递归开销。

关键结论:在 game/players.py 中MinimaxComputerPlayer = MinimaxComputerPlayerV4,即项目默认的 AI 玩家用的正是预计算评分表方案。

玩家抽象体系

game/players.py 定义了清晰的三层抽象:

  • Player:持有棋子标记,make_move()负责校验"是否轮到本方",实际走法交给抽象方法get_move();
  • ComputerPlayer:在取走法前先time.sleep(delay_seconds)(默认 0.25 秒),制造自然的思考停顿,再调用抽象方法get_computer_move();
  • 具体实现:RandomComputerPlayer(随机落子)、MinimaxComputerPlayerV1~V4(对应三档 minimax 方案)。

游戏引擎主循环

game/engine.py 的TicTacToe.play()是同步引擎的主循环:创建空盘GameState,渲染当前状态,若未结束则让当前玩家走一步,捕获InvalidMove交给可选的error_handler,如此往复直到game_over。构造引擎时还会校验两个玩家棋子不同。

异步版 game/engine_async.py 的AsyncTicTacToe结构与同步版一致,只是把player.make_move()换成await player.make_move(),专供浏览器前端使用。

三个前端:从终端到 GUI 再到浏览器

在 frontends 下运行任一前端前,请先完成前面"安装与打包"的步骤,然后切换到前端父目录:

$ cd tic-tac-toe/frontends/

终端前端(console)

终端前端最灵活,可通过命令行参数自由搭配玩家:

(venv) $ python -m console

参数说明(见 frontends/console/args.py):

参数可选值默认值含义
-Xhuman/random/minimaxhuman执 X 的玩家
-Ohuman/random/minimaxminimax执 O 的玩家
--startingX/OX先手方

例如让 X 用 minimax、O 用随机策略,且 O 先走:

(venv) $ python -m console -X minimax -O random --starting O

从 args.py 可以看到,当--starting O时内部会把两个玩家对象对调,以符合引擎"player1 先手"的约定。

人类玩家的落子格式是坐标制:A1或1A均可(字母表示列 A~C,数字表示行 1~3)。frontends/console/players.py 中的grid_to_index()负责把坐标转换为 0~8 的格子索引,并会给出"Please provide coordinates in the form of A1 or 1A""That cell is already occupied."等友好提示。

frontends/console/renderers.py 的渲染器每次开局前清屏,绘制带行列表头的 ASCII 棋盘,获胜时用 ANSI 闪烁效果高亮胜利连线,并打印X wins 🎉或平局提示No one wins this time 😐。

桌面 GUI 前端(window)

用 Tkinter 构建的桌面窗口应用,默认配置是"人类玩家(X)对战 minimax 电脑(O)":

(venv) $ python -m window

玩家组合目前是硬编码的。要更换玩家,需要编辑 frontends/window/cli.py 中game_loop的片段:

def game_loop(window: Window, events: Queue) -> None: player1 = WindowPlayer(Mark("X"), events) player2 = MinimaxComputerPlayer(Mark("O")) starting_mark = Mark("X") TicTacToe(player1, player2, WindowRenderer(window)).play(starting_mark)

实现要点:Window(见 frontends/window/renderers.py)用 3×3 的ttk.Button铺成棋盘,点击事件把格子索引写入Queue;WindowPlayer(见 frontends/window/players.py)通过events.get()阻塞等待点击,从而把 Tkinter 事件循环与引擎的同步主循环衔接起来——main()中引擎被放到一个守护线程里运行,窗口本身则由window.mainloop()驱动。获胜时窗口标题变为X wins 🎉,胜利连线上的按钮会加粗显示。

浏览器前端(browser)

基于 PyScript 的浏览器前端,无需虚拟环境、无需安装引擎库——它从打包好的 wheel 文件动态加载引擎:

$ python -m browser

该命令会启动一个本地 HTTP 服务器,并在浏览器中打开页面。frontends/browser/index.html 中通过<py-env>声明依赖:

<py-env> - tic_tac_toe-1.0.0-py3-none-any.whl - paths: - players.py - renderers.py </py-env>

页面顶部提供了 X/O 的玩家下拉框(Human / Random / Minimax),棋盘由内联 SVG 绘制。frontends/browser/script.py 把点击事件放入asyncio.Queue,由BrowserPlayer消费(见 frontends/browser/players.py),再交给异步引擎AsyncTicTacToe与异步 minimax 玩家(AsyncMinimaxComputerPlayer同样使用预计算评分表方案)驱动整局游戏;结束时显示Replay?按钮可立即重开。

目录速览与分步学习

想要循序渐进地理解项目,可以对照源码快照阅读:

  • source_code_step_1:搭建包结构、数据模型与校验器;
  • source_code_step_2:加入引擎、玩家抽象与渲染器基类;
  • source_code_step_3:实现终端前端完整交互;
  • source_code_final:教程正文的最终版本;
  • source_code_bonus:扩展版,额外包含 window、browser 两个前端、异步引擎以及 wheel 文件。

小结

通过本项目的源码与文档,可以看到一个教科书级别的分层设计:logic层把棋盘、规则、minimax 算法做成与界面无关的纯 Python 库;game层用"玩家 + 渲染器 + 引擎"的组合把交互抽象出来;而console、window、browser三个前端则分别演示了同步命令行、事件驱动 GUI 与浏览器异步运行三种承载方式。默认 AI 采用预计算评分表(V4),配合 minimax 的完整递归、alpha-beta 剪枝与开局随机化等变体,完整覆盖了从理论到工程落地的整条路径。

  • 示例工程
  • 教程

【免费下载链接】materials

Bonus materials, exercises, and example projects for our Python tutorials

项目地址:https://gitcode.com/gh_mirrors/ma/materials
点击查看免费下载
上一篇:5 大最常用部署策略详解:Big Bang、Rolling、Blue-Green、Canary 与 Feature Toggle(system-design-101 实战指南)
下一篇:Frontend Slides 的 Coral 模板设计系统:Bebas Neue + Inter 双字体海报体系的完整设计规范与实现指南

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进