zhongchuyi commited on
Commit
20984d5
·
1 Parent(s): 26b751d

Initial deployment

Browse files
README.md CHANGED
@@ -1,10 +1,10 @@
1
  ---
2
  title: MahjongGameDesigner
3
  emoji: 🀄️
4
- colorFrom: blue
5
  colorTo: yellow
6
  sdk: gradio
7
- sdk_version: 5.49.1
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
- m = re.search(r"^###\\s*设计日志(创新推演摘\\s*$", content, re.MULTILINE)
 
 
 
 
309
  if not m:
310
  return ""
311
  start = m.start()
312
 
313
- m2 = re.search(r"^###\\s+.+$", content[m.end():], re.MULTILINE)
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
- "建议:请重新执行一次迭代,明确只改允许范围内字段,或切换为自由迭代。\n"
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 [], "", [], "", "", "", "", {}, 0, "DesignState:未建立(先生成一版玩法)", False, "READY_TO_GENERATE:未知"
 
 
 
 
 
 
 
 
 
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
- # 优化版麻将mGDL生成Prompt(完整工程级规范
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
- 1. 普通4倍 × 2倍 = 8倍
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
- **错误1混用 mult fan**
339
- ```lisp
340
- (yaku qingyise (mult 4) (fan 6)) ; 严重错误!
341
- ```
 
 
 
342
 
343
- ❌ **错误2:计分模式与字段不匹配**
344
- ```lisp
345
- (scoring (mode "multiplier"))
346
- (yaku qingyise (fan 6)) ; 错误!倍数制应使用 mult
347
- ```
348
 
349
- **错误3:stacking 方式错误**
350
- ```lisp
351
- (scoring (mode "fan_system"))
352
- (fan_table (stacking "multiply")) ; 错误!番数制应使用 add_fan
353
- ```
354
 
355
- **示例:血战麻将**
356
- ```lisp
357
- (scoring (mode "multiplier") (base_point 1))
358
- (fan_table
359
- (stacking "multiply")
360
- (yaku qingyise (mult 4) (category "special") (desc "清一色"))
361
- (yaku zimu (mult 2) (category "event") (desc "自摸"))
362
- )
363
- ```
 
 
 
364
 
365
- #### 设计建议
366
-
367
- 1. **血战/血流麻将**:必须使倍数制,封顶通常为16-32倍
368
- 2. **国标麻将**:必须使用番数制,封顶通常为8-13番
369
- 3. **捉鸡麻将**:使用混合制,通过 category 区分不同计分维度
370
- 4. **简化麻将**:可使 + `stacking="add"` 倍数相加
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
- **这是用户最关注的部分,必须作输出的重点,位于 mGDL 代码块之前或之后均可但必须显著突出。**
523
-
524
- **输出要求**
525
- 1. **唯一真理地位**此部分的描述必须详尽、准确、无歧义,不仅仅是 mGDL 的翻译,更是游戏玩法的**完整说明书**。
526
- 2. **内容完备性**
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
- ## 可控迭代:Analyse 模式(单问题迭代,强制
769
 
770
- 当且仅当用户输入中包含:
771
- `<ANALYSE_MODE>true</ANALYSE_MODE>`
 
 
 
 
 
772
 
773
- 你必须进入【Analyse 模式】,遵守以下规则(用于多可控迭代,而不是一性生成大稿):
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
- Analyse 模式下输出格式固定为
780
- - 一句话总结当前理解(≤60字)
781
- - 本轮唯一问题(只问1个)
782
- - 思维日志(本轮增量,≤200字)只解释本轮什么问这个问题/它影哪些设计选择/你打算如何收敛创新”,不得展开完整规则
783
- - ```json DesignState ```合法JSON
784
- - `READY_TO_GENERATE: true/false`
785
 
786
- **⚠️ Analyse 模式的思维日志要求(硬性)**
787
- - 必须确:本轮问题的“目的”(解决哪个不确定点)
788
- - 必须明确:它将影响的“创新旋钮”(触发/代价/收益/反制/计分/流程中的哪一处)
789
- - 必须避免:给出两条以上的新问题,或提前输出完整规则细节
790
 
791
- ### 思考与自检过程
792
- 请在此处用自然语言描述:
793
- - 你执行了哪些关键检查
794
- - 发现了哪些 FAIL 项
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
- ### DesignStateJSON,可机读
809
- 请输出一个 JSON 作为“中间表示”,用于后续多轮迭代的定向修改。要求
810
- 1. 必须是法 JSON(不要写注释,不要用省略号)
811
- 2. 字段尽量完整,未知时填空字符串或空数组
812
- 3. `mechanics[].mgdl_path` 必须是点号路径(不要写到 mGDL 代码里)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
813
 
