天凤牌谱互操作知识

生成与解析 tenhou.net JSON 牌谱(tenhou.net/5#json= / tenhou.net/6#json=)的成文知识: 逐字段必需约束、两处高危易错点、座位旋转公式、以及唯一可信的目验方法。 这些结论全部从天凤官方前端与社区工具源码逆向核对得出(核对基线见 §9)——公开资料里没有 同等密度的系统整理,多数散落在各工具源码里。

在 majojson 中,这段知识对应文件顶层的四个键 title/name/rule/log(”天凤段”): 仅日麻、且仅本机完整对局导出时附带(见规范 §0/§1)。 无论你是否使用 majojson,只要要生成或解析天凤牌谱,本文的结论都用得上。


1. 整体形态

目标形态是 tenhou.net/6 JSON(天凤官方 /5 查看器与 /6 编辑器通过 URL 片段 #json=<urlencoded> 读它; mjai-reviewer 的 convlog 转换层也读它)。本文按参考实现逐字段给出必需约束—— 天凤没有公开的格式文档,这些约束是工具”安静地出错”的地方。


2. 顶层字段

键 必需 口径
log ✓ 硬必需 每手一个 17 元素数组(§3)
name ✓(mjai-reviewer 硬必需;查看器容忍缺失) 四家昵称,按 §5 旋转后的顺序
rule ✓(同上;且 rule.disp 被工具解析,见下) { "disp": …, "aka": 枚数 } 或 { "disp": …, "aka51": …, "aka52": …, "aka53": … }
title — 仅查看器标题栏显示用,两行字符串数组如 ["Majo", "日麻"];mjai-reviewer 根本没有这个字段
lobby/dan/rate/sx/ratingc — 不写(查看器不需要;mjai-reviewer 的对应字段可选)

2.1 rule.disp 不能随便写

工具据它判断规则,两种误写都有实际后果:

  • mjai-reviewer 的 convlog 用 disp.contains('東') || contains("East") 判东风战, 而 Mortal 对 -e mortal 要求半庄(不是半庄就 bail!("Mortal supports hanchan games only"))→ 半庄的 disp 必须含”南”且不含”東”(如 "Majo 南喰赤"),东风战才含”東”;
  • 同一处还有”三麻”判定(含”三”即 NotFourPlayer)→ disp 不得含”三”。

生成方口径:赛制为东风战(tonpu)→ disp 含”東”;半庄及以上(half/full/infinite)→ 含”南”不含”東”。 真实天凤日志用终局风命名(半庄 般南喰赤/鳳南喰赤、东风 般東喰赤速),沿用同一习惯即可。

2.2 rule.aka*

由逐花色的红五张数派生(每花色 1 枚的常见配置 → 三值全为 1);三值相等时可塌缩为单个 aka (真实天凤日志两种写法都有,两者都合法)。


3. 每手 17 元素(索引写死,不能少不能多)

索引 内容 口径
0 [局序, 本場, 供託] 局序 = 圈序×4 + 局内序(东一 = 0);三者取本手起始值(首帧时刻)
1 四家点数 [int×4] 原始点数(25000 满点口径),不除 100(mjai-reviewer 直接透传进 mjai 的 scores,而 mjai 就是 25000 口径)
2 宝牌指示牌数组(可增长) 用天凤牌码;杠后追加(对应 mjlog 的 <DORA>)
3 里宝指示牌数组 只在立直者与牌时填;其余情况空数组
4 / 7 / 10 / 13 各家起手恰好 13 张 庄家第 14 张不在这里(见索引 5)
5 / 8 / 11 / 14 各家”取”数组:摸牌码 / 副露字符串(吃c、碰p、大明杠m) 摸牌码用天凤牌码;杠补摸牌也算普通摸牌(与天凤一致,不区分)
6 / 9 / 12 / 15 各家”出”数组:打牌码 / 60(摸切)/ "r"+码(立直宣言)/ 加杠k 与暗杠a 字符串 / 0(大明杠占位) 见 §4
16 结果数组 见 §6
  • 座位顺序:索引 4+3c/5+3c/6+3c 对应第 c 家(c = 旋转后的索引,§5); 查看器加载器读 f[3c+4]/f[3c+5]/f[3c+6],与上表一致。
  • 庄家第 14 张进”取”数组(索引 5+3c 的首元素)——这是硬性结构(起手是固定 13 元数组)。 生成方在第 14 张上:即”发牌后、庄家打出第一张前”庄家手中的那张。
  • 一手一文件在查看器里是正常的:局名/本場/供託都取自索引 0,局一览面板只在 log 多于一手时出现, 终局面板只在有 sc 时出现——log 恰好 1 个元素不会让查看器出错(官方 testdata 里也有单手文件)。

4. 牌码与副露字符串

项 编码
数牌 m1..m9 = 11..19、p = 21..29、s = 31..39;字牌 z1..z7 = 41..47(东南西北白发中)
红五 51(0m)/ 52(0p)/ 53(0s)
摸切 60(写在”出”数组里,替代具体牌码)
立直宣言的那张打牌 字符串 "r" + 码(如 "r14");立直且摸切 → "r60"(真实日志里有此写法,且这是唯一能同时表达两者的编码)
吃(c)/ 碰(p)/ 大明杠(m) 副露字符串写进鸣牌者自己的”取“数组(占该次摸/鸣的位置),形如标记字母 + 牌码序列
大明杠的 0 占位 鸣牌者自己的”出”数组在同一位置补整数 0(”这次打牌被明杠取代”)——两个数组要保持同下标对齐,查看器按 取[k] ↔ 出[k] 配对("m" != 标记 时跳过该槽)
加杠(k)/ 暗杠(a) 副露字符串写进”出“数组(这两者在打牌时点宣告;暗杠也在”出”数组,不是”取”)

副露字符串的写法:标记字母紧贴在被鸣张之前(即被鸣张紧跟在标记字母之后;真实日志例: "c151416" = 吃,被鸣张 15 紧随 c);其余牌码在标记前或后皆可。 吃时被鸣张位于牌序列最前、其余两张升序续在后面;碰/大明杠/加杠/暗杠三或四张同种,牌码顺序无影响。 (查看器的 cb() 会把标记后紧跟的那张置零并按副露方向横置渲染。) 官方正则:^(\d\d)?(\d\d)?(\d\d)?([pmkac])(\d\d)(\d\d)?(\d\d)?(\d\d)?$。 参考实现 @kobalab/tenhou-url-log 的 mianzi() 即此口径。

⚠ 两处易错(都会让查看器打不开或整手错位): ① 暗杠归属”出”数组——查看器的”取”数组只认 [pmc]、”出”数组只认 [karf],把 a 放进”取” 会 null[0] 抛未捕获异常(真实日志里暗杠串确实在”出”数组,如 "161616a16"); ② 大明杠的 0 在鸣牌者自己的”出”数组(不是被鸣者的),否则该家两数组从此错位一张、 后半段全部牌面错位。

⚠ 天凤牌码与 mjlog 的 m= 位编码是两套完全不同的编码,不要混用。


5. 座位旋转(本手庄家落在索引 局序 % 4)

天凤的座位是半庄级的(索引 0 = 起家,即整局第一手的庄家),而一手一文件的归档不知道整局起家。 约定是源码可证的:官方查看器 1129.js 直接用 meta[0] & 3 当本手庄家座位 (W.Sa = b.na ? b.na & 3 : 0、遍历起点 p = h.na ? h.na & 3 : 0、局一览 "東南西北"[~~(l/4)&3] + ((l&3)+1) + "局"), 即 本手庄家必须落在索引 局序 % 4(logconv 用 player_id[l] = (qijia + jushu + l) % 4 做同一层换算, qijia = 0 时即此式)。

实现:整体旋转 rot = (本手庄家座位 - 局序 % 4 + 4) % 4,索引 i ↔ 生成方座位 (i + rot) % 4, name 同步旋转。(等价写法:先算 rot 再按”座位 s → 索引 (s - rot + 4) % 4“映射——两者只差一个符号, 实现时务必用一句断言钉住:”旋转后索引 局序 % 4 的座位 == 本手庄家”。)

目验时把它当复核而非探索:打开天凤查看器时要专门看庄家标记与四家门风是否与对局一致(§8)。


6. 结果数组(索引 16)

形态 结构
和了 ["和了", [Δ分×4], [赢家索引, 放铳者索引, 包责者, 点数描述, 役名(翻)…]];每个赢家各一对 [Δ分, 明细](两赢家 = 5 个元素,两个 Δ 数组并不相同,如 double_ron 真实日志 ["和了",[13000,0,0,-12000],[…],[0,0,2000,-2000],[…]])
荒牌流局 ["流局", [Δ分×4]](logconv 把”荒牌平局”改成”流局”)
流局满贯 ["流し満貫", [Δ分×4]](保留原名,与”荒牌平局”的处理不同)
途中流局(九种九牌/四风连打/四家立直/三家和/四槓散了) [名称](只有名称、无 Δ分);名称用天凤用词:九種九牌/四風連打/四家立直/三家和了/四槓散了
  • Δ分 用四家分差(原始点,与索引 1 同口径)。注意 majojson 的 wins[].deltas / ryukyoku.deltas 是稀疏的(只含有变动的座位)——生成天凤 Δ分 数组时需按座位补零凑齐四家。
  • 第 4 项起的”点数描述/役名”是给人看的文本,工具不用它算分(mjai-reviewer 只取 [赢家, 放铳者] 与前一项的 Δ分)。
  • 役名按天凤 JSON 原样输出(权威来源 = mjlog2json 的 tenhou-json/src/model.rs 的 YAKU_NAME 0-54 表 + 天凤自身产出 JSON 实测双向核对):立直/断幺九/両立直/国士無双13面、 役牌 白(空格分隔)、場風 東/自風 南(按实际风拼)。
  • 名称用天凤用词(logconv 的映射表即口径):三家和了(非”三家和”)、四槓散了(非”四開槓”)。

7. 已知限制(明确接受)

  • 一手一文件:log 只放本手;整局多手需要容器形态(majojson 的 format 2,见规范 §8);
  • 导入的牌谱与崩溃恢复局不产天凤段(理由不是”派生不了”——原样拷贝是可以的——而是 不复制未经验证的第三方内容,也避免同一手出现两份可能互相矛盾的表示;崩溃恢复局缺起手, 拼出来的 log 起手不是 13 张,语义上是伪造的一手)。因此”日麻文件一定能在天凤打开”这句话 只对本机完整打完的手成立;
  • 天凤段不含牌山(天凤 JSON 没有牌山字段、也不含乱数)→ 只读天凤段的外部工具无法复算牌墙。 majojson 不受此限:完整墙序在 majo.replay.wall、种子在 majo.meta.seed(有 seed 路径才有墙)。

8. 目验方法(唯一能真目验的路径)

目标 做法
官方查看器 只把天凤四键拼进 URL:https://tenhou.net/5/#json=<urlencode({"title":…,"name":…,"rule":…,"log":[…]})>,逐项核对:牌面与摸切横置、副露(含暗杠/大明杠)、点数(25000 口径)、和牌/流局的收支、庄家标记与四家门风(§5 的复核点)
官方编辑器 https://tenhou.net/6/#json=…(”LOAD FROM URL” 同样走 window.prompt,同样受长度限制)
AI 复盘 mjai-reviewer -e mortal -i <file>.json -a <seat>——-e 不能省;-a 是旋转后的座位号(按 §5 把生成方座位换算过去)

⚠ URL 长度是硬约束:含完整载荷的文件直接 urlencode 必然超限 (编辑器脚注:IE 2083 / Apache 8177 / HTML 1024–65536)。目验要走两步: 先从文件里摘出天凤四键(一行 jq 即可:jq '{title,name,rule,log}'),再拼 URL; 整局文件另需按手拆分(jq '.log | length' 确认手数、jq '{title,name,rule,log: [.log[0]]}' 取第 1 手)。


9. 来源与核对基线

本文每条结论都可溯源到下列实现(均为外部公开项目),改版时应重走核对:

来源 核对了什么
天凤官方前端 1129.js(tenhou.net/5 加载器) 17 元素索引读取(f[3c+4..6])、meta[0] & 3 座位约定、未知键忽略行为
天凤官方 /6 编辑器 17 == f.length 才导入该手、副露正则未捕获异常、URL 长度脚注
logconv(Kobalab,mjlog→天凤 JSON) 结果数组形态、player_id[l] 换算、名称映射(三家和了/四槓散了)
@kobalab/tenhou-url-log(npm,天凤 JSON↔URL 解析参考实现) 副露字符串官方正则与 mianzi() 口径
mjai-reviewer(Equim-chan;其 convlog 转换层) name/rule 硬必需、disp 东风战/三麻判定、log 解析失败面
Mortal(libriichi,mjai 引擎与事件定义) mjai 方言字段名(ura_markers 等)、MJAI_PAI_STRINGS 牌码
mjlog2json(Equim-chan;tenhou-json 的 YAKU_NAME 表) YAKU_NAME 0-54 役名表(天凤 JSON 原样役名的权威)
tenhou_archive.rb(gimite 的天凤→mjai 转换器) mjai ryukyoku.reason 七值口径

外加实测双向核对:真实导出物用 tenhou.net/5#json= 真打开逐项核对(§8),与官方产出的天凤 JSON 对照役名与结构。天凤格式无公开文档、随时可能变——发现行为不符时,先回到这些源码重核,再改本文。