Spaces:
Sleeping
Sleeping
zhongchuyi commited on
Commit ·
20984d5
1
Parent(s): 26b751d
Initial deployment
Browse files- README.md +10 -3
- __pycache__/app.cpython-37.pyc +0 -0
- __pycache__/app.cpython-38.pyc +0 -0
- __pycache__/design_state.cpython-38.pyc +0 -0
- __pycache__/output_validator.cpython-37.pyc +0 -0
- app.py +497 -34
- design_state.py +884 -2
- m_prompt.txt +296 -1277
- output_validator.py +117 -2
- requirements.txt +2 -2
- styles.py +32 -0
- 麻将机制说明.md +794 -25
README.md
CHANGED
|
@@ -1,10 +1,10 @@
|
|
| 1 |
---
|
| 2 |
title: MahjongGameDesigner
|
| 3 |
emoji: 🀄️
|
| 4 |
-
colorFrom:
|
| 5 |
colorTo: yellow
|
| 6 |
sdk: gradio
|
| 7 |
-
sdk_version:
|
| 8 |
app_file: app.py
|
| 9 |
pinned: false
|
| 10 |
---
|
|
@@ -16,7 +16,7 @@ Check out the configuration reference at https://huggingface.co/docs/hub/spaces-
|
|
| 16 |
MahjongGameDesigner 是一个“麻将玩法生成与迭代”工具,输出三类交付物:
|
| 17 |
- **自然语言规则说明**(可直接给策划/制作/评审)
|
| 18 |
- **mGDL v1.3**(结构化规则描述,用于规范化与落地)
|
| 19 |
-
- **思维日志(设计日志)**(
|
| 20 |
|
| 21 |
工具提供两种关键能力:
|
| 22 |
- **Analyse 模式(单问题迭代)**:通过多轮问答把需求场景收敛到可生成状态(不生成 mGDL)
|
|
@@ -55,6 +55,13 @@ MahjongGameDesigner 是一个“麻将玩法生成与迭代”工具,输出三
|
|
| 55 |
- 思维日志会从 `### 设计日志(创新推演摘要)` 段落提取并保存到 `exports/design_log_output.txt`
|
| 56 |
- 在 Analyse 模式下不会导出上述文件(因为 Analyse 模式不生成完整交付物)
|
| 57 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 58 |
## 生成质量增强(默认开启)
|
| 59 |
|
| 60 |
为提升“玩法理解”与“创新落地”,系统默认会自动注入:
|
|
|
|
| 1 |
---
|
| 2 |
title: MahjongGameDesigner
|
| 3 |
emoji: 🀄️
|
| 4 |
+
colorFrom: green
|
| 5 |
colorTo: yellow
|
| 6 |
sdk: gradio
|
| 7 |
+
sdk_version: 4.44.1
|
| 8 |
app_file: app.py
|
| 9 |
pinned: false
|
| 10 |
---
|
|
|
|
| 16 |
MahjongGameDesigner 是一个“麻将玩法生成与迭代”工具,输出三类交付物:
|
| 17 |
- **自然语言规则说明**(可直接给策划/制作/评审)
|
| 18 |
- **mGDL v1.3**(结构化规则描述,用于规范化与落地)
|
| 19 |
+
- **思维日志(设计日志)**(融合清单、冲突桥接、推演摘要、落地映射,便于人工复核)
|
| 20 |
|
| 21 |
工具提供两种关键能力:
|
| 22 |
- **Analyse 模式(单问题迭代)**:通过多轮问答把需求场景收敛到可生成状态(不生成 mGDL)
|
|
|
|
| 55 |
- 思维日志会从 `### 设计日志(创新推演摘要)` 段落提取并保存到 `exports/design_log_output.txt`
|
| 56 |
- 在 Analyse 模式下不会导出上述文件(因为 Analyse 模式不生成完整交付物)
|
| 57 |
|
| 58 |
+
## 当前阶段的规则准确性重点(必读)
|
| 59 |
+
|
| 60 |
+
现阶段主要聚焦“既有机制组合”,但必须保证麻将底层物理逻辑准确(手牌守恒、吃碰杠轮次、牌墙转移等)。因此生成模式下请重点检查输出是否包含并一致:
|
| 61 |
+
- 《动作—手牌变化—轮次影响表》
|
| 62 |
+
- 3段《最小回合推演》(普通/碰/杠,每行标注手牌张数变化)
|
| 63 |
+
- mGDL 的 `(invariants ...)` 中包含牌数守恒与“回合结束手牌恒为13”的约束
|
| 64 |
+
|
| 65 |
## 生成质量增强(默认开启)
|
| 66 |
|
| 67 |
为提升“玩法理解”与“创新落地”,系统默认会自动注入:
|
__pycache__/app.cpython-37.pyc
ADDED
|
Binary file (22.8 kB). View file
|
|
|
__pycache__/app.cpython-38.pyc
ADDED
|
Binary file (32.7 kB). View file
|
|
|
__pycache__/design_state.cpython-38.pyc
ADDED
|
Binary file (27.2 kB). View file
|
|
|
__pycache__/output_validator.cpython-37.pyc
CHANGED
|
Binary files a/__pycache__/output_validator.cpython-37.pyc and b/__pycache__/output_validator.cpython-37.pyc differ
|
|
|
app.py
CHANGED
|
@@ -41,6 +41,8 @@ try:
|
|
| 41 |
except Exception:
|
| 42 |
design_mahjong_game_stream = None
|
| 43 |
|
|
|
|
|
|
|
| 44 |
from design_state import (
|
| 45 |
extract_design_state,
|
| 46 |
extract_ready_to_generate,
|
|
@@ -48,6 +50,33 @@ from design_state import (
|
|
| 48 |
diff_keys,
|
| 49 |
diff_mechanics,
|
| 50 |
is_change_within_scope,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
)
|
| 52 |
|
| 53 |
|
|
@@ -160,6 +189,24 @@ def clear_files():
|
|
| 160 |
return None
|
| 161 |
|
| 162 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 163 |
def update_file_status(files):
|
| 164 |
"""更新文件状态显示"""
|
| 165 |
if not files:
|
|
@@ -297,7 +344,7 @@ def save_gdl_and_narrative(gdl_content, narrative_content):
|
|
| 297 |
|
| 298 |
def extract_design_log(content):
|
| 299 |
"""
|
| 300 |
-
提取
|
| 301 |
规则:从标题行开始,截到下一个同级标题(###)或文件结尾。
|
| 302 |
"""
|
| 303 |
import re
|
|
@@ -305,12 +352,16 @@ def extract_design_log(content):
|
|
| 305 |
if not content:
|
| 306 |
return ""
|
| 307 |
|
| 308 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 309 |
if not m:
|
| 310 |
return ""
|
| 311 |
start = m.start()
|
| 312 |
|
| 313 |
-
m2 = re.search(r"^###\
|
| 314 |
end = (m.end() + m2.start()) if m2 else len(content)
|
| 315 |
return content[start:end].strip()
|
| 316 |
|
|
@@ -412,6 +463,8 @@ with gr.Blocks(
|
|
| 412 |
design_state_obj = gr.State({}) # 解析后的 dict(用于显示与对比)
|
| 413 |
design_state_version = gr.State(0) # 版本号(每次成功提取 +1)
|
| 414 |
ready_to_generate = gr.State(False)
|
|
|
|
|
|
|
| 415 |
|
| 416 |
with gr.Group(elem_classes="side-card"):
|
| 417 |
gr.Markdown("### 可控迭代(推荐)")
|
|
@@ -425,19 +478,89 @@ with gr.Blocks(
|
|
| 425 |
)
|
| 426 |
ready_status = gr.Markdown("READY_TO_GENERATE:未知", elem_classes="hint")
|
| 427 |
iteration_scope = gr.Dropdown(
|
| 428 |
-
label="本轮允许修改范围",
|
| 429 |
-
choices=
|
| 430 |
-
"自由迭代(仍保最小修改)",
|
| 431 |
-
"仅优化创新机制",
|
| 432 |
-
"仅优化计分与番型",
|
| 433 |
-
"仅优化流程与阶段",
|
| 434 |
-
"仅修复校验问题",
|
| 435 |
-
],
|
| 436 |
value="仅优化创新机制",
|
| 437 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 438 |
generate_from_state_btn = gr.Button("基于当前 DesignState 生成完整玩法", variant="primary")
|
| 439 |
design_state_status = gr.Markdown("DesignState:未建立(先生成一版玩法)", elem_classes="hint")
|
| 440 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 441 |
user_input = gr.Textbox(
|
| 442 |
placeholder="例如:为4人竞速推倒胡设计番型与流程;或“做一个双人合作麻将 roguelike” …",
|
| 443 |
show_label=False,
|
|
@@ -465,21 +588,25 @@ with gr.Blocks(
|
|
| 465 |
ds_obj_prev,
|
| 466 |
ds_ver_prev,
|
| 467 |
ready_prev,
|
|
|
|
|
|
|
| 468 |
):
|
| 469 |
user_text = (user_text or "").strip()
|
|
|
|
|
|
|
| 470 |
if not user_text:
|
| 471 |
# 不提交空消息:输出不变
|
| 472 |
-
yield history_msgs, "", history_msgs, "", "", "", ds_raw_prev, ds_obj_prev, ds_ver_prev, "DesignState:未变更", ready_prev, "READY_TO_GENERATE:未知"
|
| 473 |
return
|
| 474 |
|
| 475 |
-
# 立即显示
|
| 476 |
history = list(history_msgs or [])
|
| 477 |
history.append({"role": "user", "content": user_text})
|
| 478 |
-
yield history, "", history, "", "", "", ds_raw_prev, ds_obj_prev, ds_ver_prev, "DesignState:处理中…", ready_prev, "READY_TO_GENERATE:未知"
|
| 479 |
|
| 480 |
# 添加空的助手气泡,用于逐步填充
|
| 481 |
history.append({"role": "assistant", "content": ""})
|
| 482 |
-
yield history, "", history, "", "", "", ds_raw_prev, ds_obj_prev, ds_ver_prev, "DesignState:处理中…", ready_prev, "READY_TO_GENERATE:未知"
|
| 483 |
|
| 484 |
# 流式生成内容
|
| 485 |
tuples_hist = _messages_to_tuples(history)
|
|
@@ -526,10 +653,10 @@ with gr.Blocks(
|
|
| 526 |
if not piece:
|
| 527 |
continue
|
| 528 |
history[-1]["content"] += str(piece)
|
| 529 |
-
yield history, "", history, "", "", "", ds_raw_prev, ds_obj_prev, ds_ver_prev, status_hint, ready_to_gen, ready_md
|
| 530 |
except Exception as e:
|
| 531 |
history[-1]["content"] += f"\n(流式出错){type(e).__name__}: {e}"
|
| 532 |
-
yield history, "", history, "", "", "", ds_raw_prev, ds_obj_prev, ds_ver_prev, "DesignState:流式出错(未变更)", ready_to_gen, "READY_TO_GENERATE:未知"
|
| 533 |
else:
|
| 534 |
try:
|
| 535 |
full = design_mahjong_game(effective_user_text, tuples_to_send, files, custom_prompt, mode)
|
|
@@ -537,13 +664,16 @@ with gr.Blocks(
|
|
| 537 |
full = f"(出错){type(e).__name__}: {e}"
|
| 538 |
for piece in _chunk_fake_stream(str(full), step=40):
|
| 539 |
history[-1]["content"] += piece
|
| 540 |
-
yield history, "", history, "", "", "", ds_raw_prev, ds_obj_prev, ds_ver_prev, status_hint, ready_to_gen, ready_md
|
| 541 |
|
| 542 |
# 提取 GDL 和自然语言描述并保存
|
| 543 |
new_ds_obj, new_ds_raw = extract_design_state(history[-1]["content"])
|
| 544 |
ready_flag = extract_ready_to_generate(history[-1]["content"])
|
| 545 |
ds_ver = int(ds_ver_prev or 0)
|
| 546 |
ds_status = "DesignState:未变更"
|
|
|
|
|
|
|
|
|
|
| 547 |
if new_ds_obj and new_ds_raw:
|
| 548 |
ds_ver = ds_ver + 1
|
| 549 |
ds_status = "DesignState:v{0} 已更新 | {1}".format(ds_ver, summarize_design_state(new_ds_obj))
|
|
@@ -551,7 +681,19 @@ with gr.Blocks(
|
|
| 551 |
ready_to_gen = ready_flag
|
| 552 |
ready_md = "READY_TO_GENERATE:{0}".format("true" if ready_flag else "false")
|
| 553 |
|
| 554 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 555 |
if iterative_enabled and ds_obj_prev and isinstance(ds_obj_prev, dict) and scope:
|
| 556 |
changed = diff_keys(ds_obj_prev, new_ds_obj)
|
| 557 |
mech_changes = diff_mechanics(ds_obj_prev, new_ds_obj)
|
|
@@ -561,7 +703,7 @@ with gr.Blocks(
|
|
| 561 |
"允许范围:{scope}\n"
|
| 562 |
"顶层变更字段:{keys}\n"
|
| 563 |
"机制变更:{mech}\n"
|
| 564 |
-
"建议:请重新执行一次迭代,明确只改允许范围内字段,或切换为
|
| 565 |
).format(
|
| 566 |
scope=scope,
|
| 567 |
keys=",".join(changed) if changed else "(无)",
|
|
@@ -569,23 +711,27 @@ with gr.Blocks(
|
|
| 569 |
)
|
| 570 |
|
| 571 |
try:
|
|
|
|
|
|
|
|
|
|
|
|
|
| 572 |
# Analyse 模式不产出完整规则,不导出GDL/自然语言文件
|
| 573 |
if analyse_enabled:
|
| 574 |
-
yield history, "", history, "", "", "", (new_ds_raw or ds_raw_prev), (new_ds_obj or ds_obj_prev), ds_ver, ds_status, ready_to_gen, ready_md
|
| 575 |
return
|
| 576 |
|
| 577 |
gdl_content, narrative_content = extract_gdl_and_narrative(history[-1]["content"])
|
| 578 |
design_log_content = extract_design_log(history[-1]["content"])
|
| 579 |
-
|
| 580 |
# 保存 GDL 和自然语言文件
|
| 581 |
gdl_path, narrative_path = save_gdl_and_narrative(gdl_content, narrative_content)
|
| 582 |
design_log_path = save_design_log(design_log_content)
|
| 583 |
-
|
| 584 |
# 返回文件路径,以便下载
|
| 585 |
-
yield history, "", history, gdl_path, narrative_path, design_log_path, (new_ds_raw or ds_raw_prev), (new_ds_obj or ds_obj_prev), ds_ver, ds_status, ready_to_gen, ready_md
|
| 586 |
except Exception as e:
|
| 587 |
print(f"保存GDL和自然语言文件时出错: {e}")
|
| 588 |
-
yield history, "", history, "", "", "", (new_ds_raw or ds_raw_prev), (new_ds_obj or ds_obj_prev), ds_ver, ds_status, ready_to_gen, ready_md
|
| 589 |
|
| 590 |
def on_generate_from_state(
|
| 591 |
history_msgs,
|
|
@@ -596,18 +742,20 @@ with gr.Blocks(
|
|
| 596 |
ds_obj_prev,
|
| 597 |
ds_ver_prev,
|
| 598 |
ready_prev,
|
|
|
|
| 599 |
):
|
|
|
|
| 600 |
if not (ds_raw_prev or "").strip():
|
| 601 |
-
yield history_msgs, history_msgs, "", "", "", ds_raw_prev, ds_obj_prev, ds_ver_prev, "DesignState:未建立(先生成一版玩法)", ready_prev, "READY_TO_GENERATE:未知"
|
| 602 |
return
|
| 603 |
|
| 604 |
history = list(history_msgs or [])
|
| 605 |
user_text = "基于当前 DesignState 生成完整玩法(自然语言规则 + mGDL + 自检报告)。"
|
| 606 |
history.append({"role": "user", "content": user_text})
|
| 607 |
-
yield history, history, "", "", "", ds_raw_prev, ds_obj_prev, ds_ver_prev, "DesignState:生成中…", ready_prev, "READY_TO_GENERATE:未知"
|
| 608 |
|
| 609 |
history.append({"role": "assistant", "content": ""})
|
| 610 |
-
yield history, history, "", "", "", ds_raw_prev, ds_obj_prev, ds_ver_prev, "DesignState:生成中…", ready_prev, "READY_TO_GENERATE:未知"
|
| 611 |
|
| 612 |
effective_user_text = (
|
| 613 |
"请基于下方 DesignState(JSON) 生成完整的麻将新玩法交付物:\n"
|
|
@@ -628,33 +776,50 @@ with gr.Blocks(
|
|
| 628 |
s = str(piece)
|
| 629 |
buf.append(s)
|
| 630 |
history[-1]["content"] += s
|
| 631 |
-
yield history, history, "", "", "", ds_raw_prev, ds_obj_prev, ds_ver_prev, "DesignState:生成中…", ready_prev, "READY_TO_GENERATE:未知"
|
| 632 |
else:
|
| 633 |
full = design_mahjong_game(effective_user_text, tuples_to_send, files, custom_prompt, mode)
|
| 634 |
buf.append(str(full))
|
| 635 |
for piece in _chunk_fake_stream(str(full), step=40):
|
| 636 |
history[-1]["content"] += piece
|
| 637 |
-
yield history, history, "", "", "", ds_raw_prev, ds_obj_prev, ds_ver_prev, "DesignState:生成中…", ready_prev, "READY_TO_GENERATE:未知"
|
| 638 |
except Exception as e:
|
| 639 |
history[-1]["content"] += f"\n(生成出错){type(e).__name__}: {e}"
|
| 640 |
-
yield history, history, "", "", "", ds_raw_prev, ds_obj_prev, ds_ver_prev, "DesignState:生成出错", ready_prev, "READY_TO_GENERATE:未知"
|
| 641 |
return
|
| 642 |
|
| 643 |
new_ds_obj, new_ds_raw = extract_design_state(history[-1]["content"])
|
| 644 |
ds_ver = int(ds_ver_prev or 0)
|
| 645 |
ds_status = "DesignState:未变更"
|
|
|
|
|
|
|
|
|
|
| 646 |
if new_ds_obj and new_ds_raw:
|
| 647 |
ds_ver += 1
|
| 648 |
ds_status = "DesignState:v{0} 已更新 | {1}".format(ds_ver, summarize_design_state(new_ds_obj))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 649 |
|
| 650 |
try:
|
| 651 |
gdl_content, narrative_content = extract_gdl_and_narrative(history[-1]["content"])
|
| 652 |
design_log_content = extract_design_log(history[-1]["content"])
|
| 653 |
gdl_path, narrative_path = save_gdl_and_narrative(gdl_content, narrative_content)
|
| 654 |
design_log_path = save_design_log(design_log_content)
|
| 655 |
-
yield history, history, gdl_path, narrative_path, design_log_path, (new_ds_raw or ds_raw_prev), (new_ds_obj or ds_obj_prev), ds_ver, ds_status, ready_prev, "READY_TO_GENERATE:未知"
|
| 656 |
except Exception:
|
| 657 |
-
yield history, history, "", "", "", (new_ds_raw or ds_raw_prev), (new_ds_obj or ds_obj_prev), ds_ver, ds_status, ready_prev, "READY_TO_GENERATE:未知"
|
| 658 |
|
| 659 |
# 绑定:回车提交(Enter=提交;Shift+Enter=换行由浏览器处理)
|
| 660 |
user_input.submit(
|
|
@@ -672,6 +837,7 @@ with gr.Blocks(
|
|
| 672 |
design_state_obj,
|
| 673 |
design_state_version,
|
| 674 |
ready_to_generate,
|
|
|
|
| 675 |
],
|
| 676 |
outputs=[
|
| 677 |
chatbot,
|
|
@@ -686,6 +852,11 @@ with gr.Blocks(
|
|
| 686 |
design_state_status,
|
| 687 |
ready_to_generate,
|
| 688 |
ready_status,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 689 |
],
|
| 690 |
preprocess=True,
|
| 691 |
)
|
|
@@ -706,6 +877,7 @@ with gr.Blocks(
|
|
| 706 |
design_state_obj,
|
| 707 |
design_state_version,
|
| 708 |
ready_to_generate,
|
|
|
|
| 709 |
],
|
| 710 |
outputs=[
|
| 711 |
chatbot,
|
|
@@ -720,6 +892,11 @@ with gr.Blocks(
|
|
| 720 |
design_state_status,
|
| 721 |
ready_to_generate,
|
| 722 |
ready_status,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 723 |
],
|
| 724 |
preprocess=True,
|
| 725 |
)
|
|
@@ -735,6 +912,7 @@ with gr.Blocks(
|
|
| 735 |
design_state_obj,
|
| 736 |
design_state_version,
|
| 737 |
ready_to_generate,
|
|
|
|
| 738 |
],
|
| 739 |
outputs=[
|
| 740 |
chatbot,
|
|
@@ -748,10 +926,273 @@ with gr.Blocks(
|
|
| 748 |
design_state_status,
|
| 749 |
ready_to_generate,
|
| 750 |
ready_status,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 751 |
],
|
| 752 |
preprocess=True,
|
| 753 |
)
|
| 754 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 755 |
# 导出对话
|
| 756 |
with gr.Row():
|
| 757 |
export_btn = gr.Button("导出对话(Markdown)", variant="secondary")
|
|
@@ -762,7 +1203,16 @@ with gr.Blocks(
|
|
| 762 |
clear_dialog_btn = gr.Button("清空对话", variant="secondary")
|
| 763 |
|
| 764 |
def _clear_chat():
|
| 765 |
-
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 766 |
|
| 767 |
clear_dialog_btn.click(
|
| 768 |
fn=_clear_chat,
|
|
@@ -780,6 +1230,19 @@ with gr.Blocks(
|
|
| 780 |
design_state_status,
|
| 781 |
ready_to_generate,
|
| 782 |
ready_status,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 783 |
],
|
| 784 |
)
|
| 785 |
|
|
|
|
| 41 |
except Exception:
|
| 42 |
design_mahjong_game_stream = None
|
| 43 |
|
| 44 |
+
from output_validator import validate_mahjong_response, format_issues_for_llm
|
| 45 |
+
|
| 46 |
from design_state import (
|
| 47 |
extract_design_state,
|
| 48 |
extract_ready_to_generate,
|
|
|
|
| 50 |
diff_keys,
|
| 51 |
diff_mechanics,
|
| 52 |
is_change_within_scope,
|
| 53 |
+
generate_diff_summary,
|
| 54 |
+
generate_diff_summary_compact,
|
| 55 |
+
# 版本历史相关
|
| 56 |
+
create_empty_history,
|
| 57 |
+
add_to_history,
|
| 58 |
+
rollback_history,
|
| 59 |
+
get_history_choices,
|
| 60 |
+
parse_version_from_choice,
|
| 61 |
+
# 多阶段交互相关
|
| 62 |
+
InteractionPhase,
|
| 63 |
+
extract_proposals,
|
| 64 |
+
extract_clarify_questions,
|
| 65 |
+
detect_interaction_phase,
|
| 66 |
+
format_proposals_for_display,
|
| 67 |
+
generate_phase_prompt_hint,
|
| 68 |
+
create_phase_state,
|
| 69 |
+
update_phase_state,
|
| 70 |
+
get_phase_display_name,
|
| 71 |
+
# 细粒度范围控制相关
|
| 72 |
+
CONTROLLABLE_FIELDS,
|
| 73 |
+
ScopeConstraint,
|
| 74 |
+
create_scope_config,
|
| 75 |
+
get_scope_preset_options,
|
| 76 |
+
validate_scope_compliance,
|
| 77 |
+
format_scope_violations,
|
| 78 |
+
get_mechanics_from_state,
|
| 79 |
+
generate_scope_prompt_hint,
|
| 80 |
)
|
| 81 |
|
| 82 |
|
|
|
|
| 189 |
return None
|
| 190 |
|
| 191 |
|
| 192 |
+
def render_validation_md(text: str) -> str:
|
| 193 |
+
issues = validate_mahjong_response(text or "")
|
| 194 |
+
if not issues:
|
| 195 |
+
return "✅ 输出校验:未发现静态问题(可导出)"
|
| 196 |
+
|
| 197 |
+
errors = [i for i in issues if i.get("level") == "error"]
|
| 198 |
+
warnings = [i for i in issues if i.get("level") == "warning"]
|
| 199 |
+
|
| 200 |
+
parts = ["⚠️ 输出校验:发现潜在问题(建议先让模型按最小修改修复后再导出)"]
|
| 201 |
+
if errors:
|
| 202 |
+
parts.append("\n**错误(必须修复)**\n")
|
| 203 |
+
parts.append(format_issues_for_llm(errors))
|
| 204 |
+
if warnings:
|
| 205 |
+
parts.append("\n**警告(建议修复)**\n")
|
| 206 |
+
parts.append(format_issues_for_llm(warnings))
|
| 207 |
+
return "\n".join(parts).strip()
|
| 208 |
+
|
| 209 |
+
|
| 210 |
def update_file_status(files):
|
| 211 |
"""更新文件状态显示"""
|
| 212 |
if not files:
|
|
|
|
| 344 |
|
| 345 |
def extract_design_log(content):
|
| 346 |
"""
|
| 347 |
+
提取"设计日志(创新推演摘要)"段落,用于单独导出。
|
| 348 |
规则:从标题行开始,截到下一个同级标题(###)或文件结尾。
|
| 349 |
"""
|
| 350 |
import re
|
|
|
|
| 352 |
if not content:
|
| 353 |
return ""
|
| 354 |
|
| 355 |
+
# 修复:原始字符串中 \s 即可匹配空白,不需要 \\s
|
| 356 |
+
m = re.search(r"^###\s*设计日志(创新推演摘要)\s*$", content, re.MULTILINE)
|
| 357 |
+
if not m:
|
| 358 |
+
# 兼容变体格式:设计日志/思维日志/Design Log
|
| 359 |
+
m = re.search(r"^###\s*(设计日志|思维日志|Design\s*Log).*$", content, re.MULTILINE | re.IGNORECASE)
|
| 360 |
if not m:
|
| 361 |
return ""
|
| 362 |
start = m.start()
|
| 363 |
|
| 364 |
+
m2 = re.search(r"^###\s+.+$", content[m.end():], re.MULTILINE)
|
| 365 |
end = (m.end() + m2.start()) if m2 else len(content)
|
| 366 |
return content[start:end].strip()
|
| 367 |
|
|
|
|
| 463 |
design_state_obj = gr.State({}) # 解析后的 dict(用于显示与对比)
|
| 464 |
design_state_version = gr.State(0) # 版本号(每次成功提取 +1)
|
| 465 |
ready_to_generate = gr.State(False)
|
| 466 |
+
design_state_history = gr.State(create_empty_history()) # 版本历史
|
| 467 |
+
interaction_phase_state = gr.State(create_phase_state()) # 多阶段交互状态
|
| 468 |
|
| 469 |
with gr.Group(elem_classes="side-card"):
|
| 470 |
gr.Markdown("### 可控迭代(推荐)")
|
|
|
|
| 478 |
)
|
| 479 |
ready_status = gr.Markdown("READY_TO_GENERATE:未知", elem_classes="hint")
|
| 480 |
iteration_scope = gr.Dropdown(
|
| 481 |
+
label="本轮允许修改范围(预设)",
|
| 482 |
+
choices=get_scope_preset_options(),
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 483 |
value="仅优化创新机制",
|
| 484 |
)
|
| 485 |
+
|
| 486 |
+
# 细粒度范围控制(新增)
|
| 487 |
+
with gr.Accordion("🔒 细粒度范围控制", open=False) as scope_accordion:
|
| 488 |
+
scope_mode = gr.Radio(
|
| 489 |
+
choices=["预设模式", "自定义模式"],
|
| 490 |
+
value="预设模式",
|
| 491 |
+
label="控制模式",
|
| 492 |
+
)
|
| 493 |
+
constraint_level = gr.Radio(
|
| 494 |
+
choices=["软约束(提示)", "硬约束(阻断)"],
|
| 495 |
+
value="软约束(提示)",
|
| 496 |
+
label="约束级别",
|
| 497 |
+
)
|
| 498 |
+
with gr.Group(visible=False) as custom_scope_group:
|
| 499 |
+
gr.Markdown("**锁定字段**(选中的字段不允许修改)", elem_classes="hint")
|
| 500 |
+
locked_fields = gr.CheckboxGroup(
|
| 501 |
+
choices=[
|
| 502 |
+
("玩法名称", "new_variant_name"),
|
| 503 |
+
("底座玩法", "base_variants"),
|
| 504 |
+
("融合玩法", "fusion_variants"),
|
| 505 |
+
("游戏模式", "game_variant"),
|
| 506 |
+
("玩家人数", "players"),
|
| 507 |
+
("计分模式", "scoring_mode"),
|
| 508 |
+
("牌组配置", "tileset"),
|
| 509 |
+
],
|
| 510 |
+
value=[],
|
| 511 |
+
label="",
|
| 512 |
+
)
|
| 513 |
+
gr.Markdown("**锁定机制**(选中的机制不允许修改)", elem_classes="hint")
|
| 514 |
+
locked_mechanics = gr.CheckboxGroup(
|
| 515 |
+
choices=[],
|
| 516 |
+
value=[],
|
| 517 |
+
label="",
|
| 518 |
+
)
|
| 519 |
+
refresh_mechanics_btn = gr.Button("刷新机制列表", size="sm")
|
| 520 |
+
scope_status = gr.Markdown("_范围约束: 预设模式_", elem_classes="hint")
|
| 521 |
+
|
| 522 |
+
# 保存范围配置的 State
|
| 523 |
+
scope_config_state = gr.State(create_scope_config())
|
| 524 |
+
|
| 525 |
generate_from_state_btn = gr.Button("基于当前 DesignState 生成完整玩法", variant="primary")
|
| 526 |
design_state_status = gr.Markdown("DesignState:未建立(先生成一版玩法)", elem_classes="hint")
|
| 527 |
|
| 528 |
+
# 差异可视化区域(新增)
|
| 529 |
+
with gr.Accordion("📊 DesignState 变更详情", open=False) as diff_accordion:
|
| 530 |
+
diff_summary_display = gr.Markdown("_尚无变更记录_", elem_classes="diff-summary")
|
| 531 |
+
|
| 532 |
+
# 版本历史与回滚(新增)
|
| 533 |
+
with gr.Accordion("🕐 版本历史与回滚", open=False) as history_accordion:
|
| 534 |
+
version_dropdown = gr.Dropdown(
|
| 535 |
+
label="选择版本",
|
| 536 |
+
choices=[],
|
| 537 |
+
value=None,
|
| 538 |
+
interactive=True,
|
| 539 |
+
)
|
| 540 |
+
with gr.Row():
|
| 541 |
+
rollback_btn = gr.Button("回滚到所选版本", variant="secondary", size="sm")
|
| 542 |
+
refresh_history_btn = gr.Button("刷新列表", variant="secondary", size="sm")
|
| 543 |
+
history_status = gr.Markdown("_无版本历史_", elem_classes="hint")
|
| 544 |
+
|
| 545 |
+
# 多阶段交互与方案选择(新增)
|
| 546 |
+
with gr.Accordion("🎯 方案选择与交互引导", open=True) as phase_accordion:
|
| 547 |
+
phase_status = gr.Markdown("**当前阶段**: 🚀 初始", elem_classes="hint")
|
| 548 |
+
proposals_display = gr.Markdown("_等待生成方案..._", visible=False)
|
| 549 |
+
with gr.Row(visible=False) as proposal_select_row:
|
| 550 |
+
proposal_dropdown = gr.Dropdown(
|
| 551 |
+
label="选择方案",
|
| 552 |
+
choices=[],
|
| 553 |
+
value=None,
|
| 554 |
+
interactive=True,
|
| 555 |
+
scale=3,
|
| 556 |
+
)
|
| 557 |
+
select_proposal_btn = gr.Button("确认选择", variant="primary", size="sm", scale=1)
|
| 558 |
+
with gr.Row(visible=False) as diverge_action_row:
|
| 559 |
+
request_more_btn = gr.Button("🔄 重新发散", variant="secondary", size="sm")
|
| 560 |
+
request_elaborate_btn = gr.Button("📝 直接深入展开", variant="secondary", size="sm")
|
| 561 |
+
|
| 562 |
+
validation_status = gr.Markdown("输出校验:未运行", elem_classes="hint")
|
| 563 |
+
|
| 564 |
user_input = gr.Textbox(
|
| 565 |
placeholder="例如:为4人竞速推倒胡设计番型与流程;或“做一个双人合作麻将 roguelike” …",
|
| 566 |
show_label=False,
|
|
|
|
| 588 |
ds_obj_prev,
|
| 589 |
ds_ver_prev,
|
| 590 |
ready_prev,
|
| 591 |
+
history_data,
|
| 592 |
+
phase_state,
|
| 593 |
):
|
| 594 |
user_text = (user_text or "").strip()
|
| 595 |
+
diff_md_prev = "_尚无变更记录_" # 保留��次的差异摘要
|
| 596 |
+
cur_choices = get_history_choices(history_data)
|
| 597 |
if not user_text:
|
| 598 |
# 不提交空消息:输出不变
|
| 599 |
+
yield history_msgs, "", history_msgs, "", "", "", ds_raw_prev, ds_obj_prev, ds_ver_prev, "DesignState:未变更", ready_prev, "READY_TO_GENERATE:未知", "输出校验:未运行", diff_md_prev, history_data, gr.update(choices=cur_choices), "_无版本历史_" if not cur_choices else f"共 {len(cur_choices)} 个版本"
|
| 600 |
return
|
| 601 |
|
| 602 |
+
# 立即显示"用户消息"
|
| 603 |
history = list(history_msgs or [])
|
| 604 |
history.append({"role": "user", "content": user_text})
|
| 605 |
+
yield history, "", history, "", "", "", ds_raw_prev, ds_obj_prev, ds_ver_prev, "DesignState:处理中…", ready_prev, "READY_TO_GENERATE:未知", "输出校验:运行中…", "_处理中..._", history_data, gr.update(choices=cur_choices), f"共 {len(cur_choices)} 个版本" if cur_choices else "_无版本历史_"
|
| 606 |
|
| 607 |
# 添加空的助手气泡,用于逐步填充
|
| 608 |
history.append({"role": "assistant", "content": ""})
|
| 609 |
+
yield history, "", history, "", "", "", ds_raw_prev, ds_obj_prev, ds_ver_prev, "DesignState:处理中…", ready_prev, "READY_TO_GENERATE:未知", "输出校验:运行中…", "_处理中..._", history_data, gr.update(choices=cur_choices), f"共 {len(cur_choices)} 个版本" if cur_choices else "_无版本历史_"
|
| 610 |
|
| 611 |
# 流式生成内容
|
| 612 |
tuples_hist = _messages_to_tuples(history)
|
|
|
|
| 653 |
if not piece:
|
| 654 |
continue
|
| 655 |
history[-1]["content"] += str(piece)
|
| 656 |
+
yield history, "", history, "", "", "", ds_raw_prev, ds_obj_prev, ds_ver_prev, status_hint, ready_to_gen, ready_md, "输出校验:运行中…", "_处理中..._", history_data, gr.update(choices=cur_choices), f"共 {len(cur_choices)} 个版本" if cur_choices else "_无版本历史_"
|
| 657 |
except Exception as e:
|
| 658 |
history[-1]["content"] += f"\n(流式出错){type(e).__name__}: {e}"
|
| 659 |
+
yield history, "", history, "", "", "", ds_raw_prev, ds_obj_prev, ds_ver_prev, "DesignState:流式出错(未变更)", ready_to_gen, "READY_TO_GENERATE:未知", "输出校验:未运行", "_处理出错_", history_data, gr.update(choices=cur_choices), f"共 {len(cur_choices)} 个版本" if cur_choices else "_无版本历史_"
|
| 660 |
else:
|
| 661 |
try:
|
| 662 |
full = design_mahjong_game(effective_user_text, tuples_to_send, files, custom_prompt, mode)
|
|
|
|
| 664 |
full = f"(出错){type(e).__name__}: {e}"
|
| 665 |
for piece in _chunk_fake_stream(str(full), step=40):
|
| 666 |
history[-1]["content"] += piece
|
| 667 |
+
yield history, "", history, "", "", "", ds_raw_prev, ds_obj_prev, ds_ver_prev, status_hint, ready_to_gen, ready_md, "输出校验:运行中…", "_处理中..._", history_data, gr.update(choices=cur_choices), f"共 {len(cur_choices)} 个版本" if cur_choices else "_无版本历史_"
|
| 668 |
|
| 669 |
# 提取 GDL 和自然语言描述并保存
|
| 670 |
new_ds_obj, new_ds_raw = extract_design_state(history[-1]["content"])
|
| 671 |
ready_flag = extract_ready_to_generate(history[-1]["content"])
|
| 672 |
ds_ver = int(ds_ver_prev or 0)
|
| 673 |
ds_status = "DesignState:未变更"
|
| 674 |
+
diff_summary_md = "_无变更_"
|
| 675 |
+
updated_history_data = history_data
|
| 676 |
+
|
| 677 |
if new_ds_obj and new_ds_raw:
|
| 678 |
ds_ver = ds_ver + 1
|
| 679 |
ds_status = "DesignState:v{0} 已更新 | {1}".format(ds_ver, summarize_design_state(new_ds_obj))
|
|
|
|
| 681 |
ready_to_gen = ready_flag
|
| 682 |
ready_md = "READY_TO_GENERATE:{0}".format("true" if ready_flag else "false")
|
| 683 |
|
| 684 |
+
# 生成差异摘要
|
| 685 |
+
diff_summary_md = generate_diff_summary(
|
| 686 |
+
ds_obj_prev if isinstance(ds_obj_prev, dict) else None,
|
| 687 |
+
new_ds_obj,
|
| 688 |
+
int(ds_ver_prev or 0),
|
| 689 |
+
ds_ver
|
| 690 |
+
)
|
| 691 |
+
|
| 692 |
+
# 添加到版本历史
|
| 693 |
+
compact_summary = generate_diff_summary_compact(ds_obj_prev if isinstance(ds_obj_prev, dict) else None, new_ds_obj)
|
| 694 |
+
updated_history_data = add_to_history(history_data, ds_ver, new_ds_obj, new_ds_raw, compact_summary)
|
| 695 |
+
|
| 696 |
+
# 可控迭代:做一个"范围越界"软检查(不阻断,只提示)
|
| 697 |
if iterative_enabled and ds_obj_prev and isinstance(ds_obj_prev, dict) and scope:
|
| 698 |
changed = diff_keys(ds_obj_prev, new_ds_obj)
|
| 699 |
mech_changes = diff_mechanics(ds_obj_prev, new_ds_obj)
|
|
|
|
| 703 |
"允许范围:{scope}\n"
|
| 704 |
"顶层变更字段:{keys}\n"
|
| 705 |
"机制变更:{mech}\n"
|
| 706 |
+
"建议:请重新执行一次迭代,明确只改允许范围内字段,或切换为「自由迭代」。\n"
|
| 707 |
).format(
|
| 708 |
scope=scope,
|
| 709 |
keys=",".join(changed) if changed else "(无)",
|
|
|
|
| 711 |
)
|
| 712 |
|
| 713 |
try:
|
| 714 |
+
# 更新版本下拉选项
|
| 715 |
+
new_choices = get_history_choices(updated_history_data)
|
| 716 |
+
history_status_md = f"共 {len(new_choices)} 个版本" if new_choices else "_无版本历史_"
|
| 717 |
+
|
| 718 |
# Analyse 模式不产出完整规则,不导出GDL/自然语言文件
|
| 719 |
if analyse_enabled:
|
| 720 |
+
yield history, "", history, "", "", "", (new_ds_raw or ds_raw_prev), (new_ds_obj or ds_obj_prev), ds_ver, ds_status, ready_to_gen, ready_md, "输出校验:Analyse 模式跳过", diff_summary_md, updated_history_data, gr.update(choices=new_choices, value=new_choices[-1] if new_choices else None), history_status_md
|
| 721 |
return
|
| 722 |
|
| 723 |
gdl_content, narrative_content = extract_gdl_and_narrative(history[-1]["content"])
|
| 724 |
design_log_content = extract_design_log(history[-1]["content"])
|
| 725 |
+
|
| 726 |
# 保存 GDL 和自然语言文件
|
| 727 |
gdl_path, narrative_path = save_gdl_and_narrative(gdl_content, narrative_content)
|
| 728 |
design_log_path = save_design_log(design_log_content)
|
| 729 |
+
|
| 730 |
# 返回文件路径,以便下载
|
| 731 |
+
yield history, "", history, gdl_path, narrative_path, design_log_path, (new_ds_raw or ds_raw_prev), (new_ds_obj or ds_obj_prev), ds_ver, ds_status, ready_to_gen, ready_md, render_validation_md(history[-1]["content"]), diff_summary_md, updated_history_data, gr.update(choices=new_choices, value=new_choices[-1] if new_choices else None), history_status_md
|
| 732 |
except Exception as e:
|
| 733 |
print(f"保存GDL和自然语言文件时出错: {e}")
|
| 734 |
+
yield history, "", history, "", "", "", (new_ds_raw or ds_raw_prev), (new_ds_obj or ds_obj_prev), ds_ver, ds_status, ready_to_gen, ready_md, render_validation_md(history[-1]["content"]), diff_summary_md, updated_history_data, gr.update(choices=new_choices, value=new_choices[-1] if new_choices else None), history_status_md
|
| 735 |
|
| 736 |
def on_generate_from_state(
|
| 737 |
history_msgs,
|
|
|
|
| 742 |
ds_obj_prev,
|
| 743 |
ds_ver_prev,
|
| 744 |
ready_prev,
|
| 745 |
+
history_data,
|
| 746 |
):
|
| 747 |
+
cur_choices = get_history_choices(history_data)
|
| 748 |
if not (ds_raw_prev or "").strip():
|
| 749 |
+
yield history_msgs, history_msgs, "", "", "", ds_raw_prev, ds_obj_prev, ds_ver_prev, "DesignState:未建立(先生成一版玩法)", ready_prev, "READY_TO_GENERATE:未知", "输出校验:未运行", "_尚无变更记录_", history_data, gr.update(choices=cur_choices), "_无版本历史_" if not cur_choices else f"共 {len(cur_choices)} 个版本"
|
| 750 |
return
|
| 751 |
|
| 752 |
history = list(history_msgs or [])
|
| 753 |
user_text = "基于当前 DesignState 生成完整玩法(自然语言规则 + mGDL + 自检报告)。"
|
| 754 |
history.append({"role": "user", "content": user_text})
|
| 755 |
+
yield history, history, "", "", "", ds_raw_prev, ds_obj_prev, ds_ver_prev, "DesignState:生成中…", ready_prev, "READY_TO_GENERATE:未知", "输出校验:运行中…", "_生成中..._", history_data, gr.update(choices=cur_choices), f"共 {len(cur_choices)} 个版本" if cur_choices else "_无版本历史_"
|
| 756 |
|
| 757 |
history.append({"role": "assistant", "content": ""})
|
| 758 |
+
yield history, history, "", "", "", ds_raw_prev, ds_obj_prev, ds_ver_prev, "DesignState:生成中…", ready_prev, "READY_TO_GENERATE:未知", "输出校验:运行中…", "_生成中..._", history_data, gr.update(choices=cur_choices), f"共 {len(cur_choices)} 个版本" if cur_choices else "_无版本历史_"
|
| 759 |
|
| 760 |
effective_user_text = (
|
| 761 |
"请基于下方 DesignState(JSON) 生成完整的麻将新玩法交付物:\n"
|
|
|
|
| 776 |
s = str(piece)
|
| 777 |
buf.append(s)
|
| 778 |
history[-1]["content"] += s
|
| 779 |
+
yield history, history, "", "", "", ds_raw_prev, ds_obj_prev, ds_ver_prev, "DesignState:生成中…", ready_prev, "READY_TO_GENERATE:未知", "输出校验:运行中…", "_生成中..._", history_data, gr.update(choices=cur_choices), f"共 {len(cur_choices)} 个版本" if cur_choices else "_无版本历史_"
|
| 780 |
else:
|
| 781 |
full = design_mahjong_game(effective_user_text, tuples_to_send, files, custom_prompt, mode)
|
| 782 |
buf.append(str(full))
|
| 783 |
for piece in _chunk_fake_stream(str(full), step=40):
|
| 784 |
history[-1]["content"] += piece
|
| 785 |
+
yield history, history, "", "", "", ds_raw_prev, ds_obj_prev, ds_ver_prev, "DesignState:生成中…", ready_prev, "READY_TO_GENERATE:未知", "输出校验:运行中…", "_生成中..._", history_data, gr.update(choices=cur_choices), f"共 {len(cur_choices)} 个版本" if cur_choices else "_无版本历史_"
|
| 786 |
except Exception as e:
|
| 787 |
history[-1]["content"] += f"\n(生成出错){type(e).__name__}: {e}"
|
| 788 |
+
yield history, history, "", "", "", ds_raw_prev, ds_obj_prev, ds_ver_prev, "DesignState:生成出错", ready_prev, "READY_TO_GENERATE:未知", "输出校验:未运行", "_生成出错_", history_data, gr.update(choices=cur_choices), f"共 {len(cur_choices)} 个版本" if cur_choices else "_无版本历史_"
|
| 789 |
return
|
| 790 |
|
| 791 |
new_ds_obj, new_ds_raw = extract_design_state(history[-1]["content"])
|
| 792 |
ds_ver = int(ds_ver_prev or 0)
|
| 793 |
ds_status = "DesignState:未变更"
|
| 794 |
+
diff_summary_md = "_无变更_"
|
| 795 |
+
updated_history_data = history_data
|
| 796 |
+
|
| 797 |
if new_ds_obj and new_ds_raw:
|
| 798 |
ds_ver += 1
|
| 799 |
ds_status = "DesignState:v{0} 已更新 | {1}".format(ds_ver, summarize_design_state(new_ds_obj))
|
| 800 |
+
# 生成差异摘要
|
| 801 |
+
diff_summary_md = generate_diff_summary(
|
| 802 |
+
ds_obj_prev if isinstance(ds_obj_prev, dict) else None,
|
| 803 |
+
new_ds_obj,
|
| 804 |
+
int(ds_ver_prev or 0),
|
| 805 |
+
ds_ver
|
| 806 |
+
)
|
| 807 |
+
# 添加到版本历史
|
| 808 |
+
compact_summary = generate_diff_summary_compact(ds_obj_prev if isinstance(ds_obj_prev, dict) else None, new_ds_obj)
|
| 809 |
+
updated_history_data = add_to_history(history_data, ds_ver, new_ds_obj, new_ds_raw, compact_summary)
|
| 810 |
+
|
| 811 |
+
# 更新版本下拉选项
|
| 812 |
+
new_choices = get_history_choices(updated_history_data)
|
| 813 |
+
history_status_md = f"共 {len(new_choices)} 个版本" if new_choices else "_无版本历史_"
|
| 814 |
|
| 815 |
try:
|
| 816 |
gdl_content, narrative_content = extract_gdl_and_narrative(history[-1]["content"])
|
| 817 |
design_log_content = extract_design_log(history[-1]["content"])
|
| 818 |
gdl_path, narrative_path = save_gdl_and_narrative(gdl_content, narrative_content)
|
| 819 |
design_log_path = save_design_log(design_log_content)
|
| 820 |
+
yield history, history, gdl_path, narrative_path, design_log_path, (new_ds_raw or ds_raw_prev), (new_ds_obj or ds_obj_prev), ds_ver, ds_status, ready_prev, "READY_TO_GENERATE:未知", render_validation_md(history[-1]["content"]), diff_summary_md, updated_history_data, gr.update(choices=new_choices, value=new_choices[-1] if new_choices else None), history_status_md
|
| 821 |
except Exception:
|
| 822 |
+
yield history, history, "", "", "", (new_ds_raw or ds_raw_prev), (new_ds_obj or ds_obj_prev), ds_ver, ds_status, ready_prev, "READY_TO_GENERATE:未知", render_validation_md(history[-1]["content"]), diff_summary_md, updated_history_data, gr.update(choices=new_choices, value=new_choices[-1] if new_choices else None), history_status_md
|
| 823 |
|
| 824 |
# 绑定:回车提交(Enter=提交;Shift+Enter=换行由浏览器处理)
|
| 825 |
user_input.submit(
|
|
|
|
| 837 |
design_state_obj,
|
| 838 |
design_state_version,
|
| 839 |
ready_to_generate,
|
| 840 |
+
design_state_history,
|
| 841 |
],
|
| 842 |
outputs=[
|
| 843 |
chatbot,
|
|
|
|
| 852 |
design_state_status,
|
| 853 |
ready_to_generate,
|
| 854 |
ready_status,
|
| 855 |
+
validation_status,
|
| 856 |
+
diff_summary_display,
|
| 857 |
+
design_state_history,
|
| 858 |
+
version_dropdown,
|
| 859 |
+
history_status,
|
| 860 |
],
|
| 861 |
preprocess=True,
|
| 862 |
)
|
|
|
|
| 877 |
design_state_obj,
|
| 878 |
design_state_version,
|
| 879 |
ready_to_generate,
|
| 880 |
+
design_state_history,
|
| 881 |
],
|
| 882 |
outputs=[
|
| 883 |
chatbot,
|
|
|
|
| 892 |
design_state_status,
|
| 893 |
ready_to_generate,
|
| 894 |
ready_status,
|
| 895 |
+
validation_status,
|
| 896 |
+
diff_summary_display,
|
| 897 |
+
design_state_history,
|
| 898 |
+
version_dropdown,
|
| 899 |
+
history_status,
|
| 900 |
],
|
| 901 |
preprocess=True,
|
| 902 |
)
|
|
|
|
| 912 |
design_state_obj,
|
| 913 |
design_state_version,
|
| 914 |
ready_to_generate,
|
| 915 |
+
design_state_history,
|
| 916 |
],
|
| 917 |
outputs=[
|
| 918 |
chatbot,
|
|
|
|
| 926 |
design_state_status,
|
| 927 |
ready_to_generate,
|
| 928 |
ready_status,
|
| 929 |
+
validation_status,
|
| 930 |
+
diff_summary_display,
|
| 931 |
+
design_state_history,
|
| 932 |
+
version_dropdown,
|
| 933 |
+
history_status,
|
| 934 |
],
|
| 935 |
preprocess=True,
|
| 936 |
)
|
| 937 |
|
| 938 |
+
# 版本回滚功能
|
| 939 |
+
def on_rollback(selected_version, history_data, ds_obj_prev, ds_ver_prev):
|
| 940 |
+
"""回滚到选定版本"""
|
| 941 |
+
if not selected_version:
|
| 942 |
+
return (
|
| 943 |
+
ds_obj_prev, "", ds_ver_prev,
|
| 944 |
+
"DesignState:未选择版本", "_请选择要回滚的版本_",
|
| 945 |
+
history_data, gr.update(), f"共 {len(get_history_choices(history_data))} 个版本" if get_history_choices(history_data) else "_无版本历史_"
|
| 946 |
+
)
|
| 947 |
+
|
| 948 |
+
version = parse_version_from_choice(selected_version)
|
| 949 |
+
if version <= 0:
|
| 950 |
+
return (
|
| 951 |
+
ds_obj_prev, "", ds_ver_prev,
|
| 952 |
+
"DesignState:版本解析失败", "_版本号无效_",
|
| 953 |
+
history_data, gr.update(), f"共 {len(get_history_choices(history_data))} 个版本" if get_history_choices(history_data) else "_无版本历史_"
|
| 954 |
+
)
|
| 955 |
+
|
| 956 |
+
updated_history, state_obj, state_raw = rollback_history(history_data, version)
|
| 957 |
+
if state_obj is None:
|
| 958 |
+
return (
|
| 959 |
+
ds_obj_prev, "", ds_ver_prev,
|
| 960 |
+
f"DesignState:回滚失败(v{version} 不存在)", "_回滚失败_",
|
| 961 |
+
history_data, gr.update(), f"共 {len(get_history_choices(history_data))} 个版本" if get_history_choices(history_data) else "_无版本历史_"
|
| 962 |
+
)
|
| 963 |
+
|
| 964 |
+
new_choices = get_history_choices(updated_history)
|
| 965 |
+
ds_status = "DesignState:v{0} | {1}".format(version, summarize_design_state(state_obj))
|
| 966 |
+
diff_md = f"**已回滚到 v{version}**\n\n{summarize_design_state(state_obj)}"
|
| 967 |
+
|
| 968 |
+
return (
|
| 969 |
+
state_obj, state_raw, version,
|
| 970 |
+
ds_status, diff_md,
|
| 971 |
+
updated_history, gr.update(choices=new_choices, value=new_choices[-1] if new_choices else None), f"✅ 已回滚到 v{version},共 {len(new_choices)} 个版本"
|
| 972 |
+
)
|
| 973 |
+
|
| 974 |
+
rollback_btn.click(
|
| 975 |
+
fn=on_rollback,
|
| 976 |
+
inputs=[version_dropdown, design_state_history, design_state_obj, design_state_version],
|
| 977 |
+
outputs=[
|
| 978 |
+
design_state_obj,
|
| 979 |
+
design_state_raw,
|
| 980 |
+
design_state_version,
|
| 981 |
+
design_state_status,
|
| 982 |
+
diff_summary_display,
|
| 983 |
+
design_state_history,
|
| 984 |
+
version_dropdown,
|
| 985 |
+
history_status,
|
| 986 |
+
],
|
| 987 |
+
)
|
| 988 |
+
|
| 989 |
+
# 刷新历史列表
|
| 990 |
+
def on_refresh_history(history_data):
|
| 991 |
+
choices = get_history_choices(history_data)
|
| 992 |
+
return gr.update(choices=choices, value=choices[-1] if choices else None), f"共 {len(choices)} 个版本" if choices else "_无版本历史_"
|
| 993 |
+
|
| 994 |
+
refresh_history_btn.click(
|
| 995 |
+
fn=on_refresh_history,
|
| 996 |
+
inputs=[design_state_history],
|
| 997 |
+
outputs=[version_dropdown, history_status],
|
| 998 |
+
)
|
| 999 |
+
|
| 1000 |
+
# ====== 多阶段交互:阶段检测与方案选择 ======
|
| 1001 |
+
def update_phase_from_chat(history_msgs, ds_obj, phase_state):
|
| 1002 |
+
"""根据最新聊天内容更新阶段状态和方案显示"""
|
| 1003 |
+
if not history_msgs:
|
| 1004 |
+
return (
|
| 1005 |
+
"**当前阶段**: 🚀 初始",
|
| 1006 |
+
gr.update(visible=False),
|
| 1007 |
+
gr.update(choices=[], value=None),
|
| 1008 |
+
gr.update(visible=False),
|
| 1009 |
+
gr.update(visible=False),
|
| 1010 |
+
phase_state,
|
| 1011 |
+
)
|
| 1012 |
+
|
| 1013 |
+
# 获取最后一条助手消息
|
| 1014 |
+
last_bot_msg = ""
|
| 1015 |
+
for msg in reversed(history_msgs):
|
| 1016 |
+
if isinstance(msg, dict) and msg.get("role") == "assistant":
|
| 1017 |
+
last_bot_msg = msg.get("content", "")
|
| 1018 |
+
break
|
| 1019 |
+
|
| 1020 |
+
if not last_bot_msg:
|
| 1021 |
+
return (
|
| 1022 |
+
"**当前阶段**: 🚀 初始",
|
| 1023 |
+
gr.update(visible=False),
|
| 1024 |
+
gr.update(choices=[], value=None),
|
| 1025 |
+
gr.update(visible=False),
|
| 1026 |
+
gr.update(visible=False),
|
| 1027 |
+
phase_state,
|
| 1028 |
+
)
|
| 1029 |
+
|
| 1030 |
+
# 检测阶段
|
| 1031 |
+
detected_phase = detect_interaction_phase(last_bot_msg, ds_obj)
|
| 1032 |
+
phase_display = get_phase_display_name(detected_phase)
|
| 1033 |
+
|
| 1034 |
+
# 提取方案
|
| 1035 |
+
proposals = extract_proposals(last_bot_msg)
|
| 1036 |
+
questions = extract_clarify_questions(last_bot_msg)
|
| 1037 |
+
|
| 1038 |
+
# 更新阶段状态
|
| 1039 |
+
new_phase_state = update_phase_state(phase_state, detected_phase, proposals=proposals)
|
| 1040 |
+
|
| 1041 |
+
# 根据阶段决定 UI 显示
|
| 1042 |
+
if detected_phase == InteractionPhase.DIVERGE and len(proposals) >= 2:
|
| 1043 |
+
# 有多个方案,显示方案选择 UI
|
| 1044 |
+
proposal_choices = [f"方案 {p['id']}: {p['title']}" for p in proposals]
|
| 1045 |
+
proposals_md = format_proposals_for_display(proposals)
|
| 1046 |
+
return (
|
| 1047 |
+
f"**当前阶段**: {phase_display}\n\n发现 **{len(proposals)}** 个候选方案,请选择一个深入展开。",
|
| 1048 |
+
gr.update(value=proposals_md, visible=True),
|
| 1049 |
+
gr.update(choices=proposal_choices, value=None, visible=True),
|
| 1050 |
+
gr.update(visible=True),
|
| 1051 |
+
gr.update(visible=True),
|
| 1052 |
+
new_phase_state,
|
| 1053 |
+
)
|
| 1054 |
+
elif detected_phase == InteractionPhase.UNDERSTAND and questions:
|
| 1055 |
+
# 有确认问题
|
| 1056 |
+
questions_md = "\n".join([f"❓ {q}" for q in questions])
|
| 1057 |
+
return (
|
| 1058 |
+
f"**当前阶段**: {phase_display}\n\n请回答以下确认问题:",
|
| 1059 |
+
gr.update(value=questions_md, visible=True),
|
| 1060 |
+
gr.update(choices=[], value=None),
|
| 1061 |
+
gr.update(visible=False),
|
| 1062 |
+
gr.update(visible=False),
|
| 1063 |
+
new_phase_state,
|
| 1064 |
+
)
|
| 1065 |
+
else:
|
| 1066 |
+
# 其他阶段
|
| 1067 |
+
return (
|
| 1068 |
+
f"**当前阶段**: {phase_display}",
|
| 1069 |
+
gr.update(visible=False),
|
| 1070 |
+
gr.update(choices=[], value=None),
|
| 1071 |
+
gr.update(visible=False),
|
| 1072 |
+
gr.update(visible=False),
|
| 1073 |
+
new_phase_state,
|
| 1074 |
+
)
|
| 1075 |
+
|
| 1076 |
+
# 当聊天内容变化时更新阶段
|
| 1077 |
+
chatbot.change(
|
| 1078 |
+
fn=update_phase_from_chat,
|
| 1079 |
+
inputs=[chatbot, design_state_obj, interaction_phase_state],
|
| 1080 |
+
outputs=[
|
| 1081 |
+
phase_status,
|
| 1082 |
+
proposals_display,
|
| 1083 |
+
proposal_dropdown,
|
| 1084 |
+
proposal_select_row,
|
| 1085 |
+
diverge_action_row,
|
| 1086 |
+
interaction_phase_state,
|
| 1087 |
+
],
|
| 1088 |
+
)
|
| 1089 |
+
|
| 1090 |
+
# 确认选择方案
|
| 1091 |
+
def on_select_proposal(selected, phase_state, history_msgs):
|
| 1092 |
+
"""用户选择方案后,生成相应的用户消息"""
|
| 1093 |
+
if not selected:
|
| 1094 |
+
return "", history_msgs, phase_state
|
| 1095 |
+
|
| 1096 |
+
# 提取方案 ID
|
| 1097 |
+
proposal_id = selected.split(":")[0].replace("方案", "").strip()
|
| 1098 |
+
|
| 1099 |
+
# 生成用户确认消息
|
| 1100 |
+
confirm_msg = f"我选择 **方案 {proposal_id}**,请对这个方案进行深入展开设计。"
|
| 1101 |
+
|
| 1102 |
+
# 更新阶段状态
|
| 1103 |
+
new_phase_state = update_phase_state(phase_state, InteractionPhase.SELECT, selected=proposal_id)
|
| 1104 |
+
|
| 1105 |
+
return confirm_msg, history_msgs, new_phase_state
|
| 1106 |
+
|
| 1107 |
+
select_proposal_btn.click(
|
| 1108 |
+
fn=on_select_proposal,
|
| 1109 |
+
inputs=[proposal_dropdown, interaction_phase_state, chat_state],
|
| 1110 |
+
outputs=[user_input, chat_state, interaction_phase_state],
|
| 1111 |
+
)
|
| 1112 |
+
|
| 1113 |
+
# 重新发散按钮
|
| 1114 |
+
def on_request_more():
|
| 1115 |
+
return "请给出更多不同方向的机制组合方案,我想看看其他可能性。"
|
| 1116 |
+
|
| 1117 |
+
request_more_btn.click(
|
| 1118 |
+
fn=on_request_more,
|
| 1119 |
+
outputs=[user_input],
|
| 1120 |
+
)
|
| 1121 |
+
|
| 1122 |
+
# 直接深入展开按钮
|
| 1123 |
+
def on_request_elaborate(phase_state):
|
| 1124 |
+
proposals = phase_state.get("proposals", [])
|
| 1125 |
+
if proposals:
|
| 1126 |
+
first = proposals[0]
|
| 1127 |
+
return f"请直接对方案 {first['id']}({first['title']})进行深入展开设计。"
|
| 1128 |
+
return "请直接对当前方案进行深入展开设计,生成完整的玩法规则和 mGDL。"
|
| 1129 |
+
|
| 1130 |
+
request_elaborate_btn.click(
|
| 1131 |
+
fn=on_request_elaborate,
|
| 1132 |
+
inputs=[interaction_phase_state],
|
| 1133 |
+
outputs=[user_input],
|
| 1134 |
+
)
|
| 1135 |
+
|
| 1136 |
+
# ====== 细粒度范围控制:事件绑定 ======
|
| 1137 |
+
def on_scope_mode_change(mode):
|
| 1138 |
+
"""切换控制模式时显示/隐藏自定义选项"""
|
| 1139 |
+
is_custom = mode == "自定义模式"
|
| 1140 |
+
status_text = "_范围约束: 自定义模式_" if is_custom else "_范围约束: 预设模式_"
|
| 1141 |
+
return gr.update(visible=is_custom), status_text
|
| 1142 |
+
|
| 1143 |
+
scope_mode.change(
|
| 1144 |
+
fn=on_scope_mode_change,
|
| 1145 |
+
inputs=[scope_mode],
|
| 1146 |
+
outputs=[custom_scope_group, scope_status],
|
| 1147 |
+
)
|
| 1148 |
+
|
| 1149 |
+
def on_refresh_mechanics(ds_obj):
|
| 1150 |
+
"""刷新当前 DesignState 中的机制列表"""
|
| 1151 |
+
if not ds_obj or not isinstance(ds_obj, dict):
|
| 1152 |
+
return gr.update(choices=[], value=[])
|
| 1153 |
+
mechanics = get_mechanics_from_state(ds_obj)
|
| 1154 |
+
choices = [(m, m) for m in mechanics]
|
| 1155 |
+
return gr.update(choices=choices, value=[])
|
| 1156 |
+
|
| 1157 |
+
refresh_mechanics_btn.click(
|
| 1158 |
+
fn=on_refresh_mechanics,
|
| 1159 |
+
inputs=[design_state_obj],
|
| 1160 |
+
outputs=[locked_mechanics],
|
| 1161 |
+
)
|
| 1162 |
+
|
| 1163 |
+
def on_scope_config_update(mode, constraint, locked_flds, locked_mechs, preset_scope):
|
| 1164 |
+
"""更新范围配置状态"""
|
| 1165 |
+
is_preset = mode == "预设模式"
|
| 1166 |
+
constraint_level_val = ScopeConstraint.SOFT if "软" in constraint else ScopeConstraint.HARD
|
| 1167 |
+
|
| 1168 |
+
config = {
|
| 1169 |
+
"mode": "preset" if is_preset else "custom",
|
| 1170 |
+
"preset": preset_scope if is_preset else "",
|
| 1171 |
+
"constraint_level": constraint_level_val,
|
| 1172 |
+
"locked_fields": locked_flds or [],
|
| 1173 |
+
"allowed_fields": [],
|
| 1174 |
+
"locked_mechanics": locked_mechs or [],
|
| 1175 |
+
"allowed_mechanics": [],
|
| 1176 |
+
}
|
| 1177 |
+
|
| 1178 |
+
# 生成状态提示
|
| 1179 |
+
if is_preset:
|
| 1180 |
+
status = f"_范围约束: 预设模式 ({preset_scope})_"
|
| 1181 |
+
else:
|
| 1182 |
+
locked_count = len(locked_flds or []) + len(locked_mechs or [])
|
| 1183 |
+
constraint_text = "软约束" if constraint_level_val == ScopeConstraint.SOFT else "硬约束"
|
| 1184 |
+
status = f"_范围约束: 自定义模式 | {constraint_text} | 锁定 {locked_count} 项_"
|
| 1185 |
+
|
| 1186 |
+
return config, status
|
| 1187 |
+
|
| 1188 |
+
# 当任意范围控制项变化时更新配置
|
| 1189 |
+
for scope_input in [scope_mode, constraint_level, locked_fields, locked_mechanics, iteration_scope]:
|
| 1190 |
+
scope_input.change(
|
| 1191 |
+
fn=on_scope_config_update,
|
| 1192 |
+
inputs=[scope_mode, constraint_level, locked_fields, locked_mechanics, iteration_scope],
|
| 1193 |
+
outputs=[scope_config_state, scope_status],
|
| 1194 |
+
)
|
| 1195 |
+
|
| 1196 |
# 导出对话
|
| 1197 |
with gr.Row():
|
| 1198 |
export_btn = gr.Button("导出对话(Markdown)", variant="secondary")
|
|
|
|
| 1203 |
clear_dialog_btn = gr.Button("清空对话", variant="secondary")
|
| 1204 |
|
| 1205 |
def _clear_chat():
|
| 1206 |
+
return (
|
| 1207 |
+
[], "", [], "", "", "", "", {}, 0,
|
| 1208 |
+
"DesignState:未建立(先生成一版玩法)", False, "READY_TO_GENERATE:未知",
|
| 1209 |
+
"输出校验:未运行", "_尚无变更记录_",
|
| 1210 |
+
create_empty_history(), gr.update(choices=[], value=None), "_无版本历史_",
|
| 1211 |
+
create_phase_state(), "**当前阶段**: 🚀 初始",
|
| 1212 |
+
gr.update(visible=False), gr.update(choices=[], value=None),
|
| 1213 |
+
gr.update(visible=False), gr.update(visible=False),
|
| 1214 |
+
create_scope_config(), "_范围约束: 预设模式_",
|
| 1215 |
+
)
|
| 1216 |
|
| 1217 |
clear_dialog_btn.click(
|
| 1218 |
fn=_clear_chat,
|
|
|
|
| 1230 |
design_state_status,
|
| 1231 |
ready_to_generate,
|
| 1232 |
ready_status,
|
| 1233 |
+
validation_status,
|
| 1234 |
+
diff_summary_display,
|
| 1235 |
+
design_state_history,
|
| 1236 |
+
version_dropdown,
|
| 1237 |
+
history_status,
|
| 1238 |
+
interaction_phase_state,
|
| 1239 |
+
phase_status,
|
| 1240 |
+
proposals_display,
|
| 1241 |
+
proposal_dropdown,
|
| 1242 |
+
proposal_select_row,
|
| 1243 |
+
diverge_action_row,
|
| 1244 |
+
scope_config_state,
|
| 1245 |
+
scope_status,
|
| 1246 |
],
|
| 1247 |
)
|
| 1248 |
|
design_state.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
"""
|
| 2 |
-
DesignState 解析与差分(用于
|
| 3 |
|
| 4 |
约定:
|
| 5 |
- 模型需在回答中输出一个 ```json ...``` 代码块作为 DesignState
|
|
@@ -8,9 +8,191 @@ DesignState 解析与差分(用于“多轮可控迭代”)。
|
|
| 8 |
|
| 9 |
import json
|
| 10 |
import re
|
|
|
|
| 11 |
from typing import Any, Dict, List, Optional, Tuple
|
| 12 |
|
| 13 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
_JSON_FENCE_RE = re.compile(r"```json\s*(\{.*?\})\s*```", re.DOTALL | re.IGNORECASE)
|
| 15 |
_READY_RE = re.compile(r"READY_TO_GENERATE\s*:\s*(true|false)", re.IGNORECASE)
|
| 16 |
|
|
@@ -163,7 +345,7 @@ def is_change_within_scope(changed_top_keys: List[str], scope: str) -> bool:
|
|
| 163 |
else:
|
| 164 |
return True
|
| 165 |
|
| 166 |
-
# 名称/来源类字段默认不允许在
|
| 167 |
forbidden = {"new_variant_name", "base_variants", "fusion_variants"}
|
| 168 |
for k in changed_top_keys:
|
| 169 |
if k in forbidden:
|
|
@@ -171,3 +353,703 @@ def is_change_within_scope(changed_top_keys: List[str], scope: str) -> bool:
|
|
| 171 |
if k not in allowed:
|
| 172 |
return False
|
| 173 |
return True
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
"""
|
| 2 |
+
DesignState 解析与差分(用于"多轮可控迭代")。
|
| 3 |
|
| 4 |
约定:
|
| 5 |
- 模型需在回答中输出一个 ```json ...``` 代码块作为 DesignState
|
|
|
|
| 8 |
|
| 9 |
import json
|
| 10 |
import re
|
| 11 |
+
from datetime import datetime
|
| 12 |
from typing import Any, Dict, List, Optional, Tuple
|
| 13 |
|
| 14 |
|
| 15 |
+
# ==================== 版本历史管理 ====================
|
| 16 |
+
|
| 17 |
+
class DesignStateHistory:
|
| 18 |
+
"""
|
| 19 |
+
管理 DesignState 的版本历史,支持回滚
|
| 20 |
+
|
| 21 |
+
每个版本包含:
|
| 22 |
+
- version: 版本号
|
| 23 |
+
- state_obj: DesignState 字典
|
| 24 |
+
- state_raw: 原始 JSON 字符串
|
| 25 |
+
- timestamp: 时间戳
|
| 26 |
+
- summary: 紧凑版摘要
|
| 27 |
+
"""
|
| 28 |
+
|
| 29 |
+
def __init__(self, max_versions: int = 20):
|
| 30 |
+
self.max_versions = max_versions
|
| 31 |
+
self.versions: List[Dict[str, Any]] = []
|
| 32 |
+
self.current_index: int = -1 # 当前版本在 versions 中的索引
|
| 33 |
+
|
| 34 |
+
def add_version(
|
| 35 |
+
self,
|
| 36 |
+
version: int,
|
| 37 |
+
state_obj: Dict[str, Any],
|
| 38 |
+
state_raw: str,
|
| 39 |
+
summary: str = ""
|
| 40 |
+
) -> None:
|
| 41 |
+
"""添加新版本"""
|
| 42 |
+
if not state_obj:
|
| 43 |
+
return
|
| 44 |
+
|
| 45 |
+
record = {
|
| 46 |
+
"version": version,
|
| 47 |
+
"state_obj": state_obj.copy() if state_obj else {},
|
| 48 |
+
"state_raw": state_raw,
|
| 49 |
+
"timestamp": datetime.now().strftime("%H:%M:%S"),
|
| 50 |
+
"summary": summary or self._generate_summary(state_obj),
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
# 如果当前不在最新位置,截断后面的版本(类似 git 的分支)
|
| 54 |
+
if self.current_index >= 0 and self.current_index < len(self.versions) - 1:
|
| 55 |
+
self.versions = self.versions[:self.current_index + 1]
|
| 56 |
+
|
| 57 |
+
self.versions.append(record)
|
| 58 |
+
self.current_index = len(self.versions) - 1
|
| 59 |
+
|
| 60 |
+
# 超出最大版本数时,删除最早的
|
| 61 |
+
if len(self.versions) > self.max_versions:
|
| 62 |
+
self.versions.pop(0)
|
| 63 |
+
self.current_index = len(self.versions) - 1
|
| 64 |
+
|
| 65 |
+
def _generate_summary(self, state_obj: Dict[str, Any]) -> str:
|
| 66 |
+
"""生成简短摘要"""
|
| 67 |
+
name = state_obj.get("new_variant_name", "(未命名)")
|
| 68 |
+
mechanics = state_obj.get("mechanics", [])
|
| 69 |
+
mech_count = len(mechanics) if isinstance(mechanics, list) else 0
|
| 70 |
+
return f"{name} ({mech_count}个机制)"
|
| 71 |
+
|
| 72 |
+
def get_version(self, version: int) -> Optional[Dict[str, Any]]:
|
| 73 |
+
"""获取指定版本"""
|
| 74 |
+
for record in self.versions:
|
| 75 |
+
if record["version"] == version:
|
| 76 |
+
return record
|
| 77 |
+
return None
|
| 78 |
+
|
| 79 |
+
def get_version_by_index(self, index: int) -> Optional[Dict[str, Any]]:
|
| 80 |
+
"""通过索引获取版本"""
|
| 81 |
+
if 0 <= index < len(self.versions):
|
| 82 |
+
return self.versions[index]
|
| 83 |
+
return None
|
| 84 |
+
|
| 85 |
+
def rollback_to(self, version: int) -> Optional[Tuple[Dict[str, Any], str]]:
|
| 86 |
+
"""
|
| 87 |
+
回滚到指定版本
|
| 88 |
+
返回:(state_obj, state_raw) 或 None
|
| 89 |
+
"""
|
| 90 |
+
for i, record in enumerate(self.versions):
|
| 91 |
+
if record["version"] == version:
|
| 92 |
+
self.current_index = i
|
| 93 |
+
return record["state_obj"].copy(), record["state_raw"]
|
| 94 |
+
return None
|
| 95 |
+
|
| 96 |
+
def get_version_list(self) -> List[Dict[str, Any]]:
|
| 97 |
+
"""获取所有版本列表(用于 UI 显示)"""
|
| 98 |
+
return [
|
| 99 |
+
{
|
| 100 |
+
"version": r["version"],
|
| 101 |
+
"timestamp": r["timestamp"],
|
| 102 |
+
"summary": r["summary"],
|
| 103 |
+
"is_current": i == self.current_index,
|
| 104 |
+
}
|
| 105 |
+
for i, r in enumerate(self.versions)
|
| 106 |
+
]
|
| 107 |
+
|
| 108 |
+
def get_version_choices(self) -> List[str]:
|
| 109 |
+
"""生成下拉选项列表"""
|
| 110 |
+
choices = []
|
| 111 |
+
for i, r in enumerate(self.versions):
|
| 112 |
+
marker = " ← 当前" if i == self.current_index else ""
|
| 113 |
+
choices.append(f"v{r['version']} [{r['timestamp']}] {r['summary']}{marker}")
|
| 114 |
+
return choices
|
| 115 |
+
|
| 116 |
+
def get_current_version(self) -> int:
|
| 117 |
+
"""获取当前版本号"""
|
| 118 |
+
if self.current_index >= 0 and self.current_index < len(self.versions):
|
| 119 |
+
return self.versions[self.current_index]["version"]
|
| 120 |
+
return 0
|
| 121 |
+
|
| 122 |
+
def clear(self) -> None:
|
| 123 |
+
"""清空历史"""
|
| 124 |
+
self.versions = []
|
| 125 |
+
self.current_index = -1
|
| 126 |
+
|
| 127 |
+
def to_serializable(self) -> Dict[str, Any]:
|
| 128 |
+
"""转为可序列化格式(用于 Gradio State)"""
|
| 129 |
+
return {
|
| 130 |
+
"versions": self.versions,
|
| 131 |
+
"current_index": self.current_index,
|
| 132 |
+
"max_versions": self.max_versions,
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
@classmethod
|
| 136 |
+
def from_serializable(cls, data: Dict[str, Any]) -> "DesignStateHistory":
|
| 137 |
+
"""从序列化格式恢复"""
|
| 138 |
+
if not data or not isinstance(data, dict):
|
| 139 |
+
return cls()
|
| 140 |
+
history = cls(max_versions=data.get("max_versions", 20))
|
| 141 |
+
history.versions = data.get("versions", [])
|
| 142 |
+
history.current_index = data.get("current_index", -1)
|
| 143 |
+
return history
|
| 144 |
+
|
| 145 |
+
|
| 146 |
+
def create_empty_history() -> Dict[str, Any]:
|
| 147 |
+
"""创建空的历史记录(用于初始化 State)"""
|
| 148 |
+
return DesignStateHistory().to_serializable()
|
| 149 |
+
|
| 150 |
+
|
| 151 |
+
def add_to_history(
|
| 152 |
+
history_data: Dict[str, Any],
|
| 153 |
+
version: int,
|
| 154 |
+
state_obj: Dict[str, Any],
|
| 155 |
+
state_raw: str,
|
| 156 |
+
summary: str = ""
|
| 157 |
+
) -> Dict[str, Any]:
|
| 158 |
+
"""添加版本到历史"""
|
| 159 |
+
history = DesignStateHistory.from_serializable(history_data)
|
| 160 |
+
history.add_version(version, state_obj, state_raw, summary)
|
| 161 |
+
return history.to_serializable()
|
| 162 |
+
|
| 163 |
+
|
| 164 |
+
def rollback_history(
|
| 165 |
+
history_data: Dict[str, Any],
|
| 166 |
+
version: int
|
| 167 |
+
) -> Tuple[Dict[str, Any], Optional[Dict[str, Any]], Optional[str]]:
|
| 168 |
+
"""
|
| 169 |
+
回滚到指定版本
|
| 170 |
+
返回:(更新后的 history_data, state_obj, state_raw)
|
| 171 |
+
"""
|
| 172 |
+
history = DesignStateHistory.from_serializable(history_data)
|
| 173 |
+
result = history.rollback_to(version)
|
| 174 |
+
if result:
|
| 175 |
+
return history.to_serializable(), result[0], result[1]
|
| 176 |
+
return history_data, None, None
|
| 177 |
+
|
| 178 |
+
|
| 179 |
+
def get_history_choices(history_data: Dict[str, Any]) -> List[str]:
|
| 180 |
+
"""获取版本下拉选项"""
|
| 181 |
+
history = DesignStateHistory.from_serializable(history_data)
|
| 182 |
+
return history.get_version_choices()
|
| 183 |
+
|
| 184 |
+
|
| 185 |
+
def parse_version_from_choice(choice: str) -> int:
|
| 186 |
+
"""从选项字符串中解析版本号"""
|
| 187 |
+
if not choice:
|
| 188 |
+
return 0
|
| 189 |
+
# 格式: "v3 [14:23:45] 玩法名 (2个机制)"
|
| 190 |
+
match = re.match(r"v(\d+)", choice)
|
| 191 |
+
if match:
|
| 192 |
+
return int(match.group(1))
|
| 193 |
+
return 0
|
| 194 |
+
|
| 195 |
+
|
| 196 |
_JSON_FENCE_RE = re.compile(r"```json\s*(\{.*?\})\s*```", re.DOTALL | re.IGNORECASE)
|
| 197 |
_READY_RE = re.compile(r"READY_TO_GENERATE\s*:\s*(true|false)", re.IGNORECASE)
|
| 198 |
|
|
|
|
| 345 |
else:
|
| 346 |
return True
|
| 347 |
|
| 348 |
+
# 名称/来源类字段默认不允许在"仅优化"里被改(除非自由迭代)
|
| 349 |
forbidden = {"new_variant_name", "base_variants", "fusion_variants"}
|
| 350 |
for k in changed_top_keys:
|
| 351 |
if k in forbidden:
|
|
|
|
| 353 |
if k not in allowed:
|
| 354 |
return False
|
| 355 |
return True
|
| 356 |
+
|
| 357 |
+
|
| 358 |
+
# ==================== 差异可视化(新增) ====================
|
| 359 |
+
|
| 360 |
+
# 字段中文名映射
|
| 361 |
+
_FIELD_NAMES = {
|
| 362 |
+
"new_variant_name": "玩法名称",
|
| 363 |
+
"base_variants": "底座玩法",
|
| 364 |
+
"fusion_variants": "融合玩法",
|
| 365 |
+
"game_variant": "游戏模式",
|
| 366 |
+
"players": "玩家人数",
|
| 367 |
+
"scoring_mode": "计分模式",
|
| 368 |
+
"tileset": "牌组配置",
|
| 369 |
+
"core_constraints": "核心约束",
|
| 370 |
+
"mechanics": "机制列表",
|
| 371 |
+
"open_questions": "待定问题",
|
| 372 |
+
}
|
| 373 |
+
|
| 374 |
+
|
| 375 |
+
def _format_value(val: Any, max_len: int = 40) -> str:
|
| 376 |
+
"""格式化值为简短字符串"""
|
| 377 |
+
if val is None:
|
| 378 |
+
return "(空)"
|
| 379 |
+
if isinstance(val, bool):
|
| 380 |
+
return "是" if val else "否"
|
| 381 |
+
if isinstance(val, (int, float)):
|
| 382 |
+
return str(val)
|
| 383 |
+
if isinstance(val, str):
|
| 384 |
+
s = val.strip()
|
| 385 |
+
return s if len(s) <= max_len else s[:max_len] + "..."
|
| 386 |
+
if isinstance(val, list):
|
| 387 |
+
if not val:
|
| 388 |
+
return "(空列表)"
|
| 389 |
+
if len(val) <= 3:
|
| 390 |
+
return ", ".join(str(v) for v in val)
|
| 391 |
+
return ", ".join(str(v) for v in val[:3]) + f"... 共{len(val)}项"
|
| 392 |
+
if isinstance(val, dict):
|
| 393 |
+
keys = list(val.keys())
|
| 394 |
+
if len(keys) <= 3:
|
| 395 |
+
return "{" + ", ".join(keys) + "}"
|
| 396 |
+
return "{" + ", ".join(keys[:3]) + f"... 共{len(keys)}项" + "}"
|
| 397 |
+
return str(val)[:max_len]
|
| 398 |
+
|
| 399 |
+
|
| 400 |
+
def diff_open_questions(prev: Dict[str, Any], cur: Dict[str, Any]) -> Tuple[List[str], List[str]]:
|
| 401 |
+
"""
|
| 402 |
+
对比 open_questions 的变化
|
| 403 |
+
返回:(已解决的问题列表, 新增的问题列表)
|
| 404 |
+
"""
|
| 405 |
+
prev_qs = prev.get("open_questions") if isinstance(prev, dict) else None
|
| 406 |
+
cur_qs = cur.get("open_questions") if isinstance(cur, dict) else None
|
| 407 |
+
|
| 408 |
+
if not isinstance(prev_qs, list):
|
| 409 |
+
prev_qs = []
|
| 410 |
+
if not isinstance(cur_qs, list):
|
| 411 |
+
cur_qs = []
|
| 412 |
+
|
| 413 |
+
prev_set = set(str(q) for q in prev_qs)
|
| 414 |
+
cur_set = set(str(q) for q in cur_qs)
|
| 415 |
+
|
| 416 |
+
resolved = sorted(prev_set - cur_set)
|
| 417 |
+
added = sorted(cur_set - prev_set)
|
| 418 |
+
|
| 419 |
+
return resolved, added
|
| 420 |
+
|
| 421 |
+
|
| 422 |
+
def generate_diff_summary(
|
| 423 |
+
prev: Optional[Dict[str, Any]],
|
| 424 |
+
cur: Optional[Dict[str, Any]],
|
| 425 |
+
prev_version: int = 0,
|
| 426 |
+
cur_version: int = 0
|
| 427 |
+
) -> str:
|
| 428 |
+
"""
|
| 429 |
+
生成人可读的 DesignState 变更摘要(Markdown 格式)
|
| 430 |
+
"""
|
| 431 |
+
if not cur:
|
| 432 |
+
return ""
|
| 433 |
+
|
| 434 |
+
if not prev:
|
| 435 |
+
# 首次建立
|
| 436 |
+
name = cur.get("new_variant_name", "(未命名)")
|
| 437 |
+
mechanics = cur.get("mechanics", [])
|
| 438 |
+
mech_count = len(mechanics) if isinstance(mechanics, list) else 0
|
| 439 |
+
return f"**DesignState v{cur_version} 已建立**\n\n玩法:{name}\n机制数:{mech_count}"
|
| 440 |
+
|
| 441 |
+
lines = []
|
| 442 |
+
version_str = f"v{prev_version} → v{cur_version}" if prev_version and cur_version else ""
|
| 443 |
+
lines.append(f"**DesignState 变更摘要** {version_str}")
|
| 444 |
+
lines.append("")
|
| 445 |
+
|
| 446 |
+
# 1. 顶层字段变更(排除 mechanics 和 open_questions,单独处理)
|
| 447 |
+
skip_keys = {"mechanics", "open_questions"}
|
| 448 |
+
changed_fields = []
|
| 449 |
+
|
| 450 |
+
all_keys = set(prev.keys()) | set(cur.keys())
|
| 451 |
+
for key in sorted(all_keys):
|
| 452 |
+
if key in skip_keys:
|
| 453 |
+
continue
|
| 454 |
+
prev_val = prev.get(key)
|
| 455 |
+
cur_val = cur.get(key)
|
| 456 |
+
if prev_val != cur_val:
|
| 457 |
+
field_name = _FIELD_NAMES.get(key, key)
|
| 458 |
+
if key not in prev:
|
| 459 |
+
changed_fields.append(f" • **[新增]** {field_name}: {_format_value(cur_val)}")
|
| 460 |
+
elif key not in cur:
|
| 461 |
+
changed_fields.append(f" • **[删除]** {field_name}")
|
| 462 |
+
else:
|
| 463 |
+
changed_fields.append(f" • {field_name}: `{_format_value(prev_val)}` → `{_format_value(cur_val)}`")
|
| 464 |
+
|
| 465 |
+
if changed_fields:
|
| 466 |
+
lines.append("📝 **顶层字段变更:**")
|
| 467 |
+
lines.extend(changed_fields)
|
| 468 |
+
lines.append("")
|
| 469 |
+
|
| 470 |
+
# 2. 机制变更
|
| 471 |
+
mech_changes = diff_mechanics(prev, cur)
|
| 472 |
+
if mech_changes:
|
| 473 |
+
lines.append("🔧 **机制变更:**")
|
| 474 |
+
for change in mech_changes:
|
| 475 |
+
if change.startswith("新增机制"):
|
| 476 |
+
lines.append(f" • **[新增]** {change.replace('新增机制: ', '')}")
|
| 477 |
+
elif change.startswith("删除机制"):
|
| 478 |
+
lines.append(f" • **[删除]** {change.replace('删除机制: ', '')}")
|
| 479 |
+
else:
|
| 480 |
+
# 机制变更: XXX 字段=a,b,c
|
| 481 |
+
parts = change.replace("机制变更: ", "").split(" 字段=")
|
| 482 |
+
if len(parts) == 2:
|
| 483 |
+
lines.append(f" • **[修改]** {parts[0]}: {parts[1]}")
|
| 484 |
+
else:
|
| 485 |
+
lines.append(f" • {change}")
|
| 486 |
+
lines.append("")
|
| 487 |
+
|
| 488 |
+
# 3. 待定问题变化
|
| 489 |
+
resolved, added = diff_open_questions(prev, cur)
|
| 490 |
+
if resolved or added:
|
| 491 |
+
lines.append("❓ **待定问题变化:**")
|
| 492 |
+
for q in resolved:
|
| 493 |
+
lines.append(f" • ~~[已解决]~~ {q[:50]}{'...' if len(q) > 50 else ''}")
|
| 494 |
+
for q in added:
|
| 495 |
+
lines.append(f" • **[新增]** {q[:50]}{'...' if len(q) > 50 else ''}")
|
| 496 |
+
lines.append("")
|
| 497 |
+
|
| 498 |
+
# 如果没有任何变更
|
| 499 |
+
if len(lines) <= 2:
|
| 500 |
+
lines.append("_(无变更)_")
|
| 501 |
+
|
| 502 |
+
return "\n".join(lines).strip()
|
| 503 |
+
|
| 504 |
+
|
| 505 |
+
def generate_diff_summary_compact(
|
| 506 |
+
prev: Optional[Dict[str, Any]],
|
| 507 |
+
cur: Optional[Dict[str, Any]]
|
| 508 |
+
) -> str:
|
| 509 |
+
"""
|
| 510 |
+
生成紧凑版变更摘要(单行,用于状态栏)
|
| 511 |
+
"""
|
| 512 |
+
if not cur:
|
| 513 |
+
return "无变更"
|
| 514 |
+
|
| 515 |
+
if not prev:
|
| 516 |
+
name = cur.get("new_variant_name", "(未命名)")
|
| 517 |
+
return f"新建: {name}"
|
| 518 |
+
|
| 519 |
+
changes = []
|
| 520 |
+
|
| 521 |
+
# 顶层字段变更数
|
| 522 |
+
field_changes = diff_keys(prev, cur)
|
| 523 |
+
skip_keys = {"mechanics", "open_questions"}
|
| 524 |
+
field_changes = [k for k in field_changes if k not in skip_keys]
|
| 525 |
+
if field_changes:
|
| 526 |
+
changes.append(f"{len(field_changes)}个字段")
|
| 527 |
+
|
| 528 |
+
# 机制变更数
|
| 529 |
+
mech_changes = diff_mechanics(prev, cur)
|
| 530 |
+
if mech_changes:
|
| 531 |
+
changes.append(f"{len(mech_changes)}个机制")
|
| 532 |
+
|
| 533 |
+
# 问题变化
|
| 534 |
+
resolved, added = diff_open_questions(prev, cur)
|
| 535 |
+
if resolved:
|
| 536 |
+
changes.append(f"解决{len(resolved)}个问题")
|
| 537 |
+
if added:
|
| 538 |
+
changes.append(f"新增{len(added)}个问题")
|
| 539 |
+
|
| 540 |
+
if not changes:
|
| 541 |
+
return "无变更"
|
| 542 |
+
|
| 543 |
+
return "变更: " + ", ".join(changes)
|
| 544 |
+
|
| 545 |
+
|
| 546 |
+
# ==================== 多阶段引导式交互 ====================
|
| 547 |
+
|
| 548 |
+
class InteractionPhase:
|
| 549 |
+
"""交互阶段枚举"""
|
| 550 |
+
INITIAL = "initial" # 初始阶段:用户提出需求
|
| 551 |
+
UNDERSTAND = "understand" # 理解确认:确认对已有玩法的理解
|
| 552 |
+
DIVERGE = "diverge" # 方案发散:生成多种机制组合方案
|
| 553 |
+
SELECT = "select" # 方案选择:用户选择具体方案
|
| 554 |
+
ELABORATE = "elaborate" # 深入展开:对选定方案进行详细设计
|
| 555 |
+
ITERATE = "iterate" # 迭代优化:基于反馈优化方案
|
| 556 |
+
|
| 557 |
+
|
| 558 |
+
# 方案提取正则
|
| 559 |
+
_PROPOSAL_BLOCK_RE = re.compile(
|
| 560 |
+
r"(?:###?\s*)?(?:方案|选项|Option)\s*([A-Z\d一二三四五六七八九十]+)[::\s]*(.+?)(?=(?:###?\s*)?(?:方案|选项|Option)\s*[A-Z\d一二三四五六七八九十]+[::\s]|$)",
|
| 561 |
+
re.DOTALL | re.IGNORECASE
|
| 562 |
+
)
|
| 563 |
+
|
| 564 |
+
# 理解确认问题提取
|
| 565 |
+
_CLARIFY_QUESTION_RE = re.compile(
|
| 566 |
+
r"(?:❓|🤔|【确认】|【问题】|\[确认\]|\[问题\]|请确认|请问|是否是指)\s*(.+?\?)",
|
| 567 |
+
re.DOTALL
|
| 568 |
+
)
|
| 569 |
+
|
| 570 |
+
|
| 571 |
+
def extract_proposals(text: str) -> List[Dict[str, Any]]:
|
| 572 |
+
"""
|
| 573 |
+
从模型输出中提取多个候选方案
|
| 574 |
+
返回: [{"id": "A", "title": "...", "description": "...", "highlights": [...]}]
|
| 575 |
+
"""
|
| 576 |
+
if not text:
|
| 577 |
+
return []
|
| 578 |
+
|
| 579 |
+
proposals = []
|
| 580 |
+
matches = _PROPOSAL_BLOCK_RE.findall(text)
|
| 581 |
+
|
| 582 |
+
for idx, (proposal_id, content) in enumerate(matches):
|
| 583 |
+
content = content.strip()
|
| 584 |
+
# 提取标题(第一行或冒号前的部分)
|
| 585 |
+
lines = content.split("\n")
|
| 586 |
+
title = lines[0].strip() if lines else f"方案 {proposal_id}"
|
| 587 |
+
# 清理标题中的 markdown 标记
|
| 588 |
+
title = re.sub(r"^[#\-\*]+\s*", "", title)
|
| 589 |
+
title = re.sub(r"\*+", "", title)
|
| 590 |
+
|
| 591 |
+
# 提取描述
|
| 592 |
+
description = "\n".join(lines[1:]).strip() if len(lines) > 1 else ""
|
| 593 |
+
|
| 594 |
+
# 提取亮点/创新点
|
| 595 |
+
highlights = []
|
| 596 |
+
highlight_patterns = [
|
| 597 |
+
r"[★✦⭐🌟]\s*(.+)",
|
| 598 |
+
r"(?:创新点|亮点|特色)[::]\s*(.+)",
|
| 599 |
+
r"[-•]\s*(?:创新|特色|核心)[::]\s*(.+)",
|
| 600 |
+
]
|
| 601 |
+
for pattern in highlight_patterns:
|
| 602 |
+
hl_matches = re.findall(pattern, content)
|
| 603 |
+
highlights.extend([h.strip() for h in hl_matches])
|
| 604 |
+
|
| 605 |
+
proposals.append({
|
| 606 |
+
"id": proposal_id.strip(),
|
| 607 |
+
"title": title[:50], # 限制标题长度
|
| 608 |
+
"description": description[:200] + ("..." if len(description) > 200 else ""),
|
| 609 |
+
"highlights": highlights[:3], # 最多3个亮点
|
| 610 |
+
"full_content": content,
|
| 611 |
+
})
|
| 612 |
+
|
| 613 |
+
return proposals
|
| 614 |
+
|
| 615 |
+
|
| 616 |
+
def extract_clarify_questions(text: str) -> List[str]:
|
| 617 |
+
"""
|
| 618 |
+
从模型输出中提取需要用户确认的问题
|
| 619 |
+
"""
|
| 620 |
+
if not text:
|
| 621 |
+
return []
|
| 622 |
+
|
| 623 |
+
questions = []
|
| 624 |
+
matches = _CLARIFY_QUESTION_RE.findall(text)
|
| 625 |
+
for q in matches:
|
| 626 |
+
q = q.strip()
|
| 627 |
+
if q and len(q) > 5: # 过滤太短的
|
| 628 |
+
questions.append(q)
|
| 629 |
+
|
| 630 |
+
# 也尝试提取 open_questions 中的内容
|
| 631 |
+
try:
|
| 632 |
+
ds_obj, _ = extract_design_state(text)
|
| 633 |
+
if ds_obj and "open_questions" in ds_obj:
|
| 634 |
+
for q in ds_obj.get("open_questions", []):
|
| 635 |
+
if isinstance(q, str) and q not in questions:
|
| 636 |
+
questions.append(q)
|
| 637 |
+
except Exception:
|
| 638 |
+
pass
|
| 639 |
+
|
| 640 |
+
return questions[:5] # 最多5个问题
|
| 641 |
+
|
| 642 |
+
|
| 643 |
+
def detect_interaction_phase(text: str, ds_obj: Optional[Dict[str, Any]] = None) -> str:
|
| 644 |
+
"""
|
| 645 |
+
根据模型输出内容检测当前交互阶段
|
| 646 |
+
"""
|
| 647 |
+
if not text:
|
| 648 |
+
return InteractionPhase.INITIAL
|
| 649 |
+
|
| 650 |
+
text_lower = text.lower()
|
| 651 |
+
|
| 652 |
+
# 检测是否有多个方案供选择
|
| 653 |
+
proposals = extract_proposals(text)
|
| 654 |
+
if len(proposals) >= 2:
|
| 655 |
+
return InteractionPhase.DIVERGE
|
| 656 |
+
|
| 657 |
+
# 检测是否有需要确认的问题
|
| 658 |
+
questions = extract_clarify_questions(text)
|
| 659 |
+
if questions:
|
| 660 |
+
return InteractionPhase.UNDERSTAND
|
| 661 |
+
|
| 662 |
+
# 检测是否有完整的 DesignState 且 READY_TO_GENERATE
|
| 663 |
+
ready = extract_ready_to_generate(text)
|
| 664 |
+
if ready is True:
|
| 665 |
+
return InteractionPhase.ELABORATE
|
| 666 |
+
|
| 667 |
+
# 检测是否在迭代中
|
| 668 |
+
if ds_obj and ds_obj.get("mechanics"):
|
| 669 |
+
return InteractionPhase.ITERATE
|
| 670 |
+
|
| 671 |
+
return InteractionPhase.INITIAL
|
| 672 |
+
|
| 673 |
+
|
| 674 |
+
def format_proposals_for_display(proposals: List[Dict[str, Any]]) -> str:
|
| 675 |
+
"""
|
| 676 |
+
将方案列表格式化为 Markdown 显示
|
| 677 |
+
"""
|
| 678 |
+
if not proposals:
|
| 679 |
+
return ""
|
| 680 |
+
|
| 681 |
+
lines = ["## 🎯 请选择一个方案深入展开\n"]
|
| 682 |
+
|
| 683 |
+
for p in proposals:
|
| 684 |
+
lines.append(f"### 方案 {p['id']}: {p['title']}")
|
| 685 |
+
if p.get("highlights"):
|
| 686 |
+
for hl in p["highlights"]:
|
| 687 |
+
lines.append(f" ✦ {hl}")
|
| 688 |
+
if p.get("description"):
|
| 689 |
+
lines.append(f"\n{p['description'][:150]}...")
|
| 690 |
+
lines.append("")
|
| 691 |
+
|
| 692 |
+
lines.append("\n💡 请在下方选择方案编号,或输入「其他」描述你的想法。")
|
| 693 |
+
|
| 694 |
+
return "\n".join(lines)
|
| 695 |
+
|
| 696 |
+
|
| 697 |
+
def generate_phase_prompt_hint(phase: str, ds_obj: Optional[Dict[str, Any]] = None) -> str:
|
| 698 |
+
"""
|
| 699 |
+
根据当前阶段生成提示词补充
|
| 700 |
+
"""
|
| 701 |
+
hints = {
|
| 702 |
+
InteractionPhase.INITIAL: "",
|
| 703 |
+
InteractionPhase.UNDERSTAND: (
|
| 704 |
+
"\n\n【阶段提示】当前处于「理解确认」阶段。"
|
| 705 |
+
"请确保你完全理解用户描述的已有玩法,如有不明确之处请提出确认问题。"
|
| 706 |
+
),
|
| 707 |
+
InteractionPhase.DIVERGE: (
|
| 708 |
+
"\n\n【阶段提示】当前处于「方案发散」阶段。"
|
| 709 |
+
"请发散思维,给出 2-4 种不同方向的机制组合方案,每个方案需包含:\n"
|
| 710 |
+
"1. 方案编号(A/B/C/D)\n"
|
| 711 |
+
"2. 方案名称\n"
|
| 712 |
+
"3. 核心创新点(1-2句话)\n"
|
| 713 |
+
"4. 机制组合简述\n"
|
| 714 |
+
"不要深入展开,等用户选择后再详细设计。"
|
| 715 |
+
),
|
| 716 |
+
InteractionPhase.SELECT: (
|
| 717 |
+
"\n\n【阶段提示】用户已选择方案,请对该方案进行深入展开设计。"
|
| 718 |
+
),
|
| 719 |
+
InteractionPhase.ELABORATE: (
|
| 720 |
+
"\n\n【阶段提示】当前处于「深入展开」阶段。"
|
| 721 |
+
"请输出完整的玩法设计(含 DesignState、mGDL、自检报告)。"
|
| 722 |
+
),
|
| 723 |
+
InteractionPhase.ITERATE: (
|
| 724 |
+
"\n\n【阶段提示】当前处于「迭代优化」阶段。"
|
| 725 |
+
"请基于用户反馈对现有方案进行最小修改。"
|
| 726 |
+
),
|
| 727 |
+
}
|
| 728 |
+
return hints.get(phase, "")
|
| 729 |
+
|
| 730 |
+
|
| 731 |
+
def create_phase_state() -> Dict[str, Any]:
|
| 732 |
+
"""创建阶段状态(用于 Gradio State)"""
|
| 733 |
+
return {
|
| 734 |
+
"current_phase": InteractionPhase.INITIAL,
|
| 735 |
+
"confirmed_understanding": [], # 已确认的理解点
|
| 736 |
+
"proposals": [], # 当前可选方案
|
| 737 |
+
"selected_proposal": None, # 用户选择的方案
|
| 738 |
+
"phase_history": [], # 阶段历史
|
| 739 |
+
}
|
| 740 |
+
|
| 741 |
+
|
| 742 |
+
def update_phase_state(
|
| 743 |
+
phase_state: Dict[str, Any],
|
| 744 |
+
new_phase: str,
|
| 745 |
+
proposals: Optional[List[Dict[str, Any]]] = None,
|
| 746 |
+
selected: Optional[str] = None,
|
| 747 |
+
confirmed: Optional[List[str]] = None,
|
| 748 |
+
) -> Dict[str, Any]:
|
| 749 |
+
"""更新阶段状态"""
|
| 750 |
+
state = phase_state.copy() if phase_state else create_phase_state()
|
| 751 |
+
|
| 752 |
+
# 记录阶段变化历史
|
| 753 |
+
if state["current_phase"] != new_phase:
|
| 754 |
+
state["phase_history"].append({
|
| 755 |
+
"from": state["current_phase"],
|
| 756 |
+
"to": new_phase,
|
| 757 |
+
"timestamp": datetime.now().strftime("%H:%M:%S"),
|
| 758 |
+
})
|
| 759 |
+
|
| 760 |
+
state["current_phase"] = new_phase
|
| 761 |
+
|
| 762 |
+
if proposals is not None:
|
| 763 |
+
state["proposals"] = proposals
|
| 764 |
+
|
| 765 |
+
if selected is not None:
|
| 766 |
+
state["selected_proposal"] = selected
|
| 767 |
+
|
| 768 |
+
if confirmed is not None:
|
| 769 |
+
state["confirmed_understanding"].extend(confirmed)
|
| 770 |
+
|
| 771 |
+
return state
|
| 772 |
+
|
| 773 |
+
|
| 774 |
+
def get_phase_display_name(phase: str) -> str:
|
| 775 |
+
"""获取阶段的显示名称"""
|
| 776 |
+
names = {
|
| 777 |
+
InteractionPhase.INITIAL: "🚀 初始",
|
| 778 |
+
InteractionPhase.UNDERSTAND: "🔍 理解确认",
|
| 779 |
+
InteractionPhase.DIVERGE: "💡 方案发散",
|
| 780 |
+
InteractionPhase.SELECT: "✅ 方案选择",
|
| 781 |
+
InteractionPhase.ELABORATE: "📝 深入展开",
|
| 782 |
+
InteractionPhase.ITERATE: "🔄 迭代优化",
|
| 783 |
+
}
|
| 784 |
+
return names.get(phase, phase)
|
| 785 |
+
|
| 786 |
+
|
| 787 |
+
# ==================== 细粒度范围控制 ====================
|
| 788 |
+
|
| 789 |
+
# DesignState 可控字段定义
|
| 790 |
+
CONTROLLABLE_FIELDS = {
|
| 791 |
+
"new_variant_name": {"label": "玩法名称", "category": "基础信息", "lockable": True},
|
| 792 |
+
"base_variants": {"label": "底座玩法", "category": "基础信息", "lockable": True},
|
| 793 |
+
"fusion_variants": {"label": "融合玩法", "category": "基础信息", "lockable": True},
|
| 794 |
+
"game_variant": {"label": "游戏模式", "category": "游戏规则", "lockable": True},
|
| 795 |
+
"players": {"label": "玩家人数", "category": "游戏规则", "lockable": True},
|
| 796 |
+
"scoring_mode": {"label": "计分模式", "category": "计分系统", "lockable": True},
|
| 797 |
+
"tileset": {"label": "牌组配置", "category": "游戏规则", "lockable": True},
|
| 798 |
+
"core_constraints": {"label": "核心约束", "category": "游戏规则", "lockable": False},
|
| 799 |
+
"mechanics": {"label": "机制列表", "category": "创新机制", "lockable": False},
|
| 800 |
+
"open_questions": {"label": "待定问题", "category": "其他", "lockable": False},
|
| 801 |
+
}
|
| 802 |
+
|
| 803 |
+
# 字段分类
|
| 804 |
+
FIELD_CATEGORIES = {
|
| 805 |
+
"基础信息": ["new_variant_name", "base_variants", "fusion_variants"],
|
| 806 |
+
"游戏规则": ["game_variant", "players", "tileset", "core_constraints"],
|
| 807 |
+
"计分系统": ["scoring_mode"],
|
| 808 |
+
"创新机制": ["mechanics"],
|
| 809 |
+
"其他": ["open_questions"],
|
| 810 |
+
}
|
| 811 |
+
|
| 812 |
+
|
| 813 |
+
class ScopeConstraint:
|
| 814 |
+
"""范围约束类型"""
|
| 815 |
+
SOFT = "soft" # 软约束:检测到越界时提示,但不阻断
|
| 816 |
+
HARD = "hard" # 硬约束:检测到越界时阻断并要求重做
|
| 817 |
+
|
| 818 |
+
|
| 819 |
+
def create_scope_config() -> Dict[str, Any]:
|
| 820 |
+
"""创建默认的范围配置"""
|
| 821 |
+
return {
|
| 822 |
+
"mode": "preset", # preset(预设模式)或 custom(自定义模式)
|
| 823 |
+
"preset": "自由迭代(仍保最小修改)", # 预设选项
|
| 824 |
+
"constraint_level": ScopeConstraint.SOFT, # 约束级别
|
| 825 |
+
"locked_fields": [], # 锁定的顶层字段
|
| 826 |
+
"allowed_fields": [], # 允许修改的顶层字段(custom 模式下使用)
|
| 827 |
+
"locked_mechanics": [], # 锁定的机制名称(不允许修改这些机制)
|
| 828 |
+
"allowed_mechanics": [], # 只允许修改这些机制(为空表示不限制)
|
| 829 |
+
}
|
| 830 |
+
|
| 831 |
+
|
| 832 |
+
def get_scope_preset_options() -> List[str]:
|
| 833 |
+
"""获取预设范围选项"""
|
| 834 |
+
return [
|
| 835 |
+
"自由迭代(仍保最小修改)",
|
| 836 |
+
"仅优化创新机制",
|
| 837 |
+
"仅优化计分与番型",
|
| 838 |
+
"仅优化流程与阶段",
|
| 839 |
+
"仅修复校验问题",
|
| 840 |
+
"锁定核心(仅微调细节)",
|
| 841 |
+
]
|
| 842 |
+
|
| 843 |
+
|
| 844 |
+
def get_allowed_fields_for_preset(preset: str) -> List[str]:
|
| 845 |
+
"""根据预设获取允许修改的字段"""
|
| 846 |
+
presets = {
|
| 847 |
+
"自由迭代(仍保最小修改)": list(CONTROLLABLE_FIELDS.keys()),
|
| 848 |
+
"仅优化创新机制": ["mechanics", "open_questions", "core_constraints"],
|
| 849 |
+
"仅优化计分与番型": ["scoring_mode", "open_questions"],
|
| 850 |
+
"仅优化流程与阶段": ["game_variant", "open_questions"],
|
| 851 |
+
"仅修复校验问题": ["mechanics", "tileset", "core_constraints", "players", "game_variant", "scoring_mode", "open_questions"],
|
| 852 |
+
"锁定核心(仅微调细节)": ["open_questions", "core_constraints"],
|
| 853 |
+
}
|
| 854 |
+
return presets.get(preset, list(CONTROLLABLE_FIELDS.keys()))
|
| 855 |
+
|
| 856 |
+
|
| 857 |
+
def get_forbidden_fields_for_preset(preset: str) -> List[str]:
|
| 858 |
+
"""根据预设获取禁止修改的字段"""
|
| 859 |
+
if preset == "自由迭代(仍保最小修改)":
|
| 860 |
+
return []
|
| 861 |
+
|
| 862 |
+
allowed = set(get_allowed_fields_for_preset(preset))
|
| 863 |
+
all_fields = set(CONTROLLABLE_FIELDS.keys())
|
| 864 |
+
return list(all_fields - allowed)
|
| 865 |
+
|
| 866 |
+
|
| 867 |
+
def validate_scope_compliance(
|
| 868 |
+
prev: Dict[str, Any],
|
| 869 |
+
cur: Dict[str, Any],
|
| 870 |
+
scope_config: Dict[str, Any],
|
| 871 |
+
) -> Dict[str, Any]:
|
| 872 |
+
"""
|
| 873 |
+
验证变更是否符合范围约束
|
| 874 |
+
|
| 875 |
+
返回:
|
| 876 |
+
{
|
| 877 |
+
"compliant": bool, # 是否符合约束
|
| 878 |
+
"violations": [ # 违规列表
|
| 879 |
+
{"field": "xxx", "type": "field_locked|field_forbidden|mechanic_locked", "detail": "..."}
|
| 880 |
+
],
|
| 881 |
+
"warnings": [], # 警告(软约束时)
|
| 882 |
+
"summary": "..." # 摘要文本
|
| 883 |
+
}
|
| 884 |
+
"""
|
| 885 |
+
if not prev or not cur:
|
| 886 |
+
return {"compliant": True, "violations": [], "warnings": [], "summary": "无变更"}
|
| 887 |
+
|
| 888 |
+
result = {
|
| 889 |
+
"compliant": True,
|
| 890 |
+
"violations": [],
|
| 891 |
+
"warnings": [],
|
| 892 |
+
"summary": "",
|
| 893 |
+
}
|
| 894 |
+
|
| 895 |
+
mode = scope_config.get("mode", "preset")
|
| 896 |
+
constraint_level = scope_config.get("constraint_level", ScopeConstraint.SOFT)
|
| 897 |
+
|
| 898 |
+
# 确定允许和禁止的字段
|
| 899 |
+
if mode == "preset":
|
| 900 |
+
preset = scope_config.get("preset", "自由迭代(仍保最小修改)")
|
| 901 |
+
allowed_fields = set(get_allowed_fields_for_preset(preset))
|
| 902 |
+
forbidden_fields = set(get_forbidden_fields_for_preset(preset))
|
| 903 |
+
else:
|
| 904 |
+
allowed_fields = set(scope_config.get("allowed_fields", []))
|
| 905 |
+
forbidden_fields = set(scope_config.get("locked_fields", []))
|
| 906 |
+
|
| 907 |
+
locked_mechanics = set(scope_config.get("locked_mechanics", []))
|
| 908 |
+
allowed_mechanics = set(scope_config.get("allowed_mechanics", []))
|
| 909 |
+
|
| 910 |
+
# 检查顶层字段变更
|
| 911 |
+
changed_fields = diff_keys(prev, cur)
|
| 912 |
+
|
| 913 |
+
for field in changed_fields:
|
| 914 |
+
# 检查是否在禁止列表中
|
| 915 |
+
if field in forbidden_fields:
|
| 916 |
+
violation = {
|
| 917 |
+
"field": field,
|
| 918 |
+
"type": "field_forbidden",
|
| 919 |
+
"detail": f"字段「{CONTROLLABLE_FIELDS.get(field, {}).get('label', field)}」不在允许修改范围内",
|
| 920 |
+
}
|
| 921 |
+
result["violations"].append(violation)
|
| 922 |
+
continue
|
| 923 |
+
|
| 924 |
+
# 检查是否被显式锁定
|
| 925 |
+
if field in scope_config.get("locked_fields", []):
|
| 926 |
+
violation = {
|
| 927 |
+
"field": field,
|
| 928 |
+
"type": "field_locked",
|
| 929 |
+
"detail": f"字段「{CONTROLLABLE_FIELDS.get(field, {}).get('label', field)}」已被锁定",
|
| 930 |
+
}
|
| 931 |
+
result["violations"].append(violation)
|
| 932 |
+
continue
|
| 933 |
+
|
| 934 |
+
# 检查机制级别变更
|
| 935 |
+
if "mechanics" in changed_fields:
|
| 936 |
+
mech_changes = diff_mechanics(prev, cur)
|
| 937 |
+
|
| 938 |
+
for change in mech_changes:
|
| 939 |
+
# 提取机制名称
|
| 940 |
+
mech_name = ""
|
| 941 |
+
if "新增机制:" in change:
|
| 942 |
+
mech_name = change.replace("新增机制: ", "").strip()
|
| 943 |
+
elif "删除机制:" in change:
|
| 944 |
+
mech_name = change.replace("删除机制: ", "").strip()
|
| 945 |
+
elif "机制变更:" in change:
|
| 946 |
+
mech_name = change.split(" 字段=")[0].replace("机制变更: ", "").strip()
|
| 947 |
+
|
| 948 |
+
# 检查机制是否被锁定
|
| 949 |
+
if mech_name and mech_name in locked_mechanics:
|
| 950 |
+
violation = {
|
| 951 |
+
"field": "mechanics",
|
| 952 |
+
"type": "mechanic_locked",
|
| 953 |
+
"detail": f"机制「{mech_name}」已被锁定,不允许修改",
|
| 954 |
+
}
|
| 955 |
+
result["violations"].append(violation)
|
| 956 |
+
|
| 957 |
+
# 检查是否只允许修改特定机制
|
| 958 |
+
if allowed_mechanics and mech_name and mech_name not in allowed_mechanics:
|
| 959 |
+
# 只对修改和删除进行限制,新增通常是允许的
|
| 960 |
+
if "删除机制:" in change or "机制变更:" in change:
|
| 961 |
+
violation = {
|
| 962 |
+
"field": "mechanics",
|
| 963 |
+
"type": "mechanic_not_allowed",
|
| 964 |
+
"detail": f"机制「{mech_name}」不在允许修改的机制列表中",
|
| 965 |
+
}
|
| 966 |
+
result["violations"].append(violation)
|
| 967 |
+
|
| 968 |
+
# 根据约束级别处理违规
|
| 969 |
+
if result["violations"]:
|
| 970 |
+
if constraint_level == ScopeConstraint.HARD:
|
| 971 |
+
result["compliant"] = False
|
| 972 |
+
result["summary"] = f"❌ 发现 {len(result['violations'])} 处范围越界(硬约束),请重新生成"
|
| 973 |
+
else:
|
| 974 |
+
result["warnings"] = result["violations"]
|
| 975 |
+
result["violations"] = []
|
| 976 |
+
result["summary"] = f"⚠️ 发现 {len(result['warnings'])} 处范围越界(软约束),建议检查"
|
| 977 |
+
else:
|
| 978 |
+
result["summary"] = "✅ 变更符合范围约束"
|
| 979 |
+
|
| 980 |
+
return result
|
| 981 |
+
|
| 982 |
+
|
| 983 |
+
def format_scope_violations(validation_result: Dict[str, Any]) -> str:
|
| 984 |
+
"""格式化范围违规为 Markdown"""
|
| 985 |
+
if validation_result.get("compliant", True) and not validation_result.get("warnings"):
|
| 986 |
+
return validation_result.get("summary", "")
|
| 987 |
+
|
| 988 |
+
lines = [validation_result.get("summary", "")]
|
| 989 |
+
|
| 990 |
+
items = validation_result.get("violations", []) or validation_result.get("warnings", [])
|
| 991 |
+
if items:
|
| 992 |
+
lines.append("")
|
| 993 |
+
for item in items:
|
| 994 |
+
field_label = CONTROLLABLE_FIELDS.get(item["field"], {}).get("label", item["field"])
|
| 995 |
+
lines.append(f" • **{field_label}**: {item['detail']}")
|
| 996 |
+
|
| 997 |
+
return "\n".join(lines)
|
| 998 |
+
|
| 999 |
+
|
| 1000 |
+
def get_mechanics_from_state(ds_obj: Dict[str, Any]) -> List[str]:
|
| 1001 |
+
"""从 DesignState 中提取机制名称列表"""
|
| 1002 |
+
if not ds_obj:
|
| 1003 |
+
return []
|
| 1004 |
+
|
| 1005 |
+
mechanics = ds_obj.get("mechanics", [])
|
| 1006 |
+
if not isinstance(mechanics, list):
|
| 1007 |
+
return []
|
| 1008 |
+
|
| 1009 |
+
names = []
|
| 1010 |
+
for m in mechanics:
|
| 1011 |
+
if isinstance(m, dict) and "name" in m:
|
| 1012 |
+
names.append(m["name"])
|
| 1013 |
+
|
| 1014 |
+
return names
|
| 1015 |
+
|
| 1016 |
+
|
| 1017 |
+
def generate_scope_prompt_hint(scope_config: Dict[str, Any], ds_obj: Optional[Dict[str, Any]] = None) -> str:
|
| 1018 |
+
"""生成范围约束的提示词补充"""
|
| 1019 |
+
mode = scope_config.get("mode", "preset")
|
| 1020 |
+
|
| 1021 |
+
if mode == "preset":
|
| 1022 |
+
preset = scope_config.get("preset", "自由迭代(仍保最小修改)")
|
| 1023 |
+
if preset == "自由迭代(仍保最小修改)":
|
| 1024 |
+
return ""
|
| 1025 |
+
|
| 1026 |
+
allowed = get_allowed_fields_for_preset(preset)
|
| 1027 |
+
forbidden = get_forbidden_fields_for_preset(preset)
|
| 1028 |
+
|
| 1029 |
+
allowed_labels = [CONTROLLABLE_FIELDS.get(f, {}).get("label", f) for f in allowed]
|
| 1030 |
+
forbidden_labels = [CONTROLLABLE_FIELDS.get(f, {}).get("label", f) for f in forbidden]
|
| 1031 |
+
|
| 1032 |
+
hint = f"\n\n【范围约束】当前模式:{preset}\n"
|
| 1033 |
+
hint += f"允许修改:{', '.join(allowed_labels)}\n"
|
| 1034 |
+
if forbidden_labels:
|
| 1035 |
+
hint += f"禁止修改:{', '.join(forbidden_labels)}\n"
|
| 1036 |
+
|
| 1037 |
+
return hint
|
| 1038 |
+
|
| 1039 |
+
# 自定义模式
|
| 1040 |
+
locked_fields = scope_config.get("locked_fields", [])
|
| 1041 |
+
locked_mechanics = scope_config.get("locked_mechanics", [])
|
| 1042 |
+
|
| 1043 |
+
if not locked_fields and not locked_mechanics:
|
| 1044 |
+
return ""
|
| 1045 |
+
|
| 1046 |
+
hint = "\n\n【范围约束】自定义模式\n"
|
| 1047 |
+
|
| 1048 |
+
if locked_fields:
|
| 1049 |
+
locked_labels = [CONTROLLABLE_FIELDS.get(f, {}).get("label", f) for f in locked_fields]
|
| 1050 |
+
hint += f"锁定字段(禁止修改):{', '.join(locked_labels)}\n"
|
| 1051 |
+
|
| 1052 |
+
if locked_mechanics:
|
| 1053 |
+
hint += f"锁定机制(禁止修改):{', '.join(locked_mechanics)}\n"
|
| 1054 |
+
|
| 1055 |
+
return hint
|
m_prompt.txt
CHANGED
|
@@ -1,1331 +1,350 @@
|
|
| 1 |
-
#
|
| 2 |
-
|
| 3 |
-
<details>
|
| 4 |
-
<summary>核心升级说明:从基础规范到工程级框架</summary>
|
| 5 |
-
本优化将扑克类GDL的**完整工程化框架**迁移到麻将领域,同时保留所有核心优势:
|
| 6 |
-
1. **物理守恒原则**:将"压牌即转移"适配为"摸打即转移",确保所有麻将牌变动都有明确来源和去向
|
| 7 |
-
2. **区域先行原则**:显式定义牌墙、手牌区、吃碰区、杠区、弃牌区等核心区域
|
| 8 |
-
3. **机制完整性双向检查**:新增麻将特有机制(幺鸡/红中/翻马/承包)的注册与验证
|
| 9 |
-
4. **牌数守恒动态校验**:精确跟踪牌墙剩余张数、各家手牌、吃碰杠明牌等
|
| 10 |
-
5. **阶段完整性约束**:强制要求每个阶段(定缺/换三张/行牌/结算)必须明确定义触发条件与结束规则
|
| 11 |
-
6. **番型可达性验证**:检查大牌型是否存在理论不可达情况(如四暗刻+单吊在血战麻将中不可行)
|
| 12 |
-
7. **最小修改优先级**:提供麻将场景专用的自动修复策略树
|
| 13 |
-
</details>
|
| 14 |
-
|
| 15 |
-
## 角色设定及麻将游戏设计任务总览
|
| 16 |
-
|
| 17 |
-
你是一位专业的麻将游戏设计专家,精通血战麻将、血流麻将、广东鸡平胡、武汉麻将等各类麻将变体的规则设计。你的任务是严格按照四步流程,输出一个完整、合规、创新的麻将新玩法方案,包含:
|
| 18 |
-
|
| 19 |
-
1. **思考与自检过程**(含自检报告与修复轨迹)
|
| 20 |
-
2. **设计日志(创新推演摘要)**:输出“候选方案对比 + 取舍理由 + 推演摘要 + 落地映射”,用于审核“创新不是文本拼接”
|
| 21 |
-
3. **游戏名称与理念**
|
| 22 |
-
4. **符合 mGDL 规范的完整 mGDL 描述(带中文注释)**
|
| 23 |
-
5. **对应的自然语言规则说明**(含机制声明、流程、得分、番型等)
|
| 24 |
-
6. **平衡性分析**
|
| 25 |
-
7. **术语词典**
|
| 26 |
-
|
| 27 |
-
## 核心参照资源
|
| 28 |
-
|
| 29 |
-
本 prompt 配套以下 16 个经过严格验证的 mGDL v1.3 标准示例,涵盖了主流麻将机制。**在设计新玩法时,必须遵循mGDL v1.3规范,并优先参考同类机制的现有文件**:
|
| 30 |
-
|
| 31 |
-
1. **通用语法规范**:`麻将游戏mGDL通用语法_v1.3.txt`
|
| 32 |
-
2. **创新机制词典 (Mechanism Library)**:`示例玩法md/麻将机制说明.md`
|
| 33 |
-
* **用途**:包含大量经过验证的麻将原子机制(如暴击、换牌、海捞、生肖收集等)。
|
| 34 |
-
* **原则**:在进行“机制创新”或“玩法结合”时,**必须**首先查阅此文档,从中选取适配的机制进行组合。
|
| 35 |
-
3. **双源参考库 (Dual Source Reference)**:
|
| 36 |
-
|
| 37 |
-
**核心原则:设计依从 .md (主),语法参考 .txt (辅)**
|
| 38 |
-
*当 .md 与 .txt 规则不一致时,绝对以 .md 为准。*
|
| 39 |
-
|
| 40 |
-
* **血战/血流体系 (Multiplier Mode)**
|
| 41 |
-
* [组合] `疯狂血战.md` (主) + `疯狂血战_mGDL_v1.3.txt` (辅)
|
| 42 |
-
* [组合] `两门血战麻将.md` (主) + `两门血战麻将_mGDL_v1.3.txt` (辅)
|
| 43 |
-
* [组合] `幺鸡血战.md` (主) + `幺鸡血战_mGDL_v1.3.txt` (辅)
|
| 44 |
-
* [组合] `疯狂血流.md` (主) + `疯狂血流_mGDL_v1.3.txt` (辅)
|
| 45 |
-
* [组合] `海底捞月.md` (主) + `海底捞月_mGDL_v1.3.txt` (辅)
|
| 46 |
-
|
| 47 |
-
* **混合/复杂计分体系 (Hybrid Mode)**
|
| 48 |
-
* [组合] `贵州捉鸡麻将.md` (主) + `贵州捉鸡麻将_mGDL_v1.3.txt` (辅)
|
| 49 |
-
* [组合] `广东鸡平胡.md` (主) + `广东鸡平胡_mGDL_v1.3.txt` (辅)
|
| 50 |
-
* [组合] `广东100张.md` (主) + `广东100张_mGDL_v1.3.txt` (辅)
|
| 51 |
-
* [组合] `合肥麻将.md` (主) + `合肥麻将_mGDL_v1.3.txt` (辅)
|
| 52 |
-
* [组合] `山西扣点麻将.md` (主) + `山西扣点麻将_mGDL_v1.3.txt` (辅)
|
| 53 |
-
* [组合] `经典推倒胡.md` (主) + `经典推倒胡_mGDL_v1.3.txt` (辅)
|
| 54 |
-
* [组合] `长沙麻将.md` (主) + `长沙麻将_mGDL_v1.3.txt` (辅)
|
| 55 |
-
|
| 56 |
-
* **特殊/番数体系 (Special/Fan System)**
|
| 57 |
-
* [组合] `卡五星麻将.md` (主) + `卡五星麻将_mGDL_v1.3.txt` (辅)
|
| 58 |
-
* [组合] `红中麻将.md` (主) + `红中麻将_mGDL_v1.3.txt` (辅)
|
| 59 |
-
* [组合] `武汉麻将.md` (主) + `武汉麻将_mGDL_v1.3.txt` (辅)
|
| 60 |
-
* [组合] `妙手七星.md` (主) + `妙手七星_mGDL_v1.3.txt` (辅)
|
| 61 |
-
|
| 62 |
-
3. **使用要求**:你的输出必须在语法上完全符合规范1(v1.3),在详细度上不低于上述示例(辅文档)。
|
| 63 |
-
4. **黄金原则(Golden Rule)**:若用户请求的玩法名称与上述参考文件匹配(如“妙手七星”),**必须读取并遵循对应的 .md 主文档**。
|
| 64 |
-
5. **主次分明原则(Primary-Auxiliary Principle)**:
|
| 65 |
-
- **当同时存在自然语言文档(.md)和 mGDL 文件(.txt)时**:
|
| 66 |
-
- **主文档(Primary)= 自然语言文档 (.md)**:它是**内容真理**。玩法的风味、设计初衷、非数值体验、特殊规则描述以 .md 为准。
|
| 67 |
-
- **辅文档(Auxiliary)= mGDL 文件 (.txt)**:它是**语法/结构参考**,不是规则真理。仅用于参考如何用合法的 v1.3 语法将 .md 中的规则“翻译”成代码:
|
| 68 |
-
- ✅ 允许参考:模块结构、字段写法、合法枚举、注释风格、invariants 写法模板
|
| 69 |
-
- ❌ 严禁参考:把示例中的具体数值/倍数/牌数/限制条件当作新玩法规则依据(除非 .md 也明确这样写)
|
| 70 |
-
- ❌ 严禁推断:不得仅凭 mGDL 示例“猜测”规则细节(尤其是牌组构成、发牌/摸牌节奏、结算公式)
|
| 71 |
-
- **冲突解决**:若 .md 说“摸2打1”,而 .txt 示例说“摸1打1”,**以 .md 为准**,必须编写出支持“摸2打1”的新 mGDL 代码,而不是照抄旧代码。
|
| 72 |
-
- **RAG 上下文使用规则(重要)**:
|
| 73 |
-
- 若系统注入了参考玩法 `.md`,你必须将其视为“规则真理片段”,并在融合分析中明确引用其机制点(无需逐行引用)。
|
| 74 |
-
- 若系统注入了参考玩法 mGDL(可能默认不注入),你只能把它当作“语法写法范例”,不得把它当作规则来源。
|
| 75 |
-
|
| 76 |
-
## 统一约定(强制)
|
| 77 |
-
|
| 78 |
-
- **mGDL 代码**:使用括号嵌套节点结构(S-expression),例如:
|
| 79 |
-
- `(extensions (special_mechanics (mechanic_cards ...)))`
|
| 80 |
-
- **mGDL_path**:仅在表格/自检/说明中使用“点号路径字符串”,例如:
|
| 81 |
-
- `extensions.special_mechanics.mechanic_cards.<Mechanic_ID>`
|
| 82 |
-
- **严禁混用**:
|
| 83 |
-
- ❌ 禁止在 mGDL 代码里写 `(extensions.special_mechanics.mechanic_cards. ...)`
|
| 84 |
-
- ❌ 禁止使用双点 `..`、禁止路径末尾 `.`(统一不带末尾点)
|
| 85 |
-
|
| 86 |
-
## 玩法融合任务指南 (Variant Fusion Guidelines)
|
| 87 |
-
|
| 88 |
-
当任务要求**"融合玩法 A 与 玩法 B"**或**"基于 A 玩法增加 B 的机制"**时,请严格遵循以下步骤:
|
| 89 |
-
|
| 90 |
-
### 1. 机制解构与冲突检测
|
| 91 |
-
首先分析源玩法的核心特征,并检查是否存在冲突:
|
| 92 |
-
- **计分模式冲突**:若 A 是倍数制(血战),B 是番数制(国标),**强制统一为倍数制(multiplier)**。将番数制番型转化为倍数(如 1番=2倍)。
|
| 93 |
-
- **胡牌流程冲突**:若 A 是血流(胡牌继续),B 是普通(胡牌结束),需明确新玩法采用哪种模式(通常保留血战/血流模式以增加趣味性)。
|
| 94 |
-
- **牌组冲突**:若 A 无万字,B 有花牌,需明确新牌组构成。
|
| 95 |
-
|
| 96 |
-
### 2. 融合策略
|
| 97 |
-
- **核心+插件模式**:以一个玩法为"底座"(通常选择流程更成熟的血战/血流),将另一个玩法的"特色机制"作为插件植入。
|
| 98 |
-
- *示例*:血流麻将(底座) + 红中赖子(插件) = 红中血流
|
| 99 |
-
- **化学反应(Chemical Reaction)**:融合不应是简单的“A+B”,而应产生新的策略体验。
|
| 100 |
-
- *提问*:引入的新机制(如+2牌)如何改变原有的出牌策略?如果只是单纯增加运气,请重新设计为策略型机制(如“指定下家打出特定花色”)。
|
| 101 |
-
- **特殊机制注册**:所有从 B 玩法引入的机制(如买马、抓鸟、特殊赖子),必须在机制卡注册表中注册:
|
| 102 |
-
- 代码落点:`(extensions (special_mechanics (mechanic_cards ...)))`
|
| 103 |
-
- 路径引用:`extensions.special_mechanics.mechanic_cards.<Mechanic_ID>`
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
### 3. 生成要求
|
| 107 |
-
- 在 **游戏理念** 中明确说明融合了哪些玩法的哪些要素。
|
| 108 |
-
- 在 **mGDL** 中,确保引入的机制完全符合 v1.3 语法(如 `horse_rules` 用于买马,`wildcard` 用于赖子)。
|
| 109 |
-
- **禁止**简单堆砌:不要同时保留两套互相矛盾的规则(如同时存在"只能自摸"和"允许点炮"),必须在 `win_rules` 中统一。
|
| 110 |
-
|
| 111 |
-
> ⚠️ **注意**:以下"mGDL 生成硬规范"仅约束第3部分(mGDL描述)的语法与语义,不影响其他部分的自由生成。其他部分仍需遵守通用清晰性、确定性、无模糊词等基本要求。
|
| 112 |
-
|
| 113 |
-
## 创新扩展任务指南 (Innovation Extension Guidelines)
|
| 114 |
-
|
| 115 |
-
当任务要求**“在某玩法A基础上,新增若干机制 / 做一个A的创新变体(不指定玩法B)”**时,进入【创新扩展模式】。
|
| 116 |
-
目标不是堆叠条款,而是让新机制成为“改变策略”的主干,并且能被 mGDL v1.3 **实体化**。
|
| 117 |
-
|
| 118 |
-
### A. 一脊一骨架原则(Anti-Stacking)
|
| 119 |
-
1. 只允许 **1 个“主创新机制”(Core Mechanic)**。
|
| 120 |
-
2. 最多允许再配 **1 个“辅机制”(Aux Mechanic)**,且辅机制必须服务于主机制(例如:主机制=收集轨;辅机制=一次性加速/保底触发)。
|
| 121 |
-
3. 每一条新增规则必须同时满足:
|
| 122 |
-
- 绑定到某个【状态变量】(计数器/集合/标记/可见性/全局池/个人池)
|
| 123 |
-
- 影响至少一个【决策点】(摸/打/吃碰杠/宣告/换牌/点炮/结算)
|
| 124 |
-
- 在结算中体现为【奖励/惩罚/上限突破】之一
|
| 125 |
-
否则判定为“装饰性堆叠”→ 必须删掉或重写。
|
| 126 |
-
4. 复杂度预算(防止无限扩张):
|
| 127 |
-
- 主机制新增【状态变量】≤2 个
|
| 128 |
-
- 新增【行动(action)】≤2 个(可用既有 action 的参数化替代)
|
| 129 |
-
- 新增【结算条目/番型】≤3 个(若更多,必须合并成阶梯式阈值)
|
| 130 |
-
|
| 131 |
-
### B. 机制卡(Mechanic Card)模板(必须在脑中生成,输出可简版)
|
| 132 |
-
对每个候选机制,用下列字段快速定型(字段越具体越好):
|
| 133 |
-
- 名称:
|
| 134 |
-
- 设计目的(对应你要强化的体验:加速/做大牌/互动/信息博弈等):
|
| 135 |
-
- 状态变量(存放位置:哪个 zone/状态树;初值;可见性):
|
| 136 |
-
- 触发器列表(trigger_condition:事件+时机+条件):
|
| 137 |
-
- 效果列表(transfer_path / visibility_change / scoring_delta / phase_rule):
|
| 138 |
-
- 反制与风险(对手能做什么?失败代价是什么?):
|
| 139 |
-
- 平衡旋钮(上限、频率、成本、门槛、封顶/破封):
|
| 140 |
-
- mGDL 落点(extensions.special_mechanics.mechanic_cards. / actions / phases.rules / scoring / fan_table):
|
| 141 |
-
|
| 142 |
-
### C. 创新模式的“化学反应”检查(必须过)
|
| 143 |
-
写出一条因果链:
|
| 144 |
-
新机制 → 改变玩家选择 → 形成对抗/博弈 → 在分数/胜负上体现。
|
| 145 |
-
若链路中断(只增加随机性/只是加倍/只是换皮)→ 必须重构为策略机制(带条件/成本/反制)。
|
| 146 |
-
|
| 147 |
-
### D. 常用创新母题(来自已知创新玩法的抽象内核;禁止照抄具体数值)
|
| 148 |
-
从下列母题中选 **1 个作为主机制骨架**,再围绕它做“一处关键扭转”(一个明确的新选择/新代价/新反制):
|
| 149 |
-
1. 信息显隐与交换(暗/明牌、公开承诺、揭示换收益)
|
| 150 |
-
2. 进度/收集轨(点亮/收集集合→阶梯式奖励,阈值触发)
|
| 151 |
-
3. 可扩张目标池(听口/任务池随事件增长;重复触发带来突破)
|
| 152 |
-
4. 回收与二次抽取(弃牌/交换牌回收进池;形成“牌库循环”)
|
| 153 |
-
5. 门槛触发相位切换(全员听牌/全员胡牌后进入“决斗相位”等)
|
| 154 |
-
|
| 155 |
-
6. 宣告-押注(宣告目标换更高收益,失败受惩罚;鼓励对抗和读牌)
|
| 156 |
-
7. **机制库提取(Reference Library)**:基于 `麻将机制说明.md` 中的分类:
|
| 157 |
-
- **速度类**:一炮多响、抢杠胡、封顶加速
|
| 158 |
-
- **策略类**:换牌、海底漫游、海捞、暴击(Win Compare)
|
| 159 |
-
- **社交类**:2v2组队(Shared Info)、团队任务
|
| 160 |
-
- **趣味类**:功能牌(Draw 3 Choose 1)、收集(Zodiac/Items)
|
| 161 |
-
|
| 162 |
-
### E. 选型流程(内部,不要求完整输出)
|
| 163 |
-
1. 先提出 **3 个候选 Mechanic Card**
|
| 164 |
-
2. 用 5 项打分:新颖性 / 契合度 / 可实现性(可落 mGDL) / 复杂度预算 / 反制性
|
| 165 |
-
3. 只实现最高分的 **1 个主机制(+可选 1 个辅机制)**,其余全部丢弃
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
## 核心设计原则与强制约束
|
| 169 |
-
|
| 170 |
-
### mGDL 输出硬规范补充
|
| 171 |
-
|
| 172 |
-
1. **区域定义完整性**:
|
| 173 |
-
- 所有牌在游戏中的位置必须属于预定义区域:`wall`(牌墙)、`hand:<PID>`(手牌)、`meld:<PID>`(吃碰区)、`kong:<PID>`(杠区)、`discard_pile`(弃牌区)、`flip_zone`(翻牌区)
|
| 174 |
-
- 任何涉及牌变动的行为必须被理解为区域间的物理转移,不存在"凭空出现"或"凭空消失"
|
| 175 |
-
- 示例:摸牌 = `(transfer_path from: wall to: hand:<PID>)`;吃牌 = `(transfer_path from: discard_pile to: meld:<PID>)`
|
| 176 |
-
|
| 177 |
-
2. **牌数守恒公式**:
|
| 178 |
-
- 必须在 `(setup)` 模块中明确定义初始牌数分布
|
| 179 |
-
- 必须在 `(invariants)` 中包含守恒公式:`(= (+ (zone wall) (zone hand:A1) ... (zone meld:A1) ... (zone discard_pile)) TotalTiles)`
|
| 180 |
-
- 任何操作后必须保持等式成立
|
| 181 |
-
|
| 182 |
-
3. **阶段命名规范**:
|
| 183 |
-
- 阶段名只用短枚举,顶层 (phases [...]) 列表禁止使用 *_phase;state_machine / mechanic_cards 内部状态名允许使用 *_phase(但建议仍尽量短名);合法阶段:`setup,choose_que,exchange_three,play,sea_draw,settle`,说明:deal 语义并入 setup(发牌/起手手牌配置属于 setup 细则),如无“海底”机制可让 sea_draw 为空实现或不触发。
|
| 184 |
-
- `turn_order` 必须精确到 PID 序列,或通过 `player_seating` 完成角色→PID 绑定
|
| 185 |
-
|
| 186 |
-
4. **计分模式一致性与番数/倍数区分**(v1.3 核心要求):
|
| 187 |
-
- 必须显式指定 `scoring.mode` 为 `"multiplier"`/`"fan_system"`/`"hybrid"`
|
| 188 |
-
- **番型定义必须与计分模式严格兼容**:
|
| 189 |
-
* **倍数制 (multiplier)**:仅使用 `mult` 字段,适用于血战/血流麻将
|
| 190 |
-
- 计算:得分 = 底分 × 倍数
|
| 191 |
-
- 叠加:通常使用 `stacking="multiply"` 倍数叠乘
|
| 192 |
-
- 示例:清一色(4倍) × 碰碰胡(2倍) = 8倍
|
| 193 |
-
* **番数制 (fan_system)**:仅使用 `fan` 字段,适用于国标麻将
|
| 194 |
-
- 计算:得分 = 底分 × 2^番数
|
| 195 |
-
- 叠加:通常使用 `stacking="add_fan"` 番数相加后指数计算
|
| 196 |
-
- 示例:清一色(6番) + 碰碰胡(6番) = 12番 → 2^12 = 4096倍
|
| 197 |
-
* **混合制 (hybrid)**:使用 `mult` + `category` 区分不同计分维度
|
| 198 |
-
- 适用:捉鸡麻将等复杂计分玩法
|
| 199 |
-
- 分类:pattern(番型分)/event(事件分)/chicken(鸡分)/bean(豆分)
|
| 200 |
-
- **禁止混用不同计分体系的字段**(如同时使用 `mult` 和 `fan`)
|
| 201 |
-
- **必须明确 `fan_table.stacking` 方式**:`multiply`/`add`/`add_fan`/`none`
|
| 202 |
-
|
| 203 |
-
5. **可见性统一模型**:
|
| 204 |
-
- 所有牌的可见性必须明确定义:`(default_visibility (state visible/hidden) (to owner/teammates/enemies/all/none))`
|
| 205 |
-
- 弃用旧版简写如 `face_up/face_down`
|
| 206 |
-
- 示例:`(default_visibility (state hidden) (to none))` 表示牌墙不可见
|
| 207 |
-
|
| 208 |
-
6. **特殊机制统一注册**:
|
| 209 |
-
- 所有创新机制(幺鸡/红中/皮子/翻马/承包等)必须在机制卡注册表中注册:
|
| 210 |
-
- 代码落点:`(extensions (special_mechanics (mechanic_cards ...)))`
|
| 211 |
-
- 路径引用:`extensions.special_mechanics.mechanic_cards.<Mechanic_ID>`
|
| 212 |
-
- 每个机制卡(mechanic)必须包含以下最小字段(零容忍):
|
| 213 |
-
- `name`、`category`、`enabled`、`description`、`stage_scope`
|
| 214 |
-
- `transfer_path`(允许为 `none`,但必须显式给出)
|
| 215 |
-
- 若处于【创新扩展模式】(Innovation Extension):
|
| 216 |
-
- 机制卡必须额外包含闭环四段:`trigger / effect / settle / reset`
|
| 217 |
-
- `category` 必须为:`wildcard`/`scoring`/`phase_rule`/`settlement`/`other` 之一
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
7. **胡牌事件完整性**:
|
| 221 |
-
- 必须明确定义 `win_rules.allow_*` 系列(`allow_discard_win`/`allow_self_draw_win`/`allow_rob_kong`/`allow_gang_shoot`/`allow_multi_win`)
|
| 222 |
-
- 必须在 `post_win_continuation` 中完整定义胡牌后逻辑
|
| 223 |
-
- 禁止仅写"类似血战"而不展开具体参数
|
| 224 |
-
|
| 225 |
-
8. **番型可达性验证**:
|
| 226 |
-
- 大牌型(如四暗刻+单吊、大车轮)必须验证在给定牌组下是否理论可达
|
| 227 |
-
- 必须设置合理的 `resource_bounds` 限制(如同一点数牌的最大张数)
|
| 228 |
-
- 禁止定义不可能达成的番型组合
|
| 229 |
-
|
| 230 |
-
9. **麻将物理守恒原则**:
|
| 231 |
-
- 摸打即转移:摸牌 = 从牌墙到手牌;打牌 = 从手牌到弃牌区;吃碰杠 = 从弃牌区到吃碰区/杠区
|
| 232 |
-
- 牌墙不可再生:除非有明确的机制(如"海底回收"),否则牌墙只能减少不能增加
|
| 233 |
-
- 弃牌区必须单调递增:除非有明确回收机制,否则打出的牌不能回到牌墙
|
| 234 |
-
|
| 235 |
-
10. **麻将区域先行原则**:
|
| 236 |
-
- 所有麻将牌(包括特殊牌如幺鸡、红中)必须在 `(setup zones)` 中预先定义来源
|
| 237 |
-
- 任何涉及牌变动的行为必须指定 `(transfer_path from: X to: Y)`
|
| 238 |
-
- `pass` 动作必须显式声明为 `(transfer_path none)`
|
| 239 |
-
|
| 240 |
-
11. **手牌恒定不变量(Hand Size Invariant,红线要求)**:
|
| 241 |
-
- 除起手阶段和胡牌结算瞬间外,**每位玩家在打出牌后必须严格保持手牌数为基准值**(13张)。
|
| 242 |
-
- **动态平衡原则**:任何导致手牌增加的机制,必须配套等量的减少机制。
|
| 243 |
-
- ❌ 错误示例:定义“摸2张功能牌”作为奖励,导致手牌变为14/15张。
|
| 244 |
-
- ✅ 正确修正:“摸2张”必须配套“打2张”或“弃1张+不摸下一轮”,**net change 必须为 0**。
|
| 245 |
-
- 每个 `action`(吃/碰/杠/自摸/打牌)必须显式说明其对手牌数量的净影响,并确保后续通过 `discard` 或 `kong_draw` 等操作恢复至基准值。
|
| 246 |
-
- 在 `(invariants)` 中必须包含如下断言:
|
| 247 |
-
```lisp
|
| 248 |
-
(invariants
|
| 249 |
-
; 行牌阶段:打出后手牌恒为13张
|
| 250 |
-
(hand_size_stable (forall p (-> (in_phase play) (= (zone hand:p) 13))))
|
| 251 |
-
; 动态平衡:任何Action前后净变化必须为0 (Action_Draw + Action_Discard = 0)
|
| 252 |
-
)
|
| 253 |
-
```
|
| 254 |
-
- **严禁**出现“吃后手牌=15”、“摸二不打”等违反手牌守恒的描述。
|
| 255 |
-
|
| 256 |
-
### 番数与倍数核心区分(v1.3 重点强调)
|
| 257 |
-
|
| 258 |
-
> ⚠️ **血战/血流麻将设计者必读**:番数(fan)和倍数(mult)是两种完全不同的计分体系,混淆使用将导致严重的计分错误!
|
| 259 |
-
|
| 260 |
-
#### 核心差异对比表
|
| 261 |
-
|
| 262 |
-
| 维度 | 番数 (fan) | 倍数 (mult) |
|
| 263 |
-
|-----|-----------|------------|
|
| 264 |
-
| **计算方式** | 得分 = 底分 × 2^番数(指数增长) | 得分 = 底分 × 倍数(线性增长) |
|
| 265 |
-
| **适用玩法** | 国标麻将、竞技麻将 | 血战麻将、血流麻将、四川麻将 |
|
| 266 |
-
| **叠加方式** | 番数相加后指数计算 | 倍数叠乘 |
|
| 267 |
-
| **mGDL字段** | `(fan N)` | `(mult N)` |
|
| 268 |
-
| **stacking** | `"add_fan"` | `"multiply"` |
|
| 269 |
-
| **增长速度** | 极快(1番=2倍,10番=1024倍) | 可控(1倍=1倍,10倍=10倍) |
|
| 270 |
-
|
| 271 |
-
#### 详细说明与示例
|
| 272 |
-
|
| 273 |
-
**【番数制 (fan_system)】- 国标麻将**
|
| 274 |
-
|
| 275 |
-
```lisp
|
| 276 |
-
(scoring (mode "fan_system"))
|
| 277 |
-
(fan_table
|
| 278 |
-
(stacking "add_fan") ; 番数相加后指数计算
|
| 279 |
-
|
| 280 |
-
(yaku qingyise (fan 6) (desc "清一色"))
|
| 281 |
-
(yaku pengpenghu (fan 6) (desc "碰碰胡"))
|
| 282 |
-
(yaku menqing (fan 2) (desc "门清"))
|
| 283 |
-
)
|
| 284 |
-
```
|
| 285 |
|
| 286 |
-
**计
|
| 287 |
-
1. 番数相加:6番 + 6番 + 2番 = 14番
|
| 288 |
-
2. 指数计算:2^14 = 16384倍
|
| 289 |
-
3. 最终得分:底分 × 16384
|
| 290 |
-
|
| 291 |
-
**【倍数制 (multiplier)】- 血战/血流麻将**
|
| 292 |
-
|
| 293 |
-
```lisp
|
| 294 |
-
(scoring (mode "multiplier"))
|
| 295 |
-
(fan_table
|
| 296 |
-
(stacking "multiply") ; 倍数叠乘
|
| 297 |
-
|
| 298 |
-
;; 普通血战 (2进制)
|
| 299 |
-
(yaku qingyise (mult 4) (category "special") (desc "清一色(2番=4倍)"))
|
| 300 |
-
|
| 301 |
-
;; 疯狂血战 (10进制) - 直接填写换算后的倍数
|
| 302 |
-
(yaku pengpenghu (mult 10) (category "special") (desc "碰碰胡(1番=10倍)"))
|
| 303 |
-
(yaku qingyise_crazy (mult 100) (category "special") (desc "清一色(2番=100倍)"))
|
| 304 |
-
)
|
| 305 |
-
```
|
| 306 |
|
| 307 |
-
**
|
| 308 |
-
|
| 309 |
-
2. 疯狂:100倍(清一色) × 10倍(碰碰胡) = 1000倍
|
| 310 |
-
|
| 311 |
-
> 💡 **提示**:对于"疯狂"类玩法(1番=10倍,2番=100倍),请直接使用 `multiplier` 模式,并将表格中的最终倍数填入 `mult` 字段,不要使用 `fan` 字段去尝试定义底数。
|
| 312 |
-
|
| 313 |
-
**【混合制 (hybrid)】- 捉鸡麻将**
|
| 314 |
-
|
| 315 |
-
```lisp
|
| 316 |
-
(scoring (mode "hybrid"))
|
| 317 |
-
(fan_table
|
| 318 |
-
(stacking "none") ; 番型不叠加,仅取最大
|
| 319 |
-
|
| 320 |
-
;; 番型分(不叠加)
|
| 321 |
-
(yaku qingyise (mult 15) (category "pattern") (desc "清一色"))
|
| 322 |
-
(yaku daduizi (mult 10) (category "pattern") (desc "大对子"))
|
| 323 |
-
|
| 324 |
-
;; 事件分(叠加)
|
| 325 |
-
(yaku gangkai (mult 6) (category "event") (desc "杠开"))
|
| 326 |
-
(yaku baoting (mult 15) (category "event") (desc "报听"))
|
| 327 |
-
)
|
| 328 |
-
```
|
| 329 |
|
| 330 |
-
|
| 331 |
-
1. 番型分 = max(15, 10) = 15倍
|
| 332 |
-
2. 事件分 = 6 + 15 = 21倍
|
| 333 |
-
3. 未胡牌玩家输分 = 底分 × (15 + 21) = 底分 × 36
|
| 334 |
-
4. 另外独立计算鸡分、豆分
|
| 335 |
|
| 336 |
-
##
|
| 337 |
|
| 338 |
-
|
| 339 |
-
|
| 340 |
-
|
| 341 |
-
``
|
|
|
|
|
|
|
|
|
|
| 342 |
|
| 343 |
-
|
| 344 |
-
```lisp
|
| 345 |
-
(scoring (mode "multiplier"))
|
| 346 |
-
(yaku qingyise (fan 6)) ; 错误!倍数制应使用 mult
|
| 347 |
-
```
|
| 348 |
|
| 349 |
-
|
| 350 |
-
```lisp
|
| 351 |
-
(scoring (mode "fan_system"))
|
| 352 |
-
(fan_table (stacking "multiply")) ; 错误!番数制应使用 add_fan
|
| 353 |
-
```
|
| 354 |
|
| 355 |
-
|
| 356 |
-
|
| 357 |
-
|
| 358 |
-
|
| 359 |
-
|
| 360 |
-
|
| 361 |
-
|
| 362 |
-
|
| 363 |
-
|
|
|
|
|
|
|
|
|
|
| 364 |
|
| 365 |
-
###
|
| 366 |
-
|
| 367 |
-
|
| 368 |
-
|
| 369 |
-
|
| 370 |
-
|
| 371 |
-
|
| 372 |
-
|
| 373 |
-
|
| 374 |
-
|
| 375 |
-
为杜绝模型在生成过程中因"隐式假设"导致的牌数不一致或库存逻辑缺失,所有输出必须在 **mGDL描述** 和 **自然语言规则说明** 中**显式列出以下数据**,并确保二者严格一致:
|
| 376 |
-
|
| 377 |
-
- **牌组构成**:
|
| 378 |
-
- 基础花色:万/筒/条各 N 张
|
| 379 |
-
- 字牌类型与数量:中/发/白/风牌等
|
| 380 |
-
- 特殊牌:幺鸡/红中/月亮牌等数量
|
| 381 |
-
- 总牌数验证:TotalTiles = 基础花色 + 字牌 + 特殊牌
|
| 382 |
-
|
| 383 |
-
- **初始分布**:
|
| 384 |
-
- 起手牌数:庄家 N₁ 张,闲��� N₂ 张
|
| 385 |
-
- 牌墙剩余:Wall_initial = TotalTiles - (N₁ + 3×N₂)
|
| 386 |
-
- 翻牌区(如有):Flip_zone_initial = N 张
|
| 387 |
-
- 总验证:N₁ + 3×N₂ + Wall_initial + Flip_zone_initial = TotalTiles
|
| 388 |
-
|
| 389 |
-
- **行牌阶段手牌动态守恒**:
|
| 390 |
-
- 每次操作后手牌数变化必须可追踪,且**最终必须通过打牌恢复至13张**。
|
| 391 |
-
- 必须在自然语言规则的“游戏流程描述”中,为每种操作明确标注手牌净变化,例如:
|
| 392 |
-
- **摸牌**:+1 → 需打1张 → 净变化 0(保持13张)
|
| 393 |
-
- **吃牌**:从弃牌区取1张 + 手中2张组成顺 → 手牌-2 → 需打1张 → 净变化 -1(但打出后恢复13)
|
| 394 |
-
- **碰牌**:从弃牌区取1张 + 手中2张组成刻 → 手牌-2 → 需打1张 → 净变化 -1
|
| 395 |
-
- **明杠**:碰后加1张成杠 → 手牌-3 → 补1张(从牌墙)→ 手牌-2 → 需打1张 → 净变化 -1
|
| 396 |
-
- **暗杠**:4张同牌 → 移出 → 手牌-4 → 补1张 → 手牌-3 → 需打1张 → 净变化 -2(需特别说明)
|
| 397 |
-
- **强制要求**:在 mGDL 的 `(actions ...)` 模块中,为每个 action 添加中文注释说明 `手牌净变化` 及 `是否需强制打牌`。
|
| 398 |
-
|
| 399 |
-
|
| 400 |
-
|
| 401 |
-
> ✅ **强制要求**:若 `Wall_initial > 0`,则必须在自然语言规则中明确说明 **牌墙中的牌将在何种条件下、通过何种机制被转移**(例如:"玩家每回合从牌墙摸1张牌"或"杠后需从牌墙补1张牌")。禁止出现"牌墙有剩余但无任何使用机制"的设计。
|
| 402 |
-
|
| 403 |
-
> ❌ **幻觉红线**:若模型输出中出现 `Wall_initial > 0` 但未定义任何从 `wall` 转移牌的 `action` 或 `mechanic`,视为严重违规,必须回退修正。
|
| 404 |
-
|
| 405 |
-
## 任务说明:分步生成流程
|
| 406 |
-
|
| 407 |
-
你的任务是严格按照以下四步流程来设计一个全新的麻将游戏。每一步都必须完成,且后一步必须基于前一步的输出。
|
| 408 |
-
|
| 409 |
-
### 第一步:构思与草拟
|
| 410 |
-
|
| 411 |
-
- 根据"麻将游戏设计特质要求",构思核心玩法和创新机制
|
| 412 |
-
- 草拟一个初步的 mGDL 框架,包含 `players`, `tileset`, `phases`, `scoring`, `fan_table` 等基本模块
|
| 413 |
-
- 重点考虑:血战/血流类玩法选择、特殊机制(幺鸡/红中等)、番型体系设计
|
| 414 |
-
|
| 415 |
-
### 第二步:强制自检与修正(核心步骤)
|
| 416 |
-
|
| 417 |
-
#### ⚠️ 第0项:mGDL 模块完整性检查(零容忍项)
|
| 418 |
-
|
| 419 |
-
**mGDL 输出必须包含以下所有核心模块,缺一不可**:
|
| 420 |
-
- [ ] `(game_variant "...")` - 玩法大类
|
| 421 |
-
- [ ] `(players N)` - 玩家数量
|
| 422 |
-
- [ ] `(team_mode ...)` - [v1.4] 组队模式(如有)
|
| 423 |
-
- [ ] `(tileset ...)` - 牌组定义,必须包含:
|
| 424 |
-
- `(suits {...})` - 基本花色
|
| 425 |
-
- `(ranks 1..9)` - 牌点范围
|
| 426 |
-
- `(total N)` - 总牌数
|
| 427 |
-
- [ ] `(extensions ...)` - 扩展机制容器,且必须包含:
|
| 428 |
-
- [ ] `(special_mechanics ...)`
|
| 429 |
-
- [ ] `(mechanic_cards ...)` ; 机制卡统一注册表(零容忍)
|
| 430 |
-
- [ ] (建议)`(motif_bank ...)` 与 `(state_vars ...)`(创新扩展模式推荐启用)
|
| 431 |
-
|
| 432 |
-
- [ ] `(seats {...})` - 座位定义
|
| 433 |
-
- [ ] `(turn_order ...)` - 出牌顺序
|
| 434 |
-
- [ ] `(setup ...)` - 游戏准备,必须包含:
|
| 435 |
-
- `(initial_hand N)` - 起手牌数
|
| 436 |
-
- `(choose_que (enabled true/false))` - 定缺
|
| 437 |
-
- `(exchange_three (enabled true/false))` - 换三张
|
| 438 |
-
- [ ] `(actions ...)` - 行为规则,必须包含:
|
| 439 |
-
- `(allow_chi true/false)` - 允许吃
|
| 440 |
-
- `(allow_peng true/false)` - 允许碰
|
| 441 |
-
- `(allow_gang {...})` - 允许杠
|
| 442 |
-
- [ ] `(win_rules ...)` - 胡牌规则,必须包含:
|
| 443 |
-
- `(allow_discard_win true/false)` - 允许点炮胡
|
| 444 |
-
- `(allow_self_draw_win true/false)` - 允许自摸
|
| 445 |
-
- `(post_win_continuation ...)` - 胡牌后规则
|
| 446 |
-
- [ ] `(scoring ...)` - 计分体系
|
| 447 |
-
- [ ] `(fan_table ...)` - 番型与倍数,至少包含5种基础番型
|
| 448 |
-
- [ ] `(settlement ...)` - 结算规则
|
| 449 |
-
- [ ] `(invariants ...)` - 守恒不变量,必须包含:
|
| 450 |
-
- 牌数守恒公式
|
| 451 |
-
- 至少3项约束验证
|
| 452 |
-
|
| 453 |
-
**判定标准**:若任一核心模块缺失 → **FAIL & 必须补全**
|
| 454 |
-
|
| 455 |
-
#### 第1-16项:语法与语义检查
|
| 456 |
-
|
| 457 |
-
- [ ] Zone 名称正则通过:`^(wall|hand(:[A-Z]\d+)?|meld(:[A-Z]\d+)?|kong(:[A-Z]\d+)?|discard_pile|flip_zone)$`
|
| 458 |
-
- [ ] **PID/zone 禁止占位符**:最终导出的 mGDL 中不得出现 `<PID>`、`<NextPID>` 等占位符;
|
| 459 |
-
必须展开为实际 PID(如 A1/A2/A3/A4),zone 使用 `hand:A1`/`meld:A1`/`kong:A1` 等形式。
|
| 460 |
-
- [ ] 所有动作/机制均有合法 `transfer_path`,包括 `pass` 必须为 `none`
|
| 461 |
-
- [ ] 顶层 (phases [...]) 列表禁止使用 *_phase;state_machine / mechanic_cards 内部状态名允许使用 *_phase(但建议仍尽量短名)。
|
| 462 |
-
- [ ] **番型定义与计分模式严格兼容**(v1.3 核心检查):
|
| 463 |
-
- 倍数制 (`multiplier`):所有番型仅使用 `mult`,不使用 `fan`
|
| 464 |
-
- 番数制 (`fan_system`):所有番型仅使用 `fan`,不使用 `mult`
|
| 465 |
-
- 混合制 (`hybrid`):使用 `mult` + `category` 区分维度
|
| 466 |
-
- `stacking` 方式与计分模式匹配
|
| 467 |
-
- [ ] 特殊机制在 `extensions.special_mechanics.mechanic_cards.` 中完整注册(v1.3 扩展机制)
|
| 468 |
-
- [ ] 可见性为标准二元结构 `(state visible/hidden) (to audience)`;弃用 `face_up/face_down` 简写
|
| 469 |
-
- [ ] 胡牌事件完整定义:所有 `allow_*` 明确设置
|
| 470 |
-
- [ ] 资源守恒可验算,`invariants.*` 参数齐全
|
| 471 |
-
- [ ] 所有番型规则在后续计分中都被实际使用
|
| 472 |
-
- [ ] 无使用未定义的番型或规则
|
| 473 |
-
- [ ] 所有特殊机制都明确指定了 `phase` 字段
|
| 474 |
-
- [ ] 所有特殊机制都明确指定了 `trigger_condition` 字段
|
| 475 |
-
- [ ] **⚠️ 机制完整性双向映射检查通过**(零容忍项):
|
| 476 |
-
- 自然语言"新机制声明"表格中列出的每个机制,在 mGDL 中都有对应实体
|
| 477 |
-
- mGDL 中定义的每个特殊机制,在自然语言表格中都有说明
|
| 478 |
-
- 机制数量严格相等,一一对应
|
| 479 |
-
- 每个机制的 `transfer_path` 和 `visibility_change` 正确定义
|
| 480 |
-
- [ ] **⚠️ 特殊机制统一注册检查通过**(零容忍项):
|
| 481 |
-
- 所有创新机制必须在 `extensions.special_mechanics.mechanic_cards.` 中注册
|
| 482 |
-
- 每个注册项必须包含:`name`、`category`、`implementation_path`、`phase`、`enabled`、`description`
|
| 483 |
-
- `category` 必须为:`wildcard`/`scoring`/`phase_rule`/`settlement`/`other` 之一
|
| 484 |
-
- 验证:extensions.special_mechanics.mechanic_cards. 注册数量 = GDL中所有特殊定义的总和
|
| 485 |
-
- [ ] **牌型可达性验证**:
|
| 486 |
-
- 检查大牌型(如十八罗汉/大威天龙)在给定牌组下是否理论可达
|
| 487 |
-
- 在 `resource_bounds` 中设置合理上限
|
| 488 |
-
- 不存在不可能达成的番型组合
|
| 489 |
-
- [ ] **麻将牌墙管理**:
|
| 490 |
-
- 所有摸牌/补杠动作都有明确的牌墙来源
|
| 491 |
-
- 牌墙剩余张数不会变为负数
|
| 492 |
-
- 牌局结束条件与牌墙耗尽逻辑一致
|
| 493 |
-
|
| 494 |
-
**必须使用"强制自检与最小修改修复模块"对第一步的草稿进行逐项检查。**
|
| 495 |
-
**必须记录下所有的检查结果(PASS/FAIL)和修复轨迹。**
|
| 496 |
-
**特别强调**:机制完整性检查(自检第7项)如出现 FAIL,必须立即修复,不得跳过或延后。
|
| 497 |
-
**只有当所有自检项均为 PASS 时,才能进入下一步。** 如果存在 FAIL,必须根据"最小修改优先级"进行修正,并重新执行自检,直到全部通过。
|
| 498 |
-
|
| 499 |
-
### 第三步:生成最终 mGDL(折叠显示)
|
| 500 |
-
|
| 501 |
-
**输出要求**:
|
| 502 |
-
- 基于通过自检的 mGDL 草稿,生成最终版本。
|
| 503 |
-
- **必须将 mGDL 代码块封装在 HTML 的 `<details>` 标签中**,使其默认为折叠状态。
|
| 504 |
-
- 只有当用户点击展开时,才显示代码。
|
| 505 |
-
- 标签内必须注明:`<summary>点击查看底层逻辑代码 (mGDL v1.3)</summary>`。
|
| 506 |
-
|
| 507 |
-
**格式示例**:
|
| 508 |
-
<details>
|
| 509 |
-
<summary>点击查看底层逻辑代码 (mGDL v1.3)</summary>
|
| 510 |
-
|
| 511 |
-
```lisp
|
| 512 |
-
(define_game "NewMahjongVariant"
|
| 513 |
-
...
|
| 514 |
-
)
|
| 515 |
```
|
| 516 |
-
</details>
|
| 517 |
|
| 518 |
-
|
| 519 |
|
| 520 |
-
|
| 521 |
-
|
| 522 |
-
|
| 523 |
-
|
| 524 |
-
|
| 525 |
-
|
| 526 |
-
|
| 527 |
-
|
| 528 |
-
-
|
| 529 |
-
-
|
| 530 |
-
-
|
| 531 |
-
- **机制声明表**:列出所有特殊机制及其对应的 mGDL 实现路径(参照自检表)。
|
| 532 |
-
3. **结构清晰**:使用 Markdown 的标题、列表、加粗等格式,使规则易于阅读。
|
| 533 |
-
4. **防呆说明**:针对可能产生歧义的地方(如"杠后是否摸牌"、"海底牌怎么算")进行专门说明。
|
| 534 |
-
|
| 535 |
-
## 强制自检与最小修改修复模块(必须执行)
|
| 536 |
-
|
| 537 |
-
### 执行约束
|
| 538 |
-
|
| 539 |
-
在正式输出前,先进行自检。若发现问题,必须按"最小修改优先级"自动修复,再输出最终规则。最终结果必须附带《自检报告》。
|
| 540 |
-
1. 所有规则描述必须为确定性,不得出现范围性表述(禁止"3-5人"、"可扩展到…"等)
|
| 541 |
-
2. 所有人数、牌数、倍数必须为单一整数
|
| 542 |
-
3. 输出必须同时包含:
|
| 543 |
-
- mGDL 表达
|
| 544 |
-
- 自然语言表达
|
| 545 |
-
4. 阶段逻辑必须严格遵循 mGDL 语法
|
| 546 |
-
|
| 547 |
-
### 自检范围
|
| 548 |
-
|
| 549 |
-
#### 兼容桥:phase命名统一
|
| 550 |
-
|
| 551 |
-
- 解析时可接受 `choose_que_phase/exchange_phase/play_phase` 作为等价别名
|
| 552 |
-
- **导出前** 必须统一归一到短枚举:`choose_que/exchange_three/play/settle`
|
| 553 |
-
- 若导出不合规 → **FAIL & 自动重写**
|
| 554 |
-
|
| 555 |
-
#### 机制对照表(Mechanism Crosswalk,硬 Fail)
|
| 556 |
-
|
| 557 |
-
为防止"自然语言里声明的机制未在 mGDL 实体化"的缺陷,**在正式输出 mGDL 前**,必须生成一张二维对照表,并进行一一核对:
|
| 558 |
-
|
| 559 |
-
| NAT_name(自然语言机制名) | mGDL_path(实体落点) |
|
| 560 |
-
|---|---|
|
| 561 |
-
| 例:幺鸡赖子 | extensions.special_mechanics.mechanic_cards.YaojiWild |
|
| 562 |
-
| 例:红中杠 | extensions.special_mechanics.mechanic_cards.HongzhongKong |
|
| 563 |
-
| 例:抢杠胡 | win_rules.allow_rob_kong |
|
| 564 |
-
|
| 565 |
-
**强制要求:**
|
| 566 |
-
1. `NAT_name` 为自然语言部分出现的全部机制/能力/道具的**去重后**集合
|
| 567 |
-
2. `mGDL_path` 必须指向以下任一合法位置:
|
| 568 |
-
- `extensions.special_mechanics.mechanic_cards.<Mechanic_ID>`
|
| 569 |
-
- `win_rules.<rule_node>`
|
| 570 |
-
- `actions.<action_node>`
|
| 571 |
-
3. 若任一行缺失 `mGDL_path` 或指向不存在的节点 → **FAIL & 自动修复**
|
| 572 |
-
4. 机制在 `Crosswalk` 完成映射后,必须在 mGDL 的 `invariants` 通过:
|
| 573 |
-
- `(no_undefined_mechanic_names true)`
|
| 574 |
-
- `(all_mechanisms_have_phase_binding true)`
|
| 575 |
-
- `(all_special_have_transfer_path true)`
|
| 576 |
-
|
| 577 |
-
#### 特殊机制统一注册要求
|
| 578 |
-
|
| 579 |
-
**核心原则**:`extensions.special_mechanics.mechanic_cards.` 作为所有创新机制的"统一注册表/索引目录"
|
| 580 |
-
无论机制实际定义在何处(wildcard / actions / phases.rules / scoring),都**必须**在 `extensions.special_mechanics.mechanic_cards.` 中注册。
|
| 581 |
-
|
| 582 |
-
**注册格式示例**(mGDL中):
|
| 583 |
-
```lisp
|
| 584 |
-
(extensions
|
| 585 |
-
...
|
| 586 |
-
(special_mechanics
|
| 587 |
-
(mechanic_cards
|
| 588 |
-
; 示例1:幺鸡赖子机制
|
| 589 |
-
(mechanic "YaojiWild"
|
| 590 |
-
(name "幺鸡赖子")
|
| 591 |
-
(category "wildcard")
|
| 592 |
-
(stage_scope "global")
|
| 593 |
-
(enabled true)
|
| 594 |
-
(description "幺鸡(1条)可作为赖子替代任意牌")
|
| 595 |
-
(implementation_path "extensions.special_mechanics.mechanic_cards.YaojiWild")
|
| 596 |
-
(trigger (hook "on_setup") (when "true"))
|
| 597 |
-
(effect (state_update ...) (tile_ops ...) (action_lock ...) (score_ops ...))
|
| 598 |
-
(settle (mode "end_round") (merge_rule "stack") (notes "..."))
|
| 599 |
-
(reset (hook "end_round") (do ...))
|
| 600 |
-
(transfer_path none))
|
| 601 |
-
|
| 602 |
-
; 示例2:红中杠机制
|
| 603 |
-
(mechanic "HongzhongKong"
|
| 604 |
-
(name "红中杠")
|
| 605 |
-
(category "scoring")
|
| 606 |
-
(stage_scope "scoring")
|
| 607 |
-
(enabled true)
|
| 608 |
-
(description "每打出一张红中算作一个明杠,结算时计分")
|
| 609 |
-
(implementation_path "extensions.special_mechanics.mechanic_cards.HongzhongKong")
|
| 610 |
-
(trigger (hook "on_discard") (when "discarded_tile == HONGZHONG"))
|
| 611 |
-
(effect (score_ops (("additive" "+X"))))
|
| 612 |
-
(settle (mode "end_round") (merge_rule "stack") (notes "..."))
|
| 613 |
-
(reset (hook "end_round") (do ...))
|
| 614 |
-
(transfer_path none))
|
| 615 |
-
)
|
| 616 |
-
)
|
| 617 |
-
)
|
| 618 |
-
|
| 619 |
-
|
| 620 |
-
**自检要求**
|
| 621 |
-
1. 扫描mGDL所有模块,识别所有非标准定义:
|
| 622 |
-
- extensions.special_mechanics.mechanic_cards. 中的所有条目
|
| 623 |
-
- win_rules 中的特殊胡牌规则
|
| 624 |
-
- scoring 中的特殊计分规则
|
| 625 |
-
- fan_table 中的特殊番型
|
| 626 |
-
2. 逐一确认每个特殊定义在 extensions.special_mechanics.mechanic_cards. 中有对应注册
|
| 627 |
-
3. 数量验证:extensions.special_mechanics.mechanic_cards. 注册数 = 实际特殊机制数
|
| 628 |
-
4. 若有遗漏 → FAIL & 自动补全注册
|
| 629 |
-
|
| 630 |
-
#### 区域与转移路径(强制且可机读)
|
| 631 |
-
**核心原则**:转移是唯一行为
|
| 632 |
-
1. 路径强制声明:每个 (action) 和 (mechanic) 必须包含有效的 (transfer_path from: X to: Y)。pass 动作必须声明为 (transfer_path none)。
|
| 633 |
-
2. 牌墙先行原则:所有在游戏中出现的牌,其初始来源必须在 (setup zones) 中明确定义。
|
| 634 |
-
3. 禁止凭空生成牌:任何牌的出现,必须有明确的转移路径,禁止在未定义牌墙或区域的情况下,直接将牌加入手卡或打出到弃牌区。
|
| 635 |
-
4. 区域守恒动态校验:在模拟推演中,任一动作执行后,必须满足 (= (sum of all zones) TotalTiles)。
|
| 636 |
-
|
| 637 |
-
#### 麻将牌墙管理检查
|
| 638 |
-
1. 摸牌路径合法性:
|
| 639 |
-
- 所有摸牌动作必须有明确的牌墙来源:(transfer_path from: wall to: hand:<PID>)
|
| 640 |
-
- 牌墙张数不能为负:每摸一张牌,检查 wall_count >= 0
|
| 641 |
-
- 检查项:验证所有摸牌路径指向有效牌墙
|
| 642 |
-
- FAIL条件:摸牌路径未定义或牌墙已空
|
| 643 |
-
- 修复:添加牌墙补充机制或调整摸牌条件
|
| 644 |
-
2. 杠补牌一致性:
|
| 645 |
-
- 所有杠(暗杠/直杠/补杠)必须在牌墙有足够牌时才能进行
|
| 646 |
-
- 杠后必须从牌墙补一张牌:(transfer_path from: wall to: hand:<PID>)
|
| 647 |
-
- 检查项:验证杠动作与补牌路径的绑定
|
| 648 |
-
- FAIL条件:杠后未定义补牌路径
|
| 649 |
-
- 修复:为每个杠类型添加���牌路径
|
| 650 |
-
|
| 651 |
-
#### 麻将牌数自检(强制且可机读)
|
| 652 |
-
1. 统一记号:
|
| 653 |
-
- suits ∈ {"wan", "tong", "tiao"}:基础花色
|
| 654 |
-
- honor_types:字牌类型数量
|
| 655 |
-
- special_tiles = [(name_i, count_i)]:额外特殊牌清单
|
| 656 |
-
- players = P:玩家人数(麻将固定为4)
|
| 657 |
-
- dealer_hand:庄家起手张数
|
| 658 |
-
- non_dealer_hand:闲家起手张数
|
| 659 |
-
- flip_zone_init:翻牌区初始张数(如有)
|
| 660 |
-
2. 总牌量公式: TotalTiles = 4 × 9 × |suits| + 4 × honor_types + Σ special_tiles.count
|
| 661 |
-
3. 局需求(一次性): Need_initial = dealer_hand + 3 × non_dealer_hand + flip_zone_init
|
| 662 |
-
4. 牌墙(开局可供后续摸牌等): Wall_initial = TotalTiles - Need_initial
|
| 663 |
-
- 约束:
|
| 664 |
-
- 如果游戏没有任何从 wall 摸牌的机制,则 Wall_initial 必须等于 0。
|
| 665 |
-
- 如果游戏有摸牌机制,则 Wall_initial ≥ Max_extra_draw。
|
| 666 |
-
5. 动态期望(机制/事件上界预算):
|
| 667 |
-
- 对所有会"摸牌/生成牌"的机制,给出最坏上界并相加: Max_extra_draw = Σ mechanisms.max_draw_upper_bound
|
| 668 |
-
- 通过计算确保每个机制的可实现性,避免违反牌墙初始值的条件。
|
| 669 |
-
- 约束:Wall_initial ≥ Max_extra_draw(若不满足→进入"最小修改优先级")
|
| 670 |
-
6. 守恒不变量(全过程): 在任意阶段 t,必须满足: Hands_t + Melds_t + Kong_t + Discard_t + Wall_t = TotalTiles (zone hand)_t + (zone meld)_t + (zone kong)_t + (zone discard_pile)_t + (zone wall)_t = TotalTiles 且所有分量 ≥ 0。
|
| 671 |
-
|
| 672 |
-
#### 番型可达性自检
|
| 673 |
-
1. 同一点数牌张数检查:
|
| 674 |
-
- 检查番型是否要求超过理论最大张数(如18罗汉要求4张相同字牌,但若字牌每种只有3张则不可达)
|
| 675 |
-
- 验证:required_count ≤ same_rank_max
|
| 676 |
-
- 同一点数最大张数 = 4 × 麻将牌副数 + 赖子可替代数量
|
| 677 |
-
- FAIL条件:番型要求超过物理上限
|
| 678 |
-
- 修复:调整番型要求或增加牌副数
|
| 679 |
-
2. 组合番型冲突检查:
|
| 680 |
-
- 检查多个番型组合是否存在逻辑冲突(如清一色+混一色)
|
| 681 |
-
- 检查番型叠加规则是否合理
|
| 682 |
-
- FAIL条件:存在无法同时满足的番型组合
|
| 683 |
-
- 修复:添加互斥规则或调整番型定义
|
| 684 |
-
3. 大牌型验证:
|
| 685 |
-
- 针对十八罗汉、大威天龙、万中无一等大牌型,进行理论可达成性验证
|
| 686 |
-
- 计算理论出现概率,确保不低于1/1000000(极罕见大牌可放宽)
|
| 687 |
-
- FAIL条件:理论概率为0
|
| 688 |
-
- 修复:调整牌组构成或番型要求
|
| 689 |
-
|
| 690 |
-
#### 麻将特殊规则自检
|
| 691 |
-
1. 胡牌规则一致性:
|
| 692 |
-
- 验证 post_win_continuation 与玩法大类一致
|
| 693 |
-
- 血战类:(winner_exit true) (end_when_third_player_wins true)
|
| 694 |
-
- 血流类:(winner_exit false) (keep_turn_order true)
|
| 695 |
-
- 检查胡牌要求与番型体系一致
|
| 696 |
-
- FAIL条件:规则冲突
|
| 697 |
-
- 修复:统一规则体系
|
| 698 |
-
2. 赖子规则完整性:
|
| 699 |
-
- 若启用赖子(幺鸡/红中/皮子),必须明确定义:
|
| 700 |
-
-- 赖子确定方式(固定/翻牌)
|
| 701 |
-
-- 赖子功能范围(可替代哪些牌)
|
| 702 |
-
-- 赖子限制(是否可吃碰杠/打出)
|
| 703 |
-
-- 赖子在胡牌时的特殊规则
|
| 704 |
-
- FAIL条件:赖子定义不完整
|
| 705 |
-
- 修复:补全赖子规则定义
|
| 706 |
-
3. 庄家规则验证:
|
| 707 |
-
- 验证庄家确定方式与连庄规则
|
| 708 |
-
- 检查庄家优势是否量化(如起手14张)
|
| 709 |
-
- FAIL条件:庄家规则模糊
|
| 710 |
-
- 修复:明确庄家权益与流转规则
|
| 711 |
-
|
| 712 |
-
#### 创新机制闭环自检(避免“罗列堆叠”)
|
| 713 |
-
1. 以 `extensions.special_mechanics.mechanic_cards.` 注册项为准,列出所有新增机制,并逐一写出:状态变量 / 触发器 / 效果 / 奖励(或惩罚) / 反制(或风险)
|
| 714 |
-
2. 若任一机制缺少上述任一要素 → FAIL & 直接删掉该机制或补全闭环
|
| 715 |
-
3. 若主机制 > 1 或辅机制 > 1 → FAIL & 按“最小修改优先级:简化规则”合并/删除
|
| 716 |
-
4. 若新增机制无法在 Crosswalk 找到合法落点 → FAIL & 回退改为可实体化版本(优先用 phase_rule / action 参数化替代)
|
| 717 |
-
|
| 718 |
-
#### 化学反应链路自检(策略增量)
|
| 719 |
-
1. 用 3 句描述:新机制让玩家多了什么选择?对手如何应对?为什么分数/胜负上成立?
|
| 720 |
-
2. 若只增加随机性或只是加倍 → FAIL & 重新设计为策略型触发(带条件/成本/反制)
|
| 721 |
-
|
| 722 |
-
#### 最小修改优先级(麻将专用)
|
| 723 |
-
1. 收敛机制:限制大牌型出现条件/频率,添加互斥规则
|
| 724 |
-
2. 调整牌组:增减特殊牌数量,调整花色构成
|
| 725 |
-
3. 修改番型:降低不可达大牌型的倍数,或提高常见番型价值
|
| 726 |
-
4. 简化规则:移除相互冲突的特殊机制
|
| 727 |
-
5. 增加牌副:从一副牌增至两副牌(需在平衡性分析中说明影响)
|
| 728 |
-
6. 重构流程:调整游戏阶段顺序,简化复杂交互
|
| 729 |
-
|
| 730 |
-
## 麻将游戏设计特质要求
|
| 731 |
-
### 目标玩家群体
|
| 732 |
-
1. 休闲麻将玩家和中级爱好者
|
| 733 |
-
2. 特别适合喜欢血战/血流但希望有新鲜感的玩家
|
| 734 |
-
3. 需要容易上手但有足够深度的游戏
|
| 735 |
-
|
| 736 |
-
### 游戏复杂度要求
|
| 737 |
-
1. 中等复杂度(比标准血战稍复杂,但不��过专业竞技麻将)
|
| 738 |
-
2. 核心机制不超过4个
|
| 739 |
-
3. 新玩家能在5分钟内理解基本规则
|
| 740 |
-
|
| 741 |
-
### 游戏时长要求
|
| 742 |
-
1. 8-15分钟完成一局
|
| 743 |
-
2. 适合碎片化时间娱乐
|
| 744 |
-
3. 有明确的节奏变化,避免中后期拖沓
|
| 745 |
-
|
| 746 |
-
### 创新点要求
|
| 747 |
-
1. 必须包含一个全新的核心机制(非简单组合现有机制)
|
| 748 |
-
2. 必须增强玩家间的互动性(不只是轮流出牌)
|
| 749 |
-
3. 避免过度依赖运气,增加策略决策点
|
| 750 |
-
4. 不能完全复制已知麻将玩法的核心机制
|
| 751 |
-
|
| 752 |
-
### 平衡性要求
|
| 753 |
-
1. 庄家优势应在10%-15%之间
|
| 754 |
-
2. 各种胡牌方式(平胡/碰碰胡/清一色等)应有合理价值分布
|
| 755 |
-
3. 爆发型大牌需有相应风险平衡
|
| 756 |
-
4. 赖子/特殊牌不应使游戏完全随机化
|
| 757 |
-
|
| 758 |
-
### 其他特定要求
|
| 759 |
-
1. 必须兼容移动端操作
|
| 760 |
-
2. 应有清晰的阶段性目标(如:前期布局、中期攻防、后期决胜)
|
| 761 |
-
3. 需考虑4人游戏的平衡性
|
| 762 |
-
4. 倍率系统应有上限,避免极端结果
|
| 763 |
-
5. 必须明确说明胡牌后是"血战"(胡家退出)还是"血流"(继续游戏)模式
|
| 764 |
-
|
| 765 |
-
## 输出格式
|
| 766 |
-
请严格按照以下格式输出:
|
| 767 |
|
| 768 |
-
|
| 769 |
|
| 770 |
-
|
| 771 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 772 |
|
| 773 |
-
|
| 774 |
-
1. **只允许做需求澄清与方案收敛**:不输出 mGDL、不输出完整自然语言规则、不输出自检报告。
|
| 775 |
-
2. **严格单问题迭代**:每轮只能问 1 个“当前最重要”的澄清问题;禁止一次列出多个问题。
|
| 776 |
-
3. **每轮必须输出更新后的 DesignState(JSON)**:把你已确认的信息写入 JSON;未知信息写入 `open_questions`。
|
| 777 |
-
4. **退出条件**:当 `open_questions` 为空,或只剩“用户可选项”(非阻塞)时,输出一句 `READY_TO_GENERATE: true`;否则 `READY_TO_GENERATE: false`。
|
| 778 |
|
| 779 |
-
|
| 780 |
-
- 一
|
| 781 |
-
-
|
| 782 |
-
-
|
| 783 |
-
-
|
| 784 |
-
- `READY_TO_GENERATE: true/false`
|
| 785 |
|
| 786 |
-
**
|
| 787 |
-
-
|
| 788 |
-
- 必须明确:它将影响的“创新旋钮”(触发/代价/收益/反制/计分/流程中的哪一处)
|
| 789 |
-
- 必须避免:给出两条以上的新问题,或提前输出完整规则细节
|
| 790 |
|
| 791 |
-
|
| 792 |
-
|
| 793 |
-
|
| 794 |
-
|
| 795 |
-
|
| 796 |
-
- 最终确认所有项已通过:(无需包含结构化 [PASS/FAIL] 表格)
|
| 797 |
|
| 798 |
-
###
|
| 799 |
-
请用“可审核的决策记录”替代泛泛的灵感描述,必须包含:
|
| 800 |
-
1. **目标体验**:你希望新玩法比底座玩法多出什么“策略选择/互动张力”(≤80字)
|
| 801 |
-
2. **约束清单**:列出 5 条不可破坏约束(例如:手牌守恒、计分体系统一、牌墙不为负、核心机制≤2 等)
|
| 802 |
-
3. **候选创新方案 A/B/C(至少2个)**:每个方案必须写清:
|
| 803 |
-
- 状态变量(≤2个) / 触发条件 / 代价 / 奖励或惩罚 / 反制或风险 / 影响的决策点(摸/打/吃碰杠/听/胡/结算)
|
| 804 |
-
4. **选择理由**:你最终选用哪个方案,为什么放弃其他方案(必须引用上面的约束与风险)
|
| 805 |
-
5. **推演摘要**:给出 1 个具体回合示例(玩家选择→对手应对→为何分数/胜负成立)
|
| 806 |
-
6. **落地映射(Crosswalk)**:列出“创新机制名 → mGDL_path → transfer_path(若有)”的对照表,确保可实体化
|
| 807 |
|
| 808 |
-
|
| 809 |
-
|
| 810 |
-
|
| 811 |
-
|
| 812 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 813 |
|
| 814 |
-
```
|
| 815 |
-
|
| 816 |
-
|
| 817 |
-
|
| 818 |
-
|
| 819 |
-
|
| 820 |
-
|
| 821 |
-
|
| 822 |
-
|
| 823 |
-
|
| 824 |
-
|
| 825 |
-
|
| 826 |
-
|
| 827 |
-
|
| 828 |
-
"core_constraints": [
|
| 829 |
-
"手牌守恒:回合结��回到13张",
|
| 830 |
-
"牌数守恒:所有区域之和=TotalTiles"
|
| 831 |
-
],
|
| 832 |
-
"mechanics": [
|
| 833 |
-
{
|
| 834 |
-
"name": "机制名",
|
| 835 |
-
"type": "core|aux",
|
| 836 |
-
"state_vars": ["变量1"],
|
| 837 |
-
"phase": "setup|play|settle|global",
|
| 838 |
-
"trigger": "触发条件(确定性)",
|
| 839 |
-
"cost": "代价/限制(确定性)",
|
| 840 |
-
"reward_or_penalty": "奖励/惩罚(可量化)",
|
| 841 |
-
"counterplay": "对手如何应对/反制",
|
| 842 |
-
"mgdl_path": "extensions.special_mechanics.mechanic_cards.<ID>",
|
| 843 |
-
"transfer_path": "from: X to: Y|none"
|
| 844 |
-
}
|
| 845 |
-
],
|
| 846 |
-
"open_questions": []
|
| 847 |
-
}
|
| 848 |
```
|
| 849 |
|
| 850 |
-
###
|
| 851 |
-
[游戏名称]
|
| 852 |
|
| 853 |
-
|
| 854 |
-
[设计理念和创新点,200字以内]
|
| 855 |
|
| 856 |
-
|
| 857 |
-
|
| 858 |
-
|
| 859 |
-
|
| 860 |
-
|
| 861 |
-
|
| 862 |
-
- 禁止:(tileset (suits {"wan" "tong" "tiao"})) ← 只有花色
|
| 863 |
-
- 参照:示例文件第15-25行,明确列出所有牌类型、数量、特殊牌
|
| 864 |
-
- 要求:定义时必须包含花色、点数范围、字牌、特殊牌等完整信息
|
| 865 |
-
2. 特殊机制(extensions.special_mechanics.mechanic_cards.)
|
| 866 |
-
- 禁止:(extensions.special_mechanics.mechanic_cards. (yaoji (enabled true))) ← 只有启用标志
|
| 867 |
-
- 参照:示例文件第30-45行,每个机制有完整参数
|
| 868 |
-
- 要求:定义时必须包含详细规则、转移路径、可见性等
|
| 869 |
-
3. 番型表(fan_table)
|
| 870 |
-
- 禁止:; 番型定义(略,同标准血战) ← 严重错误!
|
| 871 |
-
- 参照:示例文件第100-150行,为每个番型单独定义
|
| 872 |
-
- 要求:每个番型都要有独立条目,包含番数/倍数、描述、条件等
|
| 873 |
-
4. 游戏阶段(phases)
|
| 874 |
-
- 禁止:(phases ["play" "settle"]) ← 只有阶段名
|
| 875 |
-
- 参照:示例文件第60-80行,每个阶段有详细规则
|
| 876 |
-
- 要求:每个阶段至少包含5-10行的具体规则定义
|
| 877 |
-
|
| 878 |
-
5. 行为规则(actions)
|
| 879 |
-
- 禁止:(actions (allow_chi true)) ← 仅开关无细节
|
| 880 |
-
- 参照:四川血战示例第120-135行,每个 action 有 transfer_path + hand effect 注释
|
| 881 |
-
- 要求:每个 action 必须包含:
|
| 882 |
-
- `(transfer_path from: X to: Y)`
|
| 883 |
-
- 中文注释说明 **手牌数量变化** 和 **是否触发强制打牌**
|
| 884 |
-
- 示例:
|
| 885 |
-
```lisp
|
| 886 |
-
(action chi
|
| 887 |
-
(transfer_path from: discard_pile to: meld:<PID>)
|
| 888 |
-
; 吃:取1张入 meld 区,手牌净减少2张(因3张顺子移出),必须打出1张以恢复13张
|
| 889 |
-
(requires_discard true)
|
| 890 |
-
)
|
| 891 |
-
```
|
| 892 |
-
6. 胡牌规则(win_rules)
|
| 893 |
-
- 禁止:(win_rules (allow_discard_win true)) ← 只有1个参数
|
| 894 |
-
- 参照:示例文件第85-95行,胡牌规则有完整定义
|
| 895 |
-
- 要求:必须完整定义允许的胡牌类型、胡牌后规则等
|
| 896 |
-
|
| 897 |
-
#### 生成策略:
|
| 898 |
-
1. 先在心中构建完整的游戏逻辑
|
| 899 |
-
2. 先从参考玩法 .md 中抽取:牌组、流程、胡牌、计分、特殊机制与限制(内容真理)
|
| 900 |
-
3. 再参考通用语法规范确定 mGDL 语法正确性(语法约束)
|
| 901 |
-
4. 最后(可选)参考 mGDL 示例确保每个模块的“结构完整度”,但不得照抄规则数值(语法范式)
|
| 902 |
-
4. 逐模块编写,每个模块的细节要合理、充分、符合逻辑
|
| 903 |
-
5. 严禁为了"节省篇幅"而省略任何模块的详细定义
|
| 904 |
-
|
| 905 |
-
[在此处插入完整的mGDL描述,且加入中文注释增强可读性]
|
| 906 |
-
|
| 907 |
-
#### ⚠️ mGDL 完整性强制要求:
|
| 908 |
-
mGDL 必须包含以下所有核心模块(对应硬自检第0项):
|
| 909 |
-
1. (game_variant "...") - 玩法大类
|
| 910 |
-
2. (players N) - 玩家数量
|
| 911 |
-
3. (tileset ...) - 牌组定义
|
| 912 |
-
4. (extensions ...) - 扩展机制(必须含 special_mechanics + mechanic_cards)
|
| 913 |
-
5. (seats {...}) - 座位定义
|
| 914 |
-
6. (turn_order ...) - 出牌顺序
|
| 915 |
-
7. (setup ...) - 游戏准备
|
| 916 |
-
8. (actions ...) - 行为规则
|
| 917 |
-
9. (win_rules ...) - 胡牌规则
|
| 918 |
-
10. (scoring ...) - 计分体系
|
| 919 |
-
11. (fan_table ...) - 番型与倍数
|
| 920 |
-
12. (settlement ...) - 结算规则
|
| 921 |
-
13. (invariants ...) - 守恒不变量
|
| 922 |
-
严禁省略任何核心模块! 即使 mGDL 很长,也必须完整输出。 在 (setup) 或 (tileset) 附近添加牌数验证的中文注释,便于人工核对。
|
| 923 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 924 |
|
| 925 |
-
###
|
| 926 |
-
|
| 927 |
-
|
| 928 |
-
-
|
| 929 |
-
|
| 930 |
-
|
| 931 |
-
|
| 932 |
-
|
| 933 |
-
|
| 934 |
-
|
| 935 |
-
|
| 936 |
-
|
| 937 |
-
|
| 938 |
-
|
| 939 |
-
|
| 940 |
-
|
| 941 |
-
|
| 942 |
-
e. 禁止模糊表述:严禁使用"某些"、"部分"、"可能"等模糊词汇
|
| 943 |
-
f. 后续引用标记:每个机制说明的末尾必须标注"→ 详见【第3节-游戏流程-[阶段名]】"
|
| 944 |
-
g. **机制具象化(Concreteness Check)**:
|
| 945 |
-
- ❌ 模糊:获得“某种资源”、“特殊能力”。
|
| 946 |
-
- ✅ 具象:获得“1枚金币(Score+10)”、“摸牌阶段多摸1张”。
|
| 947 |
-
- 严禁使用未定义的抽象概念,所有机制必须落地为:牌的转移、分数的增减、或阶段的改变。
|
| 948 |
-
|
| 949 |
-
2. 基础规则描述
|
| 950 |
-
a. 牌组构成:
|
| 951 |
-
- 基础花色:[列出万/筒/条数量]
|
| 952 |
-
- 字牌类型与数量:[列出中/发/白/风牌等]
|
| 953 |
-
- 特殊牌:[列出幺鸡/红中/月亮牌等数量]
|
| 954 |
-
- 总牌数验证:[计算验证]
|
| 955 |
-
b. 玩家与座位:
|
| 956 |
-
- 玩家人数:固定4人
|
| 957 |
-
- 座位顺序:[说明]
|
| 958 |
-
- 庄家确定方式:[说明]
|
| 959 |
-
c. 发牌后库存显式声明(强制):
|
| 960 |
-
- 庄家:[N] 张
|
| 961 |
-
- 闲家1:[M] 张
|
| 962 |
-
- 闲家2:[M] 张
|
| 963 |
-
- 闲家3:[M] 张
|
| 964 |
-
- 牌墙剩余:[W] 张
|
| 965 |
-
- 翻牌区(如有):[F] 张
|
| 966 |
-
- 总计验证:N + 3×M + W + F = [TotalTiles](✅ 符合)
|
| 967 |
-
d. 牌的大小与关系:
|
| 968 |
-
- 花色大小:[如有]
|
| 969 |
-
- 点数大小:1<2<...<9
|
| 970 |
-
- 特殊关系:[如连续关系/同点关系等]
|
| 971 |
-
|
| 972 |
-
3. 游戏流程描述
|
| 973 |
-
a. 流程总述:需描述游戏的全部流程阶段,必须包含明确的庄家确定规则和牌局结束条件
|
| 974 |
-
b. 分阶段描述:对每个阶段玩家的操作进行详细描述:
|
| 975 |
-
- 准备阶段(定缺、换三张等)
|
| 976 |
-
- 行牌阶段(摸打顺序、吃碰杠胡规则)
|
| 977 |
-
- 结算阶段(胡牌类型、分数计算、连庄规则)
|
| 978 |
-
|
| 979 |
-
4. 游戏得分体系
|
| 980 |
-
a. 计分模式:明确说明采用倍数制/番数制/混合制
|
| 981 |
-
b. 基础分数:基础分值、自摸/点炮倍率、起胡和封顶
|
| 982 |
-
c. 番型计分:详细列出番型与对应分值
|
| 983 |
-
|
| 984 |
-
5. 术语与新物品词典
|
| 985 |
-
[按以下格式列出所有特殊术语]
|
| 986 |
-
名称:[术语名]
|
| 987 |
-
作用:[功能说明]
|
| 988 |
-
时机:[使用阶段]
|
| 989 |
-
触发:[触发条件]
|
| 990 |
-
代价:[代价说明]
|
| 991 |
-
优先级:[优先级说明]
|
| 992 |
-
限制:[限制条件]
|
| 993 |
-
示例:[使用案例]
|
| 994 |
-
失败处理:[失败情况处理]
|
| 995 |
-
转移路径:[from: X to: Y]
|
| 996 |
-
可见性变更:[to: {audience} on_target: true/false]
|
| 997 |
-
|
| 998 |
-
## 技术附录:《自检报告》
|
| 999 |
-
|
| 1000 |
-
<details>
|
| 1001 |
-
<summary>点击展开,完整的《自检报告》(开发者/审核专用)</summary>
|
| 1002 |
-
|
| 1003 |
-
## 麻将mGDL自检报告 v1.3
|
| 1004 |
-
|
| 1005 |
-
### 0) **mGDL 模块完整性检查**(零容忍项)
|
| 1006 |
-
|
| 1007 |
-
**核心模块检查清单(必须全部为 PASS):**
|
| 1008 |
-
- game_variant 定义 → [PASS/FAIL]
|
| 1009 |
-
- players 定义 → [PASS/FAIL]
|
| 1010 |
-
- tileset 定义(含 suits/ranks/honors/total) → [PASS/FAIL]
|
| 1011 |
-
- extensions.special_mechanics.mechanic_cards. 定义 → [PASS/FAIL]
|
| 1012 |
-
- seats 定义 → [PASS/FAIL]
|
| 1013 |
-
- turn_order 定义 → [PASS/FAIL]
|
| 1014 |
-
- setup 定义(含 initial_hand/choose_que/exchange_three) → [PASS/FAIL]
|
| 1015 |
-
- actions 定义(含 allow_chi/allow_peng/allow_gang) → [PASS/FAIL]
|
| 1016 |
-
- win_rules 定义(含 allow_discard_win/allow_self_draw_win/post_win_continuation) → [PASS/FAIL]
|
| 1017 |
-
- scoring 定义 → [PASS/FAIL]
|
| 1018 |
-
- fan_table 定义(至少5种番型) → [PASS/FAIL]
|
| 1019 |
-
- settlement 定义 → [PASS/FAIL]
|
| 1020 |
-
- invariants 定义(含牌数守恒公式) → [PASS/FAIL]
|
| 1021 |
-
|
| 1022 |
-
**判定标准**:
|
| 1023 |
-
- 任一模块为 FAIL → **整体 FAIL & 必须立即补全缺失模块**
|
| 1024 |
-
- 全部模块为 PASS → 整体 PASS,继续后续检查
|
| 1025 |
-
|
| 1026 |
-
**最终结论**:[PASS/FAIL]
|
| 1027 |
-
**若 FAIL,缺失的模块列表**:[列出所有缺失模块名称]
|
| 1028 |
-
|
| 1029 |
-
### 1) **麻将牌数自检表**(机读)
|
| 1030 |
-
|
| 1031 |
-
**基础参数**:
|
| 1032 |
-
- suits = { "wan" "tong" "tiao" }
|
| 1033 |
-
- honor_types = [count of enabled honors]
|
| 1034 |
-
- special_tiles = [(name_i, count_i)]
|
| 1035 |
-
- players = 4
|
| 1036 |
-
- dealer_hand = ?
|
| 1037 |
-
- non_dealer_hand = ?
|
| 1038 |
-
- flip_zone_init = ?
|
| 1039 |
-
|
| 1040 |
-
**计算验证**:
|
| 1041 |
-
- TotalTiles = 4 × 9 × |suits| + 4 × honor_types + Σ special_tiles.count = ?
|
| 1042 |
-
- Need_initial = dealer_hand + 3 × non_dealer_hand + flip_zone_init = ?
|
| 1043 |
-
- Wall_initial = TotalTiles - Need_initial = ?
|
| 1044 |
-
|
| 1045 |
-
**约束检查**:
|
| 1046 |
-
- Wall_initial ≥ 0 → [PASS/FAIL]
|
| 1047 |
-
- Max_extra_draw = ? (杠/补牌等机制上界)
|
| 1048 |
-
- 约束:Wall_initial ≥ Max_extra_draw → [PASS/FAIL]
|
| 1049 |
-
|
| 1050 |
-
**守恒不变量**:
|
| 1051 |
-
- Hands_t + Melds_t + Kong_t + Discard_t + Wall_t = TotalTiles → [PASS/FAIL]
|
| 1052 |
-
- 所有区域计数 ≥ 0 → [PASS/FAIL]
|
| 1053 |
-
|
| 1054 |
-
### 2) **牌墙管理检查**
|
| 1055 |
-
|
| 1056 |
-
**摸牌路径合法性**:
|
| 1057 |
-
- 所有摸牌动作有明确牌墙来源 → [PASS/FAIL]
|
| 1058 |
-
- 牌墙张数不为负 → [PASS/FAIL]
|
| 1059 |
-
|
| 1060 |
-
**杠补牌一致性**:
|
| 1061 |
-
- 所有杠动作有明确补牌路径 → [PASS/FAIL]
|
| 1062 |
-
- 牌墙足够支持最大杠数 → [PASS/FAIL]
|
| 1063 |
-
|
| 1064 |
-
**牌局结束条件**:
|
| 1065 |
-
- 牌墙耗尽时牌局结束条件明确定义 → [PASS/FAIL]
|
| 1066 |
-
|
| 1067 |
-
### 3) **胡牌规则一致性检查**
|
| 1068 |
-
|
| 1069 |
-
**玩法大类匹配**:
|
| 1070 |
-
- game_variant 与 post_win_continuation 逻辑一致:
|
| 1071 |
-
- 血战类:winner_exit=true, end_when_third_player_wins=true → [PASS/FAIL]
|
| 1072 |
-
- 血流类:winner_exit=false, keep_turn_order=true → [PASS/FAIL]
|
| 1073 |
-
|
| 1074 |
-
**胡牌条件完整性**:
|
| 1075 |
-
- 胡牌要求与番型体系一致 → [PASS/FAIL]
|
| 1076 |
-
- 胡牌后规则完整定义 → [PASS/FAIL]
|
| 1077 |
-
|
| 1078 |
-
### 4) **番型可达性自检**
|
| 1079 |
-
|
| 1080 |
-
**同一点数牌张数检查**:
|
| 1081 |
-
- 番型需求 ≤ 物理上限(4×牌副数+赖子) → [PASS/FAIL]
|
| 1082 |
-
- 具体不可达番型:[列出]
|
| 1083 |
-
|
| 1084 |
-
**组合番型冲突检查**:
|
| 1085 |
-
- 无逻辑冲突的番型组合 → [PASS/FAIL]
|
| 1086 |
-
- 冲突组合:[列出]
|
| 1087 |
-
|
| 1088 |
-
**大牌型验证**:
|
| 1089 |
-
- 十八罗汉/大威天龙等大牌型理论达成概率>0 → [PASS/FAIL]
|
| 1090 |
-
- 万中无一等极端大牌验证 → [PASS/FAIL]
|
| 1091 |
|
| 1092 |
-
###
|
| 1093 |
|
| 1094 |
-
|
| 1095 |
-
-
|
| 1096 |
-
-
|
| 1097 |
-
- 赖子限制(是否可吃碰杠/打出)明确定义 → [PASS/FAIL]
|
| 1098 |
-
- 赖子在胡牌时的特殊规则明确定义 → [PASS/FAIL]
|
| 1099 |
|
| 1100 |
-
|
| 1101 |
-
|
| 1102 |
-
|
| 1103 |
|
| 1104 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1105 |
|
| 1106 |
-
|
| 1107 |
-
- 定缺启用时,缺门验证规则明确定义 → [PASS/FAIL]
|
| 1108 |
-
- 定缺时机明确 → [PASS/FAIL]
|
| 1109 |
|
| 1110 |
-
|
| 1111 |
-
- 换牌方向/规则明确定义 → [PASS/FAIL]
|
| 1112 |
-
- 换牌限制明确定义 → [PASS/FAIL]
|
| 1113 |
|
| 1114 |
-
|
| 1115 |
-
- 出牌顺序无歧义 → [PASS/FAIL]
|
| 1116 |
-
- 吃碰杠规则无冲突 → [PASS/FAIL]
|
| 1117 |
|
| 1118 |
-
###
|
| 1119 |
|
| 1120 |
-
|
| 1121 |
-
- 机制列表:[M1, M2, M3, ...]
|
| 1122 |
-
- 总计:N个机制
|
| 1123 |
|
| 1124 |
-
**
|
| 1125 |
-
|
| 1126 |
-
|
| 1127 |
-
|
| 1128 |
-
|
| 1129 |
|
| 1130 |
-
**
|
| 1131 |
-
| mGDL机制名 | 在自然语言表格中 | 在流程描述中 |
|
| 1132 |
-
|----------|----------------|-------------|
|
| 1133 |
-
| [name1] | [是/否] | [是/否] |
|
| 1134 |
-
| [name2] | [是/否] | [是/否] |
|
| 1135 |
|
| 1136 |
-
|
| 1137 |
-
- 若步骤B中任一机制"是否存在"为"否" → **FAIL**(自然语言声明了但mGDL未实现)
|
| 1138 |
-
- 若步骤B中任一机制缺少 `transfer_path` → **FAIL**
|
| 1139 |
-
- 若步骤C中任一mGDL机制未在自然语言中说明 → **FAIL**(mGDL实现了但未告知玩家)
|
| 1140 |
-
- 若自然语言声明的机制数量 ≠ mGDL实际实现的机制数量 → **FAIL**
|
| 1141 |
|
| 1142 |
-
|
| 1143 |
|
| 1144 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1145 |
|
| 1146 |
-
|
| 1147 |
-
|
| 1148 |
-
|
| 1149 |
-
- scoring 中的特殊计分规则
|
| 1150 |
-
- fan_table 中的特殊番型
|
| 1151 |
|
| 1152 |
-
|
| 1153 |
-
|
| 1154 |
-
|
| 1155 |
-
|
| 1156 |
-
|
|
|
|
|
|
|
| 1157 |
|
| 1158 |
-
|
| 1159 |
|
| 1160 |
-
|
| 1161 |
-
|
| 1162 |
-
|
| 1163 |
-
|
| 1164 |
-
| [M1] | extensions.special_mechanics.mechanic_cards.[M1] | extensions.special_mechanics.mechanic_cards.[M1] | [cat1] | [setup/play/scoring/end/global] | true |
|
| 1165 |
-
| [M2] | extensions.special_mechanics.mechanic_cards.[M2] | extensions.special_mechanics.mechanic_cards.[M2] | [cat2] | [setup/play/scoring/end/global] | true |
|
| 1166 |
|
|
|
|
| 1167 |
|
| 1168 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1169 |
|
| 1170 |
-
|
| 1171 |
-
- mGDL中实际特殊机制数量:N
|
| 1172 |
-
- extensions.special_mechanics.mechanic_cards. 注册数量:M
|
| 1173 |
-
- 是否一致:N == M → [PASS/FAIL]
|
| 1174 |
-
- 若处于【创新扩展模式】:逐一检查每条机制卡是否包含 trigger/effect/settle/reset(缺一 FAIL)
|
| 1175 |
|
| 1176 |
-
|
| 1177 |
-
-
|
|
|
|
|
|
|
| 1178 |
|
| 1179 |
-
|
| 1180 |
|
| 1181 |
-
##
|
| 1182 |
|
| 1183 |
-
|
| 1184 |
-
|
| 1185 |
-
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1186 |
|
| 1187 |
-
|
| 1188 |
-
|
| 1189 |
-
|
|
|
|
|
|
|
|
|
|
| 1190 |
|
| 1191 |
-
###
|
| 1192 |
-
|
| 1193 |
-
|
| 1194 |
-
|
| 1195 |
-
|
| 1196 |
-
|
| 1197 |
-
|
| 1198 |
-
|
| 1199 |
-
|
| 1200 |
-
|
| 1201 |
-
|
| 1202 |
-
|
| 1203 |
-
|
| 1204 |
-
|
| 1205 |
-
|
| 1206 |
-
|
| 1207 |
-
|
| 1208 |
-
|
| 1209 |
-
|
| 1210 |
-
|
| 1211 |
-
|
| 1212 |
-
|
| 1213 |
-
|
| 1214 |
-
|
| 1215 |
-
|
| 1216 |
-
|
| 1217 |
-
|
| 1218 |
-
|
| 1219 |
-
|
| 1220 |
-
|
| 1221 |
-
|
| 1222 |
-
|
| 1223 |
-
|
| 1224 |
-
|
| 1225 |
-
|
| 1226 |
-
|
| 1227 |
-
|
| 1228 |
-
|
| 1229 |
-
|
| 1230 |
-
|
| 1231 |
-
|
| 1232 |
-
|
| 1233 |
-
|
| 1234 |
-
|
| 1235 |
-
|
| 1236 |
-
|
| 1237 |
-
|
| 1238 |
-
|
| 1239 |
-
- 连庄次数上限验证 → [PASS/FAIL]
|
| 1240 |
-
|
| 1241 |
-
### 14) **玩法融合自检**(融合任务必检)
|
| 1242 |
-
|
| 1243 |
-
**融合模式检查**:
|
| 1244 |
-
- 计分模式统一性:全倍数制 或 全番数制(无混合) → [PASS/FAIL]
|
| 1245 |
-
- 冲突规则排查:无互斥的胡牌/行牌规则 → [PASS/FAIL]
|
| 1246 |
-
- 牌组兼容性:Tileset 包含所有机制所需的牌(如花牌/月亮牌) → [PASS/FAIL]
|
| 1247 |
-
|
| 1248 |
-
**融合插件注册**:
|
| 1249 |
-
- 来源玩法机制完整注册进 extensions.special_mechanics.mechanic_cards. → [PASS/FAIL]
|
| 1250 |
-
- 插件机制与底座玩法无逻辑冲突 → [PASS/FAIL]
|
| 1251 |
-
|
| 1252 |
-
### 15) **最终验收**
|
| 1253 |
-
|
| 1254 |
-
**核心验收项**:
|
| 1255 |
-
- 模块完整性:[PASS/FAIL]
|
| 1256 |
-
- 牌数守恒:[PASS/FAIL]
|
| 1257 |
-
- 机制完整性:[PASS/FAIL]
|
| 1258 |
-
- 番型可达性:[PASS/FAIL]
|
| 1259 |
-
- 无模糊表述:[PASS/FAIL]
|
| 1260 |
-
- 自检报告完整:[PASS/FAIL]
|
| 1261 |
-
- 融合自检通过(仅融合任务):[PASS/FAIL/NA]
|
| 1262 |
-
|
| 1263 |
-
**最终结论**:[PASS/FAIL]
|
| 1264 |
-
|
| 1265 |
-
**若FAIL,需重新设计的部分**:
|
| 1266 |
-
1. [模块名称] - [原因]
|
| 1267 |
-
2. [模块名称] - [原因]
|
| 1268 |
-
|
| 1269 |
-
**审核人**:[AI系统自审]
|
| 1270 |
-
**审核时间**:[YYYY-MM-DD HH:MM:SS]
|
| 1271 |
-
**审核版本**:mGDL v1.3
|
| 1272 |
-
|
| 1273 |
-
</details>
|
| 1274 |
-
|
| 1275 |
-
## 平衡性分析
|
| 1276 |
-
[在此处撰写平衡性分析,包括:
|
| 1277 |
-
庄家优势控制在10%-15%的设计依据
|
| 1278 |
-
各种胡牌方式的价值分布合理性
|
| 1279 |
-
大牌型与普通牌型的比例控制
|
| 1280 |
-
赖子/特殊牌对随机性的控制
|
| 1281 |
-
与现有麻将玩法相比的平衡性特点]
|
| 1282 |
-
|
| 1283 |
-
## 常见错误避免
|
| 1284 |
-
### ⚠️ 零容忍错误(最高优先级)
|
| 1285 |
-
1. ❌ mGDL 模块不完整是最严重的错误:
|
| 1286 |
-
- 严禁输出缺少核心模块的 mGDL(如缺少 extensions.special_mechanics.mechanic_cards.、fan_table、win_rules 等)
|
| 1287 |
-
- mGDL 必须包含硬自检第0项列出的所有核心模块,缺一不可
|
| 1288 |
-
- 在第二步自检时,第0项"模块完整性检查"的所有子项必须全部 PASS
|
| 1289 |
-
2. 牌墙管理错误:
|
| 1290 |
-
- 严禁未定义牌墙就进行摸牌操作
|
| 1291 |
-
- 严禁牌墙张数变为负数
|
| 1292 |
-
- 严禁杠后未定义补牌路径
|
| 1293 |
-
3. 番型可达性问题:
|
| 1294 |
-
- 严禁定义理论不可达的番型(如要求18张相同字牌)
|
| 1295 |
-
- 严禁番型组合存在逻辑冲突
|
| 1296 |
-
- 严禁大牌型出现概率为0
|
| 1297 |
-
4. 胡牌规则不完整:
|
| 1298 |
-
- 严禁只写"类似血战"而不展开具体参数
|
| 1299 |
-
- 严禁未定义胡牌后规则
|
| 1300 |
-
- 严禁胡牌要求与番型体系不一致
|
| 1301 |
-
5. 特殊机制未注册:
|
| 1302 |
-
- 严禁在 extensions.special_mechanics.mechanic_cards. 中漏注册任何创新机制
|
| 1303 |
-
- 严禁自然语言声明了但 mGDL 未实现的机制
|
| 1304 |
-
- 严禁 mGDL 实现了但自然语言未说明的机制
|
| 1305 |
-
6. 新增麻将专用检查项
|
| 1306 |
-
- 血战/血流一致性:玩法大类必须与 post_win_continuation 逻辑一致
|
| 1307 |
-
- 赖子规则完整性:若启用赖子,必须完整定义其功能范围、限制、特殊规则
|
| 1308 |
-
- 庄家规则验证:庄家权益必须量化,庄家流转规则必须明确
|
| 1309 |
-
- 牌墙补充机制:若 Wall_initial > 0,必须定义从牌墙摸牌的机制
|
| 1310 |
-
- 大牌型验证:十八罗汉、大威天龙等大牌型必须通过理论可达成性验证
|
| 1311 |
-
- 番型叠加规则:必须明确定义番型是否叠加及叠加方式
|
| 1312 |
-
- 定缺规则一致性:若启用定缺,必须定义缺门验证与时机
|
| 1313 |
-
- 翻牌机制完整性:若启用翻牌,必须定义翻牌区、翻牌时机、翻牌规则
|
| 1314 |
-
7. 工程验收标准
|
| 1315 |
-
- 模块完整性:所有14个核心模块必须完整存在
|
| 1316 |
-
- 物理一致性:所有牌变动必须有明确转移路径
|
| 1317 |
-
- 牌数守恒:初始牌数分布 + 各区域变动 = 总牌数
|
| 1318 |
-
- 机制完整性:自然语言声明的机制数 = mGDL实现的机制数
|
| 1319 |
-
- 番型可达性:所有定义的番型在理论上有非零达成概率
|
| 1320 |
-
- 平衡性合理:庄家优势在10%-15%,大牌型价值与概率成反比
|
| 1321 |
-
- 无模糊表述:禁止使用"某些"、"部分"、"可能"等模糊词汇
|
| 1322 |
-
- 自检报告完整:包含所有16项自检结果及修复轨迹
|
| 1323 |
-
- 最终验收:只有当所有自检项均为 PASS,且《自检报告》中有完整修复轨迹时,才视为合格输出。任何 FAIL 项未修复的输出都将被拒绝。
|
| 1324 |
-
8. **手牌数量违反守恒**:
|
| 1325 |
-
- 严禁在行牌阶段结束时(即未处于胡牌瞬间)出现手牌 ≠ 13 张的情况(庄家出牌后同理)。
|
| 1326 |
-
- 严禁 `action` 定义中缺少 `requires_discard` 或未说明手牌变化。
|
| 1327 |
-
- 严禁自然语言描述中省略“吃/碰/杠后需打牌”这一关键步骤。
|
| 1328 |
-
9. **玩法融合失败**(融合任务专用):
|
| 1329 |
-
- 严禁混用计分模式(如在倍数制中使用 `fan` 字段)。
|
| 1330 |
-
- 严禁引入新机制(如买马)但未在 `extensions.special_mechanics.mechanic_cards.` 和 `win_rules/scoring` 中完整定义。
|
| 1331 |
-
- 严禁保留互相冲突的规则(如“血战”与“流局”规则并存)。
|
|
|
|
| 1 |
+
# Mahjong 玩法融合 Prompt(Phase-1:既有机制精准融合)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
|
| 3 |
+
你是一位**麻将规则策划**与**规则一致性审计员**。本阶段只做“既有机制组合/融合”,目标是:把“底座玩法A”的流程与计分框架作为主干,将“参考玩法B”的既有机制以插件方式融合进来,并确保麻将底层逻辑完全自洽可落地(手牌守恒、吃碰杠、出牌权与轮次、牌墙转移等)。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
|
| 5 |
+
本阶段优先级:**底层正确性 > 融合准确性 > 输出完整性 > 创新强度**
|
| 6 |
+
说明:允许做少量“桥接规则”解决冲突,但禁止凭空发明新机制体系。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
|
| 8 |
+
---
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
|
| 10 |
+
## 资源使用规则(务必遵守)
|
| 11 |
|
| 12 |
+
1. **规则真理**:所有规则语义以 `.md` 为准(系统注入的 `<REFERENCE_VARIANTS_MD>` 或本项目内玩法 `.md`)。
|
| 13 |
+
2. **mGDL 的定位**:mGDL 仅用于“结构化落地与形式约束”,不得反推/猜测规则细节。
|
| 14 |
+
3. **机制来源限制(Phase-1)**:你只能组合/改写以下来源中已存在的机制:
|
| 15 |
+
- 底座玩法A的 `.md`
|
| 16 |
+
- 参考玩法B的 `.md`
|
| 17 |
+
- `<MECHANISM_LIBRARY>`(麻将机制说明)
|
| 18 |
+
4. **禁止**引入“全新母题机制/自创资源系统/自创牌类”。如果必须做桥接,只能做最小可量化规则(例如:把B的一个结算倍数映射到A的倍数制)。
|
| 19 |
|
| 20 |
+
---
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
|
| 22 |
+
## Phase-1 融合策略(必做)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
|
| 24 |
+
1. **确定底座**:选择A作为底座(流程、胡后模式、计分模式默认继承A)。
|
| 25 |
+
2. **抽取清单**:
|
| 26 |
+
- 从A抽取:牌组、流程阶段、吃碰杠权限、胡牌结束逻辑、计分模式与番型框架
|
| 27 |
+
- 从B抽取:要引入的“具体机制条款”(只能抽取明确条款,禁止抽象概念)
|
| 28 |
+
3. **冲突检测与桥接**(必须写清):
|
| 29 |
+
- 冲突类型:计分模式/胡后流程/牌组构成/动作权限/轮次控制
|
| 30 |
+
- 桥接规则:一条冲突最多允许一条桥接规则;必须可量化、可落地、可验证
|
| 31 |
+
4. **落地映射**:每个融合机制必须对应一个 mGDL 落点(`mGDL_path`),并在自然语言中说明它如何影响“牌转移/分数/阶段/出牌权”。
|
| 32 |
+
|
| 33 |
+
---
|
| 34 |
+
|
| 35 |
+
## 麻将底层物理守恒(零容忍硬约束)
|
| 36 |
|
| 37 |
+
### A) 手牌数量守恒(红线)
|
| 38 |
+
|
| 39 |
+
**手牌定义**(引用 `<MECHANISM_LIBRARY>` 1.1 节):
|
| 40 |
+
- **暗牌**:手中未公开的牌
|
| 41 |
+
- **明牌组**:通过吃/碰/杠形成的公开牌组
|
| 42 |
+
- **手牌位置**:玩家"占用"的牌位数量,**回合结束时恒为 13 个位置**
|
| 43 |
+
|
| 44 |
+
**守恒公式**(按位置计算):
|
| 45 |
+
```
|
| 46 |
+
回合结束手牌位置 = 暗牌 + 吃组×3 + 碰组×3 + 明杠组×3 + 暗杠组×4 = 13
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
```
|
|
|
|
| 48 |
|
| 49 |
+
**说明**:明杠(直杠/补杠)第 4 张来自他人弃牌或补摸,只占 3 位置;暗杠 4 张全来自手牌,占 4 位置。
|
| 50 |
|
| 51 |
+
1. 默认节奏(来自 `<MECHANISM_LIBRARY>`,除非底座玩法A明确覆写):
|
| 52 |
+
- 标准起手牌为 13 张;庄家开局可为 14(必须明确"仅首轮首家多1张")
|
| 53 |
+
- 默认行牌节奏为 "摸 1 → 打 1",并在回合结束回到稳定手牌数(通常为 13)
|
| 54 |
+
2. 允许的例外(必须显式写入规则与表格,并保持闭环):
|
| 55 |
+
- 吃/碰后:默认"只打不摸"(除非规则明确允许补摸/补牌)
|
| 56 |
+
- 杠后:必须补牌(补摸/补花)后再打 1 张
|
| 57 |
+
- 若引入"连续摸牌/摸三打三"等机制:必须明确本回合"摸 N → 打 N(或等价闭环)",并确保回合结束回到稳定手牌数
|
| 58 |
+
3. 禁止出现(硬性 FAIL):
|
| 59 |
+
- 出了一张牌但手牌数量不变(未说明牌去向与替代来源)
|
| 60 |
+
- 回合动作序列结束后手牌无法回到稳定值(例如 13→14→13 是最常见闭环;特殊机制也必须闭环)
|
| 61 |
+
- 摸/打/吃/碰/杠导致手牌或牌墙流转"凭空增减",无法在 transfer 与手牌净变化中解释
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 62 |
|
| 63 |
+
**各动作位置变化速查**(引用 `<MECHANISM_LIBRARY>` 第五节):
|
| 64 |
|
| 65 |
+
| 动作 | 位置变化 | 回合结束位置 |
|
| 66 |
+
| --- | --- | --- |
|
| 67 |
+
| 摸牌→打牌 | 13→14→13 | 13 |
|
| 68 |
+
| 吃/碰→打牌 | 13→14→13 | 13 |
|
| 69 |
+
| 明杠→补摸→打牌 | 13→13→14→13 | 13 |
|
| 70 |
+
| 暗杠→补摸→打牌 | 13→13→14→13 | 13 |
|
| 71 |
+
| 补杠→补摸→打牌 | 14→13→14→13(补杠发生在摸牌后)| 13 |
|
| 72 |
|
| 73 |
+
### B) 吃碰杠与轮次影响必须显式
|
|
|
|
|
|
|
|
|
|
|
|
|
| 74 |
|
| 75 |
+
必须对齐 `<MECHANISM_LIBRARY>` 的基础逻辑(除非底座玩法A明确覆写且不破坏守恒):
|
| 76 |
+
- 同一轮优先级:胡牌 > 碰牌 > 杠牌 > 吃牌
|
| 77 |
+
- 吃:仅能吃上家牌;碰/杠:可对任意玩家
|
| 78 |
+
- 胡牌触发方式:以“自摸/点炮”为基础;若引入“抢杠胡/一炮多响”等,必须说明其如何归类到自摸/点炮以及对应结算与轮次处理
|
| 79 |
+
- 行牌顺序:庄家 → 下家 → 对家 → 上家;若发生碰/吃,则由吃/碰者继续出牌;若发生杠,则由杠者补牌后继续出牌(出牌顺序不变,只改变“当前出牌权”)
|
|
|
|
| 80 |
|
| 81 |
+
自然语言规则中必须包含:**《动作—手牌变化—轮次影响表》(硬性)**,至少包含以下动作(不可省略;不允许则写 N/A 并说明原因):
|
| 82 |
+
- 摸牌、打牌、吃、碰、直杠(明杠)、补杠、暗杠、杠后补牌
|
|
|
|
|
|
|
| 83 |
|
| 84 |
+
表格列必须包含:
|
| 85 |
+
- 牌来源→去向(transfer)
|
| 86 |
+
- 手牌净变化(净变化必须能回到稳定值)
|
| 87 |
+
- 是否强制打1张恢复13
|
| 88 |
+
- 出牌权/下一手归属(轮次控制)
|
|
|
|
| 89 |
|
| 90 |
+
### C) 最小回合推演(硬性)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 91 |
|
| 92 |
+
必须提供 3 段 **《最小回合推演》**(每段 ≤6 行),并在每行标注手牌数量:
|
| 93 |
+
1. 普通回合:摸1打1
|
| 94 |
+
2. 碰回合:他人打出→我碰→我打1
|
| 95 |
+
3. 杠回合:我杠→补1→我打1
|
| 96 |
+
|
| 97 |
+
若你引入了"连续摸牌/摸三打三/海底漫游/海捞阶段"等改变默认摸打节奏的机制,必须额外提供 1 段对应机制的最小推演(≤6 行),同样标注手牌数量。
|
| 98 |
+
|
| 99 |
+
### C.1) 特殊节奏机制闭环约束(引用 `<MECHANISM_LIBRARY>` 第二节)
|
| 100 |
+
|
| 101 |
+
| 机制 | 闭环规则 | 回合结束手牌 |
|
| 102 |
+
| --- | --- | --- |
|
| 103 |
+
| 摸三打三 | 摸 3 张 → 打 3 张 | 13 张 ✅ |
|
| 104 |
+
| 海底漫游 | 摸海底牌 → 打 1 张 或 胡牌;放弃则手牌不变 | 13 张 或 胡牌 ✅ |
|
| 105 |
+
| 海捞阶段 | 各抓 1 张(不打牌)→ 直接判定胡牌或流局 | 14 张(终结阶段,无需回到 13)✅ |
|
| 106 |
+
| 连续摸牌 | 摸 N 张 → 打 N 张 | 13 张 ✅ |
|
| 107 |
+
|
| 108 |
+
若引入上述机制,必须在自然语言规则中显式写明:
|
| 109 |
+
1. 触发条件
|
| 110 |
+
2. 动作序列(完整闭环)
|
| 111 |
+
3. 中间状态是否允许吃/碰/杠/胡
|
| 112 |
+
4. 对应的最小推演段落
|
| 113 |
+
|
| 114 |
+
### C.2) 争议机制处理约束(引用 `<MECHANISM_LIBRARY>` 第三节)
|
| 115 |
+
|
| 116 |
+
若引入以下机制,必须在规则中显式说明处理方式:
|
| 117 |
+
|
| 118 |
+
| 机制 | 必须说明的内容 |
|
| 119 |
+
| --- | --- |
|
| 120 |
+
| 抢杠胡 | (1) 算自摸还是点炮?→ 点炮 (2) 杠是否生效?→ 不生效 (3) 补牌是否发生?→ 不发生 (4) 轮次归属 |
|
| 121 |
+
| 一炮多响 | (1) 采用哪种规则?(全响/头家优先/禁止)(2) 结算方式 (3) 轮次归属 |
|
| 122 |
+
| 赖子牌 | (1) 哪些牌是赖子?(2) 能否用于杠?(3) 是否影响番型计算?|
|
| 123 |
+
|
| 124 |
+
### C.3) 复杂场景处理约束(引用 `<MECHANISM_LIBRARY>` 第三(续)节)
|
| 125 |
+
|
| 126 |
+
若玩法涉及以下复杂场景,必须在规则中显式说明处理流程:
|
| 127 |
+
|
| 128 |
+
#### 连续碰杠混合场景
|
| 129 |
+
|
| 130 |
+
| 场景 | 必须说明的内容 |
|
| 131 |
+
| --- | --- |
|
| 132 |
+
| 连续碰 | 每次碰后必须打牌,不允许碰后直接再碰;碰 A 后打 B,此时 B 可被他人碰/杠/吃/胡 |
|
| 133 |
+
| 碰后杠 | 碰后打出的牌被他人响应后,轮次转移给响应者;原碰者不保留出牌权 |
|
| 134 |
+
| 杠后碰 | 杠者补摸后打出的牌可被他人碰;碰成功则出牌权转移给碰者 |
|
| 135 |
+
| 连续杠 | 允许补摸后选择再杠而非打牌;每次杠都需补摸,连续杠时只有最后一次打牌 |
|
| 136 |
+
|
| 137 |
+
**连续场景位置守恒公式**:
|
| 138 |
+
- 连续 N 次碰:最终位置 = 13(每次碰+3明牌,打1张,净增2位置×N,但暗牌相应减少)
|
| 139 |
+
- 连续 N 次明杠:最终位置 = 13(明杠不额外占位置)
|
| 140 |
+
- 连续 N 次暗杠:最终位置 = 13 + N(每次暗杠额外占1位置)
|
| 141 |
+
|
| 142 |
+
#### 抢杠胡完整流程
|
| 143 |
+
|
| 144 |
+
抢杠胡发生时的完整决策树:
|
| 145 |
|
| 146 |
+
```
|
| 147 |
+
玩家 A 宣告补杠
|
| 148 |
+
↓
|
| 149 |
+
系统暂停,询问其他玩家是否胡牌
|
| 150 |
+
├─ 有人宣告胡 → 抢杠胡生效
|
| 151 |
+
│ ├─ 杠不生效(A 的碰组保持不变)
|
| 152 |
+
│ ├─ A 不补摸
|
| 153 |
+
│ ├─ 算 A 点炮(被抢杠的牌视为 A 打出)
|
| 154 |
+
│ └─ 结算后:血战模式→胡牌者退出,A 继续;一局一胡→本局结束
|
| 155 |
+
│
|
| 156 |
+
└─ 无人胡 → 补杠正常生效
|
| 157 |
+
├─ 碰组变为杠组
|
| 158 |
+
├─ A 补摸 1 张
|
| 159 |
+
└─ A 打 1 张,轮次继续
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 160 |
```
|
| 161 |
|
| 162 |
+
#### 多人同时响应处理
|
|
|
|
| 163 |
|
| 164 |
+
当一张牌被打出,多人同时可响应时的优先级裁决:
|
|
|
|
| 165 |
|
| 166 |
+
| 优先级 | 响应类型 | 处理规则 |
|
| 167 |
+
| --- | --- | --- |
|
| 168 |
+
| 1(最高)| 胡牌 | 一炮多响时按规则处理(全响/头家优先/禁止)|
|
| 169 |
+
| 2 | 碰牌 | 仅一人可碰,距离出牌者最近的玩家优先 |
|
| 170 |
+
| 3 | 杠牌 | 同碰牌规则 |
|
| 171 |
+
| 4(最低)| 吃牌 | 仅下家可吃,与其他响应冲突时被覆盖 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 172 |
|
| 173 |
+
**一炮多响的三种规则**:
|
| 174 |
+
1. **全响**:所有胡牌者均可胡,点炮者向每位胡牌者分别结算
|
| 175 |
+
2. **头家优先**:按出牌顺序,最先轮到的胡牌者独胡
|
| 176 |
+
3. **禁止**:多人可胡时,该牌作废,无人可胡
|
| 177 |
|
| 178 |
+
#### 复杂出牌顺序规则
|
| 179 |
+
|
| 180 |
+
| 场景 | 出牌权归属 | 下一手 |
|
| 181 |
+
| --- | --- | --- |
|
| 182 |
+
| 正常摸打 | 当前玩家 | 下家(逆时针)|
|
| 183 |
+
| 吃牌后 | 吃牌者 | 吃牌者的下家 |
|
| 184 |
+
| 碰牌后 | 碰牌者 | 碰牌者的下家 |
|
| 185 |
+
| 杠牌后 | 杠牌者(补摸后)| 杠牌者的下家 |
|
| 186 |
+
| 胡牌后(血战)| 下一位未胡玩家 | 该玩家的下家(跳过已胡者)|
|
| 187 |
+
| 抢杠胡后 | 被抢杠者(若血战)| 被抢杠者的下家 |
|
| 188 |
+
|
| 189 |
+
**血战/血流模式特殊规则**:
|
| 190 |
+
- 胡牌者退出后,其位置在轮次中被跳过
|
| 191 |
+
- 剩余玩家继续按原顺序行牌
|
| 192 |
+
- 牌墙摸完或仅剩一人时结束
|
| 193 |
+
|
| 194 |
+
若引入上述复杂场景,必须在《最小回合推演》中额外提供对应场景的推演段落(≤8 行),标注每步的位置数与出牌权归属。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 195 |
|
| 196 |
+
### D) mGDL invariants(硬性)
|
| 197 |
|
| 198 |
+
`(invariants ...)` 至少包含:
|
| 199 |
+
- `tile_conservation`:总牌数守恒(与 tileset.total 一致)
|
| 200 |
+
- `hand_size_stable`(或等价表达):声明行牌阶段“回合动作序列结束后手牌回到稳定值(通常 13;首轮庄家 14 仅在首次打出前成立)”
|
|
|
|
|
|
|
| 201 |
|
| 202 |
+
---
|
| 203 |
+
|
| 204 |
+
## 可控迭代:Analyse 模式(单问题迭代,强制)
|
| 205 |
|
| 206 |
+
当且仅当用户输入中包含:`<ANALYSE_MODE>true</ANALYSE_MODE>`,你必须进入【Analyse 模式】:
|
| 207 |
+
1. 只做需求澄清与方案收敛:**不输出 mGDL**、不输出完整自然语言规则、不输出自检报告。
|
| 208 |
+
2. 严格单问题迭代:每轮只问 1 个“当前最重要”的澄清问题。
|
| 209 |
+
3. 每轮必须输出更新后的 `DesignState(JSON)`,未知信息写入 `open_questions`。
|
| 210 |
+
4. 当 `open_questions` 为空(或仅剩非阻塞可选项)时输出 `READY_TO_GENERATE: true`,否则 `false`。
|
| 211 |
+
|
| 212 |
+
Analyse 模式输出格式固定为(必须严格遵守):
|
| 213 |
+
- 一句话总结当前理解(≤60字)
|
| 214 |
+
- 本轮唯一问题(只问1个)
|
| 215 |
+
- 思维日志(本轮增量,≤200字):为什么问/影响哪些融合旋钮/如何收敛
|
| 216 |
+
- ```json (DesignState)```(合法JSON)
|
| 217 |
+
- `READY_TO_GENERATE: true/false`
|
| 218 |
|
| 219 |
+
---
|
|
|
|
|
|
|
| 220 |
|
| 221 |
+
## 多阶段引导式交互(推荐流程)
|
|
|
|
|
|
|
| 222 |
|
| 223 |
+
为确保机制组合创新的质量,建议遵循以下多阶段交互流程:
|
|
|
|
|
|
|
| 224 |
|
| 225 |
+
### 阶段一:理解确认(Understand)
|
| 226 |
|
| 227 |
+
当用户描述涉及已有玩法(如血战到底、血流成河、国标麻将等)时:
|
|
|
|
|
|
|
| 228 |
|
| 229 |
+
1. **主动确认理解**:对用户提到的已有玩法进行理解确认
|
| 230 |
+
2. **提出确认问题**:使用 `❓` 或 `【确认】` 前缀标记问题
|
| 231 |
+
3. **示例**:
|
| 232 |
+
- ❓ 您提到的"血战到底"是否指四川麻将的"三家胡完才结束"模式?
|
| 233 |
+
- 【确认】您希望保留血战的"缺一门"约束还是去掉?
|
| 234 |
|
| 235 |
+
**注意**:如果对已有玩法的理解存在歧义,必须先确认再进行下一步。
|
|
|
|
|
|
|
|
|
|
|
|
|
| 236 |
|
| 237 |
+
### 阶段二:方案发散(Diverge)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 238 |
|
| 239 |
+
在明确已有玩法后,进行机制组合创新时:
|
| 240 |
|
| 241 |
+
1. **发散思维**:给出 2-4 种不同方向的机制组合方案
|
| 242 |
+
2. **输出格式**:
|
| 243 |
+
```
|
| 244 |
+
### 方案 A: [方案名称]
|
| 245 |
+
✦ 创新点:[1-2句话概述]
|
| 246 |
+
✦ 机制组合:[简述要融合的机制]
|
| 247 |
|
| 248 |
+
### 方案 B: [方案名称]
|
| 249 |
+
✦ 创新点:[1-2句话概述]
|
| 250 |
+
✦ 机制组合:[简述要融合的机制]
|
|
|
|
|
|
|
| 251 |
|
| 252 |
+
### 方案 C: [方案名称]
|
| 253 |
+
...
|
| 254 |
+
```
|
| 255 |
+
3. **要求**:
|
| 256 |
+
- 每个方案有明确差异化(不同的创新方向/目标用户/玩法复杂度)
|
| 257 |
+
- 不要深入展开,只给概要供用户选择
|
| 258 |
+
- 方案编号使用 A/B/C/D 或 一/二/三/四
|
| 259 |
|
| 260 |
+
### 阶段三:方案选择(Select)
|
| 261 |
|
| 262 |
+
等待用户选择具体方案:
|
| 263 |
+
- 用户可选择某个方案编号
|
| 264 |
+
- 用户可提出"其他"并描述自己的想法
|
| 265 |
+
- 用户可要求"重新发散"获取更多方案
|
|
|
|
|
|
|
| 266 |
|
| 267 |
+
### 阶段四:深入展开(Elaborate)
|
| 268 |
|
| 269 |
+
用户选择方案后,对该方案进行完整设计:
|
| 270 |
+
1. 输出完整的 DesignState(JSON)
|
| 271 |
+
2. 输出完整的 mGDL
|
| 272 |
+
3. 输出自然语言规则说明
|
| 273 |
+
4. 输出自检报告
|
| 274 |
|
| 275 |
+
### 阶段判断规则
|
|
|
|
|
|
|
|
|
|
|
|
|
| 276 |
|
| 277 |
+
- 若用户首次提出需求且涉及多个已有玩法 → 先进入「理解确认」
|
| 278 |
+
- 若理解已确认但尚未给出多方案 → 进入「方案发散」
|
| 279 |
+
- 若用户从多方案中选择了一个 → 进入「深入展开」
|
| 280 |
+
- 若已有完整 DesignState 且用户提出修改 → 进入「迭代优化」
|
| 281 |
|
| 282 |
+
---
|
| 283 |
|
| 284 |
+
## 生成模式输出(必须严格按顺序)
|
| 285 |
|
| 286 |
+
### 思考与自检过程
|
| 287 |
+
用自然语言描述你做了哪些检查、发现哪些 FAIL、如何"最小修改"修复、最终确认通过。至少覆盖:
|
| 288 |
+
- 是否输出《动作—手牌变化—轮次影响表》
|
| 289 |
+
- 是否输出《最小回合推演》(普通/碰/杠三段)
|
| 290 |
+
- 胡后模式是否与底座一致(血战/血流/一局一胡)
|
| 291 |
+
- 计分字段是否与计分模式一致(multiplier→mult,fan_system→fan,hybrid→mult+category)
|
| 292 |
+
- mGDL 是否包含核心模块且无占位符
|
| 293 |
+
- invariants 是否包含 tile_conservation 与 hand_size_stable
|
| 294 |
+
- 若涉及复杂场景(连续碰杠/抢杠胡/多人响应),是否有对应推演段落与出牌权说明
|
| 295 |
|
| 296 |
+
### 设计日志(创新推演摘要)
|
| 297 |
+
本阶段的设计日志聚焦“融合决策”,必须包含:
|
| 298 |
+
1. **融合清单**:从A保留哪些核心;从B引入哪些机制(逐条)
|
| 299 |
+
2. **冲突与桥接规则**:冲突点 → 桥接方案(最小且可量化)→ 为什么不破坏底座
|
| 300 |
+
3. **底层正确性证据**:引用你后文《动作—手牌变化—轮次影响表》《最小回合推演》的关键行(用文字描述即可)
|
| 301 |
+
4. **落地映射(Crosswalk)**:机制名 → `mGDL_path` → `transfer_path(若有)`
|
| 302 |
|
| 303 |
+
### DesignState(JSON,可机读)
|
| 304 |
+
输出一个合法 JSON(不要注释、不要省略号)。至少包含:
|
| 305 |
+
```json
|
| 306 |
+
{
|
| 307 |
+
"base_variants": ["底座玩法名"],
|
| 308 |
+
"fusion_variants": ["融合玩法名"],
|
| 309 |
+
"new_variant_name": "新玩法名",
|
| 310 |
+
"game_variant": "blood_war|blood_flow|round|multi_round|custom",
|
| 311 |
+
"players": 4,
|
| 312 |
+
"scoring_mode": "multiplier|fan_system|hybrid",
|
| 313 |
+
"tileset": {"suits":["wan","tong","tiao"],"honors":0,"special_tiles":[],"total":108},
|
| 314 |
+
"core_constraints": ["手牌守恒:回合结束回到13张","牌数守恒:所有区域之和=TotalTiles"],
|
| 315 |
+
"mechanics": [
|
| 316 |
+
{"name":"机制名","type":"core|aux","phase":"setup|play|settle|global","trigger":"...","cost":"...","reward_or_penalty":"...","counterplay":"...","mgdl_path":"extensions.special_mechanics.mechanic_cards.<ID>","transfer_path":"from: X to: Y|none"}
|
| 317 |
+
],
|
| 318 |
+
"open_questions": []
|
| 319 |
+
}
|
| 320 |
+
```
|
| 321 |
+
|
| 322 |
+
### 游戏名称
|
| 323 |
+
[游戏名称]
|
| 324 |
+
|
| 325 |
+
### 游戏理念
|
| 326 |
+
[≤200字:强调“融合点与体验变化”,避免空泛口号]
|
| 327 |
+
|
| 328 |
+
### mGDL描述
|
| 329 |
+
必须输出完整 mGDL v1.3(使用 ```lisp 代码块```)。必须包含核心模块:
|
| 330 |
+
`(game_variant ...) (players ...) (tileset ...) (extensions ...) (seats ...) (turn_order ...) (setup ...) (actions ...) (win_rules ...) (scoring ...) (fan_table ...) (settlement ...) (invariants ...)`
|
| 331 |
+
|
| 332 |
+
硬性规则:
|
| 333 |
+
- 禁止 `<PID>` 等占位符,PID 展开为 A1/A2/A3/A4
|
| 334 |
+
- tileset 必须包含 `(total N)`
|
| 335 |
+
- `(extensions (special_mechanics (mechanic_cards ...)))` 必须存在(即使为空也要给结构)
|
| 336 |
+
|
| 337 |
+
### 自然语言规则说明
|
| 338 |
+
必须包含以下小节(按标题输出,禁止省略):
|
| 339 |
+
1. 新机制声明(机制声明表 + 每条机制展开 + `mGDL_path`)
|
| 340 |
+
2. 基础规则(牌组构成、起手/牌墙、座位与顺序、允许吃碰杠胡)
|
| 341 |
+
3. 游戏流程(按阶段;明确"谁出牌权/下一手是谁"的规则)
|
| 342 |
+
4. 《动作—手牌变化—轮次影响表》(硬性)
|
| 343 |
+
5. 《最小回合推演》(硬性;若涉及复杂场景需额外提��对应推演)
|
| 344 |
+
6. 得分体系(继承底座体系;B机制若影响计分必须桥接)
|
| 345 |
+
7. 复杂场景处理(若涉及连续碰杠/抢杠胡/多人响应/血战轮次,必须显式说明决策流程与出牌权归属)
|
| 346 |
+
|
| 347 |
+
### 技术附录:《自检报告》(简化)
|
| 348 |
+
用不超过 15 行列出关键 PASS/FAIL 与修复点即可,重点列:
|
| 349 |
+
手牌守恒、三段推演、mGDL模块齐全、tile_conservation、hand_size_stable、计分字段一致性。
|
| 350 |
+
若涉及复杂场景,额外检查:连续碰杠推演、抢杠胡流程、多人响应优先级、出牌权归属。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
output_validator.py
CHANGED
|
@@ -12,6 +12,8 @@ from typing import Dict, List, Optional, Tuple
|
|
| 12 |
|
| 13 |
_FENCE_RE = re.compile(r"```(?:[a-zA-Z0-9_-]+)?\n(.*?)```", re.DOTALL)
|
| 14 |
|
|
|
|
|
|
|
| 15 |
|
| 16 |
def _extract_mgdl_block(text: str) -> str:
|
| 17 |
"""
|
|
@@ -35,6 +37,15 @@ def _extract_mgdl_block(text: str) -> str:
|
|
| 35 |
return best
|
| 36 |
|
| 37 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
def validate_mahjong_response(text: str) -> List[Dict[str, str]]:
|
| 39 |
issues: List[Dict[str, str]] = []
|
| 40 |
if not text or not text.strip():
|
|
@@ -58,14 +69,104 @@ def validate_mahjong_response(text: str) -> List[Dict[str, str]]:
|
|
| 58 |
return issues
|
| 59 |
|
| 60 |
# 0.5) 思维日志(设计日志)检查:生成模式下应包含“设计日志(创新推演摘要)”
|
| 61 |
-
#
|
| 62 |
if "设计日志(创新推演摘要)" not in text:
|
| 63 |
issues.append({
|
| 64 |
"code": "NO_DESIGN_LOG",
|
| 65 |
"level": "warning",
|
| 66 |
-
"message": "未检测到“设计日志(创新推演摘要)”段落;建议补齐
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 67 |
})
|
| 68 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 69 |
# 2) 核心模块检查(按 m_prompt 的“零容忍项”)
|
| 70 |
required_markers = [
|
| 71 |
"(game_variant",
|
|
@@ -107,6 +208,20 @@ def validate_mahjong_response(text: str) -> List[Dict[str, str]]:
|
|
| 107 |
"message": "tileset 中未检测到 (total N),容易导致牌数不自洽。"
|
| 108 |
})
|
| 109 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 110 |
return issues
|
| 111 |
|
| 112 |
|
|
|
|
| 12 |
|
| 13 |
_FENCE_RE = re.compile(r"```(?:[a-zA-Z0-9_-]+)?\n(.*?)```", re.DOTALL)
|
| 14 |
|
| 15 |
+
_CJK_GT = r"[>>]"
|
| 16 |
+
|
| 17 |
|
| 18 |
def _extract_mgdl_block(text: str) -> str:
|
| 19 |
"""
|
|
|
|
| 37 |
return best
|
| 38 |
|
| 39 |
|
| 40 |
+
def _has_any(text: str, needles: List[str]) -> bool:
|
| 41 |
+
t = text or ""
|
| 42 |
+
return any(n in t for n in needles)
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
def _re_search(pattern: str, text: str) -> bool:
|
| 46 |
+
return bool(re.search(pattern, text or "", flags=re.DOTALL))
|
| 47 |
+
|
| 48 |
+
|
| 49 |
def validate_mahjong_response(text: str) -> List[Dict[str, str]]:
|
| 50 |
issues: List[Dict[str, str]] = []
|
| 51 |
if not text or not text.strip():
|
|
|
|
| 69 |
return issues
|
| 70 |
|
| 71 |
# 0.5) 思维日志(设计日志)检查:生成模式下应包含“设计日志(创新推演摘要)”
|
| 72 |
+
# Phase-1 聚焦“融合决策可审核”,避免停留在文本拼接。
|
| 73 |
if "设计日志(创新推演摘要)" not in text:
|
| 74 |
issues.append({
|
| 75 |
"code": "NO_DESIGN_LOG",
|
| 76 |
"level": "warning",
|
| 77 |
+
"message": "未检测到“设计日志(创新推演摘要)”段落;Phase-1 建议补齐融合清单/冲突桥接/推演摘要/落地映射。"
|
| 78 |
+
})
|
| 79 |
+
|
| 80 |
+
# 0.6) 底层物理守恒表达检查(当前阶段重点)
|
| 81 |
+
# 目标:强制模型在自然语言规则里显式给出“动作-手牌变化-轮次影响表”和“最小回合推演”
|
| 82 |
+
if "动作—手牌变化—轮次影响表" not in text and "动作-手牌变化-轮次影响表" not in text:
|
| 83 |
+
issues.append({
|
| 84 |
+
"code": "NO_HAND_DELTA_TABLE",
|
| 85 |
+
"level": "warning",
|
| 86 |
+
"message": "未检测到《动作—手牌变化—轮次影响表》;该表用于避免“出牌后手牌不变”等守恒错误,建议补齐。"
|
| 87 |
+
})
|
| 88 |
+
if "最小回合推演" not in text:
|
| 89 |
+
issues.append({
|
| 90 |
+
"code": "NO_MIN_SIMULATION",
|
| 91 |
+
"level": "warning",
|
| 92 |
+
"message": "未检测到“最小回合推演”(普通/碰/杠三段);建议补齐以验证手牌守恒与轮次控制。"
|
| 93 |
+
})
|
| 94 |
+
|
| 95 |
+
# 0.7) “硬真理”与机制说明对齐检查(以自然语言显式声明为主)
|
| 96 |
+
# 说明:这里做的是“声明存在性”的静态校验(不是逻辑证明),用于减少模型忘写/乱写导致的回归。
|
| 97 |
+
if not _re_search(r"(起手|初始).*13\s*张", text):
|
| 98 |
+
issues.append({
|
| 99 |
+
"code": "NO_START_HAND_13",
|
| 100 |
+
"level": "warning",
|
| 101 |
+
"message": "未显式声明“标准起手 13 张”(麻将机制说明的基础逻辑);建议在基础规则中补一句。"
|
| 102 |
+
})
|
| 103 |
+
if not _re_search(r"(摸|抓).*(14\s*张)", text):
|
| 104 |
+
issues.append({
|
| 105 |
+
"code": "NO_DRAW_TO_14",
|
| 106 |
+
"level": "warning",
|
| 107 |
+
"message": "未显式声明“摸牌后手牌为 14 张”(基础逻辑);建议补充以便审计守恒。"
|
| 108 |
+
})
|
| 109 |
+
if not _re_search(r"(打|弃|出).*(回到|恢复|为).*(13\s*张)", text):
|
| 110 |
+
issues.append({
|
| 111 |
+
"code": "NO_DISCARD_BACK_13",
|
| 112 |
+
"level": "warning",
|
| 113 |
+
"message": "未显式声明“打牌后手牌回到 13 张”(基础逻辑);建议补充以便审计守恒。"
|
| 114 |
+
})
|
| 115 |
+
|
| 116 |
+
# 胡/碰/杠/吃优先级(允许不同符号表达)
|
| 117 |
+
if not _re_search(rf"胡.*{_CJK_GT}.*碰.*{_CJK_GT}.*杠.*{_CJK_GT}.*吃", text):
|
| 118 |
+
issues.append({
|
| 119 |
+
"code": "NO_PRIORITY_ORDER",
|
| 120 |
+
"level": "warning",
|
| 121 |
+
"message": "未显式声明“胡>碰>杠>吃”的响应优先级(基础逻辑);建议补齐以避免争议场景。"
|
| 122 |
})
|
| 123 |
|
| 124 |
+
# 吃的限制
|
| 125 |
+
if not _has_any(text, ["仅能吃上家", "只能吃上家", "只可吃上家"]):
|
| 126 |
+
issues.append({
|
| 127 |
+
"code": "NO_CHI_UPWIND_ONLY",
|
| 128 |
+
"level": "warning",
|
| 129 |
+
"message": "未显式声明“吃仅能吃上家牌”(基础逻辑);建议补齐。"
|
| 130 |
+
})
|
| 131 |
+
|
| 132 |
+
# 行牌顺序与出牌权归属(声明存在性)
|
| 133 |
+
if not _has_any(text, ["庄家-下家-对家-上家", "庄家→下家→对家→上家", "庄家 → 下家 → 对家 → 上家"]):
|
| 134 |
+
issues.append({
|
| 135 |
+
"code": "NO_TURN_ORDER_BASE",
|
| 136 |
+
"level": "warning",
|
| 137 |
+
"message": "未显式声明“庄家-下家-对家-上家”的行牌顺序(基础逻辑);建议补齐。"
|
| 138 |
+
})
|
| 139 |
+
if not _has_any(text, ["由碰牌的玩家继续出牌", "由吃/碰者继续出牌", "碰后由碰者出牌"]):
|
| 140 |
+
issues.append({
|
| 141 |
+
"code": "NO_POST_PENG_RIGHTS",
|
| 142 |
+
"level": "warning",
|
| 143 |
+
"message": "未显式声明“碰/吃后由碰/吃者继续出牌”的出���权规则(基础逻辑);建议补齐。"
|
| 144 |
+
})
|
| 145 |
+
if not _has_any(text, ["由杠牌的玩家摸牌后继续出牌", "杠后由杠者补牌后继续出牌", "杠后由杠者继续出牌"]):
|
| 146 |
+
issues.append({
|
| 147 |
+
"code": "NO_POST_KONG_RIGHTS",
|
| 148 |
+
"level": "warning",
|
| 149 |
+
"message": "未显式声明“杠后由杠者补牌/摸牌后继续出牌”的出牌权规则(基础逻辑);建议补齐。"
|
| 150 |
+
})
|
| 151 |
+
|
| 152 |
+
# 自摸/点炮触发方式
|
| 153 |
+
if not (_has_any(text, ["自摸"]) and _has_any(text, ["点炮"])):
|
| 154 |
+
issues.append({
|
| 155 |
+
"code": "NO_ZIMO_DIANPAO",
|
| 156 |
+
"level": "warning",
|
| 157 |
+
"message": "未同时出现“自摸/点炮”两种胡牌触发方式(基础逻辑);建议补齐。"
|
| 158 |
+
})
|
| 159 |
+
|
| 160 |
+
# 若引入改变摸打节奏的机制,建议额外最小推演(Prompt 已要求)
|
| 161 |
+
special_rhythm_terms = ["连续摸", "摸三打三", "海底漫游", "海捞阶段", "海捞区"]
|
| 162 |
+
if _has_any(text, special_rhythm_terms):
|
| 163 |
+
if not _re_search(r"(最小回合推演).*(" + "|".join(map(re.escape, special_rhythm_terms)) + ")", text):
|
| 164 |
+
issues.append({
|
| 165 |
+
"code": "NO_SPECIAL_MIN_SIM",
|
| 166 |
+
"level": "warning",
|
| 167 |
+
"message": "检测到改变摸打节奏的机制(如 摸三打三/连续摸/海捞),但未看到对应机制的额外“最小回合推演”;建议补齐以验证守恒。"
|
| 168 |
+
})
|
| 169 |
+
|
| 170 |
# 2) 核心模块检查(按 m_prompt 的“零容忍项”)
|
| 171 |
required_markers = [
|
| 172 |
"(game_variant",
|
|
|
|
| 208 |
"message": "tileset 中未检测到 (total N),容易导致牌数不自洽。"
|
| 209 |
})
|
| 210 |
|
| 211 |
+
# 5) invariants 强制项(Prompt 硬性要求)
|
| 212 |
+
if "tile_conservation" not in mgdl:
|
| 213 |
+
issues.append({
|
| 214 |
+
"code": "NO_TILE_CONSERVATION",
|
| 215 |
+
"level": "warning",
|
| 216 |
+
"message": "(invariants ...) 中未检测到 tile_conservation;建议补齐以显式声明牌数守恒。"
|
| 217 |
+
})
|
| 218 |
+
if "hand_size_stable" not in mgdl:
|
| 219 |
+
issues.append({
|
| 220 |
+
"code": "NO_HAND_SIZE_STABLE",
|
| 221 |
+
"level": "warning",
|
| 222 |
+
"message": "(invariants ...) 中未检测到 hand_size_stable(或等价声明);建议补齐以约束回合结束手牌稳定值。"
|
| 223 |
+
})
|
| 224 |
+
|
| 225 |
return issues
|
| 226 |
|
| 227 |
|
requirements.txt
CHANGED
|
@@ -1,2 +1,2 @@
|
|
| 1 |
-
gradio=
|
| 2 |
-
openai>=1.0.0
|
|
|
|
| 1 |
+
gradio>=4.27.0
|
| 2 |
+
openai>=1.0.0
|
styles.py
CHANGED
|
@@ -229,4 +229,36 @@ gap:8px;
|
|
| 229 |
/* 头像与气泡的间距与对齐(可选) */
|
| 230 |
.custom-chatbot .message{ gap: 12px !important; align-items: flex-start !important; }
|
| 231 |
.custom-chatbot .message .message-content{ margin-top: 2px !important; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 232 |
"""
|
|
|
|
| 229 |
/* 头像与气泡的间距与对齐(可选) */
|
| 230 |
.custom-chatbot .message{ gap: 12px !important; align-items: flex-start !important; }
|
| 231 |
.custom-chatbot .message .message-content{ margin-top: 2px !important; }
|
| 232 |
+
|
| 233 |
+
/* === 差异摘要样式 === */
|
| 234 |
+
.diff-summary {
|
| 235 |
+
background: rgba(0, 0, 0, 0.25) !important;
|
| 236 |
+
border: 1px solid rgba(255, 255, 255, 0.12) !important;
|
| 237 |
+
border-radius: 12px !important;
|
| 238 |
+
padding: 12px 16px !important;
|
| 239 |
+
font-size: 13px !important;
|
| 240 |
+
line-height: 1.6 !important;
|
| 241 |
+
color: var(--ink) !important;
|
| 242 |
+
}
|
| 243 |
+
|
| 244 |
+
.diff-summary strong {
|
| 245 |
+
color: #ffcc4d !important;
|
| 246 |
+
}
|
| 247 |
+
|
| 248 |
+
.diff-summary code {
|
| 249 |
+
background: rgba(255, 255, 255, 0.08) !important;
|
| 250 |
+
padding: 2px 6px !important;
|
| 251 |
+
border-radius: 4px !important;
|
| 252 |
+
font-size: 12px !important;
|
| 253 |
+
color: #7dd3fc !important;
|
| 254 |
+
}
|
| 255 |
+
|
| 256 |
+
.diff-summary em {
|
| 257 |
+
color: #94a3b8 !important;
|
| 258 |
+
}
|
| 259 |
+
|
| 260 |
+
.diff-summary s, .diff-summary del {
|
| 261 |
+
color: #6b7280 !important;
|
| 262 |
+
text-decoration: line-through !important;
|
| 263 |
+
}
|
| 264 |
"""
|
麻将机制说明.md
CHANGED
|
@@ -1,27 +1,796 @@
|
|
| 1 |
# 麻将机制说明
|
| 2 |
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
|
| 12 |
-
|
|
| 13 |
-
|
|
| 14 |
-
|
|
| 15 |
-
|
|
| 16 |
-
|
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
|
| 22 |
-
| --- | --- |
|
| 23 |
-
|
|
| 24 |
-
|
|
| 25 |
-
|
|
| 26 |
-
|
|
| 27 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
# 麻将机制说明
|
| 2 |
|
| 3 |
+
本文档作为机制词典,用于麻将玩法融合时的创新机制选型与底层逻辑约束参考。
|
| 4 |
+
|
| 5 |
+
---
|
| 6 |
+
|
| 7 |
+
## 术语表
|
| 8 |
+
|
| 9 |
+
### 牌组类型
|
| 10 |
+
|
| 11 |
+
| 术语 | 定义 | 张数 | 示例 |
|
| 12 |
+
| --- | --- | --- | --- |
|
| 13 |
+
| **顺子** | 同花色连续 3 张牌 | 3 张 | 🀇🀈🀉(一二三万) |
|
| 14 |
+
| **刻子** | 3 张完全相同的牌 | 3 张 | 🀇🀇🀇(三张一万) |
|
| 15 |
+
| **杠子** | 4 张完全相同的牌 | 4 张 | 🀇🀇🀇🀇(四张一万) |
|
| 16 |
+
| **对子** | 2 张完全相同的牌 | 2 张 | 🀇🀇(两张一万) |
|
| 17 |
+
| **雀头** | 胡牌时必须的 1 个对子 | 2 张 | 胡牌牌型中的那个对子 |
|
| 18 |
+
|
| 19 |
+
### 明暗区分
|
| 20 |
+
|
| 21 |
+
| 术语 | 定义 | 形成方式 |
|
| 22 |
+
| --- | --- | --- |
|
| 23 |
+
| **暗牌** | 手中未公开的牌 | 自己摸的牌 |
|
| 24 |
+
| **明牌** | 公开摆在桌面的牌组 | 通过吃/碰/明杠形成 |
|
| 25 |
+
| **暗刻** | 自己摸齐的刻子(未公开) | 自己摸到 3 张相同的牌 |
|
| 26 |
+
| **明刻** | 通过碰形成的刻子(公开) | 碰他人打出的牌 |
|
| 27 |
+
| **暗杠** | 自己摸齐的杠子(扣着摆放) | 自己摸到 4 张相同的牌后宣告 |
|
| 28 |
+
| **明杠** | 通过杠他人的牌形成(公开) | 杠他人打出的牌(直杠)或补杠 |
|
| 29 |
+
|
| 30 |
+
### 动作术语
|
| 31 |
+
|
| 32 |
+
| 术语 | 定义 | 条件 |
|
| 33 |
+
| --- | --- | --- |
|
| 34 |
+
| **吃** | 用手中 2 张牌 + 上家打出的 1 张牌组成顺子 | 仅能吃上家的牌 |
|
| 35 |
+
| **碰** | 用手中 2 张牌 + 他人打出的 1 张牌组成刻子 | 可碰任意玩家的牌 |
|
| 36 |
+
| **杠** | 用 4 张相同的牌组成杠子 | 分明杠/暗杠/补杠 |
|
| 37 |
+
| **直杠(明杠)** | 用手中 3 张 + 他人打出的 1 张组成杠 | 可杠任意玩家的牌 |
|
| 38 |
+
| **补杠(加杠)** | 将手中 1 张补到已碰的刻子上变成杠 | 需先有碰出的明刻 |
|
| 39 |
+
| **暗杠** | 用手中 4 张相同的牌组成杠 | 摸牌后宣告 |
|
| 40 |
+
| **胡(和)** | 凑成完整牌型,宣告获胜 | 满足胡牌条件 |
|
| 41 |
+
| **自摸** | 自己摸到的牌凑成胡牌 | 摸牌后胡牌 |
|
| 42 |
+
| **点炮(放炮)** | 打出的牌被他人胡 | 他人胡你打出的牌 |
|
| 43 |
+
| **听牌** | 只差 1 张牌即可胡牌的状态 | 差一张即可成胡 |
|
| 44 |
+
|
| 45 |
+
### 特殊��牌
|
| 46 |
+
|
| 47 |
+
| 术语 | 定义 |
|
| 48 |
+
| --- | --- |
|
| 49 |
+
| **杠上开花** | 杠牌后补摸的牌正好能胡(自摸) |
|
| 50 |
+
| **杠上炮** | 杠牌后打出的牌被他人胡(点炮) |
|
| 51 |
+
| **抢杠胡** | 他人补杠时,胡那张补杠的牌 |
|
| 52 |
+
| **海底捞月** | 摸最后一张牌(海底牌)胡牌 |
|
| 53 |
+
| **妙手回春** | 摸最后一张牌自摸胡牌(同海底捞月) |
|
| 54 |
+
| **河底捞鱼** | 胡最后一个人打出的牌 |
|
| 55 |
+
|
| 56 |
+
### 位置与顺序
|
| 57 |
+
|
| 58 |
+
| 术语 | 定义 |
|
| 59 |
+
| --- | --- |
|
| 60 |
+
| **庄家** | 本局先出牌的玩家,通常起手 14 张 |
|
| 61 |
+
| **闲家** | 非庄家的玩家,起手 13 张 |
|
| 62 |
+
| **上家** | 你的上一个出牌者(你吃牌的来源) |
|
| 63 |
+
| **下家** | 你的下一个出牌者(你出牌后轮到的人) |
|
| 64 |
+
| **对家** | 坐在你对面的玩家 |
|
| 65 |
+
|
| 66 |
+
### 牌墙与区域
|
| 67 |
+
|
| 68 |
+
| 术语 | 定义 |
|
| 69 |
+
| --- | --- |
|
| 70 |
+
| **牌墙** | 洗牌后码好的牌,玩家从此处摸牌 |
|
| 71 |
+
| **牌河(弃牌堆)** | 玩家打出的牌摆放的区域 |
|
| 72 |
+
| **海底牌** | 牌墙中最后一张可摸的牌 |
|
| 73 |
+
| **王牌** | 牌墙末尾保留不参与摸牌的牌(部分规则) |
|
| 74 |
+
|
| 75 |
+
---
|
| 76 |
+
|
| 77 |
+
## 一、基础逻辑(底层守恒硬约束)
|
| 78 |
+
|
| 79 |
+
### 1.0 牌组构成与基础限定
|
| 80 |
+
|
| 81 |
+
**标准麻将牌组**(以四川麻将/血战为例):
|
| 82 |
+
|
| 83 |
+
| 花色 | 牌面 | 每种张数 | 小计 |
|
| 84 |
+
| --- | --- | --- | --- |
|
| 85 |
+
| 万(萬)| 一万~九万 | 各 4 张 | 36 张 |
|
| 86 |
+
| 条(索)| 一条~九条 | 各 4 张 | 36 张 |
|
| 87 |
+
| 筒(饼)| 一筒~九筒 | 各 4 张 | 36 张 |
|
| 88 |
+
| **总计** | | | **108 张** |
|
| 89 |
+
|
| 90 |
+
**完整麻将牌组**(国标/日麻等):
|
| 91 |
+
|
| 92 |
+
| 花色 | 牌面 | 每种张数 | 小计 |
|
| 93 |
+
| --- | --- | --- | --- |
|
| 94 |
+
| 万 | 一万~九万 | 各 4 张 | 36 张 |
|
| 95 |
+
| 条 | 一条~九条 | 各 4 张 | 36 张 |
|
| 96 |
+
| 筒 | 一筒~九筒 | 各 4 张 | 36 张 |
|
| 97 |
+
| 风牌 | 东、南、西、北 | 各 4 张 | 16 张 |
|
| 98 |
+
| 箭牌 | 中、发、白 | 各 4 张 | 12 张 |
|
| 99 |
+
| 花牌 | 春夏秋冬、梅兰竹菊 | 各 1 张 | 8 张 |
|
| 100 |
+
| **总计** | | | **144 张** |
|
| 101 |
+
|
| 102 |
+
**基础限定(硬约束)**:
|
| 103 |
+
- **每种牌最多 4 张**:同一种牌(如"一万")在整副牌中只有 4 张
|
| 104 |
+
- 因此:**同种牌最多组成 1 个杠**(4 张全部用完)
|
| 105 |
+
- 因此:**碰/刻子最多 4 组同种牌**(理论上,实际受手牌限制)
|
| 106 |
+
- 因此:同种牌(最多4张)最多只能组成 1 个刻子(3张)或 1 个杠(4张);其余只能作将/散张,无法形成多组同牌刻子
|
| 107 |
+
|
| 108 |
+
**牌墙与王牌**:
|
| 109 |
+
- 发牌前:所有牌洗匀后码成牌墙
|
| 110 |
+
- 王牌(部分规则):牌墙末尾保留若干张不参与摸牌(如日麻保留 14 张作为岭上牌和宝牌指示牌)
|
| 111 |
+
|
| 112 |
+
### 1.1 手牌数量定义与守恒
|
| 113 |
+
|
| 114 |
+
| 术语 | 定义 |
|
| 115 |
+
| --- | --- |
|
| 116 |
+
| 暗牌 | 手中未公开的牌 |
|
| 117 |
+
| 明牌组 | 通过吃/碰/杠形成的公开牌组(摆在桌面) |
|
| 118 |
+
| 回合结束牌数 | 玩家持有的总牌数(暗牌+明牌组)。**无杠时恒为 13 张**,每有 1 个杠则 +1 张(首轮庄家首次出牌前为 14) |
|
| 119 |
+
|
| 120 |
+
**守恒公式**(按实际牌数计算):
|
| 121 |
+
```
|
| 122 |
+
回合结束牌数 = 暗牌 + 吃组×3 + 碰组×3 + 杠组×4 = 13 + 杠组数
|
| 123 |
+
```
|
| 124 |
+
|
| 125 |
+
**说明**:
|
| 126 |
+
- 无杠时:回合结束牌数 = 13
|
| 127 |
+
- 每有 1 个杠(无论明杠/暗杠),回合结束牌数 +1
|
| 128 |
+
- 原因:杠后补摸 1 张,打 1 张,杠组本身 4 张;相比碰组(3张),杠组多 1 张牌
|
| 129 |
+
|
| 130 |
+
**各类杠的牌数变化**:
|
| 131 |
+
| 杠类型 | 牌来源 | 牌数变化 | 回合结束牌数 |
|
| 132 |
+
| --- | --- | --- | --- |
|
| 133 |
+
| 明杠(直杠)| 他人弃牌 1 张 + 手中 3 张 | 13→14→15→14(杠+1→补摸+1→打-1)| 14 |
|
| 134 |
+
| 暗杠 | 摸牌后手中 4 张 | 14→14→15→14(暗杠0→补摸+1→打-1)| 14 |
|
| 135 |
+
| 补杠 | 摸牌后手中 1 张补到碰组 | 14→14→15→14(补杠0→补摸+1→打-1)| 14 |
|
| 136 |
+
|
| 137 |
+
### 1.2 摸牌-打牌循环
|
| 138 |
+
|
| 139 |
+
| 场景 | 动作序列 | 牌数变化 | 回合结束牌数 |
|
| 140 |
+
| --- | --- | --- | --- |
|
| 141 |
+
| 普通回合 | 摸 1 张 → 打 1 张 | 13 → 14 → 13 | 13 |
|
| 142 |
+
| 吃牌后 | 取上家弃牌 + 手中 2 张组顺子 → 打 1 张 | 13 → 14 → 13 | 13 |
|
| 143 |
+
| 碰牌后 | 取他人弃牌 + 手中 2 张组刻子 → 打 1 张 | 13 → 14 → 13 | 13 |
|
| 144 |
+
| 直杠(明杠)后 | 取他人弃牌 + 手中 3 张组杠 → 补摸 → 打 1 张 | 13 → 14 → 15 → 14 | 14 |
|
| 145 |
+
| 补杠后 | 摸牌后手中 1 张补到碰组 → 补摸 → 打 1 张 | 14 → 14 → 15 → 14 | 14 |
|
| 146 |
+
| 暗杠后 | 摸牌后手中 4 张组暗杠 → 补摸 → 打 1 张 | 14 → 14 → 15 → 14 | 14 |
|
| 147 |
+
|
| 148 |
+
**牌数变化详解**:
|
| 149 |
+
- **吃/碰**:取弃牌后形成 3 张明牌组,暗牌减 2,总牌数 +1(变为14)→ 打牌后回到 13
|
| 150 |
+
- **明杠**:杠后形成 4 张杠组(取弃牌1张+手中3张),暗牌减 3,总牌数+1(1+13=14)→ 补摸(+1=15)→ 打牌(14)→ 比普通回合多 1 张
|
| 151 |
+
- **暗杠**:发生在摸牌后(14张),用手中 4 张组杠,总牌数不变(14)→ 补摸(+1=15)→ 打牌(-1=14)→ 比普通回合多 1 张
|
| 152 |
+
- **补杠**:发生在摸牌后(14张),将 1 张补到碰组变杠,总牌数不变(14)→ 补摸(+1=15)→ 打牌(-1=14)→ 比普通回合多 1 张
|
| 153 |
+
|
| 154 |
+
### 1.3 吃碰杠胡优先级
|
| 155 |
+
|
| 156 |
+
同一张弃牌被多人响应时的优先级:
|
| 157 |
+
|
| 158 |
+
```
|
| 159 |
+
胡牌 > 碰牌 > 杠牌 > 吃牌
|
| 160 |
+
胡最高;碰/杠通常同级(或按本规则设定杠优先/碰优先);吃最低;同级多人冲突按离出牌者最近优先(或按座次规则)。
|
| 161 |
+
```
|
| 162 |
+
|
| 163 |
+
- **吃**:仅能吃上家牌
|
| 164 |
+
- **碰/杠**:可对任意玩家的弃牌
|
| 165 |
+
- **胡**:可对任意玩家的弃牌(点炮)或自己摸到的牌(自摸)
|
| 166 |
+
|
| 167 |
+
### 1.4 胡牌触发条件
|
| 168 |
+
|
| 169 |
+
| 触发方式 | 说明 | 结算责任 |
|
| 170 |
+
| --- | --- | --- |
|
| 171 |
+
| 自摸 | 自己摸牌凑成胡牌型 | 其他三家各自支付(或庄家倍付,视规则) |
|
| 172 |
+
| 点炮 | 吃进其他玩家打出的牌凑成胡牌型 | 点炮者支付(或三家分摊,视规则) |
|
| 173 |
+
|
| 174 |
+
### 1.5 胡牌模式(游戏结束条件)
|
| 175 |
+
|
| 176 |
+
| 模式 | 说明 | 结算时机 |
|
| 177 |
+
| --- | --- | --- |
|
| 178 |
+
| **一局一胡** | 有人胡牌后本局立即结束 | 胡牌时结算,然后开始下一局 |
|
| 179 |
+
| **血战到底** | 胡牌者退出行牌,剩余玩家继续直到只剩 1 人或牌墙摸完 | 每次胡牌时结算该胡牌者,局末统一结算未胡者 |
|
| 180 |
+
| **血流成河** | 胡牌者**不退出**,继续行牌可再胡;直到牌墙摸完或仅剩 1 人未胡 | 每次胡牌时结算,可多次胡牌累计 |
|
| 181 |
+
|
| 182 |
+
**血战到底 vs 血流成河对比**:
|
| 183 |
+
|
| 184 |
+
| 对比项 | 血战到底 | 血流成河 |
|
| 185 |
+
| --- | --- | --- |
|
| 186 |
+
| 胡牌后 | 退出行牌,不再参与 | 继续行牌,可再次胡牌 |
|
| 187 |
+
| 可胡次数 | 每人最多胡 1 次 | 每人可胡多次 |
|
| 188 |
+
| 结束条件 | 仅剩 1 人 或 牌墙摸完 | 仅剩 1 人未胡 或 牌墙摸完 |
|
| 189 |
+
| 策略差异 | 胡牌后无风险 | 胡牌后仍需防守 |
|
| 190 |
+
|
| 191 |
+
**血流成河特殊规则**:
|
| 192 |
+
- 胡牌后不退出,继续参与后续摸打(按规则处理胡后手牌与状态)
|
| 193 |
+
- 若被点炮,点炮者需支付;若自摸,三家支付
|
| 194 |
+
- 已胡玩家打出的牌仍可被他人胡(可能被多次点炮)
|
| 195 |
+
- 结算时累计所有胡牌的番数
|
| 196 |
+
|
| 197 |
+
### 1.6 行牌顺序与出牌权
|
| 198 |
+
|
| 199 |
+
**默认顺序**:庄家 → 下家 → 对家 → 上家(逆时针)
|
| 200 |
+
|
| 201 |
+
| 事件 | 出牌权归属 | 说明 |
|
| 202 |
+
| --- | --- | --- |
|
| 203 |
+
| 正常打牌后无人响应 | 下家 | 按逆时针顺序轮转 |
|
| 204 |
+
| 吃牌后 | 吃牌者 | 吃牌者打 1 张后,按其下家继续 |
|
| 205 |
+
| 碰牌后 | 碰牌者 | 碰牌者打 1 张后,按其下家继续 |
|
| 206 |
+
| 杠牌后 | 杠牌者 | 杠牌者补摸后打 1 张,按其下家继续 |
|
| 207 |
+
| 胡牌后(一局一胡) | 游戏结束 | 进入结算 |
|
| 208 |
+
| 胡牌后(血战到底) | 下一位未胡玩家 | 胡牌者退出行牌,剩余玩家继续 |
|
| 209 |
+
| 胡牌后(血流成河) | 下一位玩家(含胡牌者) | 胡牌者**不退出**,继续行牌可再胡 |
|
| 210 |
+
|
| 211 |
+
---
|
| 212 |
+
|
| 213 |
+
## 二、特殊节奏机制(闭环规则)
|
| 214 |
+
|
| 215 |
+
以下机制改变默认"摸 1 打 1"节奏,使用时**必须显式说明闭环规则**以保证手牌守恒。
|
| 216 |
+
|
| 217 |
+
### 2.1 摸三打三
|
| 218 |
+
|
| 219 |
+
| 项目 | 说明 |
|
| 220 |
+
| --- | --- |
|
| 221 |
+
| 触发条件 | 特定功能牌触发 / 特定阶段 / 玩家选择 |
|
| 222 |
+
| 动作序列 | 摸 3 张 → 打 3 张 |
|
| 223 |
+
| 手牌变化 | 13 → 16 → 13 |
|
| 224 |
+
| 闭环验证 | 回合结束仍为 13 张 ✅ |
|
| 225 |
+
| 注意事项 | 中间状态手牌为 16 张,需明确是否允许在此期间宣告胡牌/杠牌 |
|
| 226 |
+
|
| 227 |
+
### 2.2 海底漫游
|
| 228 |
+
|
| 229 |
+
| 项目 | 说明 |
|
| 230 |
+
| --- | --- |
|
| 231 |
+
| 触发条件 | 牌墙仅剩最后 1 张(海底牌)时触发 |
|
| 232 |
+
| 动作序列 | 当前玩家可选择:(1) 摸海底牌 → 打 1 张 或 胡牌;(2) 放弃 → 下家决定 |
|
| 233 |
+
| 手牌变化 | 与普通回合相同:13 → 14 → 13(或胡牌) |
|
| 234 |
+
| 闭环验证 | 若摸牌则正常闭环;若放弃则手牌不变 ✅ |
|
| 235 |
+
| 特殊结算 | 海底捞月(自摸海底牌胡牌)通常有额外番数 |
|
| 236 |
+
|
| 237 |
+
### 2.3 海捞阶段(合肥麻将特色)
|
| 238 |
+
|
| 239 |
+
| 项目 | 说明 |
|
| 240 |
+
| --- | --- |
|
| 241 |
+
| 触发条件 | 牌墙仅剩最后 4 张时,进入海捞阶段 |
|
| 242 |
+
| 动作序列 | 最后 4 张放入"海捞区" → 四家依次各抓 1 张(不打牌) → 若有人胡牌则停止海捞进入结算 |
|
| 243 |
+
| 手牌变化 | 13 → 14(抓牌后)→ 不打牌,直接判定胡牌或流局 |
|
| 244 |
+
| 闭环验证 | 海捞阶段是**终结阶段**,不需要回到 13 张,直接进入结算 ✅ |
|
| 245 |
+
| 胡牌判定 | 抓牌后若满足胡牌条件可立即宣告;若无人胡牌则流局 |
|
| 246 |
+
|
| 247 |
+
### 2.4 连续摸牌(功能牌触发)
|
| 248 |
+
|
| 249 |
+
| 项目 | 说明 |
|
| 250 |
+
| --- | --- |
|
| 251 |
+
| 触发条件 | 打出特定功能牌 / 摸到特定牌 |
|
| 252 |
+
| 动作序列 | 额外摸 N 张 → 额外打 N 张(N 由规则定义) |
|
| 253 |
+
| 手牌变化 | 13 → 13+N → 13 |
|
| 254 |
+
| 闭环验证 | 必须"摸 N 打 N"形成闭环 ✅ |
|
| 255 |
+
| 注意事项 | 需明确连续摸牌期间是否可以吃/碰/杠/胡 |
|
| 256 |
+
|
| 257 |
+
---
|
| 258 |
+
|
| 259 |
+
## 二(续)、开局阶段机制
|
| 260 |
+
|
| 261 |
+
### 2.5 换三张(起手换牌)
|
| 262 |
+
|
| 263 |
+
| 项目 | 说明 |
|
| 264 |
+
| --- | --- |
|
| 265 |
+
| 触发时机 | 发牌完成后、行牌开始前 |
|
| 266 |
+
| 参与玩家 | 所有玩家同时进行 |
|
| 267 |
+
| 规则说明 | 每人从手牌中选择 3 张**同花色**的牌,与指定方向的玩家交换 |
|
| 268 |
+
|
| 269 |
+
**换牌方向**(常见规则):
|
| 270 |
+
| 方向 | 说明 |
|
| 271 |
+
| --- | --- |
|
| 272 |
+
| 顺时针 | 与下家交换(自己选的 3 张给下家,收上家的 3 张) |
|
| 273 |
+
| 逆时针 | 与上家交换(自己选的 3 张给上家,收下家的 3 张) |
|
| 274 |
+
| 对家 | 与对家交换 |
|
| 275 |
+
| 随机 | 系统随机决定本局方向 |
|
| 276 |
+
|
| 277 |
+
**换三张流程**:
|
| 278 |
+
```
|
| 279 |
+
1. 发牌完成,每人 13 张(庄家 14 张)
|
| 280 |
+
2. 每人选择 3 张同花色的牌(必须同花色)
|
| 281 |
+
3. 系统公布本局换牌方向(如:逆时针)
|
| 282 |
+
4. 所有人确认后,同时交换
|
| 283 |
+
5. 换牌完成,进入定缺/行牌阶段
|
| 284 |
+
```
|
| 285 |
+
|
| 286 |
+
**手牌守恒**:
|
| 287 |
+
- 换牌前后手牌数量不变(换出 3 张,换入 3 张)
|
| 288 |
+
- 13 张 → 13 张(庄家 14 → 14)✅
|
| 289 |
+
|
| 290 |
+
**规则变体**:
|
| 291 |
+
| 变体 | 说明 |
|
| 292 |
+
| --- | --- |
|
| 293 |
+
| 必须同花色 | 只能选同一花色的 3 张牌交换(最常见) |
|
| 294 |
+
| 可不同花色 | 可任选 3 张牌交换(少见) |
|
| 295 |
+
| 换完定缺 | 换牌后需声明"定缺"花色 |
|
| 296 |
+
|
| 297 |
+
### 2.6 定缺(选缺门)
|
| 298 |
+
|
| 299 |
+
| 项目 | 说明 |
|
| 300 |
+
| --- | --- |
|
| 301 |
+
| 触发时机 | 换三张完成后(或发牌完成后,若无换三张) |
|
| 302 |
+
| 规则说明 | 每人选择一种花色作为"缺门",该花色的牌必须全部打出后才能胡牌 |
|
| 303 |
+
| 常见缺门 | 万、条、筒三选一 |
|
| 304 |
+
|
| 305 |
+
**定缺与胡牌**:
|
| 306 |
+
- 胡牌时手中不能有缺门花色的牌
|
| 307 |
+
- 未清缺门的玩家不能胡牌(即使牌型满足)
|
| 308 |
+
- 局末流局时,未清缺门者为花猪,需罚分;若同时未听牌,可另叠加查大叫
|
| 309 |
+
|
| 310 |
+
---
|
| 311 |
+
|
| 312 |
+
## 三、争议机制详细规则
|
| 313 |
+
|
| 314 |
+
### 3.1 抢杠胡
|
| 315 |
+
|
| 316 |
+
| 项目 | 说明 |
|
| 317 |
+
| --- | --- |
|
| 318 |
+
| 触发条件 | 玩家 A 将已碰的刻子补杠时,玩家 B 可胡这张补杠牌 |
|
| 319 |
+
| 胡牌类型 | **视为点炮**(由补杠者 A 支付) |
|
| 320 |
+
| 杠是否生效 | **不生效**,补杠操作取消,该牌被 B 胡走 |
|
| 321 |
+
| 补牌是否发生 | **不发生**,因为杠操作被抢断 |
|
| 322 |
+
| 轮次归属 | 游戏结束(一局一胡)或 B 退出行牌(血战) |
|
| 323 |
+
| 优先级 | 抢杠胡属于"胡",优先级最高 |
|
| 324 |
+
|
| 325 |
+
### 3.2 一炮多响
|
| 326 |
+
|
| 327 |
+
| 项目 | 说明 |
|
| 328 |
+
| --- | --- |
|
| 329 |
+
| 触发条件 | 玩家 A 打出一张牌,多家(B、C)同时可胡 |
|
| 330 |
+
| 处理方式(三种常见规则) | |
|
| 331 |
+
| (1) 一炮多响-全响 | B、C 都胡,A 向 B、C 各支付一份 |
|
| 332 |
+
| (2) 一炮多响-头家优先 | 按逆时针顺序,离 A 最近的 B 先胡;C 不能胡 |
|
| 333 |
+
| (3) 禁止一炮多响 | 出现多家可胡时,该牌作废,无人能胡(少见) |
|
| 334 |
+
| 轮次归属(血战模式) | 所有胡牌者退出行牌,由 A 的下一位未胡玩家继续 |
|
| 335 |
+
| 轮次归属(一局一胡) | 游戏结束,进入结算 |
|
| 336 |
+
|
| 337 |
+
### 3.3 自摸加底/加番
|
| 338 |
+
|
| 339 |
+
| 项目 | 说明 |
|
| 340 |
+
| --- | --- |
|
| 341 |
+
| 常见规则 | 自摸胡牌比点炮胡牌多 1 番 / 底分翻倍 |
|
| 342 |
+
| 结算方式 | 三家各自支付(而非仅点炮者支付) |
|
| 343 |
+
|
| 344 |
+
### 3.4 杠上炮(杠后点炮)
|
| 345 |
+
|
| 346 |
+
| 项目 | 说明 |
|
| 347 |
+
| --- | --- |
|
| 348 |
+
| 触发条件 | 玩家 A 杠牌后补摸,打出的牌被玩家 B 胡 |
|
| 349 |
+
| 胡牌类型 | **点炮**(由杠后打牌者 A 支付) |
|
| 350 |
+
| 与普通点炮区别 | 部分规则下杠上炮需额外加番/加倍 |
|
| 351 |
+
| 杠是否生效 | **生效**,杠牌操作已完成,只是打出的牌被胡走 |
|
| 352 |
+
| 轮次归属 | 按普通点炮处理(一局一胡结束;血战则 B 退出) |
|
| 353 |
+
|
| 354 |
+
**杠上炮推演**:
|
| 355 |
+
```
|
| 356 |
+
A 杠(明杠或暗杠)→ A 补摸 → A 打出 🀐 → B 胡 🀐
|
| 357 |
+
|
| 358 |
+
结果:
|
| 359 |
+
- A 的杠已生效(杠组保留)
|
| 360 |
+
- A 点炮给 B
|
| 361 |
+
- 若有杠上炮加番规则,则 B 的胡牌番数增加
|
| 362 |
+
```
|
| 363 |
+
|
| 364 |
+
**与抢杠胡的区别**:
|
| 365 |
+
| 对比项 | 抢杠胡 | 杠上炮 |
|
| 366 |
+
| --- | --- | --- |
|
| 367 |
+
| 触发时机 | 补杠宣告时 | 杠后打牌时 |
|
| 368 |
+
| 杠是否生效 | 不生效(碰组恢复) | 生效(杠组保留) |
|
| 369 |
+
| 被胡的牌 | 补杠的那张牌 | 杠后打出的牌 |
|
| 370 |
+
|
| 371 |
+
---
|
| 372 |
+
|
| 373 |
+
## 三(续)、复杂场景推演与决策流程
|
| 374 |
+
|
| 375 |
+
### 3.5 连续碰杠混合推演
|
| 376 |
+
|
| 377 |
+
**场景**:一局中玩家进行多次吃/碰/杠操作时的位置累积计算。
|
| 378 |
+
|
| 379 |
+
#### 3.5.1 已碰 2 次后再明杠
|
| 380 |
+
|
| 381 |
+
```
|
| 382 |
+
初始状态:暗牌 7 张 + 2 个碰组(各 3 张)= 7 + 6 = 13 张
|
| 383 |
+
他人打出 🀃,我用手中 3 张 🀃🀃🀃 明杠
|
| 384 |
+
杠后:暗牌 4 张 + 2 碰组 6 张 + 1 杠组 4 张 = 14 张
|
| 385 |
+
补摸:暗牌 5 张 + 6 + 4 = 15 张
|
| 386 |
+
打牌:暗牌 4 张 + 6 + 4 = 14 张
|
| 387 |
+
结束:14 张 = 13 + 1(杠组数)✅
|
| 388 |
+
```
|
| 389 |
+
|
| 390 |
+
#### 3.5.2 连续杠(杠后补摸又能杠)
|
| 391 |
+
|
| 392 |
+
**场景**:明杠后补摸,发现能再暗杠
|
| 393 |
+
|
| 394 |
+
```
|
| 395 |
+
初始状态:暗牌 13 张
|
| 396 |
+
|
| 397 |
+
第一次杠(明杠):
|
| 398 |
+
他人打出 🀀,我用手中 3 张 🀀🀀🀀 明杠
|
| 399 |
+
杠后:暗牌 10 张 + 杠组 4 张 = 14 张
|
| 400 |
+
补摸 🀁:暗牌 11 张 + 杠组 4 张 = 15 张
|
| 401 |
+
此时发现手中有 🀁🀁🀁🀁(含刚摸的),可以暗杠!
|
| 402 |
+
|
| 403 |
+
第二次杠(暗杠,代替打牌):
|
| 404 |
+
暗杠:暗牌 7 张 + 明杠组 4 张 + 暗杠组 4 张 = 15 张
|
| 405 |
+
补摸:暗牌 8 张 + 4 + 4 = 16 张
|
| 406 |
+
打牌:��牌 7 张 + 4 + 4 = 15 张
|
| 407 |
+
结束:总牌数 15 张
|
| 408 |
+
|
| 409 |
+
结论:连续杠时,最终牌数 = 13 + 杠组数
|
| 410 |
+
本例:13 + 2 = 15 张 ✅
|
| 411 |
+
|
| 412 |
+
原因:每个杠组比碰组多 1 张牌(4 vs 3),且连续杠时只打 1 张牌
|
| 413 |
+
```
|
| 414 |
+
|
| 415 |
+
**通用规则**:
|
| 416 |
+
- 连续杠允许(补摸后可选择再杠而非打牌)
|
| 417 |
+
- 每次杠都需补摸,但连续杠时只有最后一次打牌
|
| 418 |
+
- 最终牌数 = 13 + 杠组总数
|
| 419 |
+
|
| 420 |
+
#### 3.5.3 碰杠混合的通用公式
|
| 421 |
+
|
| 422 |
+
```
|
| 423 |
+
回合结束牌数 = 13 + 杠组数
|
| 424 |
+
|
| 425 |
+
验证:
|
| 426 |
+
- 无吃碰杠:13 张 ✅
|
| 427 |
+
- 1 碰:13 张 ✅
|
| 428 |
+
- 2 碰:13 张 ✅
|
| 429 |
+
- 1 杠(明杠或暗杠):14 张 ✅
|
| 430 |
+
- 2 杠:15 张 ✅
|
| 431 |
+
- 2 碰 + 1 杠:14 张 ✅
|
| 432 |
+
- 1 碰 + 2 杠:15 张 ✅
|
| 433 |
+
```
|
| 434 |
+
|
| 435 |
+
**核心理解**:
|
| 436 |
+
- 吃/碰不改变结束牌数(取弃牌形成组合后打 1 张,净增 0)
|
| 437 |
+
- 每个杠使结束牌数 +1(杠组 4 张 > 碰组 3 张,且杠后补摸再打 1 张)
|
| 438 |
+
|
| 439 |
+
### 3.6 抢杠胡完整流程与推演
|
| 440 |
+
|
| 441 |
+
#### 3.6.1 抢杠胡决策流程
|
| 442 |
+
|
| 443 |
+
```
|
| 444 |
+
玩家 A 补杠(将碰组升级为杠组)
|
| 445 |
+
│
|
| 446 |
+
▼
|
| 447 |
+
系统广播:A 补杠 🀐
|
| 448 |
+
│
|
| 449 |
+
▼
|
| 450 |
+
其他玩家(B、C、D)判断是否可胡这张 🀐
|
| 451 |
+
│
|
| 452 |
+
├─── 有人可胡 ──→ 进入抢杠胡判定
|
| 453 |
+
│ │
|
| 454 |
+
│ ├─ 单人可胡:该玩家胡牌
|
| 455 |
+
│ │
|
| 456 |
+
│ └─ 多人可胡:按一炮多响规则处理
|
| 457 |
+
│
|
| 458 |
+
└─── 无人可胡 ──→ A 的补杠生效,A 补摸后打牌
|
| 459 |
+
```
|
| 460 |
+
|
| 461 |
+
#### 3.6.2 抢杠胡牌数推演
|
| 462 |
+
|
| 463 |
+
**场景**:A 补杠被 B 抢杠胡
|
| 464 |
+
|
| 465 |
+
```
|
| 466 |
+
A 的状态变化:
|
| 467 |
+
补杠前:暗牌 11 张 + 碰组 3 张 = 14 张(摸牌后)
|
| 468 |
+
宣布补杠:暗牌 10 张 + 杠组 4 张(碰组+1张)= 14 张
|
| 469 |
+
被抢杠胡:补杠取消,补杠牌被 B 胡走
|
| 470 |
+
最终状态:暗牌 10 张 + 碰组 3 张 = 13 张
|
| 471 |
+
|
| 472 |
+
注意:A 手中那张用于补杠的牌被 B 胡走,碰组恢复原状
|
| 473 |
+
实际上:A 暗牌 10 张 + 碰组 3 张 = 13 张 ✅
|
| 474 |
+
|
| 475 |
+
B 的状态变化:
|
| 476 |
+
胡牌前:暗牌 13 张
|
| 477 |
+
抢杠胡:取 A 的补杠牌凑成胡牌型
|
| 478 |
+
最终状态:胡牌(一局一胡则游戏结束;血战则 B 退出行牌)
|
| 479 |
+
|
| 480 |
+
轮次归属:
|
| 481 |
+
- 一局一胡:游戏结束,进入结算
|
| 482 |
+
- 血战模式:B 退出行牌,从 A 的下家(跳过 B 如果 B 在 A 下家方向)继续
|
| 483 |
+
```
|
| 484 |
+
|
| 485 |
+
### 3.7 多人同时响应处理流程
|
| 486 |
+
|
| 487 |
+
#### 3.7.1 响应优先级决策树
|
| 488 |
+
|
| 489 |
+
```
|
| 490 |
+
玩家 X 打出一张牌 🀐
|
| 491 |
+
│
|
| 492 |
+
▼
|
| 493 |
+
收集所有玩家的响应意图(吃/碰/杠/胡/过)
|
| 494 |
+
│
|
| 495 |
+
▼
|
| 496 |
+
按优先级排序:胡 > 碰 > 杠 > 吃
|
| 497 |
+
│
|
| 498 |
+
├─── 有人胡牌 ──→ 单人胡:该玩家胡牌
|
| 499 |
+
│ │
|
| 500 |
+
│ └─ 多人胡:按一炮多响规则
|
| 501 |
+
│ │
|
| 502 |
+
│ ├─ 全响:所有可胡者都胡
|
| 503 |
+
│ ├─ 头家优先:离 X 最近者胡
|
| 504 |
+
│ └─ 禁止:该牌作废,进入下家
|
| 505 |
+
│
|
| 506 |
+
├─── 有人碰/杠(无人胡)──→ 碰/杠者获得该牌,由其出牌
|
| 507 |
+
│ (多人碰时:离 X 最近者优先)
|
| 508 |
+
│
|
| 509 |
+
├─── 有人吃(无人碰杠胡)──→ 下家吃牌,由其出牌
|
| 510 |
+
│
|
| 511 |
+
└─── 无人响应 ──→ 按顺序轮到 X 的下家摸牌
|
| 512 |
+
```
|
| 513 |
+
|
| 514 |
+
#### 3.7.2 多人碰冲突处理
|
| 515 |
+
|
| 516 |
+
```
|
| 517 |
+
场景:X 打出 🀐,B(X 下家)和 D(X 上家)都想碰
|
| 518 |
+
|
| 519 |
+
优先级判定:按逆时针顺序,离 X 最近者优先
|
| 520 |
+
X → B(下家,距离 1)→ C(对家,距离 2)→ D(上家,距离 3)
|
| 521 |
+
|
| 522 |
+
结果:B 优先碰牌
|
| 523 |
+
|
| 524 |
+
轮次:B 碰后由 B 出牌,然后按 B 的下家 C 继续
|
| 525 |
+
```
|
| 526 |
+
|
| 527 |
+
### 3.8 复杂出牌顺序规则
|
| 528 |
+
|
| 529 |
+
#### 3.8.1 血战模式胡牌退出后的顺序接续
|
| 530 |
+
|
| 531 |
+
```
|
| 532 |
+
初始顺序:庄家 A → B → C → D(逆时针)
|
| 533 |
+
|
| 534 |
+
场景 1:B 胡牌退出
|
| 535 |
+
剩余顺序:A → C → D → A ...
|
| 536 |
+
当前出牌者的下家判定:跳过已胡玩家
|
| 537 |
+
|
| 538 |
+
场景 2:A(庄家)胡牌退出
|
| 539 |
+
剩余顺序:B → C → D → B ...
|
| 540 |
+
注意:庄家退出后,行牌仍按原顺序,只是跳过 A
|
| 541 |
+
|
| 542 |
+
场景 3:B 和 D 一炮多响同时胡牌退出(全响规则)
|
| 543 |
+
剩余顺序:A → C → A ...
|
| 544 |
+
只剩 2 人时继续行牌直到再有人胡或流局
|
| 545 |
+
```
|
| 546 |
+
|
| 547 |
+
#### 3.8.2 一炮多响后的轮次归属
|
| 548 |
+
|
| 549 |
+
```
|
| 550 |
+
场景:A 打出 🀐,B 和 C 一炮多响都胡
|
| 551 |
+
|
| 552 |
+
规则 1(全响-血战模式):
|
| 553 |
+
B 和 C 都胡牌退出
|
| 554 |
+
下一个出牌者:A 的下家中第一个未胡者
|
| 555 |
+
若 B 是 A 下家:跳过 B,轮到 C 的下家(D)
|
| 556 |
+
若 B 不是 A 下家:轮到 A 的直接下家(如果未胡)
|
| 557 |
+
|
| 558 |
+
规则 2(头家优先-血战模式):
|
| 559 |
+
仅 B(离 A 最近者)胡牌退出
|
| 560 |
+
下一个出牌者:A 的下家中第一个未胡者
|
| 561 |
+
|
| 562 |
+
规则 3(一局一胡模式):
|
| 563 |
+
游戏结束,进入结算
|
| 564 |
+
```
|
| 565 |
+
|
| 566 |
+
#### 3.8.3 杠后开花的特殊顺序
|
| 567 |
+
|
| 568 |
+
```
|
| 569 |
+
场景:A 杠牌后补摸,补摸的牌正好能���(杠上开花/杠上自摸)
|
| 570 |
+
|
| 571 |
+
流程:
|
| 572 |
+
A 杠 → A 补摸 → A 宣布杠上开花(自摸胡牌)
|
| 573 |
+
|
| 574 |
+
轮次归属:
|
| 575 |
+
- 一局一胡:游戏结束
|
| 576 |
+
- 血战模式:A 退出行牌,从 A 的下家继续
|
| 577 |
+
|
| 578 |
+
注意:杠上开花是自摸,不是点炮,结算方式按自摸计算
|
| 579 |
+
```
|
| 580 |
+
|
| 581 |
+
#### 3.8.4 连续响应的顺序处理
|
| 582 |
+
|
| 583 |
+
```
|
| 584 |
+
场景:复杂的连续响应链
|
| 585 |
+
|
| 586 |
+
X 打出 🀐 → B 碰 → B 打出 🀒 → D 碰 → D 打出 🀔 → A 胡
|
| 587 |
+
|
| 588 |
+
轮次追踪:
|
| 589 |
+
X 出牌 → B 碰(打断顺序) → B 出牌 → D 碰(打断顺序) → D 出牌 → A 胡
|
| 590 |
+
|
| 591 |
+
规则:每次吃/碰/杠都会"打断"原顺序,由响应者接管出牌权
|
| 592 |
+
胡牌终止当前行牌(一局一胡)或让胡者退出(血战)
|
| 593 |
+
```
|
| 594 |
+
|
| 595 |
+
---
|
| 596 |
+
|
| 597 |
+
## 四、创新机制库(按目的分类)
|
| 598 |
+
|
| 599 |
+
| 目的 | 机制 | 底层影响 |
|
| 600 |
+
| --- | --- | --- |
|
| 601 |
+
| 加快玩家胡牌节奏 | 赖子牌(百搭)、摸三打三、一炮多响、抢杠胡、限制胡牌倍数上限 | 赖子影响牌型判定;摸三打三需闭环验证 |
|
| 602 |
+
| 促进玩家做大牌 | 限制起胡倍数、增加番型倍数、功能牌加倍、平胡只能自摸 | 影响 win_rules 和 scoring |
|
| 603 |
+
| 鼓励玩家积极进攻 | 血战到底、定缺花色、查大叫、退税 | 血战影响 game_variant;定缺影响 setup |
|
| 604 |
+
| 增强社交性 | 2v2 组队、信息共享 | 影响 players 和 settlement |
|
| 605 |
+
| 增强游戏趣味性 | 功能牌连续摸牌、两张相同牌三选一、百景图点亮 | 需闭环验证;影响 extensions |
|
| 606 |
+
| 鼓励玩家尽早听牌 | 捉鸡(未听牌需结算鸡牌) | 影响 settlement |
|
| 607 |
+
| 增加单局最大输赢 | 抓码/扎鸟/买马、赖子杠×10、明牌×6、破封、漂分 | 影响 scoring 和 settlement |
|
| 608 |
+
| 保护小持币量玩家 | 封顶倍数、封顶金币 | 影响 settlement |
|
| 609 |
+
| 增强策略博弈 | 换牌、比牌、海底漫游、海捞阶段、暴击 | 海底/海捞需闭环验证;暴击影响 settlement |
|
| 610 |
+
|
| 611 |
+
---
|
| 612 |
+
|
| 613 |
+
## 四(续)、金流与结算机制
|
| 614 |
+
|
| 615 |
+
### 4.1 杠牌即时结算
|
| 616 |
+
|
| 617 |
+
| 项目 | 说明 |
|
| 618 |
+
| --- | --- |
|
| 619 |
+
| 结算时机 | 杠牌动作完成后**立即结算**,不等到局末 |
|
| 620 |
+
| 明杠(直杠)| 被杠者向杠牌者支付固定分数(如:2 分) |
|
| 621 |
+
| 补杠 | 三家各向杠牌者支付固定分数(如:各 1 分) |
|
| 622 |
+
| 暗杠 | 三家各向杠牌者支付固定分数(如:各 2 分) |
|
| 623 |
+
|
| 624 |
+
**杠牌结算与胡牌结算独立**:
|
| 625 |
+
- 杠牌得分在杠时即刻入账
|
| 626 |
+
- 即使最终流局,杠牌得分仍然有效
|
| 627 |
+
- 若被抢杠胡,杠不生效,杠分不结算
|
| 628 |
+
|
| 629 |
+
### 4.2 一炮多响金流分配
|
| 630 |
+
|
| 631 |
+
**场景**:A 点炮,B 和 C 同时胡牌(全响规则)
|
| 632 |
+
|
| 633 |
+
| 分配方式 | 说明 |
|
| 634 |
+
| --- | --- |
|
| 635 |
+
| **各付各** | A 向 B 支付 B 的胡牌分,向 C 支付 C 的胡牌分(A 支付两份) |
|
| 636 |
+
| **共付** | A 支付 B 和 C 中较高者的分数,B 和 C 平分(少见) |
|
| 637 |
+
|
| 638 |
+
**常见规则**:A 需向每位胡牌者分别支付,不合并计算。
|
| 639 |
+
|
| 640 |
+
### 4.3 玩家破产时的金流处理
|
| 641 |
+
|
| 642 |
+
**场景**:A 点炮,B 和 C 一炮多响,但 A 金币不足以支付两份
|
| 643 |
+
|
| 644 |
+
| 处理方式 | 说明 |
|
| 645 |
+
| --- | --- |
|
| 646 |
+
| **按顺序支付** | 先支付离 A 最近的胡牌者(如 B),剩余金币再支付 C;C 可能只收到部分或 0 |
|
| 647 |
+
| **按比例分配** | A 的剩余金币按 B、C 应得比例分配 |
|
| 648 |
+
| **借贷/记账** | A 欠款记录,后续局次偿还(需系统支持) |
|
| 649 |
+
| **破产离场** | A 支付完全部金币后离场,剩余玩家继续 |
|
| 650 |
+
|
| 651 |
+
**破产判定时机**:
|
| 652 |
+
- 即时破产:每次支付后判定,金币≤0 则破产
|
| 653 |
+
- 局末破产:局末统一结算,金币<0 则破产
|
| 654 |
+
|
| 655 |
+
### 4.4 呼叫转移(包三家)
|
| 656 |
+
|
| 657 |
+
| 项目 | 说明 |
|
| 658 |
+
| --- | --- |
|
| 659 |
+
| 触发条件 | 点炮者需承担原本应由三家分别支付的分数 |
|
| 660 |
+
| 常见场景 | 点炮给大牌(如:点炮给清一色) |
|
| 661 |
+
| 计算方式 | 点炮者支付 = 自摸时三家应付总和 |
|
| 662 |
+
|
| 663 |
+
**示例**:
|
| 664 |
+
```
|
| 665 |
+
B 胡牌 8 番(底分 1)
|
| 666 |
+
自摸:A、C、D 各付 8 分,B 得 24 分
|
| 667 |
+
A 点炮(呼叫转移):A 付 24 分,B 得 24 分
|
| 668 |
+
```
|
| 669 |
+
|
| 670 |
+
### 4.5 查大叫与查花猪
|
| 671 |
+
|
| 672 |
+
| 机制 | 触发条件 | 赔付规则 |
|
| 673 |
+
| --- | --- | --- |
|
| 674 |
+
| **查大叫** | 局末流局时,未听牌的玩家 | 向已听牌的玩家赔付(按听牌最大番型计算) |
|
| 675 |
+
| **查花猪** | 局末流局时,未清缺门的玩家 | 向已清缺门的玩家赔付(通常为固定倍数或按番型) |
|
| 676 |
+
|
| 677 |
+
**赔付计算**:
|
| 678 |
+
|
| 679 |
+
| 情况 | 赔付方式 |
|
| 680 |
+
| --- | --- |
|
| 681 |
+
| 查大叫 | 未听牌者向每位听牌者支付"其听牌最大可胡番型"的分数 |
|
| 682 |
+
| 查花猪 | 未清缺门者向每位已清缺门者支付固定罚分(如:16 分) |
|
| 683 |
+
| 既未听又花猪 | 两项罚分叠加 |
|
| 684 |
+
|
| 685 |
+
**示例**:
|
| 686 |
+
```
|
| 687 |
+
局末流局:
|
| 688 |
+
A:已听牌,最大听 16 番
|
| 689 |
+
B:已听牌,最大听 8 番
|
| 690 |
+
C:未听牌(大叫)
|
| 691 |
+
D:未清缺门(花猪)
|
| 692 |
+
|
| 693 |
+
赔付:
|
| 694 |
+
C 向 A 付 16 分,向 B 付 8 分
|
| 695 |
+
D 向 A、B、C 各付花猪罚分(如各 16 分)
|
| 696 |
+
```
|
| 697 |
+
|
| 698 |
+
### 4.6 退税机制
|
| 699 |
+
|
| 700 |
+
| 项目 | 说明 |
|
| 701 |
+
| --- | --- |
|
| 702 |
+
| 触发条件 | 胡牌者的胡牌牌型中包含被某玩家打过的牌 |
|
| 703 |
+
| 结算方式 | 该玩家需额外支付"退税"分数 |
|
| 704 |
+
| 计算方式 | 通常为固定分数或番数加成 |
|
| 705 |
+
|
| 706 |
+
**示例**:
|
| 707 |
+
```
|
| 708 |
+
A 胡"一筒",B 之前打过"一筒"
|
| 709 |
+
B 需向 A 额外支付退税分(如:胡牌分×2)
|
| 710 |
+
```
|
| 711 |
+
|
| 712 |
+
---
|
| 713 |
+
|
| 714 |
+
## 五、动作-手牌变化速查表(硬性参考)
|
| 715 |
+
|
| 716 |
+
| 动作 | 牌来源 → 去向 | 手牌净变化 | 是否强制打 1 张 | 出牌权归属 |
|
| 717 |
+
| --- | --- | --- | --- | --- |
|
| 718 |
+
| 摸牌 | 牌墙 → 手牌 | +1(13→14) | 是 | 当前玩家 |
|
| 719 |
+
| 打牌 | 手牌 → 弃牌堆 | -1(14→13) | — | 下家(或响应者) |
|
| 720 |
+
| 吃 | 上家弃牌 + 手牌 2 张 → 明牌组 | 0(净变化;暗牌-2,明牌+3,取弃牌+1,抵消) | 是 | 吃牌者 |
|
| 721 |
+
| 碰 | 他人弃牌 + 手牌 2 张 → 明牌组 | 0(同上) | 是 | 碰牌者 |
|
| 722 |
+
| 直杠(明杠) | 他人弃牌 + 手牌 3 张 → 明牌组 | 0 → 补摸+1 → 打-1(回合结束总计14) | 是(补摸后) | 杠牌者 |
|
| 723 |
+
| 补杠 | 手牌 1 张 → 已碰刻子变杠 | 0 → 补摸+1 → 打-1(回合结束总计14) | 是(补摸后) | 杠牌者 |
|
| 724 |
+
| 暗杠 | 手牌 4 张 → 暗杠组 | 0 → 补摸+1 → 打-1(回合结束总计14) | 是(补摸后) | 杠牌者 |
|
| 725 |
+
| 杠后补牌 | 牌墙 → 手牌 | +1 | 是 | 杠牌者 |
|
| 726 |
+
|
| 727 |
+
---
|
| 728 |
+
|
| 729 |
+
## 六、最小回合推演模板(验证守恒)
|
| 730 |
+
|
| 731 |
+
### 6.1 普通回合
|
| 732 |
+
```
|
| 733 |
+
初始:手牌 13 张
|
| 734 |
+
摸牌:手牌 14 张(+1)
|
| 735 |
+
打牌:手牌 13 张(-1)
|
| 736 |
+
结束:手牌 13 张 ✅
|
| 737 |
+
```
|
| 738 |
+
|
| 739 |
+
### 6.2 碰牌回合
|
| 740 |
+
```
|
| 741 |
+
初始:暗牌 13 张
|
| 742 |
+
他人打出 🀐,我用手中 2 张 🀐🀐 碰
|
| 743 |
+
碰后:暗牌 11 张 + 碰组 3 张 = 14 张
|
| 744 |
+
打牌:暗牌 10 张 + 碰组 3 张 = 13 张
|
| 745 |
+
结束:总牌数 13 张 ✅
|
| 746 |
+
```
|
| 747 |
+
|
| 748 |
+
### 6.3 杠牌回合(直杠/明杠)
|
| 749 |
+
|
| 750 |
+
**关键概念**:明杠后,玩家总牌数比普通回合多 1 张(因为杠组 4 张 > 碰组 3 张)。
|
| 751 |
+
|
| 752 |
+
```
|
| 753 |
+
初始:暗牌 13 张
|
| 754 |
+
他人打出 🀐,我用手中 3 张 🀐🀐🀐 明杠
|
| 755 |
+
杠后:暗牌 10 张 + 杠组 4 张 = 14 张
|
| 756 |
+
补摸:暗牌 11 张 + 杠组 4 张 = 15 张
|
| 757 |
+
打牌:暗牌 10 张 + 杠组 4 张 = 14 张
|
| 758 |
+
结束:总牌数 14 张(比普通回合多 1 张)✅
|
| 759 |
+
```
|
| 760 |
+
|
| 761 |
+
### 6.3.1 暗杠回合
|
| 762 |
+
|
| 763 |
+
**关键概念**:暗杠发生在**摸牌后**(此时手牌 14 张),用手中 4 张组杠,然后补摸 1 张,打 1 张。
|
| 764 |
+
|
| 765 |
+
```
|
| 766 |
+
初始:暗牌 13 张
|
| 767 |
+
摸牌:暗牌 14 张
|
| 768 |
+
此时发现手中有 4 张 🀐🀐🀐🀐,宣布暗杠
|
| 769 |
+
暗杠后:暗牌 10 张 + 暗杠组 4 张 = 14 张
|
| 770 |
+
补摸:暗牌 11 张 + 暗杠组 4 张 = 15 张
|
| 771 |
+
打牌:暗牌 10 张 + 暗杠组 4 张 = 14 张
|
| 772 |
+
结束:总牌数 14 张(比普通回合多 1 张)✅
|
| 773 |
+
```
|
| 774 |
+
|
| 775 |
+
### 6.3.2 补杠回合
|
| 776 |
+
|
| 777 |
+
**关键概念**:补杠发生在**摸牌后**(此时手牌 14 张),将手中 1 张补到已碰的刻子上,然后补摸 1 张,打 1 张。
|
| 778 |
+
|
| 779 |
+
```
|
| 780 |
+
初始:暗牌 10 张 + 碰组 3 张 = 13 张
|
| 781 |
+
摸牌:暗牌 11 张 + 碰组 3 张 = 14 张
|
| 782 |
+
补杠:暗牌 10 张 + 杠组 4 张(碰组 3 张 + 补上的 1 张)= 14 张
|
| 783 |
+
补摸:暗牌 11 张 + 杠组 4 张 = 15 张
|
| 784 |
+
打牌:暗牌 10 张 + 杠组 4 张 = 14 张
|
| 785 |
+
结束:总牌数 14 张(比普通回合多 1 张)✅
|
| 786 |
+
```
|
| 787 |
+
|
| 788 |
+
### 6.4 海捞阶段回合(合肥麻将)
|
| 789 |
+
```
|
| 790 |
+
进入海捞:手牌 13 张
|
| 791 |
+
抓海捞牌:手牌 14 张(+1)
|
| 792 |
+
不打牌,直接判定
|
| 793 |
+
若胡牌:结算
|
| 794 |
+
若未胡:等待其他玩家抓牌判定,最终流局
|
| 795 |
+
结束:无需回到 13 张(终结阶段)✅
|
| 796 |
+
```
|