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: 无花牌,不受影响
202 lines
18 KiB
JSON
202 lines
18 KiB
JSON
{
|
||
"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中硬编码检查>= 8(line 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始终检查chi(line 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;若都非癞子->两者都必须是258(AND逻辑)。"
|
||
},
|
||
{
|
||
"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并填充到PhaseConfig;StepTurn读取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_draw(StepTurn 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的DSL,an_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
|
||
}
|
||
}
|