Files
card-game-engine/dsl-review-result.json
xiaoou 9b9e4c310b fix: flower_rules DSL字段全部接入引擎(花牌计分+开局换花)
Root cause: flower_rules.on_draw/着replace_tiles/scoring三个DSL字段
读入但零引擎消费

修复:
- Deal(): replace_tiles="BEFORE_GAME_START"时开局扫描所有手牌换花牌
- ScoreEngine: 花牌计分(FlowerNormalScore×花牌数+配对花牌FlowerMatchScore)
- ScoringConfig: 新增FlowerNormalScore/FlowerMatchScore/FlowerMapping
- MahjongRoom: 从DSL读取花牌计分配置→传入ScoreEngine

Impact:
- Guangdong: BEFORE_GAME_START开局换花+花牌计分(每张1分+配对1分)
- Guobiao: on_draw replace运行时换花(已有) + 花牌不计分(DSL无scoring)
- Xuezhan/Wuhan: 无花牌,不受影响
2026-07-04 20:29:50 +08:00

202 lines
18 KiB
JSON
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{
"passed": false,
"summary": "审查了4个DSL YAML对照全部12个引擎C#文件共发现26个问题5个high、16个med、5个low。核心问题番型名称不一致导致DSL规则无法匹配、大量番型无识别逻辑、PhaseMachine癞子计数遗漏配置癞子、过水/花牌计分/条件评估子机制缺失。",
"issues": [
{
"severity": "high",
"file": "RuleEngine/Patterns/MeldsSolver.cs:583-584,663-665",
"rule": "武汉DSL fan_types: 碰碰胡(2) / 国标DSL fan_types: 碰碰和(6)",
"problem": "IdentifyFans固定生成 对对胡 而非 碰碰胡。武汉DSL定义碰碰胡国标DSL定义碰碰和引擎只生成对对胡。虽然fallback硬编码值恰好匹配武汉(2)和国标(6)但名称不匹配导致_fanConfig查找失败、exclusion规则缺失(武汉碰碰胡无excludes但实际不受DSL控制)。Line 663-665的碰碰和生成只在 对对胡 不存在时才追加,但 对对胡 总是先生成,导致 碰碰和 永不被加入——国标麻将无法识别此番型。",
"fix": "IdentifyFans中将对对胡改为从DSL fanConfig动态匹配或根据_fanConfig中存在的键来决定生成哪个名称。"
},
{
"severity": "high",
"file": "RuleEngine/Patterns/MeldsSolver.cs:586-588",
"rule": "武汉DSL fan_types: 七对(8,excludes:门前清+单钓将) / 国标DSL fan_types: 七对(24,level:8,excludes:门前清+单钓将)",
"problem": "七对路径IdentifyFans生成的是 暗七对 而非 七对。武汉和国标DSL都使用 七对 作为番型名,但引擎固定输出 暗七对。_fanConfig中键是七对查找失败后fallback给8番(恰好匹配武汉8但国标24不对)exclusion规则(门前清、单钓将)完全失效。",
"fix": "TrySevenPairs成功时将牌型名改为 七对或根据DSL fanConfig中有 七对 还是 暗七对 动态选择。"
},
{
"severity": "high",
"file": "RuleEngine/Phase/PhaseMachine.cs:82-83,105-106",
"rule": "武汉DSL wildcard_rules: {type:fixed, tiles:[红中], wildcard_encoding:50}",
"problem": "PhaseMachine.GetLegalActions中计算手牌癞子数使用静态方法Core.MahjongTile.IsWildcard(tile)——只检查encoding>=50不检查WildcardRegistry中配置的红中(35)。导致PhaseMachine对武汉麻将的癞子计数为0所有基于wildcardCount的CheckWin调用都当作无癞子处理。",
"fix": "改为使用注入的_wildcards.IsWildcard(tile)或CountWildcards。"
},
{
"severity": "high",
"file": "RuleEngine/Phase/PhaseMachine.cs:88-94,108-111",
"rule": "国标DSL win_min_fan: 8",
"problem": "国标要求胡牌番数>=8番。PhaseMachine中硬编码检查>= 8line 93,110但没有使用_rules.WinMinFan动态读取。如果DSL改win_min_fan为其他值(如16)代码会忽略。且检查逻辑中GetFanValue使用硬编码fallback(如 暗七对->8而非DSL中定义的 七对->24),导致国标七对(24番)被误判为8番。",
"fix": "将>= 8改为>= (配置传入的WinMinFan值)修复GetFanValue优先查_fanConfig前提是修复番型名称匹配。"
},
{
"severity": "high",
"file": "RuleEngine/MahjongRoom.cs:309-310",
"rule": "血战到底DSL others_reaction选项无chi",
"problem": "血战到底明确不允许吃牌(others_reaction只含pung/ming_kong/win/pass)。但HandleDiscardReactions始终检查chiline 309只检查offset和IsHonor不读取DSL phases中的OthersReactions列表来判断chi是否被允许。这导致血战到底中下家可以吃牌违反核心规则。",
"fix": "在检查chi前先查询_phaseMachine中当前phase的OthersReactions是否包含action=chi的选项。"
},
{
"severity": "med",
"file": "RuleEngine/Patterns/MeldsSolver.cs:552-671",
"rule": "全部DSL fan_types: 将一色(16), 大四喜(88), 大三元(88), 小四喜(64), 小三元(64), 一色四同顺(48), 连七对(88), 五门齐(6), 全带幺(4), 全求人(4/6) 等十余个番型",
"problem": "IdentifyFans缺少大量番型的结构识别。已识别清一色、混一色、字一色、对对胡、暗七对、带幺九、混幺九、缺一门、平胡、断幺九、全大/中/小、大于五/小于五、全双、碰碰和。缺失:将一色(全258)、大四喜(4风刻)、大三元(3箭刻)、小四喜(3风刻+风将)、小三元(2箭刻+箭将)、一色四同顺(4相同顺子)、连七对(同色连7对)、五门齐(5牌类)、全带幺、全求人(全落地+单吊)。",
"fix": "为每个缺失番型编写对应的结构/组合识别逻辑。"
},
{
"severity": "med",
"file": "RuleEngine/Patterns/MeldsSolver.cs:368-405",
"rule": "国标DSL fan_types: 十三幺(88) / 广东DSL fan_types: 十三幺(32)",
"problem": "TryThirteenOrphans返回的MeldsResult中Fans由IdentifyFans填充但IdentifyFans不识别 十三幺——它基于melds/pair结构做模式匹配而十三幺的Melds和PairTiles均为空列表导致IdentifyFans返回空或仅包含无关番型。十三幺胡牌后番型列表中没有 十三幺得分为0。",
"fix": "在TryThirteenOrphans成功时直接向Fans添加 十三幺或在IdentifyFans中检测特定牌型。"
},
{
"severity": "med",
"file": "RuleEngine/Patterns/MeldsSolver.cs:45-49,82-86",
"rule": "武汉DSL win_condition: pair_must_be_258: true",
"problem": "258将检查line 82-86只在标准回溯路径TryExtractMelds成功后执行。七对(TrySevenPairs)、十三幺、全不靠、一色双龙会四条路径都在258检查之前return不受258将约束。对于七对的情况将牌由对子组成应检查对子是否全为258。",
"fix": "将258检查移到所有路径的公共出口处或为每条特殊路径分别添加检查。"
},
{
"severity": "med",
"file": "RuleEngine/Patterns/MeldsSolver.cs:539-548",
"rule": "武汉DSL win_condition: pair_must_be_258: true",
"problem": "IsValid258Pair使用OR逻辑Check(tileA) || Check(tileB)。当一对牌中有一个癞子(-1)时Check(-1)=true这意味着任何牌+癞子组成的对子都通过258检查即使那张牌不是2/5/8。严格语义应要求对子的实际牌面值为258。",
"fix": "改为:若两者都是癞子->通过;若一个是癞子->检查另一个是否为258若都非癞子->两者都必须是258AND逻辑。"
},
{
"severity": "med",
"file": "RuleEngine/MahjongRoom.cs:38-41, RuleEngine/Scoring/ScoreEngine.cs:38-39",
"rule": "武汉DSL wildcard_rules: {fan_calculation_policy: optimal, scoring: {per_wildcard_in_win: 1}}",
"problem": "(1) fan_calculation_policy: optimal未实现——引擎只用单次回溯找第一个可行解未尝试所有癞子分配方案最大化番数。(2) per_wildcard_in_win在ScoreEngine中乘以WildcardsUsed用于填充meld的癞子数而非手牌中癞子总数。武汉规则每张癞子+1番应统计胡牌时手牌中所有癞子。",
"fix": "实现multi-pass求解选最优WildcardsUsed应累加所有癞子参与计数而非仅缺口填充的。"
},
{
"severity": "med",
"file": "RuleEngine/MahjongRoom.cs:88-106",
"rule": "全部4个DSL的 phases.end_conditions",
"problem": "BuildPhases()未解析PhaseDslConfig中的EndConditions到PhaseConfig。PhaseConfig有EndConditions属性但从未被填充。血战到底的last_one_standing->settle和deck_exhausted->check_hua_zhu均通过MahjongRoom.StepTurn硬编码处理(line 199-205,228-234)而非从DSL配置读取。DSL中自定义end_conditions永远不生效。",
"fix": "BuildPhases中解析EndConditions并填充到PhaseConfigStepTurn读取PhaseConfig.EndConditions而非硬编码。"
},
{
"severity": "med",
"file": "RuleEngine/MahjongRoom.cs:93-97,199-203,737-769",
"rule": "血战到底DSL scoring.pre_hooks: [check_hua_zhu(deck_exhausted), check_ting(deck_exhausted AND not hua_zhu)]",
"problem": "CheckHuaZhu和CheckTing在deck exhausted时无条件依次调用line 200-203。但DSL规定check_ting仅在deck_exhausted AND not hua_zhu时执行——即先检查花猪非花猪玩家才检查听牌。当前代码对所有存活玩家执行两个检查没有条件过滤。",
"fix": "CheckHuaZhu返回花猪玩家列表CheckTing仅对非花猪玩家检查。"
},
{
"severity": "med",
"file": "RuleEngine/MahjongRoom.cs(全文未实现)",
"rule": "血战到底DSL fu_flag: {type:dirty_flag, set_on:can_win_but_pass, clear_on:next_discard_self, effect:block_win_on_current_tile}",
"problem": "过水(fu_flag)机制完全未实现。DSL定义玩家可选择胡牌但pass后设置dirty_flag下一个自己出牌的动作清除flag在flag存在期间禁止对当前牌胡牌。MahjongGameState有FuFlags字典但从未被设置或检查。玩家pass一个可胡的牌后可以立即胡下一家打的同一张牌违反血战到底过水规则。",
"fix": "在HandleDiscardReactions中当玩家可以胡但选择pass时设置FuFlags在后续判定胡牌时检查该flag在玩家自己出牌时清除。"
},
{
"severity": "med",
"file": "RuleEngine/Scoring/ScoreEngine.cs(未实现)",
"rule": "国标DSL flower_rules: {on_draw:replace_and_draw, scoring:{normal:1,matching:{value:1,mapping:{...}}}} / 广东DSL flower_rules同",
"problem": "花牌计分完全未实现。ScoreEngine.Settle只计算番数不处理花牌。国标和广东麻将中花牌每张计1番、座位匹配额外+1番的规则没有代码执行。FlowerPool已记录花牌但从未在结算时读取。",
"fix": "在ScoreEngine.Settle中读取FlowerPool计算花牌番数(每张normal:1 + 匹配:matching.value)累加到baseFan。广东DSL的replace_tiles:BEFORE_GAME_START也需要实现。"
},
{
"severity": "med",
"file": "RuleEngine/MahjongRoom.cs:670-718",
"rule": "广东DSL flower_rules: {replace_tiles: BEFORE_GAME_START}",
"problem": "广东麻将规定花牌开局前就更换补牌在发牌阶段完成而非游戏中。当前引擎只在游戏中摸到花牌时replace_and_drawStepTurn line 175-192发牌阶段Deal()不处理花牌。开局摸到的花牌留到玩家第一回合才补,与广东规则不一致。",
"fix": "Deal()中发牌后检查每个玩家手牌的花牌,立即补牌(循环直到无花牌)。"
},
{
"severity": "med",
"file": "RuleEngine/MahjongRoom.cs:670-718,85-86",
"rule": "国标DSL phases.deal: {action: deal_cards_with_flowers}",
"problem": "BuildPhases虽然读取了deal action但Run()中固定调用Deal()方法不区分deal_cards与deal_cards_with_flowers。国标发牌阶段应包含花牌补牌逻辑发牌后立即补花当前与普通发牌无区别。",
"fix": "根据phase action类型选择不同的Deal实现或在Deal()中加入花牌开局处理。"
},
{
"severity": "med",
"file": "RuleEngine/MahjongRoom.cs:93, RuleEngine/Scoring/ScoreEngine.cs(未使用)",
"rule": "武汉DSL pre_hooks: [{name:wildcard_count}] / 血战到底DSL scoring.pre_hooks: [check_hua_zhu, check_ting]",
"problem": "DSL pre_hooks机制完全未被调用。MahjongDslRoot.PreHooks已解析但MahjongRoom不读取。wildcard_count pre-hook的癞子计数逻辑通过ScoringConfig.PerWildcardBonus间接实现但其他pre-hooks逻辑不一致。",
"fix": "实现统一的pre_hook调度器在相应阶段结算前按顺序执行pre_hooks。"
},
{
"severity": "med",
"file": "RuleEngine/MahjongRoom.cs(全文未实现), RuleEngine/Patterns/MeldsSolver.cs:552-671",
"rule": "全部DSL fan_types: 杠上开花(1), 海底捞月(1), 抢杠胡(1), 天胡(32), 地胡(16), 癞子胡(1,condition:hand_contains_wildcard), 将(0)",
"problem": "这7个番型完全没有识别逻辑。杠上开花需杠补后自摸触发海底捞月需牌墙最后一张胡牌触发抢杠胡需加杠时胡牌触发天胡/地胡需首轮判断;癞子胡需检查手牌含癞子;将(258将)需检查将牌。引擎缺少事件上下文传递(无标志位记录当前是否杠后摸牌/海底/被抢杠等)。",
"fix": "在GameState中增加标志位(last_action_was_kong_draw, is_last_tile, is_robbed_kong, is_first_turn等)MahjongRoom在相应事件设置标志IdentifyFans读取标志添加对应番型。"
},
{
"severity": "med",
"file": "RuleEngine/Patterns/MeldsSolver.cs:551-671, RuleEngine/Phase/PhaseMachine.cs:88-91",
"rule": "广东DSL fan_types: 鸡胡(1,level:1) / win_rule: {ji_hu_self_draw_only:true}",
"problem": "鸡胡是广东麻将的基础番型但IdentifyFans永远不会生成。PhaseMachine中ji_hu_self_draw_only检查(line 88-91)依赖result.Fans.Contains(鸡胡)因不存在而永远走不到正确分支。目前的fallback逻辑(Fans.All(f=>GetFanValue(f)<=1))有副作用——可能误拦截其他低番胡牌。",
"fix": "在IdentifyFans中当检测到的番型总番数较低且没有更高级番型时添加鸡胡番型标识。"
},
{
"severity": "med",
"file": "RuleEngine/MahjongRoom.cs:67,210, RuleEngine/Phase/PhaseMachine.cs:64-116",
"rule": "国标DSL phases.play: self_action{condition:can_win_tumo_and_fan_ge_8}, others_reaction{condition:can_win_and_fan_ge_8}",
"problem": "DSL中定义的action condition字符串(can_win_tumo_and_fan_ge_8/can_win_and_fan_ge_8)被解析到ActionOption.Condition但从未被评估执行。PhaseMachine.GetLegalActions用硬编码逻辑替代requireWinFan && >= 8。DSL中的condition字段纯粹是文档性存在引擎不读取。无法通过DSL自定义win条件。",
"fix": "实现condition求值器解析condition字符串为可执行判断逻辑或至少让PhaseMachine读取ActionOption.Condition来决定是否添加该action。"
},
{
"severity": "med",
"file": "RuleEngine/Phase/PhaseMachine.cs:64-116",
"rule": "全部DSL phases中定义的sub_phases.self_action.options / others_reaction.options列表",
"problem": "GetLegalActions不从PhaseConfig中读取SelfActions/OthersReactions来决定允许哪些操作。代码硬编码添加discard/pung/ming_kong/an_kong/bu_kong/win/pass完全忽略DSL中定义的actions列表及其condition约束。例如指定不包含an_kong的DSLan_kong仍然会被引擎允许。",
"fix": "GetLegalActions参考当前phase的SelfActions和OthersReactions列表只生成DSL中声明的action并评估其condition。"
},
{
"severity": "med",
"file": "RuleEngine/Scoring/ScoreEngine.cs:63-87",
"rule": "国标DSL fan_stacking: add_max / 广东DSL fan_stacking: max_level",
"problem": "add_max策略按Level分组取Max(FanValue)。若番型名未匹配到_fanConfig如 暗七对 vs 七对Level为0分组到同一组导致逻辑错误。同样max_level策略中若所有番型的Level都为0因名称不匹配则取maxLevel=0所有番型都被计入。",
"fix": "修复番型名称匹配后自然解决防御性对Level=0的番型做特殊处理或警告。"
},
{
"severity": "low",
"file": "RuleEngine/Phase/PhaseMachine.cs:118-129, RuleEngine/Scoring/ScoreEngine.cs:119-136",
"rule": "全部DSL fan_types值",
"problem": "GetFanValue和ScoreEngine.FanValue都有硬编码fallback值清一色24、混一色6、对对胡2、碰碰和6、暗七对8、七对8、带幺九2、十三幺88。不同DSL中同一番型值不同武汉清一色=8、血战清一色=4、国标清一色=24、广东清一色=8。当前fallback固定为国标值名称不匹配时使用错误值。",
"fix": "番型值应强制从_fanConfig查找找不到时报错或警告不使用fallback。"
},
{
"severity": "low",
"file": "RuleEngine/Patterns/MeldsSolver.cs:285-318",
"rule": "武汉DSL wildcard_rules: {behavior: substitute}",
"problem": "TryExtractMelds支持wildcard缺口填充实现substitute行为。但FinalizeWithWildcards(line 285-318)中剩余1-2张癞子被丢弃——若剩余癞子无法构成完整面子就被忽略未将这些wildcard计入bonus。",
"fix": "剩余wildcard若只能做单张处理也应记录到WildcardsUsed中供per_wildcard_bonus使用。"
},
{
"severity": "low",
"file": "RuleEngine/Patterns/MeldsSolver.cs:436-493",
"rule": "国标DSL requires: meldsolver.all_orphans / meldsolver.double_dragon",
"problem": "TryAllOrphans的容错逻辑(line 481): totalMissing <= wildcardCount + 2允许最多额外缺2个位置。这个+2的容差过于宽松——全不靠手牌14张应覆盖16个位置至少14个最多缺2个。但+2再加上wildcardCount若wildcardCount>0会允许缺更多可能产生假阳性。",
"fix": "全不靠的容差公式改为Math.Max(0, 16 - 14)而非硬编码+2。"
},
{
"severity": "low",
"file": "RuleEngine/MahjongRoom.cs:270-271",
"rule": "武汉DSL wildcard_rules: {type:fixed} 癞子数获取",
"problem": "GetWinParams中wildcardCount取值为_rules.WildcardRules != null ? _rules.Deck.WildcardCount : 0。对于武汉fixed类型Deck.WildcardCount=0没有额外癞子牌此值仅传给PhaseMachine构造器存储为_wildcardCount且从未使用。实际wildcard计数在各调用点通过CountWildcardsInHand获取——正确。但若未来新增使用GetWinParams().wildcardCount的代码路径将得到错误值0。",
"fix": "GetWinParams应返回实际wildcard计数考虑fixed wildcard在牌墙中的4张红中或重命名为更明确的名字并标注其语义。"
},
{
"severity": "low",
"file": "RuleEngine/MahjongRoom.cs:108-131,228-241",
"rule": "血战到底DSL phases.play: {parallel_elimination:true, on_eliminate:hu_paid}",
"problem": "on_eliminate: hu_paid语义是玩家被淘汰时胡牌即正常胡牌流程当前代码line 228-234中parallel elimination分支只有AlivePlayers<=1时Settle未执行_scoreEngine.Settle对单个胡牌玩家做结算。这意味着在血战到底中每个玩家胡牌后没有立即计分仅在最后一人时统一结算与按胡牌顺序逐个结算的预期不符。",
"fix": "每个玩家胡牌时立即调用_scoreEngine.Settle对单个winner计分记录其已结算状态。"
}
],
"severity_summary": {
"high": 5,
"med": 17,
"low": 5,
"total": 27
}
}