Estazz commited on
Commit
bc46b62
·
verified ·
1 Parent(s): af75b6d

Upload folder using huggingface_hub

Browse files
.DS_Store ADDED
Binary file (6.15 kB). View file
 
.gradio/certificate.pem ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ -----BEGIN CERTIFICATE-----
2
+ MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw
3
+ TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh
4
+ cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4
5
+ WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu
6
+ ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY
7
+ MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc
8
+ h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+
9
+ 0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U
10
+ A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW
11
+ T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH
12
+ B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC
13
+ B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv
14
+ KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn
15
+ OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn
16
+ jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw
17
+ qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI
18
+ rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV
19
+ HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq
20
+ hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL
21
+ ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ
22
+ 3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK
23
+ NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5
24
+ ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur
25
+ TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC
26
+ jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc
27
+ oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq
28
+ 4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA
29
+ mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d
30
+ emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc=
31
+ -----END CERTIFICATE-----
README.md CHANGED
@@ -1,12 +1,40 @@
1
  ---
2
- title: GDL2IR V2
3
- emoji: 🦀
4
- colorFrom: indigo
5
- colorTo: green
6
  sdk: gradio
7
  sdk_version: 5.49.1
8
- app_file: app.py
9
- pinned: false
10
  ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
 
12
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
1
  ---
2
+ title: GDL2IR_V2
3
+ app_file: app.py
 
 
4
  sdk: gradio
5
  sdk_version: 5.49.1
 
 
6
  ---
7
+ # GDL2IR v2 — Gradio Web UI
8
+
9
+ ## 安装依赖
10
+
11
+ ```bash
12
+ python3 -m venv .venv
13
+ source .venv/bin/activate
14
+ pip install -r requirements.txt
15
+ ```
16
+
17
+ ## 运行 Web UI
18
+
19
+ ```bash
20
+ python app.py
21
+ ```
22
+
23
+ 启动后访问 `http://127.0.0.1:7860`。
24
+
25
+ ## 使用说明
26
+ - 上传 GDL 文本(.txt/.gdl)。
27
+ - 可选:上传 `poker_gdl_ir.schema.v0.95.zh.json`(若不上传,将自动在当前目录下寻找)。
28
+ - 点击“运行转换与校验”,下方可以预览与下载:
29
+ - IR JSON
30
+ - Issues JSON(Schema 校验结果)
31
+ - 自检报告 Markdown
32
+
33
+ ## 命令行用法(可选)
34
+ 仍可使用原命令行:
35
+
36
+ ```bash
37
+ python cli_v2.py uno_gdl.txt
38
+ ```
39
 
40
+ 会在同目录输出 `.ir.v0.95.json`、`.issues.json`、`.selfcheck.md`。
__pycache__/gap_detector_v2.cpython-310.pyc ADDED
Binary file (928 Bytes). View file
 
__pycache__/gap_detector_v2.cpython-37.pyc ADDED
Binary file (902 Bytes). View file
 
__pycache__/gdl_parser_v2.cpython-310.pyc ADDED
Binary file (6.28 kB). View file
 
__pycache__/gdl_parser_v2.cpython-37.pyc ADDED
Binary file (6.26 kB). View file
 
__pycache__/mapper_v2.cpython-310.pyc ADDED
Binary file (18.1 kB). View file
 
__pycache__/mapper_v2.cpython-37.pyc ADDED
Binary file (18.1 kB). View file
 
__pycache__/normalizer_v2.cpython-310.pyc ADDED
Binary file (10.7 kB). View file
 
__pycache__/normalizer_v2.cpython-37.pyc ADDED
Binary file (10.8 kB). View file
 
__pycache__/validator_v2.cpython-310.pyc ADDED
Binary file (1.59 kB). View file
 
__pycache__/validator_v2.cpython-37.pyc ADDED
Binary file (1.54 kB). View file
 
