起因 链接到标题
吃四之后想骂发牌姬,但心里其实没底:到底是这副牌本来就没法打,还是我打崩了? 麻将的麻烦在于结果和过程之间隔着一层厚厚的方差,一局定不了任何事,可人一次只打得到一局。
誰だ / dareda 就是拿来解决这件事的。它把牌谱里的牌山取出来, 让 AI 在一模一样的发牌下把这局重打几十遍,然后给一个名次分布:
座2 同水平自打分布 n=30 平均顺位 2.30
1位 20.0% ████████
2位 40.0% ████████████████
3位 30.0% ████████████
4位 10.0% ████
实际 4 位;同水平自打期望 2.30 位。
判断:偏背 —— 你的同水平只有 10% 会打到 4 位或更差
翻译一下:以你自己的水平重打这副牌,平均能到 2.3 位,只有 10% 会像那局一样吃四。 这把是运气差,不是你菜。(开喷!
它到底跑了什么 链接到标题
底子是 Mortal(开源的日麻 AI)和它的 libriichi 引擎,
dareda 打了个补丁把牌山约定固定下来,使得同一副牌可以被反复重放。数据从雀魂抓包来,
解出来第一件事是 verify:逐局比对配牌是否与原局恒等,不过就不许往下跑——
后面所有结论都建立在"重放的是同一副牌"之上。
之上是三个不同的实验,区别很容易被读混,所以这里写清楚:
| 实验 | 谁在打 | 回答什么 |
|---|---|---|
replay | 四家全 Mortal,从第一张牌起 | 这副牌顶尖打法能打成什么样 |
counterfactual | 只有你换成 AI,对手照人类原样出牌 | 钉在那条已经崩掉的牌河上,你还能救回多少 |
self-luck | 四家各按自己被标定出的水平重开 | 同水平的你,重打 30 次会落在哪 |
“水平"不是拍脑袋给的:先在人类原局上逐决策测每家的 EV loss(与 Mortal 最优解的期望差距),
再反解出一个采样温度——温度越高越随机、越弱。所以 self-luck 里的对手不是三个满血 AI,
而是三个"和那晚一样菜"的人。
最后 diagnose 把这些压成一句话,靠一个恒等式:
实际名次 − 2.5 = (牌与座次) + (打法) + (运气残差)
关键在于必须跑两条基线。self-luck 的期望值里同时含了牌和打法,只有再跑一条"四家均等水平"的基线,
才能把"这副牌值几位"和"你的打法赚了几位"分开。只用 self-luck 当"牌好不好”,会把打法算进牌里。
三项相加恒等于左边,测试锁死了这条闭合性——也正因为恒等,合计那一项不是独立证据。
一份真实报告 链接到标题
下面这份是完整跑出来的东西:一份四人东南战(打到南四,K = 9 局),hero 是座 2,终局 4300 点,4 位。 同一份牌谱用两份 Mortal 权重(150k / 298k)各跑一遍,只为看结论稳不稳。
结论一句话:好牌,打得一般,而且这把运气还背。
- 牌与座次
+0.33,打法−0.13,运气−1.70,合计−1.50——从中性的 2.5 位掉到实际的 4 位,运气占了绝大部分。 - 换成 298k 权重,同样的
−1.50拆成+0.40 / −0.03 / −1.87,判词一样是"偏背"、4 位概率一样是 10%。两份权重同向,这个结论是稳的。 - 更打脸的一条:hero 的 EV loss 是四家里第 2 低(换 298k 是最低),全场打得最脏的是座 0,EV loss 0.488——而他拿了 3 位。
报告里还有一节专门讲该打的折扣,比如 counterfactual 的轨迹保真度中位数只有 51%(对手很快就脱轨了),
所以那一节的具体点数不该当数值结论;比如 montecarlo --sensitivity 的 full-mortal 档是确定性单次重放,
显示 100% 4 位说的是"那唯一一条确定轨迹是 4 位",不是 100% 的置信度。
一份分析报告里最该写清楚的往往是这些,而不是那几个大字。
末尾附了完整的耗时与内存账:每条命令都经 run_step.py 执行、用 os.wait4 逐进程采 rusage。
全套 11 个步骤合计 577.6 s(9.6 分钟),单步峰值常驻内存 440 MB。
(在 iframe 里看着挤的话,单独打开这一页。)
几个改动,和背后的判断 链接到标题
工具本身是慢慢长出来的,有几处改动值得单拎出来说,因为它们都是"数字算得出来"和"数字算得对"之间的差别。
运气不能用均值差来定义。 最早的判据是"实际名次 − 同水平期望"超过 ±0.2 位就算背/顺。
问题是自打分布经常是双峰的,均值落在没人会落到的中间地带,于是常见的 4 位被读成"惨背"。
后来改成分位:luck_q = 实际名次在自己那条分布里的中位秩,也就是"你赢过了自身多少比例的重演",
≤0.25 判背、≥0.75 判顺、中间愿赌服输。运气那几颗星也由同一个量画出来,不再和判词各说各话。
删掉了"换满血 AI"那一栏。 曾经有一栏是"你换成满血 Mortal、对手不变",本意是给"打法天花板"。 实测下来它不是天花板:固定牌山上 Mortal 反而更凶,和了和放炮都更多,平均名次跟真实水平打平。 一栏会误导用户的数字,哪怕算得没错也该删——顺带省掉三分之一算力。
强度测量跳过强制决策。 立直后的摸切、唯一合法的应对,都不是选择。 把它们算进一致率是白刷分,算进 EV loss 是拿 0 损失稀释。剔掉之后 hero 的一致率从 68% 掉到 66%、 EV loss 从 0.337 涨到 0.362,仍是全场第一;而那个重立直的对手 EV loss 从 0.656 涨到 0.741,虚高被挤掉了。 起因是一次很具体的委屈:用户吃四那局放的 18000 是立直后的强制摸切,不该算打法的锅。
把分布画出来。 “吃四=运气差"这个标签光看着就很像狡辩。后来在结果里加了名次概率横条, 逻辑就自洽了——你看得见那 10% 有多薄。黑箱变透明比多算几个数字有用。
耗时从 26 分钟砍到 9.6 分钟,靠的是三处结构性浪费,没有一处是"优化常数”:
montecarlo 的跨轨迹循环本来是裸 for(CPU 只有 106%,其余命令都在 330% 上下),套上现成的进程池后单条命令 351s → 135s,
每条轨迹按 base_seed + i 播种,jobs=1 和 jobs=4 的分布逐项相同;
--sensitivity 的 full-mortal 档没有随机源,跑 20 条是同一局算 20 遍,压到 n=1;
self-luck 本来就是 diagnose 的真子集(那次蒙特卡洛四家的分布都算了,只是只留了 hero 一份),留全即可,
加个 --with-selfluck 白送——两条都跑等于重算一遍,约占整套分析 17% 的耗时。
西入被一行代码挡了很久。 南四结束无人到 30000 会打进西一,而 KyokuReplay 的 kyoku 上界写死在 8,
于是全线报"kyoku 越界: 8"。查下去发现西场其实天然成立——Board 本来就按 bakaze = E + kyoku/4 推导,
观测编码也早把西场 clamp 成南场口径。也就是说西入一直是被那一句挡在门外,而不是不被支持。上界改成 12 就完了。
还有一批纯工程的坑,写在 README 的"遇到问题"里:Windows 260 字符路径上限会让 pip install torch 装到一半停下、
之后报一个完全无关的 No module named 'torchgen';不设 PYO3_PYTHON 编出来的扩展会链到系统 Python,
在 venv 里 import 报 symbol not found,而错误信息完全看不出是链错了解释器。
抓包脚本也从"按 D 键下载"改成了右下角悬浮按钮——原来的裸热键会在雀魂任何输入框里打字带 d 时误触。
三条须知 链接到标题
- 只用来复盘打完的牌。 不要用于正在进行的对局。
- 取牌谱需要在浏览器里改雀魂客户端,可能违反用户协议、有封号风险。 用不用自己决定,建议用小号。
- 项目不含 AI 模型,也不提供模型权重(安装脚本会自动从第三方下)。许可证 AGPL-3.0。
顺带一提,报告里那句"这局打得最不干净的是座 0,而他拿了 3 位"—— 这大概才是这个工具最诚实的用途:它同样会告诉你,你打得就是比对面差,只是那天运气替你兜住了。