814
- ```json
815
- {
816
- "base_variants": ["底座玩法名"],
817
- "fusion_variants": ["融合法名"],
818
- "new_variant_name": "新玩法名",
819
- "game_variant": "blood_war|blood_flow|round|multi_round|custom",
820
- "players": 4,
821
- "scoring_mode": "multiplier|fan_system|hybrid",
822
- "tileset": {
823
- "suits": ["wan","tong","tiao"],
824
- "honors": 0,
825
- "special_tiles": [{"name":"红中","count":6}],
826
- "total": 114
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
- ### mGDL描述
857
- ####生成前必读:完整度参照标准
858
- 在开始编写mGDL前,请在心中回顾麻将游戏mGDL通用语法_v1.3.txt的规则和 幺鸡血战_mGDL.txt 示例的详细程度,你的输出必须至少达到相同的细节水平:
859
-
860
- #### 参照示例的完整度标准:
861
- 1. 牌组定义tileset
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
- 1. 新机制声明(⚠️ 核心创新展示区,必须完整)
928
- - 格式要求:必须使用以下表格格式,逐个列出本游戏的所有特殊机制:
929
- 机制名称 核心功能 使用阶段 | 触发条件 | 实际定义位置 | mGDL注册路径
930
- 例:幺鸡赖子 | 1条可替代任意 全局 | extensions.special_mechanics.mechanic_cards..yaoji | extensions.special_mechanics.mechanic_cards..YaojiWild
931
- 例:定缺换牌 | 选择缺一门花色后换3张 | 准备阶段 | 定缺 setup.exchange_three extensions.special_mechanics.mechanic_cards..ExchangeThree
932
-
933
- - 强制要求:
934
- a. 完整性:表格必须包含本游戏所有创新机制,不得遗漏
935
- b. 实际定义位置:标注机制在mGDL中的实际定义位置
936
- c. mGDL注册路径标注机制在 extensions.special_mechanics.mechanic_cards. 中的注册路径
937
- d. 详细展开:表格下方必须对每个机制进行详细说明:
938
- - 机制的战略价值和游戏体验影响
939
- - 具体的使用方法和限制条件
940
- - 与其他机制或规则的交互关系
941
- - 使用示例(至少1个具体场景
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
- ### 5) **特殊机制完整检查**
1093
 
1094
- **赖子规则完整性**
1095
- - 赖子确定方式(固定/翻)明确定义 [PASS/FAIL]
1096
- - 赖子功能范围可替代哪些牌)明 [PASS/FAIL]
1097
- - 赖子限制(是否可吃碰杠/打出)明确定义 → [PASS/FAIL]
1098
- - 赖子在胡牌时的特殊规则明确定义 → [PASS/FAIL]
1099
 
1100
- **幺鸡/红中机制一致性**:
1101
- - 牌面标识与功能描述一致 → [PASS/FAIL]
1102
- - 计分规则与机制描述一致 → [PASS/FAIL]
1103
 
1104
- ### 6) **麻将阶段完整性检查**
 
 
 
 
 
 
 
 
 
 
 
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
- ### 7) **机制完整性与双向映射检查**⚠️ 核心防遗漏机制
1119
 
1120
- **步骤A:提取自然语言中声明的所机制**
1121
- - 机制列表:[M1, M2, M3, ...]
1122
- - 总计:N个机制
1123
 
1124
- **步骤B:验证每个机制在mGDL中的实体化**
1125
- | 机制名称 | mGDL实体位置 | 是否存在 | transfer_path |
1126
- |---------|------------|---------|--------------|
1127
- | M1 | [路径] | [/] | [有/无/none] |
1128
- | M2 | [路径] | [/否] | [有/无/none] |
1129
 
1130
- **步骤C反向验证mGDL中定义特殊机制是否自然语言中说明**
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
- **最终结论**[PASS/FAIL]
1143
 
1144
- ### 8) **特殊机制统一注册检查**(⚠️ 核心防遗漏机制
 
 
 
 
 
1145
 
1146
- **步骤A:扫描mGDL所有模块,提取所有特殊定义**
1147
- - extensions.special_mechanics.mechanic_cards. 中的所有条目
1148
- - win_rules 中特殊胡牌规则
1149
- - scoring 中的特殊计分规则
1150
- - fan_table 中的特殊番型
1151
 
1152
- **检测到的特殊机制列表**:
1153
- | 机制名称 | 定义位置 | 类别 |
1154
- |---------|---------|------|
1155
- | [M1] | [path1] | [category1] |
1156
- | [M2] | [path2] | [category2] |
 
 
1157
 
1158
- **总计**N个
1159
 
1160
- **步骤B验证 extensions.special_mechanics.mechanic_cards. 注册完整性**
1161
- **extensions.special_mechanics.mechanic_cards. 中已注册的机制**:
1162
- | 机制ID | card_path | implementation_path | category | stage_scope | enabled |
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
- **总**M个
 
 
 
 
1169
 
1170
- **步骤C:对比检查**
1171
- - mGDL中实际特殊机制数量:N
1172
- - extensions.special_mechanics.mechanic_cards. 注册数量:M
1173
- - 是否一致:N == M → [PASS/FAIL]
1174
- - 若处于【创新扩展模式】:逐一检查每条机制卡是否包含 trigger/effect/settle/reset(缺一 FAIL)
1175
 
1176
- **未在 extensions.special_mechanics.mechanic_cards. 注册的机制**:
1177
- - [列所有遗漏项及其定义位置]
 
 
1178
 
1179
- **最终结论**:[PASS/FAIL]
1180
 
1181
- ### 9) **庄家规则验证**
1182
 
1183
- **庄家确定方式**:
1184
- - 方式明确定义(随机/骰子/固定) → [PASS/FAIL]
1185
- - 连庄规则明确定义 → [PASS/FAIL]
 
 
 
 
 
 
1186
 
1187
- **庄家权益量化**:
1188
- - 庄家起手牌数=14 → [PASS/FAIL]
1189
- - 庄家倍率优势=1.1-1.15 → [PASS/FAIL]
 
 
 
1190
 
1191
- ### 10) **血战/血流一致性检查**
1192
-
1193
- **规则匹配**:
1194
- - game_variant 与 post_win_continuation 逻辑一致 → [PASS/FAIL]
1195
- - 血战类:winner_exit=true, end_when_third_player_wins=true
1196
- - 血流类:winner_exit=false, keep_turn_order=true, after_gun_next_draw="shooter_next"
1197
-
1198
- **流程描述匹配**:
1199
- - 自然语言规则与mGDL定义一致 → [PASS/FAIL]
1200
-
1201
- ### 11) **番型叠加规则检查**
1202
-
1203
- **叠加模式**:
1204
- - stacking 模式明确定义(multiply/add) → [PASS/FAIL]
1205
- - 番型互斥规则明确定义 → [PASS/FAIL]
1206
-
1207
- **封顶规则**:
1208
- - 番型上限明确定义 → [PASS/FAIL]
1209
- - 封顶规则与计分模式兼容 → [PASS/FAIL]
1210
-
1211
- ### 12) **修复轨迹**
1212
-
1213
- **自检发现问题**:
1214
- 1. [问题1描述]
1215
- - 检测位置:[具体模块/行号]
1216
- - 修正方案:[具体修改内容]
1217
- - 修正依据[最小修改优先级规则]
1218
-
1219
- 2. [问题2描述]
1220
- - 检测位置[具体模块/行号]
1221
- - 修正方案:[具体修改内容]
1222
- - 修正依据:[最小修改优先级规则]
1223
-
1224
- **修正验证**:
1225
- - 修正后重新检通过 → [PASS/FAIL]
1226
- - 修正是否影响其他模块 → [是/否]
1227
- - 需要额外验证的关联规则:[列出]
1228
-
1229
- ### 13) **麻将专用边界条件测试**
1230
-
1231
- **极端情况验证**:
1232
- - 牌墙只剩1张时摸牌 → [PASS/FAIL]
1233
- - 最后一张牌上开花 → [PASS/FAIL]
1234
- - 三家同时胡牌 → [PASS/FAIL]
1235
- - 四暗刻单吊胡牌 → [PASS/FAIL]
1236
-
1237
- **特殊机制边界**:
1238
- - 赖子使用上限验证 → [PASS/FAIL]
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==4.27.0
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
- | 增强社交性 | 2v2,组队玩家信息共享 |
12
- | 增强游戏的趣味性 | 加入功能牌,触发连续摸牌的功能/打出两张相同牌触发三选一/生肖麻将中点亮百景图可增加倍数 |
13
- | 鼓励玩家尽早听牌 | 捉鸡:若在整个局第次胡牌之后未听啤,需要结算鸡牌 |
14
- | 增加单局最大输赢 | 抓码/扎鸟/买马/赖子杠倍数×10/明倍数×6/破封,突破倍数上限/漂分 |
15
- | 保护小持币量玩家 | 限制封顶倍数和金币 |
16
- | 增强游戏的策略博弈 | 与系统或其他玩家换牌/选择出牌参与比牌/玩家胡牌后无法继续飞赖子和点亮生肖/海底漫游:最后一张牌为海底牌,此时玩家可依次选择是否要这牌/合肥麻将中的海捞阶段不同于海底捞月番型):牌桌上仅剩最后4未抓的牌时,放入“海捞区”,四家从“海捞区”各抓张牌放入手牌中,从第一个摸牌玩家逆时针开始,如果有一个玩家胡牌,则停止海捞,进入结算。/玩家胡牌后无法继续飞赖子和点亮生肖/暴击:同类牌型,倍数大的玩家胜,胜者对败者触发 “暴击”,赢取额外的金币。 |
17
-
18
- * 基础逻辑
19
-
20
-
21
- | | 说明 |
22
- | --- | --- |
23
- | -打牌循环 | 玩家轮流 “摸 1 张牌→打 1 张牌” 基础节奏,除吃碰杠后仅需打、无需情况。 |
24
- | 数量 | 标准起手为13张。玩家回合内摸牌后变为 14 张,打牌后回到 13 张。 |
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
+ ```