app.py ADDED
@@ -0,0 +1,188 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ Gradio Web UI for GDL -> IR(v0.95) pipeline
4
+
5
+ 功能:
6
+ - 上传 GDL 文本文件(.txt/.gdl)与可选 Schema(默认自动发现 poker_gdl_ir.schema.v0.95.zh.json)
7
+ - 运行 parse -> normalize -> map_to_v095 -> validate -> report 全流程
8
+ - 在线预览 IR(JSON)、Issues(JSON)、Report(Markdown)
9
+ - 支持将结果作为文件下载
10
+ """
11
+ from __future__ import annotations
12
+
13
+ import io
14
+ import json
15
+ import pathlib
16
+ from typing import Tuple, Any
17
+ import tempfile
18
+
19
+ import gradio as gr
20
+
21
+ from gdl_parser_v2 import parse_gdl
22
+ from normalizer_v2 import normalize_ir
23
+ from mapper_v2 import map_to_v095
24
+ from validator_v2 import validate_with_schema
25
+ from gap_detector_v2 import make_report
26
+
27
+
28
+ def _auto_discover_schema(cli_arg: str | None) -> pathlib.Path:
29
+ candidates = []
30
+ if cli_arg:
31
+ candidates.append(cli_arg)
32
+ here = pathlib.Path(__file__).resolve().parent
33
+ candidates.append(str(here / "poker_gdl_ir.schema.v0.95.zh.json"))
34
+ candidates.append("poker_gdl_ir.schema.v0.95.zh.json")
35
+ candidates.append("/mnt/data/poker_gdl_ir.schema.v0.95.zh.json")
36
+ for c in candidates:
37
+ try:
38
+ p = pathlib.Path(c)
39
+ if p.exists():
40
+ return p
41
+ except Exception:
42
+ pass
43
+ raise FileNotFoundError("未找到 Schema。请上传或将 poker_gdl_ir.schema.v0.95.zh.json 放在程序目录/当前目录。")
44
+
45
+
46
+ def _read_as_bytes(maybe_file: Any) -> bytes:
47
+ # Gradio File with type="binary" returns bytes directly.
48
+ if maybe_file is None:
49
+ raise gr.Error("未提供文件")
50
+ if isinstance(maybe_file, (bytes, bytearray)):
51
+ return bytes(maybe_file)
52
+ if isinstance(maybe_file, str): # filepath
53
+ return pathlib.Path(maybe_file).read_bytes()
54
+ if hasattr(maybe_file, "read") and callable(getattr(maybe_file, "read")):
55
+ return maybe_file.read()
56
+ if isinstance(maybe_file, dict):
57
+ data = maybe_file.get("data")
58
+ if isinstance(data, (bytes, bytearray)):
59
+ return bytes(data)
60
+ raise gr.Error("无法读取上传的文件内容")
61
+
62
+
63
+ def run_pipeline(gdl_file: Any, schema_file: Any | None) -> Tuple[str, str, str, bytes, bytes, bytes]:
64
+ if gdl_file is None:
65
+ raise gr.Error("请先上传 GDL 文件")
66
+
67
+ # 读取 GDL 文本
68
+ gdl_bytes = _read_as_bytes(gdl_file)
69
+ try:
70
+ gdl_txt = gdl_bytes.decode("utf-8")
71
+ except Exception:
72
+ # 尝试 gbk / latin-1 以容错
73
+ for enc in ("utf-8-sig", "gbk", "latin-1"):
74
+ try:
75
+ gdl_txt = gdl_bytes.decode(enc)
76
+ break
77
+ except Exception:
78
+ continue
79
+ else:
80
+ raise gr.Error("无法解码 GDL 文本,请确认编码为 UTF-8")
81
+
82
+ # 解析 -> 规整 -> 映射
83
+ gdl_ast = parse_gdl(gdl_txt)
84
+ nz = normalize_ir(gdl_ast, gdl_txt)
85
+ ir = map_to_v095(nz, gdl_txt)
86
+
87
+ # Schema 路径:优先使用上传,其次自动发现
88
+ if schema_file is not None:
89
+ try:
90
+ schema_bytes = _read_as_bytes(schema_file)
91
+ tmp = tempfile.NamedTemporaryFile(suffix=".json", delete=False)
92
+ tmp.write(schema_bytes)
93
+ tmp.flush()
94
+ tmp.close()
95
+ schema_path = pathlib.Path(tmp.name)
96
+ except Exception:
97
+ raise gr.Error("读取 Schema 失败,请确认文件格式正确")
98
+ else:
99
+ try:
100
+ schema_path = _auto_discover_schema(None)
101
+ except FileNotFoundError as e:
102
+ raise gr.Error(str(e))
103
+
104
+ # 校验(兼容 2/3 元组)
105
+ ret = validate_with_schema(ir, str(schema_path))
106
+ if isinstance(ret, tuple) and len(ret) == 3:
107
+ ok, issues, schema_msg = ret
108
+ elif isinstance(ret, tuple) and len(ret) == 2:
109
+ ok, issues = ret
110
+ schema_msg = f"Schema: {'OK' if ok else 'FAIL'}; issues={len(issues)}"
111
+ else:
112
+ ok = bool(ret)
113
+ issues = []
114
+ schema_msg = f"Schema: {'OK' if ok else 'FAIL'}"
115
+
116
+ report_md = make_report(ir)
117
+
118
+ ir_json_str = json.dumps(ir, ensure_ascii=False, indent=2)
119
+ issues_json_str = json.dumps(issues, ensure_ascii=False, indent=2)
120
+
121
+ # 准备下载的文件内容
122
+ ir_bytes = ir_json_str.encode("utf-8")
123
+ issues_bytes = issues_json_str.encode("utf-8")
124
+ report_bytes = report_md.encode("utf-8")
125
+
126
+ return (
127
+ ir_json_str,
128
+ issues_json_str,
129
+ report_md,
130
+ ir_bytes,
131
+ issues_bytes,
132
+ report_bytes,
133
+ )
134
+
135
+
136
+ def _save_temp(name_hint: str, data: bytes) -> str:
137
+ suffix = pathlib.Path(name_hint).suffix or ""
138
+ tmp = tempfile.NamedTemporaryFile(delete=False, suffix=suffix)
139
+ tmp.write(data)
140
+ tmp.flush()
141
+ tmp.close()
142
+ return tmp.name
143
+
144
+
145
+ with gr.Blocks(title="GDL → IR(v0.95) Web UI") as demo:
146
+ gr.Markdown("""
147
+ **GDL → IR(v0.95) 一键转换与校验**
148
+ - 上传 GDL 文本文件(.txt/.gdl)
149
+ - 可选上传 Schema(若不上传,将自动发现同目录中的 `poker_gdl_ir.schema.v0.95.zh.json`)
150
+ - 生成:IR(JSON)、Issues(JSON)、自检报告(Markdown)
151
+ """
152
+ )
153
+
154
+ with gr.Row():
155
+ gdl_in = gr.File(label="GDL 文件 (.txt/.gdl)", file_types=[".txt", ".gdl"], type="binary")
156
+ schema_in = gr.File(label="Schema 文件 (可选)", file_types=[".json"], type="binary")
157
+
158
+ run_btn = gr.Button("运行转换与校验")
159
+
160
+ with gr.Tab("IR 预览"):
161
+ ir_json = gr.Code(label="IR (v0.95)", language="json")
162
+ ir_dl = gr.File(label="下载 IR JSON", interactive=False)
163
+
164
+ with gr.Tab("Issues 预览"):
165
+ issues_json = gr.Code(label="Schema 校验问题", language="json")
166
+ issues_dl = gr.File(label="下载 Issues JSON", interactive=False)
167
+
168
+ with gr.Tab("自检报告"):
169
+ report_md = gr.Markdown()
170
+ report_dl = gr.File(label="下载 自检报告.md", interactive=False)
171
+
172
+ def _on_click(gdl_file, schema_file):
173
+ ir_str, issues_str, report_str, ir_b, issues_b, report_b = run_pipeline(gdl_file, schema_file)
174
+ ir_path = _save_temp("ir.v0.95.json", ir_b)
175
+ issues_path = _save_temp("issues.json", issues_b)
176
+ report_path = _save_temp("selfcheck.md", report_b)
177
+ return ir_str, ir_path, issues_str, issues_path, report_str, report_path
178
+
179
+ run_btn.click(
180
+ _on_click,
181
+ inputs=[gdl_in, schema_in],
182
+ outputs=[ir_json, ir_dl, issues_json, issues_dl, report_md, report_dl]
183
+ )
184
+
185
+ if __name__ == "__main__":
186
+ demo.launch(share=True)
187
+
188
+
cli_v2.py ADDED
@@ -0,0 +1,109 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ cli_v2.py — 一键执行 GDL→IR(v0.95)+ 校验 + 报告
5
+ 改动要点:
6
+ 1) policy 默认就是 merge(无需在命令行传参;也可用环境变量 GDL2IR_POLICY 覆盖)
7
+ 2) 内置 schema 校验 & 自动发现 schema
8
+ - 候选路径依次尝试:
9
+ a) 命令行 --schema
10
+ b) 脚本同目录的 poker_gdl_ir.schema.v0.95.zh.json
11
+ c) 当前工作目录的 poker_gdl_ir.schema.v0.95.zh.json
12
+ d) /mnt/data/poker_gdl_ir.schema.v0.95.zh.json
13
+ 3) 兼容 validator_v2.validate_with_schema() 返回 2 元组或 3 元组
14
+ """
15
+ import argparse, json, pathlib, sys, os
16
+
17
+ from gdl_parser_v2 import parse_gdl
18
+ from normalizer_v2 import normalize_ir
19
+ from mapper_v2 import map_to_v095
20
+ from validator_v2 import validate_with_schema
21
+ from gap_detector_v2 import make_report
22
+
23
+
24
+ def _auto_discover_schema(cli_arg: str = None) -> pathlib.Path:
25
+ # 优先级:命令行 > 脚本同目录 > 当前目录 > /mnt/data
26
+ candidates = []
27
+ if cli_arg:
28
+ candidates.append(cli_arg)
29
+
30
+ here = pathlib.Path(__file__).resolve().parent
31
+ candidates.append(str(here / "poker_gdl_ir.schema.v0.95.zh.json"))
32
+ candidates.append("poker_gdl_ir.schema.v0.95.zh.json")
33
+ candidates.append("/mnt/data/poker_gdl_ir.schema.v0.95.zh.json")
34
+
35
+ for c in candidates:
36
+ try:
37
+ p = pathlib.Path(c)
38
+ if p.exists():
39
+ return p
40
+ except Exception:
41
+ pass
42
+
43
+ raise FileNotFoundError(
44
+ "未找到 Schema。请使用 --schema 指定,或将 poker_gdl_ir.schema.v0.95.zh.json 放在脚本同目录/当前目录。"
45
+ )
46
+
47
+
48
+ def main():
49
+ ap = argparse.ArgumentParser()
50
+ ap.add_argument("gdl_file", help="源 GDL 文件(.txt/.gdl)")
51
+ ap.add_argument("--schema", help="Schema 路径(可不填,将自动发现)")
52
+ # policy 默认 merge;允许用环境变量 GDL2IR_POLICY 覆盖(可选)
53
+ # ap.add_argument("--policy", choices=["minimal", "merge", "conservative"],
54
+ # default=os.getenv("GDL2IR_POLICY", "merge"),
55
+ # help="组合策略(默认 merge):"
56
+ # "minimal=仅保留用到的;merge=保留通用并禁用无关;conservative=全保留")
57
+ ap.add_argument("--out-ir", dest="out_ir", help="IR 输出 JSON 路径")
58
+ ap.add_argument("--out-issues", dest="out_issues", help="校验问题 JSON 路径")
59
+ ap.add_argument("--out-report", dest="out_report", help="自检报告 Markdown 路径")
60
+ args = ap.parse_args()
61
+
62
+ src_path = pathlib.Path(args.gdl_file)
63
+ if not src_path.exists():
64
+ print(f"[ERROR] 源文件不存在:{src_path}", file=sys.stderr)
65
+ sys.exit(2)
66
+
67
+ try:
68
+ schema_path = _auto_discover_schema(args.schema)
69
+ except FileNotFoundError as e:
70
+ print(f"[ERROR] {e}", file=sys.stderr)
71
+ sys.exit(2)
72
+
73
+ # 读取 & 解析 & 规整
74
+ gdl_txt = src_path.read_text(encoding="utf-8")
75
+ gdl_ast = parse_gdl(gdl_txt)
76
+ nz = normalize_ir(gdl_ast, gdl_txt)
77
+
78
+ # 映射为 v0.95 IR(policy 默认 merge)
79
+ ir = map_to_v095(nz, gdl_txt)
80
+
81
+ # ✅ Schema 校验(兼容 2/3 元组返回)
82
+ ret = validate_with_schema(ir, str(schema_path))
83
+ if isinstance(ret, tuple) and len(ret) == 3:
84
+ ok, issues, schema_msg = ret
85
+ elif isinstance(ret, tuple) and len(ret) == 2:
86
+ ok, issues = ret
87
+ schema_msg = f"Schema: {'OK' if ok else 'FAIL'}; issues={len(issues)}"
88
+ else:
89
+ # 极端兜底:未知返回类型
90
+ ok = bool(ret)
91
+ issues = []
92
+ schema_msg = f"Schema: {'OK' if ok else 'FAIL'}"
93
+
94
+ # 输出
95
+ out_ir = pathlib.Path(args.out_ir or str(src_path.with_suffix(".ir.v0.95.json")))
96
+ out_issues = pathlib.Path(args.out_issues or str(src_path.with_suffix(".issues.json")))
97
+ out_report = pathlib.Path(args.out_report or str(src_path.with_suffix(".selfcheck.md")))
98
+
99
+ out_ir.write_text(json.dumps(ir, ensure_ascii=False, indent=2), encoding="utf-8")
100
+ out_issues.write_text(json.dumps(issues, ensure_ascii=False, indent=2), encoding="utf-8")
101
+ out_report.write_text(make_report(ir), encoding="utf-8")
102
+
103
+ print(f"[OK] IR 写入:{out_ir}")
104
+ print(f"[OK] Schema 校验问题写入:{out_issues}")
105
+ print(f"[OK] 自检报告写入:{out_report}")
106
+ print(f"[SUMMARY] {schema_msg}")
107
+
108
+ if __name__ == "__main__":
109
+ main()
gap_detector_v2.py ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ gap_detector_v2.py — 轻量自检报告:覆盖率/常见疏漏提示
4
+ """
5
+ from __future__ import annotations
6
+ from typing import Dict, List
7
+
8
+ def make_report(ir: Dict) -> str:
9
+ notes: List[str] = []
10
+ if not ir.get("special_mechanics"):
11
+ notes.append("- 未声明 special_mechanics(如果玩法确有特殊牌/机制可忽略)")
12
+ if "hand:*" in (ir.get("zones") or []):
13
+ notes.append("- zones 含 hand:*;建议在 mapper 展开为 hand:<id>")
14
+ if not ir.get("actions",{}).get("play"):
15
+ notes.append("- actions.play 为空,实际引擎可能无法出牌")
16
+ return "# 自检报告\n\n" + ("\n".join(notes) if notes else "无明显问题。")
gdl_parser_v2.py ADDED
@@ -0,0 +1,164 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ gdl_parser_v2.py — 宽容 S-Expr 解析器(支持 ()/[]/{}, 字符串, 数字, 标识符, 注释)
4
+ 把 S-Expr 转 Python,再做一层“键值折叠”:(k v1 (a b) c) -> {"k": {..., "_": [v1, c]}}
5
+ 兼容 Markdown 反引号围栏;支持 ; // # 行注释、#| |# 块注释。
6
+ """
7
+ from __future__ import annotations
8
+ from dataclasses import dataclass
9
+ from typing import Any, List, Dict
10
+
11
+ class ParseError(Exception): ...
12
+
13
+ @dataclass
14
+ class Tok:
15
+ kind: str # 'LP','RP','ATOM','STR','NUM'
16
+ val: Any
17
+ closer: str = ""
18
+
19
+ _PAIR = {'(' : ')', '[':']', '{':'}'}
20
+
21
+ def _is_ws(ch: str) -> bool: return ch.isspace()
22
+ def _is_ident_char(ch: str) -> bool:
23
+ return ch.isalnum() or ch in "_-+*/<>=!?&.|:$@~^%"
24
+
25
+ def _lex(text: str) -> List[Tok]:
26
+ s = text; i = 0; n = len(s); toks: List[Tok] = []
27
+ def adv(k=1):
28
+ nonlocal i; i += k
29
+ def peek(): return s[i] if i<n else ''
30
+ def startsw(p): return s.startswith(p, i)
31
+ while i<n:
32
+ ch = peek()
33
+ if _is_ws(ch): adv(); continue
34
+ # 行注释
35
+ if ch==';' or startsw('//') or ch=='#':
36
+ while i<n and s[i] != '\n': adv()
37
+ continue
38
+ # 块注释 #| ... |#
39
+ if startsw('#|'):
40
+ adv(2)
41
+ while i<n and not startsw('|#'): adv()
42
+ if startsw('|#'): adv(2)
43
+ else: raise ParseError("Unterminated block comment")
44
+ continue
45
+ # markdown 反引号行
46
+ if ch=='`':
47
+ while i<n and s[i] != '\n': adv()
48
+ continue
49
+ if ch in _PAIR:
50
+ toks.append(Tok('LP', ch, _PAIR[ch])); adv(); continue
51
+ if ch in (')',']','}'):
52
+ toks.append(Tok('RP', ch)); adv(); continue
53
+ if ch == '"':
54
+ adv(); buf=[]
55
+ while i<n:
56
+ c=peek()
57
+ if c=='"': adv(); break
58
+ if c=='\\':
59
+ adv(); esc=peek(); mp={'n':'\n','t':'\t','r':'\r','"':'"','\\':'\\'}
60
+ buf.append(mp.get(esc, esc)); adv()
61
+ else:
62
+ buf.append(c); adv()
63
+ toks.append(Tok('STR',''.join(buf))); continue
64
+ if ch.isdigit() or (ch in '+-' and i+1<n and s[i+1].isdigit()):
65
+ j=i+1
66
+ while j<n and (s[j].isdigit() or s[j] in '.eE+-'): j+=1
67
+ raw=s[i:j]
68
+ try: val=int(raw)
69
+ except:
70
+ try: val=float(raw)
71
+ except: val=raw
72
+ toks.append(Tok('NUM',val)); i=j; continue
73
+ if _is_ident_char(ch):
74
+ j=i
75
+ while j<n and _is_ident_char(s[j]): j+=1
76
+ toks.append(Tok('ATOM', s[i:j])); i=j; continue
77
+ raise ParseError(f"Unexpected char: {ch}")
78
+ return toks
79
+
80
+ def _parse_list(toks: List[Tok], i: int, closer: str):
81
+ out=[]; n=len(toks)
82
+ while i<n:
83
+ tk=toks[i]
84
+ if tk.kind=='RP':
85
+ if tk.val!=closer: raise ParseError("Mismatched closer")
86
+ return out, i+1
87
+ if tk.kind=='LP':
88
+ sub,i=_parse_list(toks,i+1,tk.closer); out.append(sub); continue
89
+ if tk.kind in ('ATOM','STR','NUM'):
90
+ out.append(tk.val); i+=1; continue
91
+ raise ParseError("Unexpected token")
92
+ raise ParseError("Unbalanced s-expr: missing closer")
93
+
94
+ def parse_sexpr(text: str):
95
+ # strip markdown codefences lines
96
+ lines = [ln for ln in text.splitlines() if ln.strip() not in ('```','````','``')]
97
+ text = "\n".join(lines)
98
+ toks=_lex(text); out=[]; i=0; n=len(toks)
99
+ while i<n:
100
+ tk=toks[i]
101
+ if tk.kind=='LP':
102
+ sub,i=_parse_list(toks,i+1,tk.closer); out.append(sub); continue
103
+ if tk.kind in ('ATOM','STR','NUM'): out.append(tk.val); i+=1; continue
104
+ if tk.kind=='RP': raise ParseError("Unmatched closer")
105
+ return out
106
+
107
+ def _is_key_list(node): return isinstance(node,list) and node and isinstance(node[0],str)
108
+
109
+ def _kv_collapse_from_positional(pos_list):
110
+ """将 ['from:', 'hand', 'to:', 'field'] 折叠为 {'from':'hand','to':'field'}"""
111
+ out={}; i=0; n=len(pos_list)
112
+ while i<n-1:
113
+ k = pos_list[i]; v = pos_list[i+1]
114
+ if isinstance(k,str) and k.endswith(':'):
115
+ out[k[:-1]] = v
116
+ i += 2
117
+ else:
118
+ i += 1
119
+ return out if out else None
120
+
121
+ def _to_obj(node):
122
+ if not isinstance(node,list): return node
123
+ if not node: return []
124
+ if _is_key_list(node):
125
+ key=node[0]; payload=node[1:]
126
+ obj={}; pos=[]
127
+ for it in payload:
128
+ if isinstance(it,list) and _is_key_list(it):
129
+ k=it[0]; v=_to_obj(it[1:]) if len(it)>1 else None
130
+ obj[k]=v
131
+ else:
132
+ pos.append(_to_obj(it))
133
+ kv = _kv_collapse_from_positional(pos)
134
+ if kv is not None:
135
+ for k,v in kv.items(): obj[k]=v
136
+ else:
137
+ if pos: obj["_"]=pos
138
+ return {key: obj}
139
+ return [_to_obj(it) for it in node]
140
+
141
+ def _merge_kv_list_to_dict(items) -> Dict[str, Any]:
142
+ out={}
143
+ for it in items:
144
+ if isinstance(it,dict) and len(it)==1:
145
+ k,v=next(iter(it.items()))
146
+ if k in out:
147
+ if not isinstance(out[k],list): out[k]=[out[k]]
148
+ out[k].append(v)
149
+ else:
150
+ out[k]=v
151
+ return out
152
+
153
+ def parse_gdl(text: str) -> dict:
154
+ sexpr = parse_sexpr(text)
155
+ # 如果是单一顶层表 (game ...)
156
+ if len(sexpr)==1 and isinstance(sexpr[0], list):
157
+ node = _to_obj(sexpr[0])
158
+ if isinstance(node, dict):
159
+ return node
160
+ items = [_to_obj(e) for e in sexpr[0]]
161
+ return _merge_kv_list_to_dict(items)
162
+ # 多个顶层
163
+ items = [_to_obj(e) for e in sexpr]
164
+ return _merge_kv_list_to_dict(items)
mapper_v2.py ADDED
@@ -0,0 +1,865 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ mapper_v2.py — 将 normalizer_v2 输出映射为 v0.95 IR(schema 友好;兼容 UNO/七夕)
4
+ """
5
+ from __future__ import annotations
6
+ from typing import Any, Dict, List
7
+ from copy import deepcopy
8
+ import re
9
+
10
+ REQUIRED = ["meta","players","cards","zones","visibility","combinations","comparison","actions","phases","turns","ending","scoring"]
11
+ ZONE_RE = re.compile(r"^(hand:[A-Za-z0-9_:-]+|field|main_deck|discard_pile|reserve_zone|public_pool|item_deck:[A-Za-z0-9_:-]+|history_deck)$")
12
+
13
+ def _as_dict(x): return x if isinstance(x,dict) else {}
14
+ def _as_list(x): return x if isinstance(x,list) else ([] if x is None else [x])
15
+
16
+ def _scalar(x):
17
+ if isinstance(x,str): return x
18
+ if isinstance(x,(int,float)): return str(x)
19
+ if isinstance(x,dict):
20
+ if "_" in x and isinstance(x["_"], list) and x["_"]: return _scalar(x["_"][0])
21
+ if "name" in x and isinstance(x["name"], str): return x["name"]
22
+ if len(x)==1:
23
+ k,v = next(iter(x.items()))
24
+ if v in (None,{}) and isinstance(k,str): return k
25
+ if isinstance(x,list) and x: return _scalar(x[0])
26
+ return ""
27
+
28
+ def _uniquify(seq: List[str]) -> List[str]:
29
+ seen={}; out=[]
30
+ for s in map(str, seq):
31
+ if s not in seen:
32
+ seen[s]=0; out.append(s)
33
+ else:
34
+ seen[s]+=1; out.append(f"{s}_{seen[s]}")
35
+ return out
36
+
37
+ def _phase_to_schema(s: str) -> str:
38
+ # Mapping GDL phase names to schema phase names
39
+ mp={
40
+ "deal":"setup", # GDL 'deal' often maps to setup for initialization tasks, but can also be a separate 'deal' phase
41
+ "setup":"setup",
42
+ "bid":"bidding",
43
+ "bidding":"bidding",
44
+ "double":"doubling",
45
+ "doubling":"doubling",
46
+ "initiative":"setup", # Map GDL's initiative_phase to schema's setup
47
+ "grouping":"grouping",
48
+ "play":"playing",
49
+ "playing":"playing",
50
+ "playing_phase":"play", # Map GDL's playing_phase to schema's play
51
+ "settle":"settlement",
52
+ "settlement":"settlement",
53
+ "settlement_phase":"settlement"
54
+ }
55
+ return mp.get(s, s)
56
+
57
+ def _derive_players(nz: Dict[str,Any]) -> Dict[str,Any]:
58
+ roles=_as_list(_as_dict(nz.get("game")).get("roles"))
59
+ order=_as_dict(nz.get("turns")).get("order") or []
60
+ ids = _uniquify([_scalar(x) for x in order]) if order else []
61
+ if not ids and roles:
62
+ tmp=[]
63
+ for r in roles:
64
+ rd=_as_dict(r)
65
+ name=_scalar(rd.get("name") or rd.get("role") or "Player")
66
+ cnt=int(rd.get("count",1) or 1)
67
+ tmp += [name]*max(1,cnt)
68
+ ids=_uniquify(tmp)
69
+ if not ids:
70
+ ids=["P0","P1","P2"]
71
+
72
+ # --- Modification: Create player instances based on roles and counts, and store in roles.players ---
73
+ # Schema v0.95 expects count and roles list with name, count, and optionally players.
74
+ out_roles = []
75
+ current_id_idx = 0
76
+ for r in roles:
77
+ rd = _as_dict(r)
78
+ role_name = _scalar(rd.get("name") or "Player")
79
+ role_count = int(rd.get("count", 1) or 1)
80
+
81
+ # Generate unique player IDs for this role
82
+ role_players = []
83
+ for i in range(role_count):
84
+ if current_id_idx < len(ids):
85
+ player_id = ids[current_id_idx]
86
+ role_players.append(player_id)
87
+ current_id_idx += 1
88
+ else:
89
+ # Fallback if ids list is shorter than expected
90
+ player_id = f"{role_name}_{i}" if i > 0 else role_name
91
+ role_players.append(player_id)
92
+
93
+ # Add the role definition (name, count, players) to the output roles list
94
+ out_roles.append({"name": role_name, "count": role_count, "players": role_players})
95
+ # The actual player IDs are used for turns.order and zone generation
96
+
97
+ return {"count": len(ids), "roles": out_roles, "_player_ids": ids} # Removed _role_definitions as not needed for schema
98
+
99
+ def _expand_zones(zs: List[Dict[str, Any]], pids: List[str]) -> List[str]:
100
+ out=[]
101
+ zone_types_map = {
102
+ "hand": "hand:{pid}",
103
+ "field": "field",
104
+ "discard_pile": "discard_pile",
105
+ "main_deck": "main_deck",
106
+ "special_deck": "item_deck:{name}" # Map special_deck to item_deck:<name>
107
+ }
108
+ for z_def in zs or []:
109
+ z_type = z_def.get("type")
110
+ if z_type in zone_types_map:
111
+ template = zone_types_map[z_type]
112
+ if z_type == "hand":
113
+ # Expand hand for each player ID
114
+ for pid in pids:
115
+ out.append(template.format(pid=pid))
116
+ elif z_type == "special_deck":
117
+ # Use the name from the definition
118
+ name = z_def.get("name", "Unknown")
119
+ out.append(template.format(name=name))
120
+ else:
121
+ # Use the template directly for other types
122
+ out.append(template)
123
+ # Add common base zones if not already present
124
+ base = ["field","discard_pile","main_deck"]
125
+ for b in base:
126
+ if b not in out: out.append(b)
127
+ # Remove duplicates while preserving order
128
+ seen=set(); uniq=[]
129
+ for z in out:
130
+ if z not in seen:
131
+ seen.add(z); uniq.append(z)
132
+ return uniq
133
+
134
+
135
+ def _expand_vis_hand_star(vis: Dict[str,Any], pids: List[str]) -> Dict[str,Any]:
136
+ vis=_as_dict(vis); vis.setdefault("defaults", {}); by=_as_dict(vis.get("by_zone"))
137
+ if "hand:*" in by:
138
+ cfg=by.pop("hand:*")
139
+ for pid in pids: by[f"hand:{pid}"]=cfg
140
+ vis["by_zone"]=by; return vis
141
+
142
+ def _ensure_cards(nz: Dict[str,Any]) -> Dict[str,Any]:
143
+ cards=_as_dict(nz.get("cards"))
144
+ ranks=_as_list(cards.get("ranks")); suits=_as_list(cards.get("suits"))
145
+ if not ranks or not suits:
146
+ # --- Modification: Derive from card_relations if available ---
147
+ card_rels = _as_dict(nz.get("card_relations"))
148
+ if card_rels:
149
+ # Derive from card_values
150
+ card_vals = _as_list(card_rels.get("card_values"))
151
+ # Map common symbols to integers
152
+ symbol_map = {
153
+ "J": 11, "Q": 12, "K": 13, "A": 14, "2": 15
154
+ }
155
+ ranks = []
156
+ for val in card_vals:
157
+ if isinstance(val, int):
158
+ ranks.append(val)
159
+ elif isinstance(val, str) and val in symbol_map:
160
+ ranks.append(symbol_map[val])
161
+ else:
162
+ ranks.append(_scalar(val)) # Fallback
163
+
164
+ # Derive from suit_relations
165
+ suit_rels = _as_dict(card_rels.get("suit_relations"))
166
+ suit_order = _as_list(suit_rels.get("order"))
167
+ suits = [s for s in suit_order if isinstance(s, str)] or ["Spade","Heart","Club","Diamond"]
168
+
169
+ # Determine jokers and suitless_ranks
170
+ jokers = {}
171
+ suitless_ranks = []
172
+ # Standard54 implies 2 jokers
173
+ if "2" in card_vals or 15 in ranks: # Assuming '2' is always high and joker-like
174
+ # Standard54 has small joker 16, big joker 17
175
+ # In our mapping, if 2 is last, it might be 15. Let's check.
176
+ # Actually, Standard54 usually has fixed values for jokers regardless of card_values order.
177
+ # Let's assume standard mapping for Standard54 deck type.
178
+ deck_info = _as_dict(_as_dict(nz.get("game")).get("deck"))
179
+ if deck_info.get("type") == "Standard54":
180
+ jokers = {"small": 16, "big": 17}
181
+ suitless_ranks = [16, 17]
182
+ # Add jokers to ranks if not already present
183
+ if 16 not in ranks: ranks.append(16)
184
+ if 17 not in ranks: ranks.append(17)
185
+
186
+ # --- Modification: Ensure suitless_ranks, copies_per_deck, num_decks are included ---
187
+ cards = {
188
+ "ranks": sorted(ranks),
189
+ "suits": suits,
190
+ "jokers": jokers,
191
+ "suitless_ranks": suitless_ranks,
192
+ "copies_per_deck": 1, # Assuming 1 copy for Standard54
193
+ "num_decks": 1 # Assuming 1 deck
194
+ }
195
+ else:
196
+ # Fallback to standard 54
197
+ ranks=list(range(3,18)) # 3..A(14), 小王16, 大王17
198
+ suits=["Spade","Heart","Club","Diamond"]
199
+ cards={"ranks":ranks, "suits":suits, "jokers":{"small":16,"big":17}}
200
+ else:
201
+ # If ranks and suits were already provided, ensure the missing fields are added if possible
202
+ # This path might be taken if the initial cards dict had some info but not all.
203
+ # We can infer suitless_ranks from jokers, and assume defaults for copies/num_decks if not present.
204
+ if "jokers" in cards and "suitless_ranks" not in cards:
205
+ jokers = cards.get("jokers", {})
206
+ small_j = jokers.get("small")
207
+ big_j = jokers.get("big")
208
+ suitless_ranks = [r for r in [small_j, big_j] if r is not None]
209
+ cards["suitless_ranks"] = suitless_ranks
210
+ if "copies_per_deck" not in cards:
211
+ cards["copies_per_deck"] = 1
212
+ if "num_decks" not in cards:
213
+ cards["num_decks"] = 1
214
+
215
+ return cards
216
+
217
+ def _ensure_combinations(nz: Dict[str,Any]) -> Dict[str,Any]:
218
+ cmb=_as_dict(nz.get("combinations"))
219
+ out={"single":{}, "pair":{}, "triple":{}, "straight":{}, "pairs_chain":{}, "airplane":{},
220
+ "triple_with_single":{}, "triple_with_pair":{}, "four_with_twoSingles":{}, "four_with_twoPairs":{}, "bomb":{}, "rocket":{}, "custom":[]}
221
+
222
+ # --- Modification: Handle combinations from GDL ---
223
+ # Get raw combinations from normalizer
224
+ raw_combinations = _as_dict(nz.get("combinations", {}))
225
+ # Handle straight and bomb with parameters
226
+ if "straight" in nz.get("game", {}).get("combinations", {}):
227
+ # Look for the argument, e.g., (straight 5)
228
+ gdl_combinations = _as_dict(nz.get("game", {}).get("combinations", {}))
229
+ if isinstance(gdl_combinations.get("straight"), (int, list)):
230
+ val = gdl_combinations.get("straight")
231
+ if isinstance(val, list) and val:
232
+ val = val[0] # Get first argument if list
233
+ out["straight"] = {"min_len": int(val)}
234
+ else:
235
+ out["straight"] = {} # Fallback if no arg found in GDL
236
+
237
+ if "bomb" in nz.get("game", {}).get("combinations", {}):
238
+ gdl_combinations = _as_dict(nz.get("game", {}).get("combinations", {}))
239
+ if isinstance(gdl_combinations.get("bomb"), (int, list)):
240
+ val = gdl_combinations.get("bomb")
241
+ if isinstance(val, list) and val:
242
+ val = val[0]
243
+ out["bomb"] = {"len": int(val)}
244
+ else:
245
+ out["bomb"] = {} # Fallback if no arg found in GDL
246
+
247
+ # Handle custom combinations from normalizer
248
+ if "custom" in raw_combinations and isinstance(raw_combinations["custom"], list):
249
+ out["custom"] = raw_combinations["custom"]
250
+ else:
251
+ # Fallback if normalizer didn't capture them correctly
252
+ out["custom"] = []
253
+
254
+ # Merge with any explicitly set values in the input cmb dict (e.g., from extensions)
255
+ for k in list(out.keys()):
256
+ if k in cmb and isinstance(cmb[k], dict):
257
+ out[k].update(cmb[k]) # Update with input values if present
258
+
259
+ return out
260
+
261
+ def _ensure_actions(nz: Dict[str,Any], pids: List[str] = None) -> Dict[str,Any]:
262
+ actions=_as_dict(nz.get("actions"))
263
+ pids = pids or []
264
+
265
+ # 解析 play 动作
266
+ play_actions = []
267
+ if "play" in actions:
268
+ play_def = actions["play"]
269
+ if isinstance(play_def, list):
270
+ # 解析器把 play 分解成了多个字典,需要合并成一个完整的 play action
271
+ play_action = _merge_play_actions(play_def, nz)
272
+ if play_action:
273
+ # 修复transfer_path中的zone ID
274
+ if "transfer_path" in play_action:
275
+ play_action["transfer_path"] = _fix_zone_ids_in_transfer_path(
276
+ play_action["transfer_path"], pids)
277
+ play_actions.append(play_action)
278
+ elif isinstance(play_def, dict):
279
+ # 单个 play 定义
280
+ play_action = _parse_play_action(play_def)
281
+ if "transfer_path" in play_action:
282
+ play_action["transfer_path"] = _fix_zone_ids_in_transfer_path(
283
+ play_action["transfer_path"], pids)
284
+ play_actions.append(play_action)
285
+
286
+ # 解析 special 动作
287
+ special_actions = []
288
+ if "special" in actions:
289
+ special_def = actions["special"]
290
+ if isinstance(special_def, list):
291
+ special_actions = _merge_special_actions(special_def)
292
+ elif isinstance(special_def, dict):
293
+ special_actions.append(_parse_special_action(special_def))
294
+
295
+ # 修复special动作中的transfer_path
296
+ for action in special_actions:
297
+ if "transfer_path" in action:
298
+ action["transfer_path"] = _fix_zone_ids_in_transfer_path(
299
+ action["transfer_path"], pids)
300
+
301
+ # 构建输出
302
+ output = {
303
+ "play": play_actions,
304
+ "pass": {"transfer_path": {"from": "field", "to": "field"}},
305
+ "cleanup_trick": [{"from": "field", "to": "discard_pile", "count": "all"}],
306
+ "other": {}
307
+ }
308
+
309
+ # 如果有 special 动作,添加到 other 中
310
+ if special_actions:
311
+ # 将special动作转换为Transfer格式
312
+ special_transfers = []
313
+ for action in special_actions:
314
+ if "transfer_path" in action:
315
+ transfer = action["transfer_path"].copy()
316
+ if "visibility_change" in action:
317
+ transfer["visibility_change"] = action["visibility_change"]
318
+ special_transfers.append(transfer)
319
+ if special_transfers:
320
+ output["other"]["special"] = special_transfers
321
+
322
+ return output
323
+
324
+ def _merge_play_actions(play_defs: List[Dict[str, Any]], gdl_actions: Dict[str, Any] = None) -> Dict[str, Any]:
325
+ """合并被解析器分解的 play 动作定义"""
326
+ merged = {}
327
+ raw_def = {}
328
+
329
+ # 首先尝试从play_defs中提取牌型列表
330
+ type_list = []
331
+ for play_def in play_defs:
332
+ if isinstance(play_def, dict) and "type" in play_def:
333
+ type_val = play_def["type"]
334
+ if isinstance(type_val, dict):
335
+ type_list = _extract_one_of_values(type_val)
336
+ break
337
+
338
+ if type_list:
339
+ # 过滤掉"one_of"和其他无效值
340
+ valid_types = [t for t in type_list if t not in ["one_of", "int?", "sequence?"]]
341
+ if valid_types:
342
+ # Schema期望type是单个字符串,不是数组,所以取第一个有效类型
343
+ merged["type"] = valid_types[0]
344
+
345
+ for play_def in play_defs:
346
+ if isinstance(play_def, dict):
347
+ # 检查是否是特定字段
348
+ if "type" in play_def and "type" not in merged:
349
+ type_val = play_def["type"]
350
+ if isinstance(type_val, dict):
351
+ if "one_of" in type_val:
352
+ # 处理 one_of 结构,提取所有牌型
353
+ type_list = _extract_one_of_values(type_val)
354
+ merged["type"] = type_list
355
+ else:
356
+ merged["type"] = _scalar(type_val)
357
+ else:
358
+ merged["type"] = _scalar(type_val)
359
+
360
+ elif "len" in play_def:
361
+ # 处理 len 字段,提取简单值
362
+ len_val = _extract_simple_value(play_def["len"])
363
+ if len_val != "int?":
364
+ merged["len"] = len_val
365
+
366
+ elif "core" in play_def:
367
+ # 处理 core 字段,提取简单值
368
+ core_val = _extract_simple_value(play_def["core"])
369
+ if core_val != "sequence?":
370
+ merged["core"] = core_val
371
+
372
+ elif "wings" in play_def:
373
+ # 处理 wings 字段,提取简单值
374
+ wings_val = _extract_simple_value(play_def["wings"])
375
+ if wings_val != "int?":
376
+ merged["wings"] = wings_val
377
+
378
+ elif "transfer_path" in play_def:
379
+ tp = play_def["transfer_path"]
380
+ if isinstance(tp, dict):
381
+ merged["transfer_path"] = tp
382
+ elif isinstance(tp, list):
383
+ merged["transfer_path"] = _parse_transfer_path_list(tp)
384
+
385
+ elif "visibility_change" in play_def:
386
+ vc = play_def["visibility_change"]
387
+ merged["visibility_change"] = _parse_visibility_change(vc)
388
+
389
+ else:
390
+ # 其他字段保存到 raw_definition
391
+ raw_def.update(play_def)
392
+
393
+ # 如果没有提取到type,设置默认值
394
+ if "type" not in merged:
395
+ merged["type"] = ["single", "pair", "triple", "straight", "bomb", "rocket"]
396
+
397
+ # 确保visibility_change有state字段
398
+ if "visibility_change" in merged:
399
+ vc = merged["visibility_change"]
400
+ if "state" not in vc:
401
+ vc["state"] = "visible"
402
+
403
+ # 保存原始定义
404
+ if raw_def:
405
+ merged["raw_definition"] = raw_def
406
+
407
+ return merged
408
+
409
+ def _extract_one_of_values(type_dict: Dict[str, Any]) -> List[str]:
410
+ """从 one_of 结构中提取值列表"""
411
+ values = []
412
+
413
+ def extract_recursive(obj):
414
+ if isinstance(obj, dict):
415
+ if "_" in obj:
416
+ if isinstance(obj["_"], list):
417
+ values.extend(obj["_"])
418
+ for k, v in obj.items():
419
+ if k != "_":
420
+ # 如果key不是"_",那么key本身就是一个值
421
+ if isinstance(v, dict):
422
+ if len(v) == 0:
423
+ # 空字典,key就是值
424
+ values.append(k)
425
+ elif "_" in v:
426
+ # 有"_"字段,key是值,然后递归处理"_"
427
+ values.append(k)
428
+ extract_recursive(v)
429
+ else:
430
+ # 没有"_"字段,递归处理
431
+ extract_recursive(v)
432
+ else:
433
+ # v不是字典,k是值
434
+ values.append(k)
435
+ elif isinstance(obj, list):
436
+ for item in obj:
437
+ extract_recursive(item)
438
+
439
+ extract_recursive(type_dict)
440
+ return values
441
+
442
+ def _extract_play_types_from_gdl(gdl_actions: Dict[str, Any]) -> List[str]:
443
+ """从GDL actions中提取play动作的牌型列表"""
444
+ if "play" not in gdl_actions:
445
+ return []
446
+
447
+ play_def = gdl_actions["play"]
448
+ if isinstance(play_def, list):
449
+ # 查找type字段
450
+ for item in play_def:
451
+ if isinstance(item, dict) and "type" in item:
452
+ type_val = item["type"]
453
+ if isinstance(type_val, dict) and "one_of" in type_val:
454
+ return _extract_one_of_values(type_val)
455
+ elif isinstance(play_def, dict) and "type" in play_def:
456
+ type_val = play_def["type"]
457
+ if isinstance(type_val, dict) and "one_of" in type_val:
458
+ return _extract_one_of_values(type_val)
459
+
460
+ # 如果从actions中没找到,尝试从game.actions中查找
461
+ game_actions = _as_dict(_as_dict(gdl_actions.get("game", {})).get("actions", {}))
462
+ if "play" in game_actions:
463
+ play_def = game_actions["play"]
464
+ if isinstance(play_def, list):
465
+ for item in play_def:
466
+ if isinstance(item, dict) and "type" in item:
467
+ type_val = item["type"]
468
+ if isinstance(type_val, dict) and "one_of" in type_val:
469
+ return _extract_one_of_values(type_val)
470
+ elif isinstance(play_def, dict) and "type" in play_def:
471
+ type_val = play_def["type"]
472
+ if isinstance(type_val, dict) and "one_of" in type_val:
473
+ return _extract_one_of_values(type_val)
474
+
475
+ return []
476
+
477
+ def _extract_simple_value(obj: Any) -> Any:
478
+ """提取简单值,处理嵌套的 {"_": [value]} 结构"""
479
+ if isinstance(obj, dict):
480
+ if "_" in obj and isinstance(obj["_"], list) and len(obj["_"]) == 1:
481
+ return obj["_"][0]
482
+ elif len(obj) == 1:
483
+ key, value = next(iter(obj.items()))
484
+ if isinstance(value, dict) and len(value) == 0:
485
+ return key
486
+ return _extract_simple_value(value)
487
+ return obj
488
+
489
+ def _parse_visibility_change(vc: Dict[str, Any]) -> Dict[str, Any]:
490
+ """解析visibility_change字段"""
491
+ result = {}
492
+
493
+ # 解析 to 字段
494
+ if "to" in vc:
495
+ to_val = vc["to"]
496
+ if isinstance(to_val, list):
497
+ # 提取所有受众
498
+ audiences = []
499
+ for item in to_val:
500
+ if isinstance(item, dict):
501
+ # 处理 {"owner": {"_": ["teammates", "enemies"]}} 结构
502
+ for key, value in item.items():
503
+ if isinstance(value, dict) and "_" in value:
504
+ audiences.extend(value["_"])
505
+ else:
506
+ audiences.append(key)
507
+ else:
508
+ audiences.append(_scalar(item))
509
+ result["to"] = audiences
510
+ else:
511
+ result["to"] = [_scalar(to_val)]
512
+
513
+ # 解析 state 字段
514
+ if "state" in vc:
515
+ state_val = _extract_simple_value(vc["state"])
516
+ if state_val != "visible":
517
+ result["state"] = state_val
518
+
519
+ # 解析 on_target 字段
520
+ if "on_target" in vc:
521
+ on_target_val = _extract_simple_value(vc["on_target"])
522
+ if on_target_val != "true":
523
+ result["on_target"] = on_target_val
524
+
525
+ return result
526
+
527
+ def _parse_play_action(play_def: Dict[str, Any]) -> Dict[str, Any]:
528
+ """解析单个 play 动作定义"""
529
+ result = {}
530
+
531
+ # 解析 type
532
+ if "type" in play_def:
533
+ type_val = play_def["type"]
534
+ if isinstance(type_val, dict) and "one_of" in type_val:
535
+ result["type"] = _as_list(type_val["one_of"])
536
+ else:
537
+ result["type"] = _scalar(type_val)
538
+
539
+ # 解析其他字段
540
+ for key in ["len", "core", "wings"]:
541
+ if key in play_def:
542
+ result[key] = play_def[key]
543
+
544
+ # 解析 transfer_path
545
+ if "transfer_path" in play_def:
546
+ tp = play_def["transfer_path"]
547
+ if isinstance(tp, dict):
548
+ result["transfer_path"] = tp
549
+ elif isinstance(tp, list):
550
+ # 处理 from: hand to: field 格式
551
+ result["transfer_path"] = _parse_transfer_path_list(tp)
552
+
553
+ # 解析 visibility_change
554
+ if "visibility_change" in play_def:
555
+ result["visibility_change"] = play_def["visibility_change"]
556
+
557
+ # 保存原始定义用于复杂情况
558
+ result["raw_definition"] = {k: v for k, v in play_def.items()
559
+ if k not in ["type", "len", "core", "wings", "transfer_path", "visibility_change"]}
560
+
561
+ return result
562
+
563
+ def _parse_special_action(special_def: Dict[str, Any]) -> Dict[str, Any]:
564
+ """解析 special 动作定义"""
565
+ result = {}
566
+
567
+ # 解析 name
568
+ if "name" in special_def:
569
+ result["name"] = _scalar(special_def["name"])
570
+
571
+ # 解析 params
572
+ if "params" in special_def:
573
+ result["params"] = special_def["params"]
574
+
575
+ # 解析 transfer_path
576
+ if "transfer_path" in special_def:
577
+ tp = special_def["transfer_path"]
578
+ if isinstance(tp, dict):
579
+ result["transfer_path"] = tp
580
+ elif isinstance(tp, list):
581
+ result["transfer_path"] = _parse_transfer_path_list(tp)
582
+
583
+ # 解析 visibility_change
584
+ if "visibility_change" in special_def:
585
+ result["visibility_change"] = _parse_visibility_change(special_def["visibility_change"])
586
+
587
+ # 保存原始定义
588
+ result["raw_definition"] = {k: v for k, v in special_def.items()
589
+ if k not in ["name", "params", "transfer_path", "visibility_change"]}
590
+
591
+ return result
592
+
593
+ def _merge_special_actions(special_defs: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
594
+ """合并被解析器分解的 special 动作定义"""
595
+ if not special_defs:
596
+ return []
597
+
598
+ # 按name分组,合并相同name的动作
599
+ grouped = {}
600
+ for special_def in special_defs:
601
+ if isinstance(special_def, dict):
602
+ name = _scalar(special_def.get("name", ""))
603
+ if name:
604
+ if name not in grouped:
605
+ grouped[name] = {}
606
+ # 合并字段
607
+ for key, value in special_def.items():
608
+ if key != "name":
609
+ grouped[name][key] = value
610
+ else:
611
+ # 没有name的,可能是参数或其他字段,需要与前面的动作合并
612
+ # 这里简化处理,直接作��独立动作
613
+ pass
614
+
615
+ # 转换为列表
616
+ result = []
617
+ for name, fields in grouped.items():
618
+ action = {"name": name}
619
+ action.update(fields)
620
+ result.append(_parse_special_action(action))
621
+
622
+ return result
623
+
624
+ def _parse_transfer_path_list(tp_list: List[Any]) -> Dict[str, str]:
625
+ """解析 from: hand to: field 格式的 transfer_path"""
626
+ result = {}
627
+ i = 0
628
+ while i < len(tp_list) - 1:
629
+ if isinstance(tp_list[i], str) and tp_list[i].endswith(":"):
630
+ key = tp_list[i][:-1] # 去掉冒号
631
+ result[key] = _scalar(tp_list[i + 1])
632
+ i += 2
633
+ else:
634
+ i += 1
635
+ return result
636
+
637
+ def _fix_zone_ids_in_transfer_path(tp: Dict[str, str], pids: List[str]) -> Dict[str, str]:
638
+ """修复transfer_path中的zone ID,将hand转换为具体的hand:PlayerID"""
639
+ if not isinstance(tp, dict):
640
+ return tp
641
+
642
+ result = tp.copy()
643
+
644
+ # 如果from是"hand",需要转换为具体的hand:PlayerID
645
+ if result.get("from") == "hand" and pids:
646
+ # 使用第一个玩家ID作为默认值
647
+ result["from"] = f"hand:{pids[0]}"
648
+
649
+ # 如果to是"hand",也需要转换
650
+ if result.get("to") == "hand" and pids:
651
+ result["to"] = f"hand:{pids[0]}"
652
+
653
+ return result
654
+
655
+ def _sanitize_mechanics(mechs: List[Dict[str,Any]]):
656
+ for m in mechs or []:
657
+ if not isinstance(m,dict): continue
658
+ # phase/timing/trigger_condition 缺省
659
+ m["phase"]=_phase_to_schema(_scalar(m.get("phase") or "playing"))
660
+ m.setdefault("timing", "during_action")
661
+ m.setdefault("enabled", True)
662
+ if str(m.get("timing") or "").strip() not in ("pre_action","post_action","during_action"):
663
+ m["timing"] = "during_action"
664
+ m.setdefault("trigger_condition", "always")
665
+ raw_def = m.get("raw_definition", {}).copy() # Work on a copy to avoid modifying original input
666
+ # --- Modification: Handle transfer_path for instantiation and Schema compliance ---
667
+ original_tp = raw_def.get("transfer_path")
668
+ if original_tp and isinstance(original_tp, dict):
669
+ from_val = original_tp.get("from")
670
+ to_val = original_tp.get("to")
671
+
672
+ # Check if 'from' or 'to' are templates (e.g., 'hand', 'hand:*') or invalid zone names
673
+ # If they are, keep the original transfer_path in raw_definition and do NOT create a top-level transfer_path
674
+ def _is_template(z):
675
+ return isinstance(z, str) and z in ("hand", "hand:*")
676
+
677
+ def _is_valid_zone(z):
678
+ return isinstance(z, str) and bool(ZONE_RE.match(z))
679
+
680
+ # If from or to is a template, or if from/to are not valid zones according to the schema's ZoneID pattern,
681
+ # then the transfer_path cannot be placed at the top level.
682
+ if _is_template(from_val) or _is_template(to_val) or not _is_valid_zone(from_val) or not _is_valid_zone(to_val):
683
+ # The raw_def already contains the original transfer_path, which is correct.
684
+ # Do not create a top-level transfer_path.
685
+ # Ensure raw_definition exists and contains the original
686
+ # --- Modification: Add note for instantiation ---
687
+ m["raw_definition"] = {"transfer_path_note": {"zone_style": "needs_instantiation"}}
688
+ # Remove transfer_path from top level if it was accidentally added (shouldn't happen here)
689
+ m.pop("transfer_path", None)
690
+ else:
691
+ # If 'from' and 'to' are concrete and valid zones, move transfer_path to top level
692
+ m["transfer_path"] = {"from": from_val, "to": to_val}
693
+ # Remove transfer_path from raw_definition if it's valid and concrete
694
+ raw_def.pop("transfer_path", None)
695
+ if raw_def: # Only keep raw_definition if it has other keys
696
+ m["raw_definition"] = raw_def
697
+ else:
698
+ m["raw_definition"] = {}
699
+ else:
700
+ # If no original transfer_path, or it's not a dict, just keep raw_def as is
701
+ m["raw_definition"] = raw_def
702
+
703
+ # 清掉 None 值,避免 schema 报错
704
+ for k in ("min_players","max_players","usage_limit","visibility_change"):
705
+ if m.get(k, "__absent__") is None:
706
+ m.pop(k, None)
707
+
708
+ def _ensure_scoring(ir: Dict[str,Any]) -> Dict[str,Any]:
709
+ sc=_as_dict(ir.get("scoring"))
710
+ base=sc.get("base")
711
+ if not isinstance(base,int) or base<0: sc["base"]=1
712
+ return sc
713
+
714
+ def map_to_v095(nz: Dict[str,Any], gdl_text: str = None) -> Dict[str,Any]:
715
+ # --- Modification: Dynamically set description based on game name ---
716
+ game_name = _scalar(_as_dict(nz.get("game")).get("name") or "UnnamedGame")
717
+ if "UNO" in game_name:
718
+ description = "由标准54张牌叠加UNO特殊牌机制的变体。"
719
+ elif "奇袭" in game_name:
720
+ description = "在传统斗地主基础上加入了【援手】和【换位】两个新机制,增加了农民间的协作和角色变换的可能性"
721
+ else:
722
+ description = f"由标准54张牌叠加特殊牌机制的变体。" # Generic fallback
723
+
724
+ # meta
725
+ meta={"name": game_name, "version":"v0.95", "origin": "Variant", "seeded": True, "description": description}
726
+
727
+ # players
728
+ players=_derive_players(nz); pids=players.pop("_player_ids", []) # Removed role_defs as not needed
729
+
730
+ # cards
731
+ cards=_ensure_cards(nz)
732
+
733
+ # zones - Modified to handle zone definitions from setup
734
+ zone_defs = _as_list(nz.get("zones", []))
735
+ zones=_expand_zones(zone_defs, pids)
736
+
737
+ # visibility
738
+ visibility=_expand_vis_hand_star(_as_dict(nz.get("visibility")), pids)
739
+
740
+ # combinations
741
+ combinations=_ensure_combinations(nz)
742
+
743
+ # comparison
744
+ comparison=_as_dict(nz.get("comparison")) or {"same_type": True, "same_len": True, "bomb_beats_all": True, "rocket_top": True, "tiebreaker": "none"}
745
+
746
+ # actions
747
+ actions=_ensure_actions(nz, pids)
748
+
749
+ # phases - Modified to handle GDL phase names and map to schema
750
+ ph=_as_list(nz.get("phases"))
751
+ allowed={"setup","deal","bid","double","initiative","play","settle","grouping"}
752
+ # Apply mapping logic using _phase_to_schema
753
+ cleaned=[]
754
+ for x in ph:
755
+ s=str(x).strip()
756
+ mapped_s = _phase_to_schema(s)
757
+ if mapped_s in allowed and mapped_s not in cleaned:
758
+ cleaned.append(mapped_s)
759
+ # Ensure 'setup' and 'settle' are present, with 'setup' first and 'settle' last
760
+ if "setup" not in cleaned: cleaned=["setup"]+[t for t in cleaned if t!="setup"]
761
+ if "settle" not in cleaned: cleaned=[t for t in cleaned if t!="settle"]+["settle"]
762
+ # Add 'deal' phase if not present, typically after 'setup'
763
+ if "deal" not in cleaned:
764
+ setup_idx = -1
765
+ for i, p in enumerate(cleaned):
766
+ if p == "setup":
767
+ setup_idx = i
768
+ break
769
+ if setup_idx != -1:
770
+ cleaned.insert(setup_idx + 1, "deal") # Insert 'deal' after 'setup'
771
+ else:
772
+ cleaned = ["deal"] + cleaned # If 'setup' is missing, add 'deal' at the beginning
773
+ # Add 'play' phase if not present, typically after 'deal'
774
+ if "play" not in cleaned:
775
+ deal_idx = -1
776
+ for i, p in enumerate(cleaned):
777
+ if p == "deal":
778
+ deal_idx = i
779
+ break
780
+ if deal_idx != -1:
781
+ cleaned.insert(deal_idx + 1, "play") # Insert 'play' after 'deal'
782
+ else:
783
+ # If 'deal' is also missing, try 'setup'
784
+ setup_idx = -1
785
+ for i, p in enumerate(cleaned):
786
+ if p == "setup":
787
+ setup_idx = i
788
+ break
789
+ if setup_idx != -1:
790
+ cleaned.insert(setup_idx + 1, "play") # Insert 'play' after 'setup'
791
+ else:
792
+ cleaned = ["play"] + cleaned # If neither 'setup' nor 'deal' is present, add 'play' at the beginning
793
+
794
+
795
+ # turns
796
+ turns=_as_dict(nz.get("turns")) or {}
797
+ # Use the uniquely generated player IDs from _derive_players for order
798
+ # If turns.order is empty or not provided, use pids
799
+ order=turns.get("order") or pids
800
+ # If turns.order was provided but is the original ["Landlord", "Peasant", "Peasant", "Peasant"],
801
+ # it should be replaced by the unique pids generated in _derive_players.
802
+ # The pids are ["Landlord", "Peasant", "Peasant_1", "Peasant_2"] which is the desired outcome.
803
+ # So, we prioritize the pids (which are unique) over the potentially non-unique turns.order.
804
+ # However, if turns.order is explicitly set *after* uniquification by normalizer, we should respect it.
805
+ # The safest way is to always use pids if they are available and represent the actual unique player turn order.
806
+ # Let's assume pids represents the final desired turn order.
807
+ # Use the unique player IDs generated by _derive_players
808
+ turns["order"]=pids
809
+ if not turns.get("leader") and pids: turns["leader"]=pids[0] # Use first unique ID as leader
810
+ turns.setdefault("trick_end_on", "two_pass")
811
+ # next_leader
812
+ ext=_as_dict(nz.get("extensions")); te=turns.get("trick_end_on","two_pass")
813
+ if te=="custom":
814
+ t_ext=_as_dict(ext.get("trick_end_on"))
815
+ next_leader = "last_non_pass" if t_ext.get("mode")=="consecutive_passes" else "fixed"
816
+ else:
817
+ next_leader = "last_non_pass"
818
+ turns.setdefault("next_leader", next_leader)
819
+
820
+ # ending & scoring
821
+ ending=_as_dict(nz.get("ending")) or {"when":"any_hand_empty"}
822
+ scoring=_ensure_scoring(nz)
823
+
824
+ # mechanics
825
+ mechs=_as_list(nz.get("special_mechanics")); _sanitize_mechanics(mechs)
826
+
827
+ # extensions - Add card mappings
828
+ extensions=_as_dict(nz.get("extensions"))
829
+ extensions["cards_mapping"] = {
830
+ "rank_name_to_int": {
831
+ "J": 11,
832
+ "Q": 12,
833
+ "K": 13,
834
+ "A": 14,
835
+ "2": 15
836
+ },
837
+ "suit_name_to_carrier": {
838
+ "Spade": "Spade",
839
+ "Heart": "Heart",
840
+ "Club": "Club",
841
+ "Diamond": "Diamond"
842
+ }
843
+ }
844
+
845
+
846
+ ir = {
847
+ "meta": meta,
848
+ "players": players,
849
+ "cards": cards,
850
+ "zones": zones,
851
+ "visibility": visibility,
852
+ "combinations": combinations,
853
+ "comparison": comparison,
854
+ "actions": actions,
855
+ "phases": cleaned,
856
+ "turns": turns,
857
+ "ending": ending,
858
+ "scoring": scoring,
859
+ "special_mechanics": mechs,
860
+ "extensions": extensions
861
+ }
862
+
863
+ missing=[k for k in REQUIRED if k not in ir]
864
+ if missing: raise ValueError(f"Missing required keys after mapping: {missing}")
865
+ return ir
normalizer_v2.py ADDED
@@ -0,0 +1,454 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ normalizer_v2.py — 语义标准化(兼容多布局,提取 mechanics 名称/阶段/时机)
4
+ """
5
+ from __future__ import annotations
6
+ from typing import Any, Dict, List
7
+ from copy import deepcopy
8
+
9
+ def _as_list(x):
10
+ if x is None: return []
11
+ return x if isinstance(x, list) else [x]
12
+
13
+ def _as_dict(x):
14
+ return x if isinstance(x, dict) else {}
15
+
16
+ def _scalar(x: Any) -> str:
17
+ if isinstance(x, str): return x
18
+ if isinstance(x, (int, float)): return str(x)
19
+ if isinstance(x, dict):
20
+ if "_" in x and isinstance(x["_"], list) and x["_"] and isinstance(x["_"][0], (str,int,float)):
21
+ return _scalar(x["_"][0])
22
+ if "name" in x and isinstance(x["name"], str): return x["name"]
23
+ if len(x)==1:
24
+ k, v = next(iter(x.items()))
25
+ if v in (None, {}) and isinstance(k, str):
26
+ return k
27
+ if isinstance(x, list) and x:
28
+ return _scalar(x[0])
29
+ return ""
30
+
31
+ def _is_standard_mechanic_structure(node: Any) -> bool:
32
+ """
33
+ 检查一个节点是否是标准的 {"mechanic": {...}} 结构。
34
+ 这是 GDL 中 (mechanic ...) 被 _to_obj 解析后的直接结果。
35
+ """
36
+ return isinstance(node, dict) and len(node) == 1 and 'mechanic' in node and isinstance(node['mechanic'], dict)
37
+
38
+ def _extract_mechanics_from_sm(sm_node: Any) -> List[Dict[str, Any]]:
39
+ """
40
+ 专门从 special_mechanics 的节点中提取 mechanic 定义。
41
+ sm_node 是 special_mechanics 键对应的值。
42
+ 这个函数严格处理 special_mechanics 的内容,避免误判内部结构。
43
+ """
44
+ mechanics = []
45
+ # sm_node 应该是 {'mechanic': [...]} 或 {'mechanic': {...}} 或 [{'mechanic': {...}}, ...]
46
+ sm_dict = _as_dict(sm_node)
47
+
48
+ if 'mechanic' in sm_dict:
49
+ mech_content = sm_dict.get("mechanic")
50
+ if isinstance(mech_content, dict):
51
+ # Case 1: {"mechanic": {"mechanic_name": {...}, "mechanic_name2": {...}}}
52
+ # 这种情况发生在 _to_obj 合并了多个同名 mechanic 时
53
+ for name, definition in mech_content.items():
54
+ if isinstance(definition, dict):
55
+ # 构造标准的 mechanic 结构
56
+ mechanic_def = definition.copy()
57
+ # 确保有 name 字段
58
+ if 'name' not in mechanic_def:
59
+ mechanic_def['name'] = name
60
+ mechanics.append({"mechanic": mechanic_def})
61
+ elif isinstance(mech_content, list):
62
+ # Case 2: {"mechanic": [...]}
63
+ for item in mech_content:
64
+ # item 应该是 {"_": ["Name", ...], ...} 或 {"name": "Name", ...} 这样的结构 inside the mechanic
65
+ # Check if item itself is the content of a mechanic (i.e., has 'name' or '_')
66
+ if isinstance(item, dict) and ('name' in item or ('_' in item and isinstance(item.get('_'), list) and item['_'] and isinstance(item['_'][0], str))):
67
+ # Wrap it in the standard {"mechanic": {...}} format
68
+ mechanics.append({"mechanic": item})
69
+ elif isinstance(sm_node, list):
70
+ # Case 3: [{"mechanic": {...}}, ...] - A list of standard structures
71
+ for item in sm_node:
72
+ if _is_standard_mechanic_structure(item):
73
+ mechanics.append(item) # item is {"mechanic": {...}}
74
+ elif _is_standard_mechanic_structure(sm_node):
75
+ # Case 4: {"mechanic": {...}} - A single standard structure
76
+ mechanics.append(sm_node) # sm_node is {"mechanic": {...}}
77
+ # If sm_node is just a dict like {"_": ["Name", ...]}, it's likely an error or edge case from _merge_kv_list_to_dict
78
+ # We ignore this case as it shouldn't happen if special_mechanics is structured correctly in GDL.
79
+
80
+ return mechanics
81
+
82
+
83
+ def _walk_collect_mechs(node, bucket, inside_special_mechanics=False):
84
+ """
85
+ 递归收集 special_mechanics / actions.special
86
+ inside_special_mechanics 标志用于防止在 mechanic 内部结构中递归查找其他 mechanic。
87
+ """
88
+ if isinstance(node, dict):
89
+ # 专门处理 special_mechanics 键 - 这是主要的 mechanic 来源
90
+ if "special_mechanics" in node:
91
+ sm_node = node.get("special_mechanics")
92
+ # Extract mechanics only from this specific key using the dedicated function
93
+ extracted = _extract_mechanics_from_sm(sm_node)
94
+ bucket.extend(extracted)
95
+ # Mark that we are now processing inside special_mechanics context for recursion
96
+ # We don't pass this flag deeper here, as _extract_mechanics_from_sm handles the context.
97
+ # 处理 actions.special
98
+ elif "actions" in node and not inside_special_mechanics: # Only process actions if not already inside special_mechanics
99
+ a = _as_dict(node.get("actions"))
100
+ special_actions = _as_list(a.get("special"))
101
+ # 检查 special_actions 中是否包含 mechanic 定义 (This is less common than in special_mechanics)
102
+ for sa in special_actions:
103
+ # sa 可能是 {"name": "...", ...} or {"_": ["Name", ...], ...}
104
+ # Check if it matches a basic mechanic-like structure
105
+ if isinstance(sa, dict) and ("name" in sa or ("_" in sa and isinstance(sa.get("_"), list) and sa["_"] and isinstance(sa["_"][0], str))):
106
+ bucket.append({"mechanic": sa}) # Wrap in standard format
107
+ # Recurse into other keys, but avoid recursing into known internal structures of a mechanic
108
+ # like transfer_path, visibility_change, etc., which are unlikely to contain top-level mechanics.
109
+ # The main risk was from 'phases' -> 'mechanic_conditions', which we handled by using _extract_mechanics_from_sm.
110
+ # We can now recurse more safely, but mark if we are inside a mechanic's content.
111
+ for k, v in node.items():
112
+ if k in ("special_mechanics", "actions"): # Already handled these above
113
+ continue
114
+ # Do not recurse into known internal structures that are not top-level mechanic containers
115
+ if k in ("transfer_path", "visibility_change", "params"): # Add other known internal keys if needed
116
+ continue
117
+ _walk_collect_mechs(v, bucket, inside_special_mechanics=inside_special_mechanics)
118
+
119
+ elif isinstance(node, list):
120
+ # If the top-level is a list containing special_mechanics, handle it
121
+ # Or if it's a list inside a mechanic, recurse carefully
122
+ for v in node:
123
+ _walk_collect_mechs(v, bucket, inside_special_mechanics)
124
+
125
+
126
+ def _collect_mechanics(parsed: Dict[str, Any]) -> List[Dict[str, Any]]:
127
+ items: List[Any] = []
128
+ # Start the walk from the top-level game structure
129
+ _walk_collect_mechs(parsed, items, inside_special_mechanics=False)
130
+ mechs: List[Dict[str, Any]] = []
131
+ for i, raw in enumerate(items):
132
+ m = _as_dict(raw)
133
+ if not m: continue
134
+ # --- Process standard structure {"mechanic": {...}} ---
135
+ m_content = m
136
+ if len(m) == 1 and 'mechanic' in m and isinstance(m['mechanic'], dict):
137
+ m_content = m['mechanic']
138
+ else:
139
+ # If the raw item is not in the expected {"mechanic": {...}} format,
140
+ # it means the extraction logic failed or it was collected incorrectly.
141
+ # For safety, skip.
142
+ continue
143
+
144
+ # Now m_content holds the actual mechanic definition like {"_": ["Name", ...], ...} or {"name": "Name", ...}
145
+ name = _scalar(m_content.get("name") or (m_content.get("_") or [None])[0] or "")
146
+ if not name:
147
+ name = f"mechanic@{i}"
148
+ phase = _scalar(m_content.get("phase") or "playing_phase") # 先拿原词
149
+ timing = _scalar(m_content.get("timing") or "")
150
+ # 修改:将 phase 映射为标准名称,例如 playing_phase -> playing
151
+ standard_phase = phase.replace("_phase", "")
152
+ mech = {
153
+ "name": name,
154
+ "enabled": True if _scalar(m_content.get("enabled")) in ("true","1","yes","True") else True,
155
+ "description": _scalar(m_content.get("description")),
156
+ "phase": standard_phase, # 使用标准名称
157
+ "timing": timing,
158
+ "trigger_condition": _scalar(m_content.get("trigger_condition")),
159
+ "raw_definition": {
160
+ k: v for k, v in m_content.items()
161
+ if k not in ("name","enabled","description","phase","timing","trigger_condition")
162
+ }
163
+ }
164
+ mechs.append(mech)
165
+ return mechs
166
+
167
+ def _collect_mechanics_from_sexpr(parsed: Dict[str, Any]) -> List[Dict[str, Any]]:
168
+ """
169
+ 直接从 S-expression 中收集 mechanics,避免 _to_obj 的键合并问题
170
+ """
171
+ from gdl_parser_v2 import parse_sexpr
172
+
173
+ mechanics = []
174
+
175
+ def find_and_extract_mechanics(obj, path=''):
176
+ if isinstance(obj, dict):
177
+ for k, v in obj.items():
178
+ if k == 'special_mechanics':
179
+ # 找到 special_mechanics,尝试提取其中的 mechanics
180
+ if isinstance(v, dict) and 'mechanic' in v:
181
+ mech_content = v['mechanic']
182
+ if isinstance(mech_content, dict):
183
+ # 处理合并后的情况:{"mechanic_name": {...}, "mechanic_name2": {...}}
184
+ for name, definition in mech_content.items():
185
+ if isinstance(definition, dict):
186
+ mechanics.append(_process_mechanic_definition(name, definition))
187
+ else:
188
+ find_and_extract_mechanics(v, f'{path}.{k}' if path else k)
189
+ elif isinstance(obj, list):
190
+ for i, item in enumerate(obj):
191
+ find_and_extract_mechanics(item, f'{path}[{i}]')
192
+
193
+ find_and_extract_mechanics(parsed)
194
+ return mechanics
195
+
196
+ def _collect_mechanics_from_raw_sexpr(gdl_text: str) -> List[Dict[str, Any]]:
197
+ """
198
+ 直接从原始 GDL 文本中收集 mechanics,避免解��器的键合并问题
199
+ """
200
+ from gdl_parser_v2 import parse_sexpr
201
+
202
+ mechanics = []
203
+
204
+ # 找到 special_mechanics 部分
205
+ lines = gdl_text.split('\n')
206
+ start_idx = -1
207
+ end_idx = -1
208
+ paren_count = 0
209
+
210
+ for i, line in enumerate(lines):
211
+ if '(special_mechanics' in line:
212
+ start_idx = i
213
+ paren_count = line.count('(') - line.count(')')
214
+ break
215
+
216
+ if start_idx != -1:
217
+ for i in range(start_idx + 1, len(lines)):
218
+ paren_count += lines[i].count('(') - lines[i].count(')')
219
+ if paren_count == 0:
220
+ end_idx = i
221
+ break
222
+
223
+ if start_idx != -1 and end_idx != -1:
224
+ special_mechanics_lines = lines[start_idx:end_idx + 1]
225
+ special_mechanics_text = '\n'.join(special_mechanics_lines)
226
+
227
+ try:
228
+ sexpr = parse_sexpr(special_mechanics_text)
229
+ if len(sexpr) > 0 and isinstance(sexpr[0], list):
230
+ for item in sexpr[0]:
231
+ if isinstance(item, list) and len(item) > 0 and item[0] == 'mechanic':
232
+ # 这是一个 mechanic 定义
233
+ name = item[1] if len(item) > 1 else "unknown"
234
+
235
+ # 解析 mechanic 的属性
236
+ definition = {}
237
+ for i in range(2, len(item)):
238
+ if isinstance(item[i], list) and len(item[i]) >= 2:
239
+ key = item[i][0]
240
+ value = item[i][1] if len(item[i]) == 2 else item[i][1:]
241
+ definition[key] = value
242
+
243
+ mechanics.append(_process_mechanic_definition(name, definition))
244
+ except Exception as e:
245
+ print(f"Error parsing special_mechanics: {e}")
246
+
247
+ return mechanics
248
+
249
+ def _process_mechanic_definition(name: str, definition: Dict[str, Any]) -> Dict[str, Any]:
250
+ """处理单个 mechanic 定义"""
251
+ phase = _scalar(definition.get("phase") or "playing_phase")
252
+ timing = _scalar(definition.get("timing") or "")
253
+ standard_phase = phase.replace("_phase", "")
254
+
255
+ return {
256
+ "name": name,
257
+ "enabled": True if _scalar(definition.get("enabled")) in ("true","1","yes","True") else True,
258
+ "description": _scalar(definition.get("description")),
259
+ "phase": standard_phase,
260
+ "timing": timing,
261
+ "trigger_condition": _scalar(definition.get("trigger_condition")),
262
+ "raw_definition": {
263
+ k: v for k, v in definition.items()
264
+ if k not in ("name","enabled","description","phase","timing","trigger_condition")
265
+ }
266
+ }
267
+
268
+ def normalize_ir(parsed: Dict[str, Any], gdl_text: str = None) -> Dict[str, Any]:
269
+ p = _as_dict(parsed)
270
+ game = _as_dict(p.get("game") or p)
271
+
272
+ out: Dict[str, Any] = {"game": {}}
273
+
274
+ # ---- name ----
275
+ nm = _scalar(game.get("name") or game.get("_") or "")
276
+ out["game"]["name"] = nm or "UnnamedGame"
277
+
278
+ # ---- players count ----
279
+ players_count = game.get("players", 0)
280
+ out["game"]["players_count"] = players_count # 为 mapper_v2 提供原始玩家数量
281
+
282
+ # ---- roles ----
283
+ roles: List[Dict[str, Any]] = []
284
+ if "roles" in game:
285
+ flat = []
286
+ for r in _as_list(game["roles"]):
287
+ if isinstance(r, list): flat.extend(r)
288
+ else: flat.append(r)
289
+ for r in flat:
290
+ rd = _as_dict(r)
291
+ if "role" in rd and isinstance(rd["role"], dict):
292
+ u = rd["role"].get("_")
293
+ if isinstance(u, list) and u:
294
+ name = _scalar(u[0]); cnt = 1
295
+ if len(u) >= 2:
296
+ try: cnt = int(u[1])
297
+ except: cnt = 1
298
+ roles.append({"name": name or "Player", "count": max(1, cnt)}); continue
299
+ name = _scalar(rd.get("name") or rd.get("role") or "Player")
300
+ cnt = rd.get("count", 1)
301
+ try: cnt=int(cnt)
302
+ except: cnt=1
303
+ roles.append({"name": name, "count": max(1,cnt)})
304
+ out["game"]["roles"] = roles or [{"name":"Player","count": game.get("players") or 0}]
305
+
306
+ # ---- turns.order from turn_order ----
307
+ order = []
308
+ to = _as_list(game.get("turn_order"))
309
+ if to:
310
+ flat=[]
311
+ for elem in to:
312
+ if isinstance(elem, list): flat.extend(elem)
313
+ else: flat.append(elem)
314
+ # {"Landlord":{"_":[...]} }
315
+ if flat and isinstance(flat[0], dict) and len(flat[0])==1:
316
+ k, v = next(iter(flat[0].items()))
317
+ seq = [k]
318
+ if isinstance(v, dict) and isinstance(v.get("_"), list): seq.extend(v.get("_"))
319
+ order = [ _scalar(x) for x in seq ]
320
+ else:
321
+ order = [ _scalar(x) for x in flat ]
322
+ if order:
323
+ out["turns"] = {"order": order}
324
+
325
+ # ---- phases ---- 兼容 *_phase 键名
326
+ phases = _as_list(game.get("phases"))
327
+ phase_names: List[str] = []
328
+ if phases:
329
+ for sub in phases:
330
+ xs = _as_list(sub) if isinstance(sub, list) else [sub]
331
+ for x in xs:
332
+ s = _scalar(x)
333
+ if s:
334
+ phase_names.append(s)
335
+ elif isinstance(x, dict) and len(x)==1:
336
+ k = next(iter(x.keys()))
337
+ if isinstance(k, str) and k.endswith("_phase"):
338
+ # 修改:将 phase 映射为标准名称
339
+ standard_name = k.replace("_phase", "")
340
+ phase_names.append(standard_name)
341
+ out["phases"] = phase_names # Ensure 'phases' key is set in output
342
+
343
+ # ---- deck info ----
344
+ deck_info = _as_dict(game.get("deck"))
345
+ if deck_info:
346
+ # Extract deck type, shuffling, deal_pattern
347
+ deck_type = _scalar(deck_info.get("_") or deck_info.get("type") or "Standard54") # Default to Standard54
348
+ shuffling = _scalar(deck_info.get("shuffling"))
349
+ deal_pattern = _scalar(deck_info.get("deal_pattern"))
350
+ out["game"]["deck"] = {
351
+ "type": deck_type,
352
+ "shuffling": shuffling,
353
+ "deal_pattern": deal_pattern
354
+ }
355
+
356
+ # ---- setup details ----
357
+ setup_info = _as_dict(game.get("setup"))
358
+ if setup_info:
359
+ # Extract zones, card_relations, deal count
360
+ zones_info = _as_dict(setup_info.get("zones"))
361
+ card_relations_info = _as_dict(setup_info.get("card_relations"))
362
+ deal_count = setup_info.get("deal")
363
+
364
+ if zones_info:
365
+ zone_defs = []
366
+ if "hand" in zones_info:
367
+ zone_defs.append({"type": "hand", **_as_dict(zones_info.get("hand"))})
368
+ if "field" in zones_info:
369
+ zone_defs.append({"type": "field", **_as_dict(zones_info.get("field"))})
370
+ if "discard_pile" in zones_info:
371
+ zone_defs.append({"type": "discard_pile", **_as_dict(zones_info.get("discard_pile"))})
372
+ if "main_deck" in zones_info:
373
+ zone_defs.append({"type": "main_deck", **_as_dict(zones_info.get("main_deck"))})
374
+ if "special_deck" in zones_info:
375
+ # Handle special_deck which can be a list or dict
376
+ sd = zones_info.get("special_deck")
377
+ if isinstance(sd, dict):
378
+ # If it's a dict like { (UNO_Cards) ... }
379
+ # We need to extract the name and details
380
+ # The structure is likely {"UNO_Cards": {details}}
381
+ for name, details in sd.items():
382
+ if isinstance(details, dict):
383
+ zone_defs.append({"type": "special_deck", "name": name, **details})
384
+ else:
385
+ zone_defs.append({"type": "special_deck", "name": name, "initial_cards": details})
386
+ elif isinstance(sd, list):
387
+ # If it's a list, handle each element
388
+ for item in sd:
389
+ if isinstance(item, dict) and len(item) == 1:
390
+ name, details = next(iter(item.items()))
391
+ zone_defs.append({"type": "special_deck", "name": name, **_as_dict(details)})
392
+ out["zones"] = zone_defs # Ensure 'zones' key is set in output
393
+
394
+ if card_relations_info:
395
+ out["card_relations"] = card_relations_info
396
+
397
+ if deal_count is not None:
398
+ out["setup"] = {"deal": deal_count}
399
+
400
+ # ---- combinations ----
401
+ combinations_info = _as_dict(game.get("combinations"))
402
+ if combinations_info:
403
+ # Extract custom_combinations
404
+ custom_combs = []
405
+ if "custom_combination" in combinations_info:
406
+ raw_customs = _as_list(combinations_info.get("custom_combination"))
407
+ for cc in raw_customs:
408
+ cc_dict = _as_dict(cc)
409
+ if "_" in cc_dict and isinstance(cc_dict["_"], list) and cc_dict["_"]:
410
+ name = _scalar(cc_dict["_"][0])
411
+ spec = {k: v for k, v in cc_dict.items() if k != "_"}
412
+ custom_combs.append({"name": name, "spec": spec})
413
+ elif "name" in cc_dict:
414
+ name = _scalar(cc_dict.get("name"))
415
+ spec = {k: v for k, v in cc_dict.items() if k != "name"}
416
+ custom_combs.append({"name": name, "spec": spec})
417
+ out["combinations"] = {"custom": custom_combs} # Ensure 'combinations' key is set in output with custom part
418
+
419
+
420
+ # ---- mechanics ----
421
+ # 如果有原始 GDL 文本,尝试直接从文本中收集 mechanics
422
+ if gdl_text:
423
+ try:
424
+ out["special_mechanics"] = _collect_mechanics_from_raw_sexpr(gdl_text)
425
+ if not out["special_mechanics"]:
426
+ # 如果新方法没有找到,回退到旧方法
427
+ out["special_mechanics"] = _collect_mechanics({"game": game})
428
+ except:
429
+ # 如果新方法出错,回退到旧方法
430
+ out["special_mechanics"] = _collect_mechanics({"game": game})
431
+ else:
432
+ # 没有原始文本,使用旧方法
433
+ out["special_mechanics"] = _collect_mechanics({"game": game})
434
+
435
+ # ---- 提取 actions(可能在 setup 中)----
436
+ actions = None
437
+ if "actions" in game:
438
+ actions = deepcopy(game["actions"])
439
+ elif "setup" in game:
440
+ setup = _as_list(game["setup"])
441
+ for item in setup:
442
+ if isinstance(item, dict) and "actions" in item:
443
+ actions = deepcopy(item["actions"])
444
+ break
445
+
446
+ if actions:
447
+ out["actions"] = actions
448
+
449
+ # ---- 保留原始信息(由 mapper 兜底)----
450
+ for k in ("visibility","invariants","scoring","extensions"):
451
+ if k in p: out[k] = deepcopy(p[k])
452
+ elif k in game: out[k] = deepcopy(game[k])
453
+
454
+ return out
poker_gdl_ir.schema.v0.95.zh.json ADDED
@@ -0,0 +1,1515 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema ",
3
+ "$id": "https://example.com/schemas/poker_gdl_ir.schema.v0.95.zh.json ",
4
+ "title": "Poker-GDL IR Schema v0.95(含中文注释)",
5
+ "description": "扑克类(牌型压制类)玩法的 GDL → IR 标准。v0.95 优化:采用策略一,引入 raw_definition 和 extensions 字段以增强 Schema 扩展性。",
6
+ "type": "object",
7
+ "required": [
8
+ "meta",
9
+ "players",
10
+ "cards",
11
+ "zones",
12
+ "visibility",
13
+ "combinations",
14
+ "comparison",
15
+ "actions",
16
+ "phases",
17
+ "turns",
18
+ "ending",
19
+ "scoring"
20
+ ],
21
+ "additionalProperties": false,
22
+ "$defs": {
23
+ "PID": {
24
+ "type": "string",
25
+ "pattern": "^[A-Za-z0-9_:-]+$",
26
+ "description": "玩家/角色标识,如 L、P1、P2"
27
+ },
28
+ "ZoneID": {
29
+ "type": "string",
30
+ "pattern": "^(hand:[A-Za-z0-9_:-]+|field|main_deck|discard_pile|reserve_zone|public_pool|item_deck:[A-Za-z0-9_:-]+|history_deck)$",
31
+ "description": "区域标识;手牌需带所属者,如 hand:L;道具牌库需带名称,如 item_deck:BulletCards"
32
+ },
33
+ "Audience": {
34
+ "type": "string",
35
+ "enum": [
36
+ "owner",
37
+ "teammates",
38
+ "enemies",
39
+ "all",
40
+ "none"
41
+ ],
42
+ "description": "可见性受众"
43
+ },
44
+ "VisibilityState": {
45
+ "type": "string",
46
+ "enum": [
47
+ "visible",
48
+ "hidden",
49
+ "partial_suit",
50
+ "partial_rank"
51
+ ],
52
+ "description": "可见性枚举"
53
+ },
54
+ "Rank": {
55
+ "type": "integer",
56
+ "description": "推荐映射:3..15=3..A, 16=小王, 17=大王"
57
+ },
58
+ "Suit": {
59
+ "type": "string",
60
+ "enum": ["Spade", "Heart", "Club", "Diamond"],
61
+ "description": "花色,如 S/H/D/C (现在使用GDL中的标准名称)"
62
+ },
63
+ "ComboType": {
64
+ "type": "string",
65
+ "enum": [
66
+ "single",
67
+ "pair",
68
+ "triple",
69
+ "straight",
70
+ "pairs_chain",
71
+ "airplane",
72
+ "triple_with_single",
73
+ "triple_with_pair",
74
+ "four_with_twoSingles",
75
+ "four_with_twoPairs",
76
+ "bomb",
77
+ "rocket",
78
+ "double_chain_bomb",
79
+ "triple_chain_bomb",
80
+ "quad_chain_bomb",
81
+ "penta_chain_bomb",
82
+ "chain_triple",
83
+ "custom"
84
+ ],
85
+ "description": "牌型名"
86
+ },
87
+ "Transfer": {
88
+ "type": "object",
89
+ "required": [
90
+ "from",
91
+ "to"
92
+ ],
93
+ "additionalProperties": false,
94
+ "description": "原子转移:从某区域移动若干牌到另一区域;所有动作最终都可分解为 Transfer 序列。",
95
+ "properties": {
96
+ "from": {
97
+ "$ref": "#/$defs/ZoneID"
98
+ },
99
+ "to": {
100
+ "$ref": "#/$defs/ZoneID"
101
+ },
102
+ "count": {
103
+ "oneOf": [
104
+ {
105
+ "type": "integer",
106
+ "minimum": 0
107
+ },
108
+ {
109
+ "const": "all"
110
+ }
111
+ ],
112
+ "description": "移动数量;all 表示全部"
113
+ },
114
+ "filter": {
115
+ "type": "object",
116
+ "additionalProperties": false,
117
+ "description": "筛选器:按点数/花色/标签选择牌",
118
+ "properties": {
119
+ "ranks": {
120
+ "type": "array",
121
+ "items": {
122
+ "$ref": "#/$defs/Rank"
123
+ }
124
+ },
125
+ "suits": {
126
+ "type": "array",
127
+ "items": {
128
+ "$ref": "#/$defs/Suit"
129
+ }
130
+ },
131
+ "tags": {
132
+ "type": "array",
133
+ "items": {
134
+ "type": "string"
135
+ }
136
+ }
137
+ }
138
+ }
139
+ }
140
+ },
141
+ "VisibilityChange": {
142
+ "type": "object",
143
+ "required": [
144
+ "to",
145
+ "state"
146
+ ],
147
+ "additionalProperties": false,
148
+ "description": "动作引发的可见性变更",
149
+ "properties": {
150
+ "to": {
151
+ "type": "array",
152
+ "items": {
153
+ "$ref": "#/$defs/Audience"
154
+ }
155
+ },
156
+ "state": {
157
+ "$ref": "#/$defs/VisibilityState"
158
+ },
159
+ "on_target": {
160
+ "type": "boolean",
161
+ "default": true
162
+ }
163
+ }
164
+ },
165
+
166
+ "Mechanic": {
167
+ "type": "object",
168
+ "required": ["name", "enabled", "phase", "timing", "trigger_condition"],
169
+ "additionalProperties": false,
170
+ "description": "特殊机制的通用结构,对应 GDL 中 special_mechanics 的 mechanic 模板。支持 raw_definition 以应对无法精确映射的情况。",
171
+ "properties": {
172
+ "name": {
173
+ "type": "string",
174
+ "description": "机制名称"
175
+ },
176
+ "enabled": {
177
+ "type": "boolean",
178
+ "description": "是否启用"
179
+ },
180
+ "description": {
181
+ "type": "string",
182
+ "description": "机制描述"
183
+ },
184
+ "phase": {
185
+ "type": "string",
186
+ "enum": ["setup", "bidding", "doubling", "initiative", "playing", "settlement", "grouping"],
187
+ "description": "所属阶段"
188
+ },
189
+ "timing": {
190
+ "type": "string",
191
+ "enum": ["pre_action", "post_action", "during_action"],
192
+ "description": "触发时机"
193
+ },
194
+ "trigger_condition": {
195
+ "type": "string",
196
+ "description": "触发条件表达式"
197
+ },
198
+ "usage_limit": {
199
+ "type": "string",
200
+ "description": "使用次数限制,如 'once_per_game', 'once_per_round', 'N_times'"
201
+ },
202
+ "min_players": {
203
+ "type": "integer",
204
+ "minimum": 2,
205
+ "description": "最小玩家数"
206
+ },
207
+ "max_players": {
208
+ "type": "integer",
209
+ "minimum": 2,
210
+ "description": "最大玩家数"
211
+ },
212
+ "required_conditions": {
213
+ "type": "array",
214
+ "items": { "type": "string" },
215
+ "description": "前置条件列表"
216
+ },
217
+ "effect_description": {
218
+ "type": "string",
219
+ "description": "效果描述"
220
+ },
221
+ "transfer_path": {
222
+ "$ref": "#/$defs/Transfer"
223
+ },
224
+ "visibility_change": {
225
+ "$ref": "#/$defs/VisibilityChange"
226
+ },
227
+
228
+ "raw_definition": {
229
+ "type": "object",
230
+ "description": "当标准字段无法精确表示 GDL 中的 mechanic 时,存储其原始定义。"
231
+ }
232
+ }
233
+ }
234
+ },
235
+ "properties": {
236
+ "meta": {
237
+ "type": "object",
238
+ "required": [
239
+ "name",
240
+ "version"
241
+ ],
242
+ "additionalProperties": false,
243
+ "description": "玩法元信息",
244
+ "properties": {
245
+ "name": {
246
+ "type": "string"
247
+ },
248
+ "version": {
249
+ "type": "string"
250
+ },
251
+ "origin": {
252
+ "type": "string",
253
+ "enum": [
254
+ "Traditional",
255
+ "Variant",
256
+ "Innovative"
257
+ ],
258
+ "default": "Variant"
259
+ },
260
+ "seeded": {
261
+ "type": "boolean",
262
+ "default": true
263
+ },
264
+ "description": {
265
+ "type": "string"
266
+ }
267
+ }
268
+ },
269
+ "players": {
270
+ "type": "object",
271
+ "required": [
272
+ "count"
273
+ ],
274
+ "additionalProperties": false,
275
+ "description": "玩家配置",
276
+ "properties": {
277
+ "count": {
278
+ "type": "integer",
279
+ "minimum": 2,
280
+ "maximum": 6
281
+ },
282
+ "roles": {
283
+ "type": "array",
284
+ "items": {
285
+ "type": "object",
286
+ "required": ["name", "count"],
287
+ "properties": {
288
+ "name": { "type": "string" },
289
+ "count": { "type": "integer", "minimum": 1 },
290
+ "players": {
291
+ "type": "array",
292
+ "items": { "$ref": "#/$defs/PID" },
293
+ "description": "属于此角色的具体玩家ID列表,例如 ['Landlord', 'Peasant_1', 'Peasant_2']。此字段为可选扩展。"
294
+ },
295
+ "team": { "type": "string" },
296
+ "special_ability": { "type": "string" }
297
+ },
298
+ "additionalProperties": false
299
+ }
300
+ },
301
+ "teams": {
302
+ "type": "array",
303
+ "items": {
304
+ "type": "array",
305
+ "items": {
306
+ "$ref": "#/$defs/PID"
307
+ }
308
+ }
309
+ }
310
+ }
311
+ },
312
+ "cards": {
313
+ "type": "object",
314
+ "required": [
315
+ "ranks",
316
+ "suits"
317
+ ],
318
+ "additionalProperties": false,
319
+ "description": "牌组配置",
320
+ "properties": {
321
+ "ranks": {
322
+ "type": "array",
323
+ "items": {
324
+ "$ref": "#/$defs/Rank"
325
+ },
326
+ "minItems": 1,
327
+ "description": "点数列表"
328
+ },
329
+ "suits": {
330
+ "type": "array",
331
+ "items": {
332
+ "$ref": "#/$defs/Suit"
333
+ },
334
+ "description": "花色列表"
335
+ },
336
+ "jokers": {
337
+ "type": "object",
338
+ "additionalProperties": false,
339
+ "description": "大小王点数映射",
340
+ "properties": {
341
+ "small": {
342
+ "$ref": "#/$defs/Rank"
343
+ },
344
+ "big": {
345
+ "$ref": "#/$defs/Rank"
346
+ }
347
+ }
348
+ },
349
+ "copies_per_deck": {
350
+ "type": "integer",
351
+ "minimum": 1,
352
+ "default": 1,
353
+ "description": "每副牌的复制份数"
354
+ },
355
+ "num_decks": {
356
+ "type": "integer",
357
+ "minimum": 1,
358
+ "default": 1,
359
+ "description": "使用副数"
360
+ },
361
+ "straight_exclude": {
362
+ "type": "array",
363
+ "items": {
364
+ "$ref": "#/$defs/Rank"
365
+ },
366
+ "description": "顺子禁入点数,如 2/王"
367
+ },
368
+ "per_rank_copies": {
369
+ "type": "object",
370
+ "additionalProperties": {
371
+ "type": "integer",
372
+ "minimum": 0
373
+ },
374
+ "description": "【v0.93】按点数单独指定张数,如 {\"16\":1,\"17\":1,\"18\":1}"
375
+ },
376
+ "suitless_ranks": {
377
+ "type": "array",
378
+ "items": {
379
+ "$ref": "#/$defs/Rank"
380
+ },
381
+ "description": "【v0.93】无花色点数(王、龙等不随花色展开)"
382
+ }
383
+ }
384
+ },
385
+ "zones": {
386
+ "type": "array",
387
+ "items": {
388
+ "$ref": "#/$defs/ZoneID"
389
+ },
390
+ "minItems": 1,
391
+ "uniqueItems": true,
392
+ "description": "区域列表:hand:*, field, main_deck, discard_pile, reserve_zone, public_pool, item_deck:*, history_deck ..."
393
+ },
394
+ "visibility": {
395
+ "type": "object",
396
+ "additionalProperties": false,
397
+ "description": "区域可见性设定",
398
+ "properties": {
399
+ "defaults": {
400
+ "type": "object",
401
+ "additionalProperties": {
402
+ "$ref": "#/$defs/VisibilityState"
403
+ },
404
+ "description": "全局默认"
405
+ },
406
+ "by_zone": {
407
+ "type": "object",
408
+ "additionalProperties": {
409
+ "type": "object",
410
+ "additionalProperties": false,
411
+ "properties": {
412
+ "state": {
413
+ "$ref": "#/$defs/VisibilityState"
414
+ },
415
+ "audience": {
416
+ "type": "array",
417
+ "items": {
418
+ "$ref": "#/$defs/Audience"
419
+ }
420
+ }
421
+ }
422
+ },
423
+ "description": "按区覆盖"
424
+ }
425
+ }
426
+ },
427
+ "invariants": {
428
+ "type": "object",
429
+ "additionalProperties": false,
430
+ "description": "不变量约束",
431
+ "properties": {
432
+ "physical_conservation": {
433
+ "type": "boolean",
434
+ "default": true,
435
+ "description": "物理守恒:总牌数不变"
436
+ },
437
+ "no_card_from_void": {
438
+ "type": "boolean",
439
+ "default": true,
440
+ "description": "禁止无源牌"
441
+ },
442
+ "max_hand_size": {
443
+ "type": "integer"
444
+ },
445
+ "max_public_pool": {
446
+ "type": "integer"
447
+ },
448
+ "same_rank_max": {
449
+ "type": "integer",
450
+ "minimum": 1
451
+ },
452
+ "sequence_span_max": {
453
+ "type": "integer",
454
+ "minimum": 1
455
+ },
456
+ "team_membership_consistent": {
457
+ "type": "boolean",
458
+ "default": true
459
+ },
460
+ "all_transfers_use_defined_zones": {
461
+ "type": "boolean",
462
+ "default": true
463
+ },
464
+ "no_card_appears_in_undefined_zone": {
465
+ "type": "boolean",
466
+ "default": true
467
+ }
468
+ }
469
+ },
470
+ "card_relations": {
471
+ "type": "object",
472
+ "additionalProperties": false,
473
+ "description": "牌值与关系定义",
474
+ "properties": {
475
+ "card_values": {
476
+ "type": "array",
477
+ "items": {
478
+ "$ref": "#/$defs/Rank"
479
+ },
480
+ "description": "牌值大小顺序,从低到高"
481
+ },
482
+ "used_in": {
483
+ "type": "array",
484
+ "items": { "type": "string" },
485
+ "description": "指定使用位置"
486
+ },
487
+ "continuous_relations": {
488
+ "type": "array",
489
+ "items": {
490
+ "type": "object",
491
+ "properties": {
492
+ "name": { "type": "string" },
493
+ "sequence": { "type": "array", "items": { "$ref": "#/$defs/Rank" } },
494
+ "used_in": { "type": "array", "items": { "type": "string" } }
495
+ }
496
+ },
497
+ "description": "连续关系定义"
498
+ },
499
+ "non_continuous_cards": {
500
+ "type": "array",
501
+ "items": {
502
+ "$ref": "#/$defs/Rank"
503
+ },
504
+ "description": "非连续牌"
505
+ },
506
+ "same_value_relation": {
507
+ "type": "string",
508
+ "enum": ["same_number", "same_suit"],
509
+ "default": "same_number"
510
+ },
511
+ "ace_position": {
512
+ "type": "string",
513
+ "enum": ["first_last_second_last", "anywhere"],
514
+ "default": "anywhere"
515
+ },
516
+ "two_position": {
517
+ "type": "string",
518
+ "enum": ["first_second_first_last", "anywhere"],
519
+ "default": "anywhere"
520
+ },
521
+ "suit_relations": {
522
+ "type": "object",
523
+ "properties": {
524
+ "order": {
525
+ "type": "array",
526
+ "items": {
527
+ "$ref": "#/$defs/Suit"
528
+ }
529
+ },
530
+ "used_in": { "type": "array", "items": { "type": "string" } }
531
+ }
532
+ },
533
+ "joker_rules": {
534
+ "type": "object",
535
+ "properties": {
536
+ "mode": {
537
+ "type": "string",
538
+ "enum": ["off", "wild_only", "power_only", "wild_and_power"],
539
+ "default": "off"
540
+ },
541
+ "exclusivity": {
542
+ "type": "string",
543
+ "enum": ["exclusive", "hybrid"]
544
+ },
545
+ "wildcard_scope": {
546
+ "type": "array",
547
+ "items": {
548
+ "$ref": "#/$defs/ComboType"
549
+ },
550
+ "description": "赖子作用域"
551
+ },
552
+ "wildcard_loses_rank": {
553
+ "type": "boolean",
554
+ "default": false,
555
+ "description": "赖子是否失去单牌牌力"
556
+ },
557
+ "power_single": {
558
+ "type": "object",
559
+ "properties": {
560
+ "enabled": { "type": "boolean" },
561
+ "beats": { "type": "string", "enum": ["all_singles", "rank_list"] },
562
+ "rank_list": { "type": "array", "items": { "type": "string" } }
563
+ }
564
+ },
565
+ "power_pair": {
566
+ "type": "object",
567
+ "properties": {
568
+ "enabled": { "type": "boolean" },
569
+ "beats": { "type": "string", "enum": ["all_pairs", "rank_list"] },
570
+ "rank_list": { "type": "array", "items": { "type": "string" } }
571
+ }
572
+ }
573
+ }
574
+ },
575
+ "unique_cards": {
576
+ "type": "array",
577
+ "items": { "type": "string" },
578
+ "description": "唯一牌列表,如 Spade3"
579
+ }
580
+ }
581
+ },
582
+ "combinations": {
583
+ "type": "object",
584
+ "additionalProperties": false,
585
+ "description": "牌型族定义",
586
+ "properties": {
587
+ "single": {
588
+ "type": "object"
589
+ },
590
+ "pair": {
591
+ "type": "object"
592
+ },
593
+ "triple": {
594
+ "type": "object"
595
+ },
596
+ "straight": {
597
+ "type": "object",
598
+ "additionalProperties": false,
599
+ "properties": {
600
+ "min_len": {
601
+ "type": "integer",
602
+ "minimum": 1
603
+ },
604
+ "no_ranks": {
605
+ "type": "array",
606
+ "items": {
607
+ "$ref": "#/$defs/Rank"
608
+ }
609
+ }
610
+ },
611
+ "description": "顺子"
612
+ },
613
+ "pairs_chain": {
614
+ "type": "object",
615
+ "additionalProperties": false,
616
+ "properties": {
617
+ "min_len": {
618
+ "type": "integer",
619
+ "minimum": 1
620
+ }
621
+ },
622
+ "description": "连对"
623
+ },
624
+ "airplane": {
625
+ "type": "object",
626
+ "additionalProperties": false,
627
+ "properties": {
628
+ "min_len": {
629
+ "type": "integer",
630
+ "minimum": 1
631
+ },
632
+ "wings": {
633
+ "type": "string",
634
+ "enum": [
635
+ "single",
636
+ "pair",
637
+ "none"
638
+ ],
639
+ "default": "none"
640
+ }
641
+ },
642
+ "description": "飞机(可带翅膀)"
643
+ },
644
+ "triple_with_single": {
645
+ "type": "object"
646
+ },
647
+ "triple_with_pair": {
648
+ "type": "object"
649
+ },
650
+ "four_with_twoSingles": {
651
+ "type": "object"
652
+ },
653
+ "four_with_twoPairs": {
654
+ "type": "object"
655
+ },
656
+ "bomb": {
657
+ "type": "object",
658
+ "additionalProperties": false,
659
+ "properties": {
660
+ "len": {
661
+ "type": "integer",
662
+ "const": 4
663
+ }
664
+ },
665
+ "description": "四张炸"
666
+ },
667
+ "rocket": {
668
+ "type": "object",
669
+ "additionalProperties": false,
670
+ "properties": {
671
+ "ranks": {
672
+ "type": "array",
673
+ "items": {
674
+ "$ref": "#/$defs/Rank"
675
+ }
676
+ }
677
+ },
678
+ "description": "王炸"
679
+ },
680
+ "custom": {
681
+ "type": "array",
682
+ "items": {
683
+ "type": "object",
684
+ "required": [
685
+ "name"
686
+ ],
687
+ "additionalProperties": false,
688
+ "properties": {
689
+ "name": {
690
+ "type": "string"
691
+ },
692
+ "spec": {
693
+ "type": "object"
694
+ }
695
+ }
696
+ },
697
+ "description": "自定义牌型/特殊牌"
698
+ }
699
+ }
700
+ },
701
+ "comparison": {
702
+ "type": "object",
703
+ "required": [
704
+ "same_type",
705
+ "same_len"
706
+ ],
707
+ "additionalProperties": false,
708
+ "description": "比较总则",
709
+ "properties": {
710
+ "same_type": {
711
+ "type": "boolean",
712
+ "description": "是否强制同型比较"
713
+ },
714
+ "same_len": {
715
+ "type": "boolean",
716
+ "description": "是否强制相同长度"
717
+ },
718
+ "bomb_beats_all": {
719
+ "type": "boolean",
720
+ "default": true,
721
+ "description": "炸弹压制普通牌"
722
+ },
723
+ "rocket_top": {
724
+ "type": "boolean",
725
+ "default": true,
726
+ "description": "王炸最大"
727
+ },
728
+ "tiebreaker": {
729
+ "type": "string",
730
+ "enum": [
731
+ "suit",
732
+ "first_played",
733
+ "none"
734
+ ],
735
+ "default": "none",
736
+ "description": "平手裁决"
737
+ }
738
+ }
739
+ },
740
+ "comparison_ex": {
741
+ "type": "object",
742
+ "additionalProperties": false,
743
+ "description": "精细比较规则",
744
+ "properties": {
745
+ "per_combo": {
746
+ "type": "object",
747
+ "additionalProperties": {
748
+ "type": "object",
749
+ "additionalProperties": false,
750
+ "properties": {
751
+ "require_same_length": {
752
+ "type": "boolean",
753
+ "default": true
754
+ },
755
+ "compare_by": {
756
+ "type": "string",
757
+ "enum": [
758
+ "sequence_start",
759
+ "sequence_end",
760
+ "highest_triple_value",
761
+ "main_rank",
762
+ "length_first_then_rank",
763
+ "custom"
764
+ ],
765
+ "default": "main_rank"
766
+ },
767
+ "side_card_rule": {
768
+ "type": "string",
769
+ "enum": [
770
+ "ignore",
771
+ "must_match_shape",
772
+ "custom"
773
+ ],
774
+ "default": "ignore"
775
+ }
776
+ }
777
+ },
778
+ "description": "按牌型的比较键设定"
779
+ },
780
+ "suit_order": {
781
+ "type": "array",
782
+ "items": {
783
+ "$ref": "#/$defs/Suit"
784
+ },
785
+ "description": "花色序(用于平手裁决)"
786
+ },
787
+ "override_matrix": {
788
+ "type": "array",
789
+ "items": {
790
+ "type": "object",
791
+ "additionalProperties": false,
792
+ "required": [
793
+ "overrider",
794
+ "overrides"
795
+ ],
796
+ "properties": {
797
+ "overrider": {
798
+ "type": "string",
799
+ "description": "覆盖者(如 dragon_card)"
800
+ },
801
+ "overrides": {
802
+ "type": "array",
803
+ "items": {
804
+ "type": "string"
805
+ },
806
+ "description": "被覆盖牌型,支持 \"*\""
807
+ },
808
+ "priority": {
809
+ "type": "integer",
810
+ "description": "覆盖优先级"
811
+ }
812
+ }
813
+ },
814
+ "description": "【v0.93】牌型/特殊牌对其它牌型的覆盖矩阵"
815
+ }
816
+ }
817
+ },
818
+ "actions": {
819
+ "type": "object",
820
+ "required": [
821
+ "play",
822
+ "pass"
823
+ ],
824
+ "additionalProperties": false,
825
+ "description": "动作定义(均归约为 Transfer)",
826
+ "properties": {
827
+ "play": {
828
+ "type": "array",
829
+ "items": {
830
+ "type": "object",
831
+ "required": [
832
+ "transfer_path"
833
+ ],
834
+ "additionalProperties": false,
835
+ "properties": {
836
+ "type": {
837
+ "$ref": "#/$defs/ComboType"
838
+ },
839
+ "len": {
840
+ "type": "integer"
841
+ },
842
+ "core": {
843
+ "type": "array",
844
+ "items": { "$ref": "#/$defs/Rank" },
845
+ "description": "核心序列,如 [7,8,9,10,11]"
846
+ },
847
+ "wings": {
848
+ "type": "string",
849
+ "enum": [
850
+ "single",
851
+ "pair",
852
+ "none"
853
+ ]
854
+ },
855
+ "wild_usage": {
856
+ "type": "string",
857
+ "enum": ["limited", "full", "none"]
858
+ },
859
+ "transfer_path": {
860
+ "$ref": "#/$defs/Transfer"
861
+ },
862
+ "visibility_change": {
863
+ "$ref": "#/$defs/VisibilityChange"
864
+ },
865
+
866
+ "raw_definition": {
867
+ "type": "object",
868
+ "description": "当标准字段无法精确表示 GDL 中的 play 动作时,存储其原始定义。"
869
+ }
870
+ }
871
+ },
872
+ "description": "出牌动作列表"
873
+ },
874
+ "pass": {
875
+ "type": "object",
876
+ "required": [
877
+ "transfer_path"
878
+ ],
879
+ "additionalProperties": false,
880
+ "description": "过牌(显式 no-op:field→field)",
881
+ "properties": {
882
+ "transfer_path": {
883
+ "type": "object",
884
+ "required": [
885
+ "from",
886
+ "to"
887
+ ],
888
+ "additionalProperties": false,
889
+ "properties": {
890
+ "from": {
891
+ "const": "field"
892
+ },
893
+ "to": {
894
+ "const": "field"
895
+ }
896
+ }
897
+ }
898
+ }
899
+ },
900
+ "reveal_bottom": {
901
+ "type": "array",
902
+ "items": {
903
+ "$ref": "#/$defs/Transfer"
904
+ },
905
+ "description": "翻底"
906
+ },
907
+ "cleanup_trick": {
908
+ "type": "array",
909
+ "items": {
910
+ "$ref": "#/$defs/Transfer"
911
+ },
912
+ "description": "清台/收墩"
913
+ },
914
+ "other": {
915
+ "type": "object",
916
+ "additionalProperties": {
917
+ "type": "array",
918
+ "items": {
919
+ "$ref": "#/$defs/Transfer"
920
+ }
921
+ },
922
+ "description": "自定义动作名称 → Transfer 序列"
923
+ }
924
+ }
925
+ },
926
+ "phases": {
927
+ "type": "array",
928
+ "items": {
929
+ "type": "string",
930
+ "enum": [
931
+ "setup",
932
+ "deal",
933
+ "bid",
934
+ "double",
935
+ "initiative",
936
+ "play",
937
+ "settle",
938
+ "grouping"
939
+ ]
940
+ },
941
+ "uniqueItems": true,
942
+ "description": "阶段流转"
943
+ },
944
+ "turns": {
945
+ "type": "object",
946
+ "required": [
947
+ "order",
948
+ "trick_end_on",
949
+ "next_leader"
950
+ ],
951
+ "additionalProperties": false,
952
+ "description": "轮转/收墩",
953
+ "properties": {
954
+ "order": {
955
+ "type": "array",
956
+ "items": {
957
+ "$ref": "#/$defs/PID"
958
+ },
959
+ "minItems": 2,
960
+ "description": "回合顺序"
961
+ },
962
+ "leader": {
963
+ "$ref": "#/$defs/PID",
964
+ "description": "首个出牌者"
965
+ },
966
+ "trick_end_on": {
967
+ "type": "string",
968
+ "enum": [
969
+ "two_pass",
970
+ "all_pass_except_last",
971
+ "custom"
972
+ ],
973
+ "description": "一墩结束条件"
974
+ },
975
+ "next_leader": {
976
+ "type": "string",
977
+ "enum": [
978
+ "last_non_pass",
979
+ "winner",
980
+ "fixed",
981
+ "custom"
982
+ ],
983
+ "description": "下一轮领出者"
984
+ }
985
+ }
986
+ },
987
+ "ending": {
988
+ "type": "object",
989
+ "required": [
990
+ "when"
991
+ ],
992
+ "additionalProperties": false,
993
+ "description": "终局条件",
994
+ "properties": {
995
+ "when": {
996
+ "type": "string",
997
+ "enum": [
998
+ "any_hand_empty",
999
+ "stock_empty",
1000
+ "rounds_exhausted",
1001
+ "custom"
1002
+ ]
1003
+ }
1004
+ }
1005
+ },
1006
+ "scoring": {
1007
+ "type": "object",
1008
+ "required": [
1009
+ "base"
1010
+ ],
1011
+ "additionalProperties": false,
1012
+ "description": "基础计分与倍率封顶",
1013
+ "properties": {
1014
+ "base": {
1015
+ "type": "integer",
1016
+ "minimum": 0,
1017
+ "default": 1
1018
+ },
1019
+ "multipliers": {
1020
+ "type": "object",
1021
+ "additionalProperties": {
1022
+ "type": "number"
1023
+ },
1024
+ "description": "事件→倍率 映射"
1025
+ },
1026
+ "cap": {
1027
+ "type": "integer",
1028
+ "description": "总封顶(简单场景)"
1029
+ },
1030
+ "spring_rule": {
1031
+ "type": "string",
1032
+ "enum": [
1033
+ "strict",
1034
+ "lenient",
1035
+ "none"
1036
+ ],
1037
+ "default": "none",
1038
+ "description": "春天/反春判定"
1039
+ },
1040
+ "expression": {
1041
+ "type": "string",
1042
+ "description": "可选:S-表达式形式的最终公式(实现自定)"
1043
+ },
1044
+ "capping": {
1045
+ "type": "object",
1046
+ "additionalProperties": false,
1047
+ "description": "【v0.93】更细粒度封顶",
1048
+ "properties": {
1049
+ "winner_max_gain": {
1050
+ "oneOf": [
1051
+ {
1052
+ "type": "number"
1053
+ },
1054
+ {
1055
+ "type": "string"
1056
+ }
1057
+ ]
1058
+ },
1059
+ "loser_max_loss": {
1060
+ "oneOf": [
1061
+ {
1062
+ "type": "number"
1063
+ },
1064
+ {
1065
+ "type": "string"
1066
+ }
1067
+ ]
1068
+ }
1069
+ }
1070
+ }
1071
+ }
1072
+ },
1073
+ "initiative": {
1074
+ "type": "object",
1075
+ "additionalProperties": false,
1076
+ "description": "先手机制",
1077
+ "properties": {
1078
+ "mode": {
1079
+ "type": "string",
1080
+ "enum": [
1081
+ "spade3",
1082
+ "random",
1083
+ "max_card",
1084
+ "min_card",
1085
+ "custom"
1086
+ ]
1087
+ },
1088
+ "params": {
1089
+ "type": "object"
1090
+ },
1091
+ "deterministic": {
1092
+ "type": "boolean",
1093
+ "default": true
1094
+ }
1095
+ }
1096
+ },
1097
+ "setup_ex": {
1098
+ "type": "object",
1099
+ "additionalProperties": false,
1100
+ "description": "扩展设置",
1101
+ "properties": {
1102
+ "special_dealing": {
1103
+ "type": "object",
1104
+ "additionalProperties": false,
1105
+ "properties": {
1106
+ "chain_bomb_distribution": {
1107
+ "type": "object",
1108
+ "additionalProperties": false,
1109
+ "properties": {
1110
+ "enabled": {
1111
+ "type": "boolean",
1112
+ "default": false
1113
+ },
1114
+ "counts": {
1115
+ "type": "object",
1116
+ "additionalProperties": {
1117
+ "type": "number"
1118
+ }
1119
+ }
1120
+ }
1121
+ }
1122
+ }
1123
+ },
1124
+ "landlord_card_selection": {
1125
+ "type": "object",
1126
+ "additionalProperties": false,
1127
+ "description": "地主底牌分配",
1128
+ "properties": {
1129
+ "timing": {
1130
+ "type": "string",
1131
+ "enum": [
1132
+ "after_bomb_distribution",
1133
+ "default",
1134
+ "custom"
1135
+ ],
1136
+ "default": "default"
1137
+ },
1138
+ "method": {
1139
+ "type": "string",
1140
+ "enum": [
1141
+ "strategic",
1142
+ "random",
1143
+ "custom"
1144
+ ],
1145
+ "default": "random"
1146
+ },
1147
+ "selection_process": {
1148
+ "type": "string"
1149
+ },
1150
+ "criteria": {
1151
+ "type": "object",
1152
+ "properties": {
1153
+ "can_form_bomb": { "type": "boolean" },
1154
+ "can_form_triple": { "type": "boolean" },
1155
+ "value_threshold": { "type": "string", "enum": ["high", "medium", "low"] }
1156
+ }
1157
+ },
1158
+ "fallback_method": {
1159
+ "type": "string",
1160
+ "enum": ["last_n_cards", "random"]
1161
+ },
1162
+ "fallback_count": { "type": "integer" },
1163
+ "distribution_pattern": { "type": "string" }
1164
+ }
1165
+ },
1166
+ "grouping": {
1167
+ "type": "object",
1168
+ "additionalProperties": false,
1169
+ "description": "【v0.93】分组设定",
1170
+ "properties": {
1171
+ "mode": {
1172
+ "type": "string"
1173
+ },
1174
+ "groups": {
1175
+ "type": "array",
1176
+ "items": {
1177
+ "type": "string"
1178
+ }
1179
+ },
1180
+ "assign_rules": {
1181
+ "type": "object"
1182
+ },
1183
+ "deal_bias": {
1184
+ "type": "object"
1185
+ },
1186
+ "bonuses": {
1187
+ "type": "object"
1188
+ }
1189
+ }
1190
+ },
1191
+ "replenishment_rules": {
1192
+ "type": "object",
1193
+ "additionalProperties": false,
1194
+ "properties": {
1195
+ "enabled": { "type": "boolean" },
1196
+ "cards_per_player_per_round": { "type": "integer", "minimum": 0 },
1197
+ "source": { "$ref": "#/$defs/ZoneID" },
1198
+ "termination_condition": { "type": "string" },
1199
+ "special_cases": {
1200
+ "type": "object",
1201
+ "properties": {
1202
+ "if_remaining_cards_less_than_n": { "type": "string" },
1203
+ "if_player_cannot_take_all": { "type": "string" }
1204
+ }
1205
+ },
1206
+ "excess_card_handling": { "type": "string" }
1207
+ }
1208
+ },
1209
+ "guessing_rules": {
1210
+ "type": "object",
1211
+ "additionalProperties": false,
1212
+ "properties": {
1213
+ "enabled": { "type": "boolean" },
1214
+ "max_extra_cards": { "type": "integer", "minimum": 0 },
1215
+ "success_reward_multiplier": { "type": "string" },
1216
+ "success_reward_multiplier_for_only_extra": { "type": "string" },
1217
+ "failure_penalty": { "type": "string" },
1218
+ "failure_consequence": { "type": "string" },
1219
+ "farmer_failure_continues_to_landlord": { "type": "boolean" },
1220
+ "source": { "$ref": "#/$defs/ZoneID" }
1221
+ }
1222
+ }
1223
+ }
1224
+ },
1225
+ "information_model": {
1226
+ "type": "object",
1227
+ "additionalProperties": false,
1228
+ "description": "信息模型(观战、日志、窥视)",
1229
+ "properties": {
1230
+ "spectator_view": {
1231
+ "type": "string",
1232
+ "enum": [
1233
+ "none",
1234
+ "public",
1235
+ "delayed"
1236
+ ],
1237
+ "default": "none"
1238
+ },
1239
+ "logs": {
1240
+ "type": "string",
1241
+ "enum": [
1242
+ "redacted",
1243
+ "full"
1244
+ ],
1245
+ "default": "redacted"
1246
+ },
1247
+ "peek_actions": {
1248
+ "type": "object",
1249
+ "additionalProperties": false,
1250
+ "properties": {
1251
+ "enabled": {
1252
+ "type": "boolean",
1253
+ "default": false
1254
+ },
1255
+ "who": {
1256
+ "type": "string"
1257
+ },
1258
+ "scope": {
1259
+ "type": "array",
1260
+ "items": {
1261
+ "$ref": "#/$defs/ZoneID"
1262
+ }
1263
+ },
1264
+ "limits": {
1265
+ "type": "object",
1266
+ "additionalProperties": false,
1267
+ "properties": {
1268
+ "per_round": {
1269
+ "type": "integer",
1270
+ "minimum": 0
1271
+ },
1272
+ "per_game": {
1273
+ "type": "integer",
1274
+ "minimum": 0
1275
+ }
1276
+ }
1277
+ }
1278
+ }
1279
+ }
1280
+ }
1281
+ },
1282
+
1283
+ "special_mechanics": {
1284
+ "type": "array",
1285
+ "items": {
1286
+ "$ref": "#/$defs/Mechanic"
1287
+ },
1288
+ "description": "特殊机制定义,对应 GDL 中 special_mechanics 的通用 mechanic 模板。支持 raw_definition 以应对无法精确映射的情况。"
1289
+ },
1290
+ "illegal_action": {
1291
+ "type": "object",
1292
+ "additionalProperties": false,
1293
+ "description": "非法动作策略",
1294
+ "properties": {
1295
+ "policy": {
1296
+ "type": "string",
1297
+ "enum": [
1298
+ "skip_turn",
1299
+ "forfeit",
1300
+ "warn"
1301
+ ],
1302
+ "default": "warn"
1303
+ },
1304
+ "penalty": {
1305
+ "type": "string"
1306
+ }
1307
+ }
1308
+ },
1309
+ "grouping_phase": {
1310
+ "type": "object",
1311
+ "additionalProperties": false,
1312
+ "description": "分组阶段(对局内)",
1313
+ "properties": {
1314
+ "enabled": {
1315
+ "type": "boolean",
1316
+ "default": false
1317
+ },
1318
+ "mode": {
1319
+ "type": "string",
1320
+ "enum": [
1321
+ "ace_grouping",
1322
+ "custom"
1323
+ ],
1324
+ "default": "custom"
1325
+ },
1326
+ "dynamic_teams": {
1327
+ "type": "boolean",
1328
+ "default": false
1329
+ },
1330
+ "first_turn": {
1331
+ "type": "string"
1332
+ }
1333
+ }
1334
+ },
1335
+ "rank_position_rules": {
1336
+ "type": "object",
1337
+ "additionalProperties": false,
1338
+ "description": "顺子/连对的点数位置限制",
1339
+ "properties": {
1340
+ "A": {
1341
+ "type": "string",
1342
+ "enum": [
1343
+ "first_last_second_last",
1344
+ "anywhere"
1345
+ ],
1346
+ "default": "anywhere"
1347
+ },
1348
+ "2": {
1349
+ "type": "string",
1350
+ "enum": [
1351
+ "first_second_first_last",
1352
+ "anywhere"
1353
+ ],
1354
+ "default": "anywhere"
1355
+ }
1356
+ }
1357
+ },
1358
+ "scoring_ex": {
1359
+ "type": "object",
1360
+ "additionalProperties": false,
1361
+ "description": "计分表达式与顺序",
1362
+ "properties": {
1363
+ "expr": {
1364
+ "type": "object",
1365
+ "description": "AST风格表达式",
1366
+ "properties": {
1367
+ "type": {
1368
+ "type": "string"
1369
+ },
1370
+ "args": {
1371
+ "type": "array",
1372
+ "items": {
1373
+ "$ref": "#/properties/scoring_ex/properties/expr"
1374
+ }
1375
+ },
1376
+ "var": {
1377
+ "type": "string"
1378
+ }
1379
+ }
1380
+ },
1381
+ "order_of_application": {
1382
+ "type": "array",
1383
+ "items": {
1384
+ "type": "string"
1385
+ }
1386
+ }
1387
+ }
1388
+ },
1389
+ "override_policy": {
1390
+ "type": "object",
1391
+ "additionalProperties": false,
1392
+ "description": "覆盖策略(谁覆盖谁)",
1393
+ "properties": {
1394
+ "turn_order": {
1395
+ "type": "string",
1396
+ "enum": [
1397
+ "playing_phase_overrides_global",
1398
+ "inherit"
1399
+ ],
1400
+ "default": "inherit"
1401
+ },
1402
+ "deal_method": {
1403
+ "type": "string",
1404
+ "enum": [
1405
+ "setup_overrides_deck",
1406
+ "inherit"
1407
+ ],
1408
+ "default": "inherit"
1409
+ },
1410
+ "unspecified": {
1411
+ "type": "string",
1412
+ "enum": [
1413
+ "inherits",
1414
+ "error"
1415
+ ],
1416
+ "default": "inherits"
1417
+ },
1418
+ "on_conflict": {
1419
+ "type": "string",
1420
+ "enum": [
1421
+ "error",
1422
+ "last_wins"
1423
+ ],
1424
+ "default": "error"
1425
+ }
1426
+ }
1427
+ },
1428
+ "history_deck": {
1429
+ "type": "object",
1430
+ "additionalProperties": false,
1431
+ "description": "历史牌堆(记牌/回放)",
1432
+ "properties": {
1433
+ "enabled": {
1434
+ "type": "boolean",
1435
+ "default": false
1436
+ },
1437
+ "size": {
1438
+ "type": "integer",
1439
+ "minimum": 0,
1440
+ "default": 0
1441
+ },
1442
+ "replacement_policy": {
1443
+ "type": "string",
1444
+ "enum": [
1445
+ "fifo",
1446
+ "lru",
1447
+ "none"
1448
+ ],
1449
+ "default": "fifo"
1450
+ },
1451
+ "max_size": {
1452
+ "type": "integer",
1453
+ "minimum": 0,
1454
+ "default": 0
1455
+ }
1456
+ }
1457
+ },
1458
+ "bidding_model": {
1459
+ "type": "object",
1460
+ "additionalProperties": false,
1461
+ "description": "【v0.93】竞价流程模型",
1462
+ "properties": {
1463
+ "mode": {
1464
+ "type": "string"
1465
+ },
1466
+ "options": {
1467
+ "type": "array",
1468
+ "items": {
1469
+ "type": "string"
1470
+ }
1471
+ },
1472
+ "order": {
1473
+ "type": "array",
1474
+ "items": {
1475
+ "$ref": "#/$defs/PID"
1476
+ }
1477
+ },
1478
+ "multiplier_effect": {
1479
+ "type": "object"
1480
+ }
1481
+ }
1482
+ },
1483
+ "doubling_model": {
1484
+ "type": "object",
1485
+ "additionalProperties": false,
1486
+ "description": "【v0.93】加倍流程模型",
1487
+ "properties": {
1488
+ "options": {
1489
+ "type": "array",
1490
+ "items": {
1491
+ "type": "string"
1492
+ }
1493
+ },
1494
+ "scope": {
1495
+ "type": "string",
1496
+ "enum": [
1497
+ "personal",
1498
+ "team",
1499
+ "global",
1500
+ "custom"
1501
+ ]
1502
+ },
1503
+ "stack_rule": {
1504
+ "type": "string"
1505
+ }
1506
+ }
1507
+ },
1508
+
1509
+ "extensions": {
1510
+ "type": "object",
1511
+ "description": "用于容纳当前 Schema 无法表示的 GDL 特性或未来扩展。实现时应谨慎处理其中的内容。",
1512
+ "additionalProperties": true
1513
+ }
1514
+ }
1515
+ }
poker_gdl_ir.schema.v0.96.zh.json ADDED
@@ -0,0 +1,1746 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema ",
3
+ "$id": "https://example.org/schemas/poker_gdl_ir.schema.v0.96.zh.json",
4
+ "title": "Poker-GDL IR Schema v0.96(含中文注释)",
5
+ "description": "扑克类(牌型压制类)玩法的 GDL → IR 标准。v0.95 优化:采用策略一,引入 raw_definition 和 extensions 字段以增强 Schema 扩展性。",
6
+ "type": "object",
7
+ "required": [
8
+ "meta",
9
+ "players",
10
+ "cards",
11
+ "zones",
12
+ "visibility",
13
+ "combinations",
14
+ "comparison",
15
+ "actions",
16
+ "phases",
17
+ "turns",
18
+ "ending",
19
+ "scoring"
20
+ ],
21
+ "additionalProperties": false,
22
+ "$defs": {
23
+ "PID": {
24
+ "type": "string",
25
+ "pattern": "^[A-Za-z0-9_:-]+$",
26
+ "description": "玩家/角色标识,如 L、P1、P2"
27
+ },
28
+ "ZoneID": {
29
+ "type": "string",
30
+ "pattern": "^(hand:[A-Za-z0-9_:-]+|field|main_deck|discard_pile|reserve_zone|public_pool|item_deck:[A-Za-z0-9_:-]+|history_deck)$",
31
+ "description": "区域标识;手牌需带所属者,如 hand:L;道具牌库需带名称,如 item_deck:BulletCards"
32
+ },
33
+ "Audience": {
34
+ "type": "string",
35
+ "enum": [
36
+ "owner",
37
+ "teammates",
38
+ "enemies",
39
+ "all",
40
+ "none"
41
+ ],
42
+ "description": "可见性受众"
43
+ },
44
+ "VisibilityState": {
45
+ "type": "string",
46
+ "enum": [
47
+ "visible",
48
+ "hidden",
49
+ "partial_suit",
50
+ "partial_rank"
51
+ ],
52
+ "description": "可见性枚举"
53
+ },
54
+ "Rank": {
55
+ "type": "integer",
56
+ "description": "推荐映射:3..15=3..A, 16=小王, 17=大王"
57
+ },
58
+ "Suit": {
59
+ "type": "string",
60
+ "enum": [
61
+ "Spade",
62
+ "Heart",
63
+ "Club",
64
+ "Diamond"
65
+ ],
66
+ "description": "花色,如 S/H/D/C (现在使用GDL中的标准名称)"
67
+ },
68
+ "ComboType": {
69
+ "type": "string",
70
+ "enum": [
71
+ "single",
72
+ "pair",
73
+ "triple",
74
+ "straight",
75
+ "pairs_chain",
76
+ "airplane",
77
+ "triple_with_single",
78
+ "triple_with_pair",
79
+ "four_with_twoSingles",
80
+ "four_with_twoPairs",
81
+ "bomb",
82
+ "rocket",
83
+ "double_chain_bomb",
84
+ "triple_chain_bomb",
85
+ "quad_chain_bomb",
86
+ "penta_chain_bomb",
87
+ "chain_triple",
88
+ "custom"
89
+ ],
90
+ "description": "牌型名"
91
+ },
92
+ "Transfer": {
93
+ "type": "object",
94
+ "required": [
95
+ "from",
96
+ "to"
97
+ ],
98
+ "additionalProperties": false,
99
+ "description": "原子转移:从某区域移动若干牌到另一区域;所有动作最终都可分解为 Transfer 序列。",
100
+ "properties": {
101
+ "from": {
102
+ "$ref": "#/$defs/ZoneID"
103
+ },
104
+ "to": {
105
+ "$ref": "#/$defs/ZoneID"
106
+ },
107
+ "count": {
108
+ "oneOf": [
109
+ {
110
+ "type": "integer",
111
+ "minimum": 0
112
+ },
113
+ {
114
+ "const": "all"
115
+ }
116
+ ],
117
+ "description": "移动数量;all 表示全部"
118
+ },
119
+ "filter": {
120
+ "type": "object",
121
+ "additionalProperties": false,
122
+ "description": "筛选器:按点数/花色/标签选择牌",
123
+ "properties": {
124
+ "ranks": {
125
+ "type": "array",
126
+ "items": {
127
+ "$ref": "#/$defs/Rank"
128
+ }
129
+ },
130
+ "suits": {
131
+ "type": "array",
132
+ "items": {
133
+ "$ref": "#/$defs/Suit"
134
+ }
135
+ },
136
+ "tags": {
137
+ "type": "array",
138
+ "items": {
139
+ "type": "string"
140
+ }
141
+ }
142
+ }
143
+ }
144
+ }
145
+ },
146
+ "VisibilityChange": {
147
+ "type": "object",
148
+ "required": [
149
+ "to",
150
+ "state"
151
+ ],
152
+ "additionalProperties": false,
153
+ "description": "动作引发的可见性变更",
154
+ "properties": {
155
+ "to": {
156
+ "type": "array",
157
+ "items": {
158
+ "$ref": "#/$defs/Audience"
159
+ }
160
+ },
161
+ "state": {
162
+ "$ref": "#/$defs/VisibilityState"
163
+ },
164
+ "on_target": {
165
+ "type": "boolean",
166
+ "default": true
167
+ }
168
+ }
169
+ },
170
+ "Mechanic": {
171
+ "type": "object",
172
+ "required": [
173
+ "name",
174
+ "enabled",
175
+ "phase",
176
+ "timing",
177
+ "trigger_condition"
178
+ ],
179
+ "additionalProperties": false,
180
+ "description": "特殊机制的通用结构,对应 GDL 中 special_mechanics 的 mechanic 模板。支持 raw_definition 以应对无法精确映射的情况。",
181
+ "properties": {
182
+ "name": {
183
+ "type": "string",
184
+ "description": "机制名称"
185
+ },
186
+ "enabled": {
187
+ "type": "boolean",
188
+ "description": "是否启用"
189
+ },
190
+ "description": {
191
+ "type": "string",
192
+ "description": "机制描述"
193
+ },
194
+ "phase": {
195
+ "type": "string",
196
+ "enum": [
197
+ "setup",
198
+ "bidding",
199
+ "doubling",
200
+ "initiative",
201
+ "playing",
202
+ "settlement",
203
+ "grouping"
204
+ ],
205
+ "description": "所属阶段"
206
+ },
207
+ "timing": {
208
+ "type": "string",
209
+ "enum": [
210
+ "pre_action",
211
+ "post_action",
212
+ "during_action"
213
+ ],
214
+ "description": "触发时机"
215
+ },
216
+ "trigger_condition": {
217
+ "type": "string",
218
+ "description": "触发条件表达式"
219
+ },
220
+ "usage_limit": {
221
+ "type": "string",
222
+ "description": "使用次数限制,如 'once_per_game', 'once_per_round', 'N_times'"
223
+ },
224
+ "min_players": {
225
+ "type": "integer",
226
+ "minimum": 2,
227
+ "description": "最小玩家数"
228
+ },
229
+ "max_players": {
230
+ "type": "integer",
231
+ "minimum": 2,
232
+ "description": "最大玩家数"
233
+ },
234
+ "required_conditions": {
235
+ "type": "array",
236
+ "items": {
237
+ "type": "string"
238
+ },
239
+ "description": "前置条件列表"
240
+ },
241
+ "effect_description": {
242
+ "type": "string",
243
+ "description": "效果描述"
244
+ },
245
+ "transfer_path": {
246
+ "$ref": "#/$defs/Transfer"
247
+ },
248
+ "visibility_change": {
249
+ "$ref": "#/$defs/VisibilityChange"
250
+ },
251
+ "raw_definition": {
252
+ "type": "object",
253
+ "description": "当标准字段无法精确表示 GDL 中的 mechanic 时,存储其原始定义。"
254
+ }
255
+ }
256
+ },
257
+ "turns_mutations": {
258
+ "type": "object",
259
+ "description": "回合顺序变更:反转、跳过或改定庄",
260
+ "properties": {
261
+ "reverse": {
262
+ "type": "boolean",
263
+ "description": "是否反转出牌顺序"
264
+ },
265
+ "skip_next": {
266
+ "type": "boolean",
267
+ "description": "是否跳过下一位"
268
+ },
269
+ "set_leader": {
270
+ "oneOf": [
271
+ {
272
+ "type": "string",
273
+ "enum": [
274
+ "winner",
275
+ "last_non_pass"
276
+ ],
277
+ "description": "根据结果设置下一墩先手"
278
+ },
279
+ {
280
+ "type": "string",
281
+ "pattern": "^fixed:P\\d+$",
282
+ "description": "固定设置先手为某 PID,如 fixed:P1"
283
+ }
284
+ ]
285
+ }
286
+ },
287
+ "additionalProperties": false
288
+ }
289
+ },
290
+ "properties": {
291
+ "meta": {
292
+ "type": "object",
293
+ "required": [
294
+ "name",
295
+ "version"
296
+ ],
297
+ "additionalProperties": false,
298
+ "description": "玩法元信息",
299
+ "properties": {
300
+ "name": {
301
+ "type": "string"
302
+ },
303
+ "version": {
304
+ "type": "string"
305
+ },
306
+ "origin": {
307
+ "type": "string",
308
+ "enum": [
309
+ "Traditional",
310
+ "Variant",
311
+ "Innovative"
312
+ ],
313
+ "default": "Variant"
314
+ },
315
+ "seeded": {
316
+ "type": "boolean",
317
+ "default": true
318
+ },
319
+ "description": {
320
+ "type": "string"
321
+ }
322
+ }
323
+ },
324
+ "players": {
325
+ "type": "object",
326
+ "required": [
327
+ "count"
328
+ ],
329
+ "additionalProperties": false,
330
+ "description": "玩家配置",
331
+ "properties": {
332
+ "count": {
333
+ "type": "integer",
334
+ "minimum": 2,
335
+ "maximum": 6
336
+ },
337
+ "roles": {
338
+ "type": "array",
339
+ "items": {
340
+ "type": "object",
341
+ "required": [
342
+ "name",
343
+ "count"
344
+ ],
345
+ "properties": {
346
+ "name": {
347
+ "type": "string"
348
+ },
349
+ "count": {
350
+ "type": "integer",
351
+ "minimum": 1
352
+ },
353
+ "players": {
354
+ "type": "array",
355
+ "items": {
356
+ "$ref": "#/$defs/PID"
357
+ },
358
+ "description": "属于此角色的具体玩家ID列表,例如 ['Landlord', 'Peasant_1', 'Peasant_2']。此字段为可选扩展。"
359
+ },
360
+ "team": {
361
+ "type": "string"
362
+ },
363
+ "special_ability": {
364
+ "type": "string"
365
+ }
366
+ },
367
+ "additionalProperties": false
368
+ }
369
+ },
370
+ "teams": {
371
+ "type": "array",
372
+ "items": {
373
+ "type": "array",
374
+ "items": {
375
+ "$ref": "#/$defs/PID"
376
+ }
377
+ }
378
+ }
379
+ }
380
+ },
381
+ "cards": {
382
+ "type": "object",
383
+ "required": [
384
+ "ranks",
385
+ "suits"
386
+ ],
387
+ "additionalProperties": false,
388
+ "description": "牌组配置",
389
+ "properties": {
390
+ "ranks": {
391
+ "type": "array",
392
+ "items": {
393
+ "$ref": "#/$defs/Rank"
394
+ },
395
+ "minItems": 1,
396
+ "description": "点数列表"
397
+ },
398
+ "suits": {
399
+ "type": "array",
400
+ "items": {
401
+ "$ref": "#/$defs/Suit"
402
+ },
403
+ "description": "花色列表"
404
+ },
405
+ "jokers": {
406
+ "type": "object",
407
+ "additionalProperties": false,
408
+ "description": "大小王点数映射",
409
+ "properties": {
410
+ "small": {
411
+ "$ref": "#/$defs/Rank"
412
+ },
413
+ "big": {
414
+ "$ref": "#/$defs/Rank"
415
+ }
416
+ }
417
+ },
418
+ "copies_per_deck": {
419
+ "type": "integer",
420
+ "minimum": 1,
421
+ "default": 1,
422
+ "description": "每副牌的复制份数"
423
+ },
424
+ "num_decks": {
425
+ "type": "integer",
426
+ "minimum": 1,
427
+ "default": 1,
428
+ "description": "使用副数"
429
+ },
430
+ "straight_exclude": {
431
+ "type": "array",
432
+ "items": {
433
+ "$ref": "#/$defs/Rank"
434
+ },
435
+ "description": "顺子禁入点数,如 2/王"
436
+ },
437
+ "per_rank_copies": {
438
+ "type": "object",
439
+ "additionalProperties": {
440
+ "type": "integer",
441
+ "minimum": 0
442
+ },
443
+ "description": "【v0.93】按点数单独指定张数,如 {\"16\":1,\"17\":1,\"18\":1}"
444
+ },
445
+ "suitless_ranks": {
446
+ "type": "array",
447
+ "items": {
448
+ "$ref": "#/$defs/Rank"
449
+ },
450
+ "description": "【v0.93】无花色点数(王、龙等不随花色展开)"
451
+ }
452
+ }
453
+ },
454
+ "zones": {
455
+ "type": "array",
456
+ "items": {
457
+ "$ref": "#/$defs/ZoneID"
458
+ },
459
+ "minItems": 1,
460
+ "uniqueItems": true,
461
+ "description": "区域列表:hand:*, field, main_deck, discard_pile, reserve_zone, public_pool, item_deck:*, history_deck ..."
462
+ },
463
+ "visibility": {
464
+ "type": "object",
465
+ "additionalProperties": false,
466
+ "description": "区域可见性设定",
467
+ "properties": {
468
+ "defaults": {
469
+ "type": "object",
470
+ "additionalProperties": {
471
+ "$ref": "#/$defs/VisibilityState"
472
+ },
473
+ "description": "全局默认"
474
+ },
475
+ "by_zone": {
476
+ "type": "object",
477
+ "additionalProperties": {
478
+ "type": "object",
479
+ "additionalProperties": false,
480
+ "properties": {
481
+ "state": {
482
+ "$ref": "#/$defs/VisibilityState"
483
+ },
484
+ "audience": {
485
+ "type": "array",
486
+ "items": {
487
+ "$ref": "#/$defs/Audience"
488
+ }
489
+ }
490
+ }
491
+ },
492
+ "description": "按区覆盖"
493
+ }
494
+ }
495
+ },
496
+ "invariants": {
497
+ "type": "object",
498
+ "additionalProperties": false,
499
+ "description": "不变量约束",
500
+ "properties": {
501
+ "physical_conservation": {
502
+ "type": "boolean",
503
+ "default": true,
504
+ "description": "物理守恒:总牌数不变"
505
+ },
506
+ "no_card_from_void": {
507
+ "type": "boolean",
508
+ "default": true,
509
+ "description": "禁止无源牌"
510
+ },
511
+ "max_hand_size": {
512
+ "type": "integer"
513
+ },
514
+ "max_public_pool": {
515
+ "type": "integer"
516
+ },
517
+ "same_rank_max": {
518
+ "type": "integer",
519
+ "minimum": 1
520
+ },
521
+ "sequence_span_max": {
522
+ "type": "integer",
523
+ "minimum": 1
524
+ },
525
+ "team_membership_consistent": {
526
+ "type": "boolean",
527
+ "default": true
528
+ },
529
+ "all_transfers_use_defined_zones": {
530
+ "type": "boolean",
531
+ "default": true
532
+ },
533
+ "no_card_appears_in_undefined_zone": {
534
+ "type": "boolean",
535
+ "default": true
536
+ }
537
+ }
538
+ },
539
+ "card_relations": {
540
+ "type": "object",
541
+ "additionalProperties": false,
542
+ "description": "牌值与关系定义",
543
+ "properties": {
544
+ "card_values": {
545
+ "type": "array",
546
+ "items": {
547
+ "$ref": "#/$defs/Rank"
548
+ },
549
+ "description": "牌值大小顺序,从低到高"
550
+ },
551
+ "used_in": {
552
+ "type": "array",
553
+ "items": {
554
+ "type": "string"
555
+ },
556
+ "description": "指定使用位置"
557
+ },
558
+ "continuous_relations": {
559
+ "type": "array",
560
+ "items": {
561
+ "type": "object",
562
+ "properties": {
563
+ "name": {
564
+ "type": "string"
565
+ },
566
+ "sequence": {
567
+ "type": "array",
568
+ "items": {
569
+ "$ref": "#/$defs/Rank"
570
+ }
571
+ },
572
+ "used_in": {
573
+ "type": "array",
574
+ "items": {
575
+ "type": "string"
576
+ }
577
+ }
578
+ }
579
+ },
580
+ "description": "连续关系定义"
581
+ },
582
+ "non_continuous_cards": {
583
+ "type": "array",
584
+ "items": {
585
+ "$ref": "#/$defs/Rank"
586
+ },
587
+ "description": "非连续牌"
588
+ },
589
+ "same_value_relation": {
590
+ "type": "string",
591
+ "enum": [
592
+ "same_number",
593
+ "same_suit"
594
+ ],
595
+ "default": "same_number"
596
+ },
597
+ "ace_position": {
598
+ "type": "string",
599
+ "enum": [
600
+ "first_last_second_last",
601
+ "anywhere"
602
+ ],
603
+ "default": "anywhere"
604
+ },
605
+ "two_position": {
606
+ "type": "string",
607
+ "enum": [
608
+ "first_second_first_last",
609
+ "anywhere"
610
+ ],
611
+ "default": "anywhere"
612
+ },
613
+ "suit_relations": {
614
+ "type": "object",
615
+ "properties": {
616
+ "order": {
617
+ "type": "array",
618
+ "items": {
619
+ "$ref": "#/$defs/Suit"
620
+ }
621
+ },
622
+ "used_in": {
623
+ "type": "array",
624
+ "items": {
625
+ "type": "string"
626
+ }
627
+ }
628
+ }
629
+ },
630
+ "joker_rules": {
631
+ "type": "object",
632
+ "properties": {
633
+ "mode": {
634
+ "type": "string",
635
+ "enum": [
636
+ "off",
637
+ "wild_only",
638
+ "power_only",
639
+ "wild_and_power"
640
+ ],
641
+ "default": "off"
642
+ },
643
+ "exclusivity": {
644
+ "type": "string",
645
+ "enum": [
646
+ "exclusive",
647
+ "hybrid"
648
+ ]
649
+ },
650
+ "wildcard_scope": {
651
+ "type": "array",
652
+ "items": {
653
+ "$ref": "#/$defs/ComboType"
654
+ },
655
+ "description": "赖子作用域"
656
+ },
657
+ "wildcard_loses_rank": {
658
+ "type": "boolean",
659
+ "default": false,
660
+ "description": "赖子是否失去单牌牌力"
661
+ },
662
+ "power_single": {
663
+ "type": "object",
664
+ "properties": {
665
+ "enabled": {
666
+ "type": "boolean"
667
+ },
668
+ "beats": {
669
+ "type": "string",
670
+ "enum": [
671
+ "all_singles",
672
+ "rank_list"
673
+ ]
674
+ },
675
+ "rank_list": {
676
+ "type": "array",
677
+ "items": {
678
+ "type": "string"
679
+ }
680
+ }
681
+ }
682
+ },
683
+ "power_pair": {
684
+ "type": "object",
685
+ "properties": {
686
+ "enabled": {
687
+ "type": "boolean"
688
+ },
689
+ "beats": {
690
+ "type": "string",
691
+ "enum": [
692
+ "all_pairs",
693
+ "rank_list"
694
+ ]
695
+ },
696
+ "rank_list": {
697
+ "type": "array",
698
+ "items": {
699
+ "type": "string"
700
+ }
701
+ }
702
+ }
703
+ }
704
+ }
705
+ },
706
+ "unique_cards": {
707
+ "type": "array",
708
+ "items": {
709
+ "type": "string"
710
+ },
711
+ "description": "唯一牌列表,如 Spade3"
712
+ }
713
+ }
714
+ },
715
+ "combinations": {
716
+ "type": "object",
717
+ "additionalProperties": false,
718
+ "description": "牌型族定义",
719
+ "properties": {
720
+ "single": {
721
+ "type": "object"
722
+ },
723
+ "pair": {
724
+ "type": "object"
725
+ },
726
+ "triple": {
727
+ "type": "object"
728
+ },
729
+ "straight": {
730
+ "type": "object",
731
+ "additionalProperties": false,
732
+ "properties": {
733
+ "min_len": {
734
+ "type": "integer",
735
+ "minimum": 1
736
+ },
737
+ "no_ranks": {
738
+ "type": "array",
739
+ "items": {
740
+ "$ref": "#/$defs/Rank"
741
+ }
742
+ }
743
+ },
744
+ "description": "顺子"
745
+ },
746
+ "pairs_chain": {
747
+ "type": "object",
748
+ "additionalProperties": false,
749
+ "properties": {
750
+ "min_len": {
751
+ "type": "integer",
752
+ "minimum": 1
753
+ }
754
+ },
755
+ "description": "连对"
756
+ },
757
+ "airplane": {
758
+ "type": "object",
759
+ "additionalProperties": false,
760
+ "properties": {
761
+ "min_len": {
762
+ "type": "integer",
763
+ "minimum": 1
764
+ },
765
+ "wings": {
766
+ "type": "string",
767
+ "enum": [
768
+ "single",
769
+ "pair",
770
+ "none"
771
+ ],
772
+ "default": "none"
773
+ }
774
+ },
775
+ "description": "飞机(可带翅膀)"
776
+ },
777
+ "triple_with_single": {
778
+ "type": "object"
779
+ },
780
+ "triple_with_pair": {
781
+ "type": "object"
782
+ },
783
+ "four_with_twoSingles": {
784
+ "type": "object"
785
+ },
786
+ "four_with_twoPairs": {
787
+ "type": "object"
788
+ },
789
+ "bomb": {
790
+ "type": "object",
791
+ "additionalProperties": false,
792
+ "properties": {
793
+ "len": {
794
+ "type": "integer",
795
+ "const": 4
796
+ }
797
+ },
798
+ "description": "四张炸"
799
+ },
800
+ "rocket": {
801
+ "type": "object",
802
+ "additionalProperties": false,
803
+ "properties": {
804
+ "ranks": {
805
+ "type": "array",
806
+ "items": {
807
+ "$ref": "#/$defs/Rank"
808
+ }
809
+ }
810
+ },
811
+ "description": "王炸"
812
+ },
813
+ "custom": {
814
+ "type": "array",
815
+ "items": {
816
+ "type": "object",
817
+ "required": [
818
+ "name"
819
+ ],
820
+ "additionalProperties": false,
821
+ "properties": {
822
+ "name": {
823
+ "type": "string"
824
+ },
825
+ "spec": {
826
+ "type": "object"
827
+ }
828
+ }
829
+ },
830
+ "description": "自定义牌型/特殊牌"
831
+ }
832
+ }
833
+ },
834
+ "comparison": {
835
+ "type": "object",
836
+ "required": [
837
+ "same_type",
838
+ "same_len"
839
+ ],
840
+ "additionalProperties": false,
841
+ "description": "比较总则",
842
+ "properties": {
843
+ "same_type": {
844
+ "type": "boolean",
845
+ "description": "是否强制同型比较"
846
+ },
847
+ "same_len": {
848
+ "type": "boolean",
849
+ "description": "是否强制相同长度"
850
+ },
851
+ "bomb_beats_all": {
852
+ "type": "boolean",
853
+ "default": true,
854
+ "description": "炸弹压制普通牌"
855
+ },
856
+ "rocket_top": {
857
+ "type": "boolean",
858
+ "default": true,
859
+ "description": "王炸最大"
860
+ },
861
+ "tiebreaker": {
862
+ "type": "string",
863
+ "enum": [
864
+ "suit",
865
+ "first_played",
866
+ "none"
867
+ ],
868
+ "default": "none",
869
+ "description": "平手裁决"
870
+ }
871
+ }
872
+ },
873
+ "comparison_ex": {
874
+ "type": "object",
875
+ "additionalProperties": false,
876
+ "description": "精细比较规则",
877
+ "properties": {
878
+ "per_combo": {
879
+ "type": "object",
880
+ "additionalProperties": {
881
+ "type": "object",
882
+ "additionalProperties": false,
883
+ "properties": {
884
+ "require_same_length": {
885
+ "type": "boolean",
886
+ "default": true
887
+ },
888
+ "compare_by": {
889
+ "type": "string",
890
+ "enum": [
891
+ "sequence_start",
892
+ "sequence_end",
893
+ "highest_triple_value",
894
+ "main_rank",
895
+ "length_first_then_rank",
896
+ "custom"
897
+ ],
898
+ "default": "main_rank"
899
+ },
900
+ "side_card_rule": {
901
+ "type": "string",
902
+ "enum": [
903
+ "ignore",
904
+ "must_match_shape",
905
+ "custom"
906
+ ],
907
+ "default": "ignore"
908
+ }
909
+ }
910
+ },
911
+ "description": "按牌型的比较键设定"
912
+ },
913
+ "suit_order": {
914
+ "type": "array",
915
+ "items": {
916
+ "$ref": "#/$defs/Suit"
917
+ },
918
+ "description": "花色序(用于平手裁决)"
919
+ },
920
+ "override_matrix": {
921
+ "type": "array",
922
+ "items": {
923
+ "type": "object",
924
+ "additionalProperties": false,
925
+ "required": [
926
+ "overrider",
927
+ "overrides"
928
+ ],
929
+ "properties": {
930
+ "overrider": {
931
+ "type": "string",
932
+ "description": "覆盖者(如 dragon_card)"
933
+ },
934
+ "overrides": {
935
+ "type": "array",
936
+ "items": {
937
+ "type": "string"
938
+ },
939
+ "description": "被覆盖牌型,支持 \"*\""
940
+ },
941
+ "priority": {
942
+ "type": "integer",
943
+ "description": "覆盖优先级"
944
+ }
945
+ }
946
+ },
947
+ "description": "【v0.93】牌型/特殊牌对其它牌型的覆盖矩阵"
948
+ }
949
+ }
950
+ },
951
+ "actions": {
952
+ "type": "object",
953
+ "required": [
954
+ "play",
955
+ "pass"
956
+ ],
957
+ "additionalProperties": false,
958
+ "description": "动作定义(均归约为 Transfer)",
959
+ "properties": {
960
+ "play": {
961
+ "type": "array",
962
+ "items": {
963
+ "type": "object",
964
+ "required": [
965
+ "transfer_path"
966
+ ],
967
+ "additionalProperties": false,
968
+ "properties": {
969
+ "type": {
970
+ "$ref": "#/$defs/ComboType"
971
+ },
972
+ "len": {
973
+ "type": "integer"
974
+ },
975
+ "core": {
976
+ "type": "array",
977
+ "items": {
978
+ "$ref": "#/$defs/Rank"
979
+ },
980
+ "description": "核心序列,如 [7,8,9,10,11]"
981
+ },
982
+ "wings": {
983
+ "type": "string",
984
+ "enum": [
985
+ "single",
986
+ "pair",
987
+ "none"
988
+ ]
989
+ },
990
+ "wild_usage": {
991
+ "type": "string",
992
+ "enum": [
993
+ "limited",
994
+ "full",
995
+ "none"
996
+ ]
997
+ },
998
+ "transfer_path": {
999
+ "$ref": "#/$defs/Transfer"
1000
+ },
1001
+ "visibility_change": {
1002
+ "$ref": "#/$defs/VisibilityChange"
1003
+ },
1004
+ "raw_definition": {
1005
+ "type": "object",
1006
+ "description": "当标准字段无法精确表示 GDL 中的 play 动作时,存储其原始定义。"
1007
+ }
1008
+ }
1009
+ },
1010
+ "description": "出牌动作列表"
1011
+ },
1012
+ "pass": {
1013
+ "type": "object",
1014
+ "required": [
1015
+ "transfer_path"
1016
+ ],
1017
+ "additionalProperties": false,
1018
+ "description": "过牌(显式 no-op:field→field)",
1019
+ "properties": {
1020
+ "transfer_path": {
1021
+ "oneOf": [
1022
+ {
1023
+ "type": "object",
1024
+ "required": [
1025
+ "from",
1026
+ "to"
1027
+ ],
1028
+ "additionalProperties": false,
1029
+ "properties": {
1030
+ "from": {
1031
+ "const": "field"
1032
+ },
1033
+ "to": {
1034
+ "const": "field"
1035
+ }
1036
+ }
1037
+ },
1038
+ {
1039
+ "const": "none"
1040
+ }
1041
+ ]
1042
+ }
1043
+ }
1044
+ },
1045
+ "reveal_bottom": {
1046
+ "type": "array",
1047
+ "items": {
1048
+ "$ref": "#/$defs/Transfer"
1049
+ },
1050
+ "description": "翻底"
1051
+ },
1052
+ "cleanup_trick": {
1053
+ "type": "array",
1054
+ "items": {
1055
+ "$ref": "#/$defs/Transfer"
1056
+ },
1057
+ "description": "清台/收墩"
1058
+ },
1059
+ "other": {
1060
+ "type": "object",
1061
+ "additionalProperties": {
1062
+ "type": "array",
1063
+ "items": {
1064
+ "oneOf": [
1065
+ {
1066
+ "$ref": "#/$defs/Transfer"
1067
+ },
1068
+ {
1069
+ "type": "object",
1070
+ "required": [
1071
+ "turns_mutations"
1072
+ ],
1073
+ "properties": {
1074
+ "turns_mutations": {
1075
+ "$ref": "#/$defs/turns_mutations"
1076
+ }
1077
+ },
1078
+ "additionalProperties": false,
1079
+ "description": "顺序副作用(反转/跳过/改定庄)"
1080
+ }
1081
+ ]
1082
+ }
1083
+ },
1084
+ "description": "自定义动作名称 → Transfer 序列"
1085
+ }
1086
+ }
1087
+ },
1088
+ "phases": {
1089
+ "type": "array",
1090
+ "items": {
1091
+ "type": "string",
1092
+ "enum": [
1093
+ "setup",
1094
+ "deal",
1095
+ "bid",
1096
+ "double",
1097
+ "initiative",
1098
+ "play",
1099
+ "settle",
1100
+ "grouping"
1101
+ ]
1102
+ },
1103
+ "uniqueItems": true,
1104
+ "description": "阶段流转"
1105
+ },
1106
+ "turns": {
1107
+ "type": "object",
1108
+ "required": [
1109
+ "order",
1110
+ "trick_end_on",
1111
+ "next_leader"
1112
+ ],
1113
+ "additionalProperties": false,
1114
+ "description": "轮转/收墩",
1115
+ "properties": {
1116
+ "order": {
1117
+ "type": "array",
1118
+ "items": {
1119
+ "$ref": "#/$defs/PID"
1120
+ },
1121
+ "minItems": 2,
1122
+ "description": "回合顺序"
1123
+ },
1124
+ "leader": {
1125
+ "$ref": "#/$defs/PID",
1126
+ "description": "首个出牌者"
1127
+ },
1128
+ "trick_end_on": {
1129
+ "oneOf": [
1130
+ {
1131
+ "type": "string",
1132
+ "enum": [
1133
+ "two_pass",
1134
+ "all_pass_except_last",
1135
+ "custom"
1136
+ ],
1137
+ "description": "一墩结束条件"
1138
+ },
1139
+ {
1140
+ "type": "object",
1141
+ "title": "passes_n",
1142
+ "required": [
1143
+ "passes_n"
1144
+ ],
1145
+ "properties": {
1146
+ "passes_n": {
1147
+ "type": "integer",
1148
+ "minimum": 2,
1149
+ "maximum": 10,
1150
+ "description": "连续 PASS 次数达到 N 则结束一墩"
1151
+ }
1152
+ },
1153
+ "additionalProperties": false,
1154
+ "description": "N 连续 PASS 结束一墩"
1155
+ }
1156
+ ]
1157
+ },
1158
+ "next_leader": {
1159
+ "type": "string",
1160
+ "enum": [
1161
+ "last_non_pass",
1162
+ "winner",
1163
+ "fixed",
1164
+ "custom"
1165
+ ],
1166
+ "description": "下一轮领出者"
1167
+ }
1168
+ }
1169
+ },
1170
+ "ending": {
1171
+ "type": "object",
1172
+ "required": [
1173
+ "when"
1174
+ ],
1175
+ "additionalProperties": false,
1176
+ "description": "终局条件",
1177
+ "properties": {
1178
+ "when": {
1179
+ "type": "string",
1180
+ "enum": [
1181
+ "any_hand_empty",
1182
+ "stock_empty",
1183
+ "rounds_exhausted",
1184
+ "custom"
1185
+ ]
1186
+ }
1187
+ }
1188
+ },
1189
+ "scoring": {
1190
+ "type": "object",
1191
+ "required": [
1192
+ "base"
1193
+ ],
1194
+ "additionalProperties": false,
1195
+ "description": "基础计分与倍率封顶",
1196
+ "properties": {
1197
+ "base": {
1198
+ "type": "integer",
1199
+ "minimum": 0,
1200
+ "default": 1
1201
+ },
1202
+ "multipliers": {
1203
+ "type": "object",
1204
+ "additionalProperties": {
1205
+ "type": "number"
1206
+ },
1207
+ "description": "事件→倍率 映射"
1208
+ },
1209
+ "cap": {
1210
+ "type": "integer",
1211
+ "description": "总封顶(简单场景)"
1212
+ },
1213
+ "spring_rule": {
1214
+ "type": "string",
1215
+ "enum": [
1216
+ "strict",
1217
+ "lenient",
1218
+ "none"
1219
+ ],
1220
+ "default": "none",
1221
+ "description": "春天/反春判定"
1222
+ },
1223
+ "expression": {
1224
+ "type": "string",
1225
+ "description": "可选:S-表达式形式的最终公式(实现自定)"
1226
+ },
1227
+ "capping": {
1228
+ "type": "object",
1229
+ "additionalProperties": false,
1230
+ "description": "【v0.93】更细粒度封顶",
1231
+ "properties": {
1232
+ "winner_max_gain": {
1233
+ "oneOf": [
1234
+ {
1235
+ "type": "number"
1236
+ },
1237
+ {
1238
+ "type": "string"
1239
+ }
1240
+ ]
1241
+ },
1242
+ "loser_max_loss": {
1243
+ "oneOf": [
1244
+ {
1245
+ "type": "number"
1246
+ },
1247
+ {
1248
+ "type": "string"
1249
+ }
1250
+ ]
1251
+ }
1252
+ }
1253
+ }
1254
+ }
1255
+ },
1256
+ "initiative": {
1257
+ "type": "object",
1258
+ "additionalProperties": false,
1259
+ "description": "先手机制",
1260
+ "properties": {
1261
+ "mode": {
1262
+ "type": "string",
1263
+ "enum": [
1264
+ "spade3",
1265
+ "random",
1266
+ "max_card",
1267
+ "min_card",
1268
+ "custom"
1269
+ ]
1270
+ },
1271
+ "params": {
1272
+ "type": "object"
1273
+ },
1274
+ "deterministic": {
1275
+ "type": "boolean",
1276
+ "default": true
1277
+ }
1278
+ }
1279
+ },
1280
+ "setup_ex": {
1281
+ "type": "object",
1282
+ "additionalProperties": false,
1283
+ "description": "扩展设置",
1284
+ "properties": {
1285
+ "special_dealing": {
1286
+ "type": "object",
1287
+ "additionalProperties": false,
1288
+ "properties": {
1289
+ "chain_bomb_distribution": {
1290
+ "type": "object",
1291
+ "additionalProperties": false,
1292
+ "properties": {
1293
+ "enabled": {
1294
+ "type": "boolean",
1295
+ "default": false
1296
+ },
1297
+ "counts": {
1298
+ "type": "object",
1299
+ "additionalProperties": {
1300
+ "type": "number"
1301
+ }
1302
+ }
1303
+ }
1304
+ }
1305
+ }
1306
+ },
1307
+ "landlord_card_selection": {
1308
+ "type": "object",
1309
+ "additionalProperties": false,
1310
+ "description": "地主底牌分配",
1311
+ "properties": {
1312
+ "timing": {
1313
+ "type": "string",
1314
+ "enum": [
1315
+ "after_bomb_distribution",
1316
+ "default",
1317
+ "custom"
1318
+ ],
1319
+ "default": "default"
1320
+ },
1321
+ "method": {
1322
+ "type": "string",
1323
+ "enum": [
1324
+ "strategic",
1325
+ "random",
1326
+ "custom"
1327
+ ],
1328
+ "default": "random"
1329
+ },
1330
+ "selection_process": {
1331
+ "type": "string"
1332
+ },
1333
+ "criteria": {
1334
+ "type": "object",
1335
+ "properties": {
1336
+ "can_form_bomb": {
1337
+ "type": "boolean"
1338
+ },
1339
+ "can_form_triple": {
1340
+ "type": "boolean"
1341
+ },
1342
+ "value_threshold": {
1343
+ "type": "string",
1344
+ "enum": [
1345
+ "high",
1346
+ "medium",
1347
+ "low"
1348
+ ]
1349
+ }
1350
+ }
1351
+ },
1352
+ "fallback_method": {
1353
+ "type": "string",
1354
+ "enum": [
1355
+ "last_n_cards",
1356
+ "random"
1357
+ ]
1358
+ },
1359
+ "fallback_count": {
1360
+ "type": "integer"
1361
+ },
1362
+ "distribution_pattern": {
1363
+ "type": "string"
1364
+ }
1365
+ }
1366
+ },
1367
+ "grouping": {
1368
+ "type": "object",
1369
+ "additionalProperties": false,
1370
+ "description": "【v0.93】分组设定",
1371
+ "properties": {
1372
+ "mode": {
1373
+ "type": "string"
1374
+ },
1375
+ "groups": {
1376
+ "type": "array",
1377
+ "items": {
1378
+ "type": "string"
1379
+ }
1380
+ },
1381
+ "assign_rules": {
1382
+ "type": "object"
1383
+ },
1384
+ "deal_bias": {
1385
+ "type": "object"
1386
+ },
1387
+ "bonuses": {
1388
+ "type": "object"
1389
+ }
1390
+ }
1391
+ },
1392
+ "replenishment_rules": {
1393
+ "type": "object",
1394
+ "additionalProperties": false,
1395
+ "properties": {
1396
+ "enabled": {
1397
+ "type": "boolean"
1398
+ },
1399
+ "cards_per_player_per_round": {
1400
+ "type": "integer",
1401
+ "minimum": 0
1402
+ },
1403
+ "source": {
1404
+ "$ref": "#/$defs/ZoneID"
1405
+ },
1406
+ "termination_condition": {
1407
+ "type": "string"
1408
+ },
1409
+ "special_cases": {
1410
+ "type": "object",
1411
+ "properties": {
1412
+ "if_remaining_cards_less_than_n": {
1413
+ "type": "string"
1414
+ },
1415
+ "if_player_cannot_take_all": {
1416
+ "type": "string"
1417
+ }
1418
+ }
1419
+ },
1420
+ "excess_card_handling": {
1421
+ "type": "string"
1422
+ }
1423
+ }
1424
+ },
1425
+ "guessing_rules": {
1426
+ "type": "object",
1427
+ "additionalProperties": false,
1428
+ "properties": {
1429
+ "enabled": {
1430
+ "type": "boolean"
1431
+ },
1432
+ "max_extra_cards": {
1433
+ "type": "integer",
1434
+ "minimum": 0
1435
+ },
1436
+ "success_reward_multiplier": {
1437
+ "type": "string"
1438
+ },
1439
+ "success_reward_multiplier_for_only_extra": {
1440
+ "type": "string"
1441
+ },
1442
+ "failure_penalty": {
1443
+ "type": "string"
1444
+ },
1445
+ "failure_consequence": {
1446
+ "type": "string"
1447
+ },
1448
+ "farmer_failure_continues_to_landlord": {
1449
+ "type": "boolean"
1450
+ },
1451
+ "source": {
1452
+ "$ref": "#/$defs/ZoneID"
1453
+ }
1454
+ }
1455
+ }
1456
+ }
1457
+ },
1458
+ "information_model": {
1459
+ "type": "object",
1460
+ "additionalProperties": false,
1461
+ "description": "信息模型(观战、日志、窥视)",
1462
+ "properties": {
1463
+ "spectator_view": {
1464
+ "type": "string",
1465
+ "enum": [
1466
+ "none",
1467
+ "public",
1468
+ "delayed"
1469
+ ],
1470
+ "default": "none"
1471
+ },
1472
+ "logs": {
1473
+ "type": "string",
1474
+ "enum": [
1475
+ "redacted",
1476
+ "full"
1477
+ ],
1478
+ "default": "redacted"
1479
+ },
1480
+ "peek_actions": {
1481
+ "type": "object",
1482
+ "additionalProperties": false,
1483
+ "properties": {
1484
+ "enabled": {
1485
+ "type": "boolean",
1486
+ "default": false
1487
+ },
1488
+ "who": {
1489
+ "type": "string"
1490
+ },
1491
+ "scope": {
1492
+ "type": "array",
1493
+ "items": {
1494
+ "$ref": "#/$defs/ZoneID"
1495
+ }
1496
+ },
1497
+ "limits": {
1498
+ "type": "object",
1499
+ "additionalProperties": false,
1500
+ "properties": {
1501
+ "per_round": {
1502
+ "type": "integer",
1503
+ "minimum": 0
1504
+ },
1505
+ "per_game": {
1506
+ "type": "integer",
1507
+ "minimum": 0
1508
+ }
1509
+ }
1510
+ }
1511
+ }
1512
+ }
1513
+ }
1514
+ },
1515
+ "special_mechanics": {
1516
+ "type": "array",
1517
+ "items": {
1518
+ "$ref": "#/$defs/Mechanic"
1519
+ },
1520
+ "description": "特殊机制定义,对应 GDL 中 special_mechanics 的通用 mechanic 模板。支持 raw_definition 以应对无法精确映射的情况。"
1521
+ },
1522
+ "illegal_action": {
1523
+ "type": "object",
1524
+ "additionalProperties": false,
1525
+ "description": "非法动作策略",
1526
+ "properties": {
1527
+ "policy": {
1528
+ "type": "string",
1529
+ "enum": [
1530
+ "skip_turn",
1531
+ "forfeit",
1532
+ "warn"
1533
+ ],
1534
+ "default": "warn"
1535
+ },
1536
+ "penalty": {
1537
+ "type": "string"
1538
+ }
1539
+ }
1540
+ },
1541
+ "grouping_phase": {
1542
+ "type": "object",
1543
+ "additionalProperties": false,
1544
+ "description": "分组阶段(对局内)",
1545
+ "properties": {
1546
+ "enabled": {
1547
+ "type": "boolean",
1548
+ "default": false
1549
+ },
1550
+ "mode": {
1551
+ "type": "string",
1552
+ "enum": [
1553
+ "ace_grouping",
1554
+ "custom"
1555
+ ],
1556
+ "default": "custom"
1557
+ },
1558
+ "dynamic_teams": {
1559
+ "type": "boolean",
1560
+ "default": false
1561
+ },
1562
+ "first_turn": {
1563
+ "type": "string"
1564
+ }
1565
+ }
1566
+ },
1567
+ "rank_position_rules": {
1568
+ "type": "object",
1569
+ "additionalProperties": false,
1570
+ "description": "顺子/连对的点数位置限制",
1571
+ "properties": {
1572
+ "A": {
1573
+ "type": "string",
1574
+ "enum": [
1575
+ "first_last_second_last",
1576
+ "anywhere"
1577
+ ],
1578
+ "default": "anywhere"
1579
+ },
1580
+ "2": {
1581
+ "type": "string",
1582
+ "enum": [
1583
+ "first_second_first_last",
1584
+ "anywhere"
1585
+ ],
1586
+ "default": "anywhere"
1587
+ }
1588
+ }
1589
+ },
1590
+ "scoring_ex": {
1591
+ "type": "object",
1592
+ "additionalProperties": false,
1593
+ "description": "计分表达式与顺序",
1594
+ "properties": {
1595
+ "expr": {
1596
+ "type": "object",
1597
+ "description": "AST风格表达式",
1598
+ "properties": {
1599
+ "type": {
1600
+ "type": "string"
1601
+ },
1602
+ "args": {
1603
+ "type": "array",
1604
+ "items": {
1605
+ "$ref": "#/properties/scoring_ex/properties/expr"
1606
+ }
1607
+ },
1608
+ "var": {
1609
+ "type": "string"
1610
+ }
1611
+ }
1612
+ },
1613
+ "order_of_application": {
1614
+ "type": "array",
1615
+ "items": {
1616
+ "type": "string"
1617
+ }
1618
+ }
1619
+ }
1620
+ },
1621
+ "override_policy": {
1622
+ "type": "object",
1623
+ "additionalProperties": false,
1624
+ "description": "覆盖策略(谁覆盖谁)",
1625
+ "properties": {
1626
+ "turn_order": {
1627
+ "type": "string",
1628
+ "enum": [
1629
+ "playing_phase_overrides_global",
1630
+ "inherit"
1631
+ ],
1632
+ "default": "inherit"
1633
+ },
1634
+ "deal_method": {
1635
+ "type": "string",
1636
+ "enum": [
1637
+ "setup_overrides_deck",
1638
+ "inherit"
1639
+ ],
1640
+ "default": "inherit"
1641
+ },
1642
+ "unspecified": {
1643
+ "type": "string",
1644
+ "enum": [
1645
+ "inherits",
1646
+ "error"
1647
+ ],
1648
+ "default": "inherits"
1649
+ },
1650
+ "on_conflict": {
1651
+ "type": "string",
1652
+ "enum": [
1653
+ "error",
1654
+ "last_wins"
1655
+ ],
1656
+ "default": "error"
1657
+ }
1658
+ }
1659
+ },
1660
+ "history_deck": {
1661
+ "type": "object",
1662
+ "additionalProperties": false,
1663
+ "description": "历史牌堆(记牌/回放)",
1664
+ "properties": {
1665
+ "enabled": {
1666
+ "type": "boolean",
1667
+ "default": false
1668
+ },
1669
+ "size": {
1670
+ "type": "integer",
1671
+ "minimum": 0,
1672
+ "default": 0
1673
+ },
1674
+ "replacement_policy": {
1675
+ "type": "string",
1676
+ "enum": [
1677
+ "fifo",
1678
+ "lru",
1679
+ "none"
1680
+ ],
1681
+ "default": "fifo"
1682
+ },
1683
+ "max_size": {
1684
+ "type": "integer",
1685
+ "minimum": 0,
1686
+ "default": 0
1687
+ }
1688
+ }
1689
+ },
1690
+ "bidding_model": {
1691
+ "type": "object",
1692
+ "additionalProperties": false,
1693
+ "description": "【v0.93】竞价流程模型",
1694
+ "properties": {
1695
+ "mode": {
1696
+ "type": "string"
1697
+ },
1698
+ "options": {
1699
+ "type": "array",
1700
+ "items": {
1701
+ "type": "string"
1702
+ }
1703
+ },
1704
+ "order": {
1705
+ "type": "array",
1706
+ "items": {
1707
+ "$ref": "#/$defs/PID"
1708
+ }
1709
+ },
1710
+ "multiplier_effect": {
1711
+ "type": "object"
1712
+ }
1713
+ }
1714
+ },
1715
+ "doubling_model": {
1716
+ "type": "object",
1717
+ "additionalProperties": false,
1718
+ "description": "【v0.93】加倍流程模型",
1719
+ "properties": {
1720
+ "options": {
1721
+ "type": "array",
1722
+ "items": {
1723
+ "type": "string"
1724
+ }
1725
+ },
1726
+ "scope": {
1727
+ "type": "string",
1728
+ "enum": [
1729
+ "personal",
1730
+ "team",
1731
+ "global",
1732
+ "custom"
1733
+ ]
1734
+ },
1735
+ "stack_rule": {
1736
+ "type": "string"
1737
+ }
1738
+ }
1739
+ },
1740
+ "extensions": {
1741
+ "type": "object",
1742
+ "description": "用于容纳当前 Schema 无法表示的 GDL 特性或未来扩展。实现时应谨慎处理其中的内容。",
1743
+ "additionalProperties": true
1744
+ }
1745
+ }
1746
+ }
qixi_gdl.txt ADDED
@@ -0,0 +1,388 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ (game "奇袭斗地主"
2
+ (types
3
+ (DeckType one_of {"Standard54"})
4
+ (ShuffleMethod one_of {"full"})
5
+ (DealMethod one_of {"random"})
6
+ (TurnOrderType one_of {"circular"})
7
+ (Visibility one_of {"face_up" "face_down" "partial"})
8
+ (ComparisonRule one_of {"same_type_only"})
9
+ (TieBreaker one_of {"suit" "first_played" "none"})
10
+ (CapType one_of {"N_times_coin" "fixed_amount" "percentage"})
11
+ (PlayerSelector one_of {"with_Spade3"})
12
+ (InitiativeMode one_of {"spade3"})
13
+ (ZoneName one_of {"hand" "field" "main_deck" "discard_pile" "reserve_zone"})
14
+ (VisibilityAudience one_of {"owner" "teammates" "enemies" "all" "none"})
15
+ (TransferPath (from ZoneName) (to ZoneName))
16
+ )
17
+
18
+ (authoring_constraints
19
+ (no_range_literals true)
20
+ (exact_counts_required true)
21
+ (no_open_ended_modifiers true)
22
+ (no_vague_terms true)
23
+ (numeric_values_must_be_exact true)
24
+ (all_cards_must_have_defined_zones true)
25
+ (all_actions_must_specify_transfer_path true)
26
+ (no_card_generation_from_void true)
27
+ )
28
+
29
+ (players 3) ; 固定3人
30
+
31
+ (deck "Standard54" ; 使用1副牌,含大小王
32
+ (shuffling "full") ; 完全洗牌
33
+ (deal_pattern "random") ; 随机发牌
34
+ )
35
+
36
+ (roles { (role "Landlord" 1) (role "Peasant" 2) }) ; 1个地主,2个农民
37
+
38
+ (turn_order { "Landlord" "Peasant" "Peasant" }) ; 地主先出,接着是农民
39
+
40
+ (override_policy
41
+ (turn_order precedence: playing_phase_overrides_global)
42
+ (deal_method precedence: setup_overrides_deck)
43
+ (unspecified inherits parent)
44
+ (on_conflict "error"))
45
+
46
+ (setup
47
+ (bidding true) ; 存在叫分环节
48
+ (deal 17) ; 每位玩家在初始发牌时获得17张牌
49
+ (reserve 3) ; 留作"底牌"的牌数
50
+ (reserve_cards "face_down") ; 底牌暗牌
51
+ (deal_method "random") ; 随机发牌
52
+
53
+ (initial_score 0) ; 初始积分为0
54
+ (initial_multiplier 1) ; 初始倍率为1
55
+
56
+ (zones
57
+ (hand
58
+ (default_visibility owner)
59
+ (initial_cards 0))
60
+ (field
61
+ (default_visibility all)
62
+ (initial_cards 0))
63
+ (discard_pile
64
+ (default_visibility all)
65
+ (initial_cards 0))
66
+ (main_deck
67
+ (default_visibility hidden)
68
+ (source "Standard54")
69
+ (initial_cards 54))
70
+ (reserve_zone
71
+ (default_visibility face_down)
72
+ (initial_cards 3))
73
+ )
74
+
75
+ (card_relations
76
+ (card_values 3 4 5 6 7 8 9 10 J Q K A 2) ; 牌值大小顺序,从低到高
77
+ (used_in { "single_card" "pair" "triple" "bomb" }) ; 用于单牌、对子、三张、炸弹比较
78
+ (continuous_relations ; 连续关系定义
79
+ (Straight_3_to_10 3 4 5 6 7 8 9 10) ; 3到10连续
80
+ (Straight_10_to_A 10 J Q K A) ; 10到A连续
81
+ (Straight_A_to_5 A 5 6 7 8) ; A到5连续(特殊顺子)
82
+ (used_in { "straight" "double_straight" "triple_chain" "airplane" }) ; 用于顺子、连对、连三、飞机
83
+ )
84
+ (non_continuous_cards { 2 } (used_in { "exceptions_in_straight_rules" })) ; 2不参与连续关系
85
+ (suit_relations
86
+ (order "Spade" "Heart" "Club" "Diamond") ; 花色大小顺序
87
+ (used_in { "tie_breaker" "spade3_first" }) ; 用于平局和黑桃3先出
88
+ )
89
+ (same_value_relation
90
+ (type "same_number") ; 同点数
91
+ (used_in { "pair" "triple" "bomb" }) ; 用于对子、三张、炸弹
92
+ )
93
+ )
94
+
95
+ (information_model
96
+ (logs redacted)
97
+ (spectator_view none)
98
+ (peek_actions
99
+ (enabled false)
100
+ (who "role")
101
+ (cost "expr?")
102
+ (scope "hand")
103
+ (limits (per_round 0) (per_game 0)))
104
+ )
105
+
106
+ (randomness
107
+ (seed 42)
108
+ (sampling "uniform")
109
+ (deck_shuffle "full"))
110
+
111
+ (resource_bounds
112
+ (same_rank_max 4)
113
+ (sequence_span_max 12)
114
+ (wildcard_policy none)
115
+ (notes "无"))
116
+
117
+ (invariants
118
+ (= (+ (zone hand) (zone field) (zone discard_pile) (zone main_deck) (zone reserve_zone)) TotalCards)
119
+ (>= (zone main_deck) 0)
120
+ (>= (zone hand) 0) (>= (zone field) 0) (>= (zone discard_pile) 0)
121
+ (all_transfers_use_defined_zones true)
122
+ (no_card_appears_in_undefined_zone true)
123
+ (visibility_audience_validity true)
124
+ (no_unauthorized_peeking true)
125
+ (team_membership_consistent true)
126
+ (card_uniqueness (for {"Spade3"}))
127
+ (authoring_sanity (no_range_literals true) (exact_counts_required true))
128
+ (initiative_phase_defined true)
129
+ (initiative_phase_has_valid_mode true)
130
+ (initiative_phase_has_deterministic_outcome true)
131
+ (all_defined_relations_are_used true)
132
+ (no_unused_card_relations true)
133
+ (no_undefined_relations_used true)
134
+ (all_mechanisms_have_phase true)
135
+ (all_mechanisms_have_trigger_condition true)
136
+ )
137
+
138
+ (combinations
139
+ (single) ; 单牌
140
+ (pair) ; 对子
141
+ (triple) ; 三张
142
+ (straight 5) ; 顺子,至少5张
143
+ (double_straight 3) ; 连对,至少3对
144
+ (triple_with_single) ; 三带一
145
+ (triple_with_pair) ; 三带二
146
+ (airplane 2) ; 飞机,至少2个连续三张
147
+ (airplane_with_single 2) ; 飞机带单
148
+ (airplane_with_pair 2) ; 飞机带对
149
+ (bomb 4) ; 炸弹,4张同点
150
+ (bomb_with_single 1) ; 炸弹带单
151
+ (bomb_with_pair 1) ; 炸弹带对
152
+ (rocket) ; 王炸
153
+ )
154
+
155
+ (actions
156
+ (pass
157
+ (transfer_path none)
158
+ (visibility_change none))
159
+ (play (type one_of {single pair triple straight double_straight
160
+ triple_with_single triple_with_pair
161
+ airplane airplane_with_single airplane_with_pair
162
+ bomb bomb_with_single bomb_with_pair rocket})
163
+ (len int?)
164
+ (core sequence?)
165
+ (wings int?)
166
+ (transfer_path from: hand to: field)
167
+ (visibility_change
168
+ (to {owner teammates enemies})
169
+ (state "visible")
170
+ (on_target true)))
171
+ (special (name "援手")
172
+ (params { "TargetPlayer" })
173
+ (transfer_path from: hand to: field)
174
+ (visibility_change
175
+ (to {all})
176
+ (state "visible")
177
+ (on_target true)))
178
+ (special (name "换位")
179
+ (params { "NewRole" })
180
+ (transfer_path from: hand to: field)
181
+ (visibility_change
182
+ (to {all})
183
+ (state "visible")
184
+ (on_target true)))
185
+ )
186
+
187
+ (illegal_action (policy "skip_turn") (penalty "0"))
188
+
189
+ (play_rules
190
+ (comparison_rule "same_type_only")
191
+ (bomb_power "override")
192
+ (rocket_power "highest")
193
+ (bomb_multiplier "doubling")
194
+ (comparison_basis "highest_card")
195
+ (trick_ending_rule "move_field_to_discard")
196
+ )
197
+
198
+ (comparison_rules
199
+ (single_card
200
+ (compare_by "card_value")
201
+ (source_relation "card_values")
202
+ (require_same_length false)
203
+ (tie_breaker "suit")
204
+ )
205
+ (pair
206
+ (compare_by "card_value")
207
+ (source_relation "card_values")
208
+ (require_same_length false)
209
+ )
210
+ (triple
211
+ (compare_by "card_value")
212
+ (source_relation "card_values")
213
+ (require_same_length false)
214
+ )
215
+ (straight
216
+ (require_same_length true)
217
+ (compare_by "highest_card_value")
218
+ (source_relation "continuous_relations")
219
+ )
220
+ (double_straight
221
+ (require_same_length true)
222
+ (compare_by "highest_pair_value")
223
+ )
224
+ (triple_with_single
225
+ (compare_by "triple_card_value")
226
+ (side_card_rule "ignore")
227
+ )
228
+ (triple_with_pair
229
+ (compare_by "triple_card_value")
230
+ (side_card_rule "ignore")
231
+ )
232
+ (airplane
233
+ (require_same_length true)
234
+ (compare_by "highest_triple_value")
235
+ )
236
+ (airplane_with_single
237
+ (require_same_length true)
238
+ (compare_by "highest_triple_value")
239
+ (side_card_rule "ignore")
240
+ )
241
+ (airplane_with_pair
242
+ (require_same_length true)
243
+ (compare_by "highest_triple_value")
244
+ (side_card_rule "ignore")
245
+ )
246
+ (bomb
247
+ (compare_by "card_value")
248
+ (require_same_length false)
249
+ )
250
+ (bomb_with_single
251
+ (compare_by "bomb_card_value")
252
+ (side_card_rule "ignore")
253
+ )
254
+ (bomb_with_pair
255
+ (compare_by "bomb_card_value")
256
+ (side_card_rule "ignore")
257
+ )
258
+ (rocket
259
+ (compare_by "highest")
260
+ (override_types { "all" })
261
+ )
262
+ )
263
+
264
+ (phases
265
+ (bidding_phase
266
+ (initial_player "with_Spade3")
267
+ (decision_order "sequential")
268
+ (decision_options { "pass" "1" "2" "3" })
269
+ (rules
270
+ (single_option_wins "immediate")
271
+ (multiple_options
272
+ (first_has_extra_choice true)
273
+ (on_first_option
274
+ (first_player_wins "immediate")
275
+ (last_player_wins "immediate")
276
+ )
277
+ (multiplier_on_each_option "doubling")
278
+ )
279
+ (special_rules
280
+ (霸王叫
281
+ (condition "has_chain_bomb_or_higher")
282
+ (effect
283
+ (immediate_landlord true)
284
+ (multiplier_increase 2)
285
+ )
286
+ )
287
+ )
288
+ )
289
+ (outcome
290
+ (assign_role "Landlord" to_winner)
291
+ (give_reserve_cards to "Landlord")
292
+ (special_effect "landlord_see_reserve_cards")
293
+ (landlord_privilege "view_reserve_cards")
294
+ )
295
+ )
296
+
297
+ (playing_phase
298
+ (first_player "with_Spade3")
299
+ (turn_order "circular")
300
+ (actions_ref actions)
301
+ (rules
302
+ (must_follow_suit_or_pass "same_type_only")
303
+ (bomb_increases_multiplier "doubling")
304
+ (chain_bomb_increases_multiplier "doubling")
305
+ (consecutive_pass_ends_trick 2)
306
+ (next_trick_starts_with_last_player true)
307
+ (last_hand_restrictions
308
+ (triple_with_single true)
309
+ (triple_with_two true)
310
+ )
311
+ (ace_placement_restriction true)
312
+ (two_placement_restriction true)
313
+ (replenish_cards_enabled false)
314
+ (active_mechanics { "援手" "换位" })
315
+ (mechanic_conditions
316
+ ("援手" "current_player.role == 'Peasant' and hand.size >= 1")
317
+ ("换位" "current_player.role == 'Landlord' and hand.size >= 2 or current_player.role == 'Peasant' and hand.size >= 2"))
318
+ )
319
+ )
320
+
321
+ (settlement_phase
322
+ (trigger_condition "first_to_empty_hand")
323
+ (win_condition
324
+ (team_with_empty_hand "wins")
325
+ (last_player_standing "loses")
326
+ )
327
+ (scoring
328
+ (winner_reward "base_mult * bombs * chains * landlord_bonus")
329
+ (loser_penalty "base_mult * bombs * chains * landlord_bonus")
330
+ (total_multiplier_calculation "base_mult * bombs * chains * landlord_bonus")
331
+ )
332
+ (capping
333
+ (winner_max_gain "N_times_coin")
334
+ (loser_max_loss "fixed_amount")
335
+ )
336
+ )
337
+
338
+ (initiative_phase
339
+ (mode "spade3")
340
+ (details
341
+ (spade3 (unique_card "Spade3") (must_include false)))
342
+ (constraints
343
+ (executed_after "deal")
344
+ (unique_card_required true)
345
+ (deterministic true)
346
+ (specified_first_player true))
347
+ )
348
+ )
349
+
350
+ (special_mechanics
351
+ (mechanic "援手"
352
+ (enabled true)
353
+ (description "当前农民玩家可以将牌权移交给另一名农民")
354
+ (phase "playing_phase")
355
+ (timing "pre_action")
356
+ (trigger_condition "current_player.role == 'Peasant' and hand.size >= 1")
357
+ (usage_limit "once_per_round")
358
+ (min_players 3)
359
+ (max_players 3)
360
+ (required_conditions { "opponent_has_at_least_1_card" })
361
+ (effect_description "当前农民玩家可以选择将牌权移交给另一名农民")
362
+ (transfer_path from: hand to: field)
363
+ (visibility_change to: {all} on_target: true))
364
+ (mechanic "换位"
365
+ (enabled true)
366
+ (description "当双方都选择过牌时,可以通过消耗特定牌型进行换位")
367
+ (phase "playing_phase")
368
+ (timing "post_action")
369
+ (trigger_condition "both_players_passed_and_has_required_cards")
370
+ (usage_limit "once_per_game")
371
+ (min_players 3)
372
+ (max_players 3)
373
+ (required_conditions { "opponent_has_at_least_1_card" })
374
+ (effect_description "当前玩家可以选择与对手进行换位,交换角色和手牌")
375
+ (transfer_path from: hand to: opponent_hand)
376
+ (visibility_change to: {all} on_target: true)))
377
+
378
+ (metadata
379
+ (author "AI")
380
+ (origin "Innovative")
381
+ (description "在传统斗地主基础上加入了【援手】和【换位】两个新机制,增加了农民间的协作和角色变换的可能性")
382
+ (strategy_depth 4)
383
+ (randomness_factor 0.3)
384
+ (game_duration "medium")
385
+ (player_interaction "hybrid")
386
+ (complexity_level 3)
387
+ )
388
+ ))
requirements.txt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ jsonschema==4.23.0
2
+ gradio==4.44.0
3
+ uvicorn==0.30.6
uno_gdl.txt ADDED
@@ -0,0 +1,116 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ (game "斗地UNO"
2
+ ;; 基础配置
3
+ (players 4)
4
+ (deck "Standard54"
5
+ (shuffling "full")
6
+ (deal_pattern "sequential"))
7
+ (roles { (role "Landlord" 1) (role "Peasant" 3) })
8
+ (turn_order { "Landlord" "Peasant" "Peasant" "Peasant" })
9
+
10
+ ;; 游戏准备阶段
11
+ (setup
12
+ (bidding false)
13
+ (deal 10)
14
+ (reserve 0)
15
+ (initial_score 0)
16
+ (initial_multiplier 1)
17
+
18
+ ;; 区域定义
19
+ (zones
20
+ (hand (default_visibility owner) (initial_cards 10))
21
+ (field (default_visibility all) (initial_cards 0))
22
+ (discard_pile (default_visibility all) (initial_cards 0))
23
+ (main_deck (default_visibility hidden) (initial_cards 88))
24
+ (special_deck (UNO_Cards) (default_visibility all) (initial_cards 24))
25
+ )
26
+
27
+ ;; 牌值与关系定义
28
+ (card_relations
29
+ (card_values 3 4 5 6 7 8 9 10 J Q K A 2)
30
+ (used_in { "single_card" "pair" "straight" "bomb" })
31
+ (continuous_relations
32
+ (PokerSequence 3 4 5 6 7 8 9 10 J Q K A))
33
+ (non_continuous_cards { 2 } (used_in { "exceptions_in_straight_rules" }))
34
+ (suit_relations
35
+ (order "Spade" "Heart" "Club" "Diamond")
36
+ (used_in { "tie_breaker" }))
37
+ (same_value_relation
38
+ (type "same_number")
39
+ (used_in { "pair" "triple" "bomb" }))
40
+ )
41
+ )
42
+
43
+ ;; 牌型系统
44
+ (combinations
45
+ (single)
46
+ (pair)
47
+ (triple)
48
+ (straight 5)
49
+ (bomb 4)
50
+ (custom_combination "UNO_Reverse"
51
+ (pattern [special_card "UNO_Reverse"])
52
+ (power_level 5)
53
+ (override_types { "all" }))
54
+ (custom_combination "UNO_Skip"
55
+ (pattern [special_card "UNO_Skip"])
56
+ (power_level 5)
57
+ (override_types { "all" }))
58
+ (custom_combination "UNO_DrawTwo"
59
+ (pattern [special_card "UNO_DrawTwo"])
60
+ (power_level 5)
61
+ (override_types { "all" }))
62
+ )
63
+
64
+ ;; 游戏阶段与流程
65
+ (phases
66
+ ;; 先手机制阶段
67
+ (initiative_phase
68
+ (mode "random")
69
+ (details
70
+ (random (seed auto) (tie_breaker "suit")))
71
+ (constraints
72
+ (executed_after "deal")
73
+ (deterministic true)))
74
+
75
+ ;; 行牌阶段
76
+ (playing_phase
77
+ (first_player "random")
78
+ (turn_order "circular")
79
+ (actions_ref actions)
80
+ (rules
81
+ (must_follow_suit_or_pass "any_type")
82
+ (bomb_increases_multiplier "doubling")
83
+ (chain_bomb_increases_multiplier "none")
84
+ (consecutive_pass_ends_trick 3)))
85
+ )
86
+
87
+ ;; 特殊机制
88
+ (special_mechanics
89
+ (mechanic "UNO_Reverse"
90
+ (enabled true)
91
+ (description "改变出牌顺序方向")
92
+ (phase "playing_phase")
93
+ (timing "during_action")
94
+ (trigger_condition "valid_play")
95
+ (transfer_path from: hand to: field)
96
+ (visibility_change to: {all} on_target: true))
97
+
98
+ (mechanic "UNO_Skip"
99
+ (enabled true)
100
+ (description "跳过下一个玩家的回合")
101
+ (phase "playing_phase")
102
+ (timing "during_action")
103
+ (trigger_condition "valid_play")
104
+ (transfer_path from: hand to: field)
105
+ (visibility_change to: {all} on_target: true))
106
+
107
+ (mechanic "UNO_DrawTwo"
108
+ (enabled true)
109
+ (description "迫使下一个玩家摸两张牌并跳过回合")
110
+ (phase "playing_phase")
111
+ (timing "during_action")
112
+ (trigger_condition "valid_play")
113
+ (transfer_path from: hand to: field)
114
+ (visibility_change to: {all} on_target: true))
115
+ )
116
+ )
validator_v2.py ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ validator_v2.py — 基于 jsonschema(draft2020-12) 的严格校验
4
+ validate_with_schema(ir, schema_path) -> (ok, issues, summary_msg)
5
+ """
6
+ from __future__ import annotations
7
+ from typing import Any, List, Tuple
8
+ import json
9
+ from jsonschema import Draft202012Validator
10
+ from jsonschema.exceptions import best_match
11
+
12
+ def _format_path(err) -> str:
13
+ if not err.path: return ""
14
+ return "/".join(str(p) for p in err.path)
15
+
16
+ def validate_with_schema(ir: dict, schema_path: str) -> Tuple[bool, List[dict], str]:
17
+ schema = json.loads(open(schema_path, "r", encoding="utf-8").read())
18
+ validator = Draft202012Validator(schema)
19
+ errors = sorted(validator.iter_errors(ir), key=lambda e: list(e.path))
20
+ issues=[]
21
+ for e in errors:
22
+ issues.append({
23
+ "severity": "ERROR",
24
+ "path": _format_path(e),
25
+ "message": e.message
26
+ })
27
+ ok = len(issues)==0
28
+ msg = f"Schema: {'OK' if ok else 'FAIL'}; issues={len(issues)}"
29
+ return ok, issues, msg