feat: EndConditions映射 + 骰子翻牌定精(random wildcard)

EndConditions:
- BuildPhases 现在映射 PhaseConfig.Endconditions (之前 DslLoader 解析但 BuildPhases 忽略)
- 4处 _rules.Phases.Any(p => p.ParallelElimination) → _playPhase.ParallelElimination
- 新增 _playPhase 字段, 游戏不再依赖原始 DSL 做运行时判断

Random wildcard (骰子翻牌定精):
- WildcardRegistry.ComputeNextTiles(): 翻到 X → 正精=X+1, 副精=X+2 (9→1 循环, 字牌序循环)
- WildcardRegistry.RevealedTile 记录翻到的牌
- Deal() 发牌后翻一张牌确定精, 支持 random_count 控制精数量
- WildcardConfig 新增 RandomCount 字段 (默认2=正精+副精)
- ValidateRuleCompliance 接受 random 类型

南昌麻将 DSL:
- 新增 dsl-examples/nanchang.yaml (136张, random精, 无吃)
- 引擎加载 7/7, 1000局 0错误, 胡牌率 61%

58 tests pass, 5 种玩法全部 0 错误验证通过
This commit is contained in:
xiaoou
2026-07-04 21:06:16 +08:00
parent 5b60255f02
commit ae9c4c4142
5 changed files with 178 additions and 8 deletions

100
dsl-examples/nanchang.yaml Normal file
View File

@ -0,0 +1,100 @@
game:
name: 南昌麻将
type: mahjong
engine_type: mahjong
players: { min: 4, max: 4 }
requires:
- deck.generator_mahjong
- meldsolver.standard_win
- meldsolver.seven_pairs
- meldsolver.wildcard
- phase.mahjong_turn
- phase.priority_arbitration
- scoring.fan_exclusion
deck:
generator: mahjong
include_honors: true
include_flowers: false
total: 136
deal:
cards_per_player: 13
dealer_extra: 1
# 南昌麻将的精(癞子)系统:骰子翻牌定精
# 翻到X则X+1为正精X+2为副精。字牌按序循环。
wildcard_rules:
type: random
random_count: 2
wildcard_encoding: 50
behavior: substitute
fan_calculation_policy: optimal
scoring:
per_wildcard_in_win: 1
fan_types:
# 基础番型
- { name: 清一色, base_fan: 4, excludes: [缺一门, 无字] }
- { name: 混一色, base_fan: 2, excludes: [缺一门] }
- { name: 对对胡, base_fan: 2 }
- { name: 暗七对, base_fan: 4 }
# 特殊牌型
- { name: 带幺九, base_fan: 2, excludes: [缺一门] }
- { name: 缺一门, base_fan: 0 }
- { name: 无字, base_fan: 0 }
# 事件番型
- { name: 杠上开花, base_fan: 1, excludes: [海底捞月] }
- { name: 海底捞月, base_fan: 1 }
- { name: 抢杠胡, base_fan: 1 }
- { name: 天胡, base_fan: 10 }
- { name: 地胡, base_fan: 5 }
# 癞子相关
- { name: 癞子胡, base_fan: 1, condition: hand_contains_wildcard }
# 兜底
- { name: 鸡胡, base_fan: 1 }
fan_stacking: add
max_fan: 999
phases:
- name: deal
type: auto
action: deal_cards
next: play
- name: play
type: mahjong_turn
turn_order: counter_clockwise
sub_phases:
draw:
type: auto
action: draw_card
on_empty_deck: exhausted
self_action:
options:
- { action: discard }
- { action: an_kong }
- { action: bu_kong, condition: has_punged_pair }
- { action: win, condition: can_win_tumo }
others_reaction:
options:
- { action: pung, priority: 2, condition: has_two_same }
- { action: ming_kong, priority: 3, condition: has_three_same }
- { action: win, priority: 4, condition: can_win }
- { action: pass, priority: 0 }
priority_policy: highest_wins
on_win: game_over
end_conditions:
- { type: player_wins, action: settle }
- { type: deck_exhausted, action: draw_game }
- name: settle
type: auto
action: calculate_scores
next: null
scoring:
mode: fan_table
max_cap: 999