Upload folder using huggingface_hub
Browse files- .DS_Store +0 -0
- .gradio/certificate.pem +31 -0
- README.md +35 -7
- __pycache__/gap_detector_v2.cpython-310.pyc +0 -0
- __pycache__/gap_detector_v2.cpython-37.pyc +0 -0
- __pycache__/gdl_parser_v2.cpython-310.pyc +0 -0
- __pycache__/gdl_parser_v2.cpython-37.pyc +0 -0
- __pycache__/mapper_v2.cpython-310.pyc +0 -0
- __pycache__/mapper_v2.cpython-37.pyc +0 -0
- __pycache__/normalizer_v2.cpython-310.pyc +0 -0
- __pycache__/normalizer_v2.cpython-37.pyc +0 -0
- __pycache__/validator_v2.cpython-310.pyc +0 -0
- __pycache__/validator_v2.cpython-37.pyc +0 -0
- app.py +188 -0
- cli_v2.py +109 -0
- gap_detector_v2.py +16 -0
- gdl_parser_v2.py +164 -0
- mapper_v2.py +865 -0
- normalizer_v2.py +454 -0
- poker_gdl_ir.schema.v0.95.zh.json +1515 -0
- poker_gdl_ir.schema.v0.96.zh.json +1746 -0
- qixi_gdl.txt +388 -0
- requirements.txt +3 -0
- uno_gdl.txt +116 -0
- validator_v2.py +29 -0
.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:
|
| 3 |
-
|
| 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 |
-
|
|
|
|
| 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
|