19 Commits

Author SHA1 Message Date
e8278fbd72 feat: add Shanghai Mahjong DSL (上海敲麻)
144 tiles, no chi, flower scoring, 11 fan types.
Knocking (declare ting) noted as unsupported engine gap.
2026-07-06 14:08:37 +08:00
196786f1a7 fix(DSL): 南昌删除非南昌规则番型(清一色/混一色/带幺九/缺一门/无字)
南昌麻将无清一色/混一色/带幺九概念,只保留:
对对胡(碰碰胡)/暗七对(七对)/杠上开花/海底捞月/抢杠胡/天胡/地胡/癞子胡/鸡胡

--check: 6→5条警告(去掉base_fan=0误报)
2026-07-05 01:45:32 +08:00
d5329e5b0c feat: 精分独立计分模式(independent_bonus)
引擎原算法: (底分+精分)×倍数 → 精分被庄家/自摸放大
南昌实际: 底分×倍数 + 精分 → 精分不参与倍数

新增DSL字段 wildcard_rules.scoring.independent_bonus:
- false(默认): 精分混入底分(现有行为,向后兼容)
- true(南昌): 精分独立加在倍数之后

代码: ScoreEngine.Settle → multiplierPart(底分×倍数) + addPart(精分)
自摸: 精分只加给赢家一次(不计入倍数)
点炮: 精分计入pay总额(放炮者全额支付)
2026-07-05 01:37:57 +08:00
a40b64cc1f fix(DSL): 南昌per_wildcard_in_win 1→2(精=2子)
南昌麻将: 每张精+2子,非+1番。鸡胡=1子,2张精=4子,
庄家点炮×2 → (1+4)×2=10。修复前per_wildcard=1→(1+2)×2=6过低
2026-07-05 01:26:03 +08:00
4bc64eda04 fix(DSL): 南昌麻将加all_orphans + --check提醒十三烂
南昌麻将需要全不靠/十三烂牌型支持:
- dsl: requires加 meldsolver.all_orphans
- --check: type=random时未配all_orphans→建议添加

能力注册: 7/7→8/8(100%), 十三烂牌型现在可识别
2026-07-05 00:50:21 +08:00
e3ff0f057e feat: DSL字段 wildcard_rules.meld_mode (standard/any)
之前: 特殊风牌组合是'隐式规则',硬编码在type=random检查中
现在: YAML声明 meld_mode: any (南昌麻将),引擎检查并报告不支持

WildcardConfig新增MeldMode字段(默认standard):
- standard: 仅顺子/刻子/对子(引擎支持)
- any: 任意3张可配面子(南昌麻将,引擎不支持)

--check: meld_mode=any → '引擎未实现→仅支持standard'
规则从'隐式'变为DSL可表达的显式规则
2026-07-05 00:40:03 +08:00
96068ed007 fix(DSL): 南昌麻将加吃牌(chi)
南昌麻将应有吃牌, DSL遗漏此操作。加后胡牌率60%→85%.
2026-07-04 23:56:17 +08:00
d734c0a384 fix: 圈风/门风/单钓将/四归一加_fanConfig守卫 + 精牌显示 + TileName修复
问题1: 圈风/门风/单钓将/四归一在南昌武汉等非Guobiao玩法中误生成
  修复: 4个新识别器加 _fanConfig.ContainsKey() 守卫,只有配了才生成
  Guobiao DSL 补充4个番型定义(圈风2/门风2/四归一2/单钓将1)

问题2: 翻牌定精信息未在demo显示
  修复: RenderDeal() 显示精牌(翻牌+正精+副精)

问题3: TileName?{tile}→ToStringCore(tile)
  修复: 带🀫前缀的数字牌显示 '🀫3万' 而非 '🀫?21'
2026-07-04 23:42:42 +08:00
488e96114f fix: excludes引用检查用enginePatterns替代fanTypes(修正误报)
根因分析:
- excludes用字符串匹配(ApplyFanExclusions操作输出名单),不依赖_fanConfig
  例: 清一色excludes缺一门→引擎生成'缺一门'时排除生效,即使缺一门不在fan_types
  之前按fan_types检查导致误报(武汉4个/国标2个)
- conflicts需要双方都在_fanConfig才能比较BaseFan数值

修复:
- excludes: 改查enginePatterns(39个引擎可生成名)替代fanNames
- conflicts: 保持查fanNames(必须双方在_fan_config)
- guobiao.yaml: conflicts[暗七对]→[七对](引擎通过FanName解析返回七对)

效果: 武汉5→1 国标15→12 (消除误报,剩余为真缺失:圈风/门风等引擎不支持)
2026-07-04 23:17:48 +08:00
8d8403ee4e fix(DSL): 血战到底加鸡胡兜底 2026-07-04 22:20:01 +08:00
f57446bd84 fix(DSL): 武汉删死番型 '将', 国标加鸡胡兜底
武汉: '将' base_fan=0 是死番型(引擎不生成,无人exclude)→删除
国标: 缺少鸡胡兜底→添加 {name:鸡胡,base_fan:1,level:0}

