namespace RuleEngine.Dsl; using YamlDotNet.Serialization; using YamlDotNet.Serialization.NamingConventions; public class CapabilityRegistry { private readonly HashSet _capabilities = new(); // All known engine capabilities (what the engine CAN do) private static readonly HashSet KnownCapabilities = new() { "meldsolver.standard_win", "meldsolver.seven_pairs", "meldsolver.thirteen_orphans", "meldsolver.all_orphans", "meldsolver.double_dragon", "meldsolver.wildcard", "deck.flower_cards", "deck.generator_mahjong", "phase.mahjong_turn", "phase.parallel_elimination", "phase.priority_arbitration", "scoring.fan_exclusion", "scoring.pre_hooks", }; public void Register(string id) => _capabilities.Add(id); public bool Has(string id) => _capabilities.Contains(id); public void Check(IEnumerable required) { var missing = required.Where(r => !_capabilities.Contains(r)).ToList(); if (missing.Count > 0) { string list = string.Join("\n ", missing.Select(m => $"❌ {m} — 引擎尚未实现")); int matched = required.Count() - missing.Count; throw new InvalidOperationException( $"DSL 需要的以下算法能力引擎尚未实现 ({matched}/{required.Count()} 匹配):\n {list}\n\n" + $"请添加对应实现后注册 Capability。\n" + $"当前引擎能力: {string.Join(", ", _capabilities)}"); } } /// Compute how many DSL-required capabilities the engine has registered. public (int required, int matched, int percent) ComputeCoverage(IEnumerable required) { int r = required.Count(); if (r == 0) return (0, 0, 100); int m = required.Count(req => _capabilities.Contains(req)); return (r, m, m * 100 / r); } /// Comprehensive rule-level compliance check — finds DSL rules the engine can't fully honor. public List ValidateRuleCompliance(MahjongDslRoot rules) { var warnings = new List(); // Fan types: check engine can recognize (structural patterns) var enginePatterns = new HashSet { "清一色","混一色","字一色","对对胡","碰碰胡","碰碰和","暗七对","七对", "带幺九","全带幺","混幺九","缺一门","平胡","平和","断幺九", "全大","全中","全小","全双","大于五","小于五", "十三幺","全不靠","一色双龙会","连七对", "将一色","大四喜","大三元","小四喜","小三元","三风","双箭刻", "一色四同顺","一色三同顺","一般高","五门齐","全求人","门前清", "鸡胡","癞子胡", "圈风","门风","单钓将","四归一","杠上开花","海底捞月","抢杠胡","天胡","地胡", "将","无字" // base_fan:0/1 markers }; foreach (var f in rules.FanTypes) { if (!enginePatterns.Contains(f.Name) && !string.IsNullOrEmpty(f.Name)) warnings.Add($"番型「{f.Name}」(base_fan:{f.BaseFan})引擎无识别逻辑→得分始终为0"); } // Fan condition strings (only warn for ones engine can't evaluate) var knownConditions = new HashSet { "hand_contains_wildcard", "can_win_tumo_and_fan_ge_8", "can_win_and_fan_ge_8" }; foreach (var f in rules.FanTypes.Where(f => !string.IsNullOrEmpty(f.Condition))) { if (!knownConditions.Contains(f.Condition!)) warnings.Add($"番型「{f.Name}」条件「{f.Condition}」引擎无法评估→番型永不触发"); } // Wildcard behavior if (rules.WildcardRules != null && !string.IsNullOrEmpty(rules.WildcardRules.Behavior) && rules.WildcardRules.Behavior != "substitute") warnings.Add($"wildcard_rules.behavior={rules.WildcardRules.Behavior} 引擎仅支持substitute→忽略"); // Wildcard type: random is now supported if (rules.WildcardRules != null && !string.IsNullOrEmpty(rules.WildcardRules.Type)) { var validTypes = new HashSet { "fixed", "random" }; if (!validTypes.Contains(rules.WildcardRules.Type)) warnings.Add($"wildcard_rules.type={rules.WildcardRules.Type} 引擎仅支持fixed/random→忽略"); } // Fu flag (过水) — now implemented // No warning needed // Pre-hooks var knownHooks = new HashSet { "wildcard_count", "check_hua_zhu", "check_ting" }; foreach (var h in rules.PreHooks.Concat(rules.Scoring.PreHooks)) { if (!string.IsNullOrEmpty(h.Name) && !knownHooks.Contains(h.Name)) warnings.Add($"pre_hook「{h.Name}」引擎未实现→{h.Description}"); } // fan_stacking 值验证 var validStackings = new HashSet { "add", "add_max", "max_level" }; if (!validStackings.Contains(rules.FanStacking)) warnings.Add($"fan_stacking={rules.FanStacking} 非法→仅支持 add/add_max/max_level→已按add计算"); // Scoring mode if (rules.Scoring.Mode != "fan_table") warnings.Add($"scoring.mode={rules.Scoring.Mode} 引擎仅支持fan_table→忽略"); // 鸡胡 兜底番型: DSL应配置以避免FanValue=0 if (!rules.FanTypes.Any(f => f.Name == "鸡胡")) warnings.Add("fan_types 缺少鸡胡兜底番型→建议添加 { name: 鸡胡, base_fan: 1 }"); // 番型值检查: base_fan=0 的标记番型需有更高番型覆盖 var zeroFans = rules.FanTypes.Where(f => f.BaseFan <= 0).ToList(); if (zeroFans.Count > 0) warnings.Add($"有 {zeroFans.Count} 个番型 base_fan=0 ({string.Join(", ", zeroFans.Select(f => f.Name))})→仅当被更高番型排除时有用"); // 番型识别率: DSL 中多少番型引擎能识别 int recognized = rules.FanTypes.Count(f => enginePatterns.Contains(f.Name)); if (recognized < rules.FanTypes.Count) warnings.Add($"番型识别率: {recognized}/{rules.FanTypes.Count}→{rules.FanTypes.Count - recognized} 个番型得分为0"); // Excludes/Conflicts 引用完整性 // excludes: 用字符串匹配(不依赖_fanConfig), 只检查引擎是否可能生成该番型名 // conflicts: 需要双方都在_fanConfig中才能比较BaseFan var fanNames = rules.FanTypes.Select(f => f.Name).ToHashSet(); foreach (var f in rules.FanTypes) { if (f.Excludes != null) foreach (var excluded in f.Excludes) if (!enginePatterns.Contains(excluded)) // excludes work on string names, check engine can generate warnings.Add($"番型「{f.Name}」excludes 引用了引擎不识别的番型「{excluded}」→排除规则无效(引擎不生成此名)"); if (f.Conflicts != null) foreach (var conflict in f.Conflicts) if (!fanNames.Contains(conflict)) // conflicts need _fanConfig for comparison warnings.Add($"番型「{f.Name}」conflicts 引用了不存在的番型「{conflict}」→冲突规则失效"); } // max_fan 合理性 if (rules.MaxFan < 0) warnings.Add($"max_fan={rules.MaxFan} 非法→应为正数或0"); // Phase type 验证 var validPhaseTypes = new HashSet { "auto", "mahjong_turn" }; foreach (var p in rules.Phases) if (!validPhaseTypes.Contains(p.Type)) warnings.Add($"phase「{p.Name}」type={p.Type} 非法→仅支持 auto/mahjong_turn"); // EndCondition type 验证 var validEndTypes = new HashSet { "player_wins", "deck_exhausted", "last_one_standing" }; foreach (var p in rules.Phases) if (p.EndConditions != null) foreach (var ec in p.EndConditions) if (!validEndTypes.Contains(ec.Type)) warnings.Add($"phase「{p.Name}」end_condition type={ec.Type} 非法→仅支持 player_wins/deck_exhausted/last_one_standing"); // Action 名验证 var validActions = new HashSet { "discard", "an_kong", "bu_kong", "win", "chi", "pung", "ming_kong", "pass" }; foreach (var p in rules.Phases) { var allActions = new List(); if (p.SubPhases?.SelfAction?.Options != null) allActions.AddRange(p.SubPhases.SelfAction.Options); if (p.SubPhases?.OthersReaction?.Options != null) allActions.AddRange(p.SubPhases.OthersReaction.Options); foreach (var a in allActions) if (!validActions.Contains(a.Action)) warnings.Add($"phase「{p.Name}」action={a.Action} 非法→引擎忽略"); } // PriorityPolicy 验证 var validPolicies = new HashSet { "highest_wins" }; foreach (var p in rules.Phases) if (p.SubPhases?.OthersReaction?.PriorityPolicy != null && !validPolicies.Contains(p.SubPhases.OthersReaction.PriorityPolicy)) warnings.Add($"priority_policy={p.SubPhases.OthersReaction.PriorityPolicy} 非法→仅支持 highest_wins"); // FanCalculationPolicy 未消费警告 if (rules.WildcardRules != null && !string.IsNullOrEmpty(rules.WildcardRules.FanCalculationPolicy) && rules.WildcardRules.FanCalculationPolicy != "optimal") warnings.Add($"wildcard_rules.fan_calculation_policy={rules.WildcardRules.FanCalculationPolicy} 引擎未实现→仅optimal生效"); // 精计数范围警告: 通用(所有有精牌的玩法) if (rules.WildcardRules?.Scoring?.PerWildcardInWin > 0) warnings.Add("精计数仅含手牌(不含副露)→per_wildcard_bonus计算范围受限"); // 冲关/德国/正副精: 南昌麻将精计分限制 if (rules.WildcardRules?.Type == "random") { warnings.Add("骰子翻牌定精: 冲关(精数×2^N)/德国(无精胡)计分规则引擎未实现"); warnings.Add("骰子翻牌定精: 正精(2子)/副精(1子)分值为引擎未实现→per_wildcard统一计分"); } // Wildcard meld mode: 引擎仅支持standard(顺子/刻子/对子) if (rules.WildcardRules?.MeldMode == "any") warnings.Add("wildcard_rules.meld_mode=any 引擎未实现→仅支持standard(顺子/刻子/对子)"); // 南昌麻将应含全不靠/十三烂支持 if (rules.WildcardRules?.Type == "random" && !rules.Requires.Contains("meldsolver.all_orphans")) warnings.Add("骰子翻牌定精玩法建议添加 meldsolver.all_orphans→支持十三烂牌型"); // === 引擎功能边界警告: DSL可配置但引擎不支持或部分支持 === // 未消费字段 if (rules.Phases.Any(p => !string.IsNullOrEmpty(p.OnEliminate))) warnings.Add("phase.on_eliminate 已配置但引擎未消费→淘汰逻辑为基本实现"); foreach (var pp in rules.Phases) if (!string.IsNullOrEmpty(pp.SubPhases?.OthersReaction?.OnWin) && pp.SubPhases.OthersReaction.OnWin != "game_over") warnings.Add($"phase.on_win={pp.SubPhases.OthersReaction.OnWin}→引擎仅支持 game_over"); if (rules.Phases.Any(p => p.FuFlag != null)) warnings.Add("phase.fu_flag 已配置但引擎用硬编码过水→DSL值不生效"); // 花牌一致性 var dealPhase = rules.Phases.FirstOrDefault(p => p.Name == "deal"); if (rules.FlowerRules != null && dealPhase?.Action != "deal_cards_with_flowers") warnings.Add("有flower_rules但deal action非deal_cards_with_flowers→初始花牌不补"); if (rules.FlowerRules != null && !string.IsNullOrEmpty(rules.FlowerRules.OnDraw) && rules.FlowerRules.OnDraw != "replace_and_draw") warnings.Add($"flower_rules.on_draw={rules.FlowerRules.OnDraw}→引擎仅支持replace_and_draw"); // 玩家数 if ((rules.Game.Players?.Min ?? 4) != 4 || (rules.Game.Players?.Max ?? 4) != 4) warnings.Add("玩家数≠4→引擎仅支持4人"); // Draw sub-phase 引擎硬编码 foreach (var p in rules.Phases) { var odf = p.SubPhases?.Draw?.OnDrawFlower; if (!string.IsNullOrEmpty(odf) && odf != "replace") warnings.Add($"phase「{p.Name}」draw.on_draw_flower={odf}→引擎仅支持replace"); var oe = p.SubPhases?.Draw?.OnEmptyDeck; if (!string.IsNullOrEmpty(oe) && oe != "exhausted" && !rules.Phases.Any(ph => ph.ParallelElimination)) warnings.Add($"phase「{p.Name}」draw.on_empty_deck={oe}→引擎硬编码处理(血战除外)"); } // Fan condition: 仅 hand_contains_wildcard 被引擎求值 foreach (var f in rules.FanTypes.Where(f => !string.IsNullOrEmpty(f.Condition))) if (f.Condition != "hand_contains_wildcard") warnings.Add($"番型「{f.Name}」condition={f.Condition}→引擎仅支持hand_contains_wildcard"); // Deck generator if (rules.Deck.Generator != "mahjong") warnings.Add($"deck.generator={rules.Deck.Generator}→引擎仅支持mahjong"); // Flower replace_tiles: warning about partial support if (rules.FlowerRules != null && !string.IsNullOrEmpty(rules.FlowerRules.ReplaceTiles) && rules.FlowerRules.ReplaceTiles != "BEFORE_GAME_START") warnings.Add($"flower_rules.replace_tiles={rules.FlowerRules.ReplaceTiles}→仅BEFORE_GAME_START完全支持"); // Condition strings: can_win*_and_fan_ge_8 is known but fanSum=0 in CheckCondition if (rules.Phases.Any(p => (p.SubPhases?.SelfAction?.Options?.Any(o => o.Condition == "can_win_tumo_and_fan_ge_8") == true) || (p.SubPhases?.OthersReaction?.Options?.Any(o => o.Condition == "can_win_and_fan_ge_8") == true))) warnings.Add("win condition can_win*_and_fan_ge_8 引擎用硬编码检查替代→DSL condition不生效"); // Scoring pre_hooks: conditions not evaluated if (rules.Scoring.PreHooks.Any(h => !string.IsNullOrEmpty(h.Condition))) warnings.Add("scoring.pre_hooks 的 condition 字段引擎不评估→条件逻辑为硬编码"); return warnings; } } // === DSL POCO types === public class MahjongDslRoot { public GameInfo Game { get; set; } = new(); public List Requires { get; set; } = new(); public DeckConfig Deck { get; set; } = new(); public DealConfig Deal { get; set; } = new(); public WildcardConfig? WildcardRules { get; set; } public WinConditionConfig? WinCondition { get; set; } public WinRuleConfig? WinRule { get; set; } public List PreHooks { get; set; } = new(); public List FanTypes { get; set; } = new(); public string FanStacking { get; set; } = "add"; public int MaxFan { get; set; } = int.MaxValue; public List Phases { get; set; } = new(); public ScoringDslConfig Scoring { get; set; } = new(); public int WinMinFan { get; set; } = 0; public FlowerDslConfig? FlowerRules { get; set; } } public class GameInfo { public string Name { get; set; } = ""; public string Type { get; set; } = ""; public string EngineType { get; set; } = ""; public PlayersConfig? Players { get; set; } } public class PlayersConfig { public int Min { get; set; } public int Max { get; set; } } public class DeckConfig { public string Generator { get; set; } = "mahjong"; public int Total { get; set; } public bool IncludeHonors { get; set; } public bool IncludeFlowers { get; set; } public int WildcardCount { get; set; } public string? WildcardTile { get; set; } } public class DealConfig { public int CardsPerPlayer { get; set; } = 13; public int DealerExtra { get; set; } = 1; } public class WildcardConfig { public string Type { get; set; } = ""; public string Behavior { get; set; } = ""; public string MeldMode { get; set; } = "standard"; // standard=仅顺子/刻子/对子, any=任意3张可配面子(南昌麻将) public int WildcardEncoding { get; set; } = 50; public int RandomCount { get; set; } = 2; // random type: how many next-tiles are 精 (1=正精 only, 2=正精+副精) public List? Tiles { get; set; } public WildcardScoringConfig? Scoring { get; set; } public string FanCalculationPolicy { get; set; } = "optimal"; } public class WildcardScoringConfig { public int PerWildcardInWin { get; set; } public bool IndependentBonus { get; set; } = false; // true=精分不参与庄家/自摸倍数(南昌麻将) } public class WinConditionConfig { [YamlMember(Alias = "pair_must_be_258")] public bool PairMustBe258 { get; set; } } public class WinRuleConfig { public bool JiHuSelfDrawOnly { get; set; } } public class PreHookConfig { public string Name { get; set; } = ""; public string Description { get; set; } = ""; public string Condition { get; set; } = ""; } public class FanTypeConfig { public string Name { get; set; } = ""; public int BaseFan { get; set; } public int Level { get; set; } public List? Excludes { get; set; } public List? Conflicts { get; set; } public string? Condition { get; set; } } public class PhaseDslConfig { public string Name { get; set; } = ""; public string Type { get; set; } = ""; public string? Action { get; set; } public string? Next { get; set; } public SubPhasesConfig? SubPhases { get; set; } public List? EndConditions { get; set; } public bool ParallelElimination { get; set; } public string? OnEliminate { get; set; } public FuFlagConfig? FuFlag { get; set; } public string TurnOrder { get; set; } = "counter_clockwise"; } public class FuFlagConfig { public string Type { get; set; } = ""; public string SetOn { get; set; } = ""; public string ClearOn { get; set; } = ""; public string Effect { get; set; } = ""; } public class SubPhasesConfig { public DrawSubPhase? Draw { get; set; } public SelfActionSubPhase? SelfAction { get; set; } public OthersReactionSubPhase? OthersReaction { get; set; } } public class DrawSubPhase { public string Type { get; set; } = "auto"; public string Action { get; set; } = "draw_card"; public string OnDrawFlower { get; set; } = ""; public string OnEmptyDeck { get; set; } = ""; } public class SelfActionSubPhase { public List Options { get; set; } = new(); } public class OthersReactionSubPhase { public List Options { get; set; } = new(); public string PriorityPolicy { get; set; } = "highest_wins"; public string? OnWin { get; set; } } public class ActionOptionDsl { public string Action { get; set; } = ""; public int Priority { get; set; } public string? Condition { get; set; } } public class EndConditionDsl { public string Type { get; set; } = ""; public string Action { get; set; } = ""; } public class ScoringDslConfig { public string Mode { get; set; } = "fan_table"; public int MaxCap { get; set; } = int.MaxValue; public List PreHooks { get; set; } = new(); public int SelfDrawMultiplier { get; set; } = 1; // 自摸: 每家付 baseFan × N public int DiscardWinMultiplier { get; set; } = 3; // 点炮: 放炮者付 baseFan × N (默认3=传统包赔) public int DealerMultiplier { get; set; } = 1; // 庄家: 庄家输赢 × N } public class FlowerDslConfig { public string OnDraw { get; set; } = ""; public string ReplaceTiles { get; set; } = ""; public FlowerScoringConfig? Scoring { get; set; } } public class FlowerScoringConfig { public int Normal { get; set; } public FlowerMatchingConfig? Matching { get; set; } } public class FlowerMatchingConfig { public int Value { get; set; } public Dictionary>? Mapping { get; set; } } public class DslLoader { private readonly CapabilityRegistry _caps; private readonly IDeserializer _deserializer; public DslLoader(CapabilityRegistry caps) { _caps = caps; _deserializer = new DeserializerBuilder() .WithNamingConvention(UnderscoredNamingConvention.Instance) .Build(); // No more IgnoreUnmatchedProperties — catch all unknown fields } public MahjongDslRoot Load(string path) { if (!File.Exists(path)) throw new FileNotFoundException($"DSL file not found: {path}"); var yaml = File.ReadAllText(path); return LoadString(yaml, path); } public MahjongDslRoot LoadString(string yaml, string source = "inline") { MahjongDslRoot dsl; try { dsl = _deserializer.Deserialize(yaml) ?? throw new InvalidOperationException($"Failed to parse DSL: {source}"); } catch (YamlDotNet.Core.YamlException ex) when (ex.Message.Contains("not found")) { throw new InvalidOperationException( $"DSL 文件包含引擎不认识的字段。请检查 YAML 键名是否与引擎定义一致。\n{ex.Message}"); } // Check capabilities _caps.Check(dsl.Requires); return dsl; } }