本系列:把这两年用 Python 写的十几个小项目挨个开源、挨个说清楚。不讲”我做了什么”,讲”做的过程中被什么打脸、最后怎么想明白的”。 第 6 篇 · 纯粹数独 PureSudoku(前几篇:离阵 / 胶囊医生 / 烬塔 / 回响回廊 / 砂漏) 源码:github.com/ghostgorge/pure_sudoku 技术栈:Python + pygame,单文件 exe
一、”纯粹”是个约束,不是形容词
没有金币、没有商店、没有体力条、没有成就墙、没有剧情。就是一张纸、一支笔、一个计时器,和五档由易到难的题。
前面几个项目我都在往里加东西——成就、工坊、每日挑战、试炼塔。这个项目故意反过来:把所有能加的都不加,看看剩下的那个核心撑不撑得住。
于是全部力气只能花在一个地方:出题质量与难度分级。
而这件事比我想的难得多。这篇文章基本就是在讲我推翻过的两个方案。
二、大部分数独 demo 是怎么出题的
随机挖掉 N 个格子,N 越大越难。
两个问题:挖出来的题可能有多解;难度完全不可控。
先说第一个——这一版每道题出完都过两道硬关:
- 唯一解:回溯求解器数到第二个解就停,只有恰好一个解才放行。
- 纯逻辑可解:用一套人类解法引擎从头推到尾,全程不需要试错回溯。
第 2 条的意义是给玩家一个承诺:专家局也一样——如果你推不动了,那一定是前面填错了,不是题的问题。
第二个问题,就是下面这两次推翻。
三、推翻方案一:用提示数当难度旋钮
实测数据(每档 12 局,随机对称挖洞):
| 挖到剩余提示数 | 难度分区间 |
|---|---|
| 45 | 48 – 61 |
| 40 | 59 – 90 |
| 34 | 66 – 114 |
| 30 | 81 – 426 |
| 26 | 94 – 426 |
| 24 | 115 – 922 |
看第 4 行:同样是 30 个提示数,最简单的和最难的差了 5 倍。
更要命的是我实测过的一道 17 提示数的题(17 是数独唯一解的理论下限,听起来应该是地狱级),它的解法序列是:
隐性唯一数 × 9 → 区块摒除 × 7 → 唯一余数 × 55
前面十几步撬开之后,剩下 55 格是一路顺推下来的。17 个提示数,实际难度只相当于中等偏上。
结论:提示数是结果,不是旋钮。
四、推翻方案二:只用”最高技巧层级”当难度
第二版把 12 种技巧分了五层:
| 层级 | 技巧 |
|---|---|
| T1 | 唯一余数、隐性唯一数 |
| T2 | 区块摒除、显性数对、隐性数对 |
| T3 | 显性/隐性三数组、四角对角线 (X-Wing) |
| T4 | 显性四数组、剑鱼 (Swordfish)、XY-Wing、XYZ-Wing |
| T5 | 都推不动,只能试错 |
然后规定:入门/简单 = T1,中等 = T2,困难 = T3,专家 = T4。跑下来撞了两堵墙。
问题 A:T1 的跨度太大了。 测了 25 局挖到 26–32 提示数的题,21 局的最高层级都是 T1。一道全靠隐性唯一数、每次全盘只有一个格子能确定的题,和一道满盘一眼能看出七八个数的题——层级完全一样,体感差着量级。
问题 B:T3/T4 天生稀缺。 高级技巧要”成为必需”,得盘面刚好卡在那个形状上。按”必须用到 X-Wing”筛困难局,命中率低到跑 40 次都出不来一道。最后只能退化成随便给一道题充数——那还不如不分级。
五、最终方案:不做求解器,做记账器
core/logic.py 里的 analyse() 不是求解器,是一个记账器。它模拟人怎么解题,一边解一边记账:
while 盘面没填满:
找出当前所有能立刻确定的格子(显性唯一余数 + 隐性唯一数),记作 S
if S:
avail = len(S) # 这一轮有多少个"能看出来"的格子
单格成本 = 1.0 + 6.0/avail + (全靠隐性唯一数 ? 2.0 : 0)
分数 += 单格成本 * avail
把 S 里的格子一次性全填掉 # 人就是这么扫的
else:
找一个 T2/T3/T4 排除步骤
分数 += {T2: 55, T3: 130, T4: 250}
三处设计值得说:
6.0/avail—— 满盘能看出 8 个数时,单格成本 1.75;全盘只剩 1 个格子能确定时,单格成本 7.0。这一项专治”问题 A”。+2.0—— 显性唯一余数只要看这一格自己,隐性唯一数要扫整个单元,后者贵一点。- 一次性填掉 S —— 如果每填一个数就重新计一次账,会把”顺推的一长串”算成很多步;实际上人顺推的时候几乎不用想。
所以最终的难度分同时包含两件事:最难要用到第几层技巧,以及每落一个子要在多少个候选里挑。第二条才是这一版和常见做法最大的区别。
校准
拿三道有公认难度的题做基准:
| 题目 | 提示数 | 最高层级 | 分数 | 落档 |
|---|---|---|---|---|
| 报纸上的入门题 | 32 | T1 | 73 | 简单 |
| 必须用 X-Wing 的题 | 46 | T3 | 368 | 困难 |
| 17 提示数那道 | 17 | T2 | 555 | 专家 |
第三行是这套打分法的最好注脚:它的最高层级只有 T2,但因为一路上”每次只有一两个格子能确定”,分数被推到 555,落进专家档——和实际体感一致。(注意它和第三节那道 17 提示数的题不是同一道,两道题同样 17 个提示数,一道中等偏上、一道专家。)
最终五档:
| 档位 | 最高技巧 | 难度分 | 典型提示数 |
|---|---|---|---|
| 入门 | T1 | 0–66 | 39–42 |
| 简单 | T1 | 66–108 | 32–34 |
| 中等 | T2 | 108–210 | 27–31 |
| 困难 | T3 | 210–440 | 24–30 |
| 专家 | T4 | 440+ | 22–26 |
提示数在这张表里是”典型值”而不是”设定值”——同样 28 个提示数,可能是中等,也可能是困难。
六、出题器:利用单调性,别碰运气
难点在于分数是挖完才知道的,不能边挖边祈祷。
好在有一条单调性可用:回填提示数只会让题变简单,分数只降不升。 于是流程做成一条单调下降的曲线:
1. 随机终盘(回溯 + 打乱候选顺序)
2. 尽量深挖,每挖一组都校验:仍是唯一解 + 仍能在本档层级内纯逻辑解出
3. 此时是这个终盘的最高分。够不到本档下限 → 换终盘重来
4. 逐个把挖掉的格子填回去,每填一个重新打分,
单调下降到落进 [下限, 上限] 为止
这比”边挖边碰运气”快一个量级。
再加两条防止难度虚标的硬约束:
- 中等档至少真的用到 1 次高级排除,困难档 2 次,专家档 4 次。否则这道题只是”扫得久”,不是”想得难”,不算数。
- 提示数也有区间,防止专家档挖出一道 20 提示数、题面稀稀拉拉的题。
40 次尝试还没正中的话,取”离目标带最近”的那一道(不是随便再挖一道)。所以最坏情况也只是难度偏一点,唯一解和纯逻辑可解永远成立。
实测(每档 10 局):入门/简单 10/10 落带、耗时 0.01s;中等 9/10、最慢 1.14s;困难 10/10;专家 10/10。50 局全部通过唯一解与纯逻辑可解校验。出题放后台线程,前台转圈,偶尔的 1 秒不卡界面。
七、一鱼两吃:同一套引擎既评级也做提示
评级用的技巧函数,和游戏里”提示”按钮用的是同一套代码。好处有三个:
- 提示给出的技巧,一定是这道题真正需要的那一步,不是凭空报答案;
- 讲解文字由技巧函数自己生成,带具体坐标: 【区块摒除】第 4 宫的 7 只能落在第 5 行里,所以第 5 行其余格子都不可能是 7。于是【隐性唯一数】第 6 宫里只有 R6C8 能放 7。
- 通关结算页能列出”这道题解开需要:唯一余数 ×37、隐性唯一数 ×14、区块摒除 ×4″——玩家能看到自己刚才到底跟什么打了一架。
按一次 H 只讲技巧和理由,再按一次才落子。如果盘面上已经有填错的数字,它会先提醒你按 C 检查——不然基于错误盘面推出来的”技巧”全是胡说。
next_hint() 还做了个小处理:如果下一步是纯排除(不落子),它会先应用这个排除、再看有没有立刻冒出来的唯一余数,把”排除 → 落子”两步一起讲。人是这么想的,一步一步报排除会很碎。
八、老实的取舍
- 没做链式技巧(Forcing Chain / Nice Loop)以上的层级。 代价是”恶魔级”出不来。理由:这类题必须在纸上画链才推得动,对一个”纯粹”的数独没必要,而且很容易变成名义上不用猜、实际上只能猜。
- 困难档大多数时候实际最高层级是 T2 而不是 T3。 这是老实话:T3 必需的题太稀缺,硬凑会拖垮出题速度。所以困难档的难度主要来自”卡住的次数多、每次可选步骤少”,菜单里的文案照实写”反复卡住,偶尔用上三数组、四角对角线“,没有虚标。
- 专家档不保证对称题面。 对称挖洞挖不深,深度和美观只能选一个。
上手
双击 run.bat # 自动找解释器、缺 pygame 会自动装
双击 build_exe.bat # 出 dist\PureSudoku.exe,先跑自测,不过就不打包
python selftest.py # 常规约 5 秒;--full 每档各出 12 题的加长回归
1–9 填数 · 空格 切换填数/笔记 · Shift+数字 临时用另一种模式写一次 · Z 撤销 · H 提示 · C 检查 · M 自动候选 · P 暂停(停表并藏盘面)· N 同难度换一局
打包脚本里有一行是上一篇烬塔留下的疤:打完会校验 exe 体积,防止收尾横幅里的
>被 cmd 当成重定向、把刚打好的 exe 覆盖成一个几十字节的文本文件。
尾巴
这个项目最值钱的一条,是关于怎么给一个纯逻辑玩法定义难度:
难度不是”最难的那一步有多难”,也不是”题面上少了多少东西”。 难度是”整个过程中,你平均要在多小的搜索面里做判断”。
一道全程只用最基础技巧、但每次全盘只有一个格子能确定的题,比一道需要 X-Wing、但其余部分一路顺推的题更难。这跟第一篇离阵得出的”决策面宽度”是同一件事的两个面:那边是决策面太宽要筛,这边是可见面太窄要找。
📦 源码:https://github.com/ghostgorge/pure_sudoku
下一篇:立方之蛇 CubeSerpent——三维贪吃蛇,一个被实机反馈直接推翻、然后整个重写的项目。回复「继续」。