两处均为纯DSL修复, 引擎代码零修改
2026-07-04 22:18:25 +08:00
17a0d054ff fix: 武汉DSL添加鸡胡(base_fan=1)
鸡胡作为兜底番型应该在DSL中配置而非引擎硬编码兜底。
之前引擎在ScoreEngine/PhaseMachine中硬编码了鸡胡fallback=1,
但正确的架构是DSL驱动规则,引擎只做防御性兜底。

此变更让武汉麻将的鸡胡从_fanConfig取值,
FanValue查找路径: DSL配置(1) > fallback(防御用)
2026-07-04 21:41:47 +08:00
248af7642d feat: 计分倍数可配置化 (自摸/点炮/庄家)
ScoringDslConfig + ScoringConfig 新增:
- self_draw_multiplier (默认1): 自摸每家付 baseFan × N
- discard_win_multiplier (默认3): 放炮者付 baseFan × N
- dealer_multiplier (默认1): 庄家输赢 × N

ScoreEngine.Settle:
- 硬编码 *3 → 读 DiscardWinMultiplier
- 庄家倍数: dealerWon→所有人付倍数; 闲家赢→仅庄家付倍数

南昌麻将 DSL: self_draw=1, discard_win=1, dealer=2

58 tests pass, 5 variants all 0 errors
2026-07-04 21:12:45 +08:00
ae9c4c4142 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 错误验证通过
2026-07-04 21:06:16 +08:00
978fcd87dd fix: 国标fan_types缺常见番型→0%胡牌率(补全28种)
Root cause: guobiao.yaml只列了11个高番型(≥24番),缺少Level 1-6的常见番型。
add_max每level只取最大值→缺中低番型拼不出8番门槛。

修复:
- DSL补全:全大/全中/全小(12番),大于五/小于五(8番),碰碰和/混一色/五门齐/全求人(6番),
  全带幺(4番),断幺九/平和/门前清(2番),缺一门/无字(1番)
- ScoreEngine/PhaseMachine fallback加'碰碰和'(国标名称)
- 随机AI 国标胡牌率: 0%→30%(纯运气,合理)

注: 完整国标有81种番型,Demo阶段补到28种够覆盖常见组合
2026-07-04 19:16:16 +08:00
cadf76999e fix: DSL total字段从未被引擎校验+广东牌库数修正
Root cause:
1. Deal()完全忽略 _rules.Deck.Total → 牌库大小由 includeFlags 决定
2. Guangdong DSL: total=136 但 include_flowers=true → 实际144张
3. DS与引擎脱节导致广东无限对局(flaky test根因之一)

修复:
- Deck构造加 expectedTotal 可选参数,不一致时抛 InvalidOperationException
- Deal()传 _rules.Deck.Total → 所有DSL变更牌数时必须引擎一致
- Guangdong DSL: total 136→144(广东麻将含花牌=144张)

Breaks: 无(向后兼容,expectedTotal默认null=不校验)
2026-07-04 18:38:15 +08:00
7c9d5b4637 fix: 癞子=136张内的牌(红中),不是额外+4张 + 宝牌HUD显示
- MahjongTile.ConfigureWildcards: 支持固定wildcard(如红中35)
- IsWildcard: 同时检查encoding 50+和配置的wildcard
- ToString: wildcard牌显示🀫中/🀫发等
- Deck: 固定wildcard作为普通牌加入(红中在136内)
- Deal: fixed类型wildcard不额外加牌
- HUD: 显示 '🀫 宝牌(癞子): 🀫中'
- Wuhan DSL: 移除wildcardCount:4
- 测试: Wuhan牌库136张
2026-07-04 17:37:20 +08:00
21bf35c453 fix: DSL命名不一致 — camelCase→underscore_case (UnderscoredNamingConvention)
DSL YAML字段includeHonors/includeFlowers/wildcardCount等使用了camelCase,
但DslLoader配置了UnderscoredNamingConvention(期望include_honors)。
导致所有DSL的deck配置(字牌/花牌/wildcard)从未生效:
- 武汉: 108张(只有数牌) → 现140张(136+4wildcard)
- 国标: 108张 → 现144张(+花+字)
- 广东: 108张 → 现136张(+花+字)

修复: 4个YAML文件统一改为underscore_case
2026-07-04 17:33:42 +08:00
3c6748bf06 [verified] feat: 麻将规则引擎 Demo 完整实现
RuleEngine 核心类:
- MahjongTile.cs — int编码 (1-29万条筒, 31-37字, 41-48花, 50-59宝牌)
- MeldsSolver.cs — 标准回溯 + wildcard缺口填充 + 七对/十三幺/全不靠
- PhaseMachine.cs — 回合机(摸打碰杠胡 + 优先级仲裁)
- ScoreEngine.cs — 番型计分 + 互斥图
- DslLoader.cs — YAML DSL加载 + 能力检查

4个DSL: 四川血战/广东鸡平胡/国标麻将/武汉麻将
控制台Demo: 交互模式 + 自动模式(--auto)
测试: 33个测试用例, 32个通过
2026-07-03 17:51:59 +08:00