From ed30351ad08908525cdf9c78084c5573514b3b7c Mon Sep 17 00:00:00 2001 From: xiaoou Date: Sat, 4 Jul 2026 20:16:47 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20DSL=E9=80=82=E9=85=8D=E7=8E=87=E6=8A=A5?= =?UTF-8?q?=E5=91=8A+=E9=9B=B6=E9=9D=99=E9=BB=98=E4=B8=A2=E5=BC=83YAML?= =?UTF-8?q?=E5=AD=97=E6=AE=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root cause: .IgnoreUnmatchedProperties()→15+YAML字段被静默丢弃 修复: 1. 移除IgnoreUnmatchedProperties→未知字段立即报错 2. 补全所有缺失C#属性:GameInfo(engine_type/players), PhaseDslConfig(turn_order/fu_flag),DrawSubPhase(on_draw_flower/on_empty_deck), WinConditionConfig(pair_must_be_258 alias),PreHookConfig(condition), FlowerDslConfig→FlowerScoringConfig→FlowerMatchingConfig完整嵌套 3. FuFlagConfig/PlayersConfig新类 4. CapabilityRegistry.ComputeCoverage→DSL适配率百分比 5. Demo显示:✅ DSL适配率:7/7(100%) 结果:4个DSL全部100%加载,无字段被静默丢弃 --- Demo/Program.cs | 10 ++++ RuleEngine/Dsl/DslLoader.cs | 109 ++++++++++++++++++++++++++++++++---- 2 files changed, 108 insertions(+), 11 deletions(-) diff --git a/Demo/Program.cs b/Demo/Program.cs index e4939cb..80dd766 100644 --- a/Demo/Program.cs +++ b/Demo/Program.cs @@ -37,6 +37,16 @@ MahjongDslRoot rules; try { rules = loader.Load(dslPath); + var (r, m, pct) = caps.ComputeCoverage(rules.Requires); + string status = pct == 100 ? "✅" : "⚠"; + Console.WriteLine($"{status} DSL 适配率: {m}/{r} ({pct}%) — {rules.Game.Name}"); + if (pct < 100) + { + var missing = rules.Requires.Where(req => !caps.Has(req)); + foreach (var miss in missing) + Console.WriteLine($" ❌ {miss} — 引擎尚未实现,相关规则将被跳过"); + } + Console.WriteLine(); } catch (Exception ex) { diff --git a/RuleEngine/Dsl/DslLoader.cs b/RuleEngine/Dsl/DslLoader.cs index 11b78d9..50706bc 100644 --- a/RuleEngine/Dsl/DslLoader.cs +++ b/RuleEngine/Dsl/DslLoader.cs @@ -7,6 +7,24 @@ 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); @@ -18,12 +36,22 @@ public class CapabilityRegistry { string list = string.Join("\n ", missing.Select(m => $"❌ {m} — 引擎尚未实现")); + int matched = required.Count() - missing.Count; throw new InvalidOperationException( - $"DSL 需要的以下算法能力引擎尚未实现:\n {list}\n\n" + + $"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); + } } // === DSL POCO types === @@ -46,7 +74,14 @@ public class MahjongDslRoot public FlowerDslConfig? FlowerRules { get; set; } } -public class GameInfo { public string Name { get; set; } = ""; public string Type { 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"; @@ -67,9 +102,18 @@ public class WildcardConfig public string FanCalculationPolicy { get; set; } = "optimal"; } public class WildcardScoringConfig { public int PerWildcardInWin { get; set; } } -public class WinConditionConfig { public bool PairMustBe258 { get; set; } } +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 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; } = ""; @@ -89,6 +133,15 @@ public class PhaseDslConfig 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 { @@ -96,7 +149,13 @@ public class SubPhasesConfig 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 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 { @@ -106,8 +165,28 @@ public class OthersReactionSubPhase } 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 class FlowerDslConfig { public string OnDraw { 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 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 { @@ -119,8 +198,7 @@ public class DslLoader _caps = caps; _deserializer = new DeserializerBuilder() .WithNamingConvention(UnderscoredNamingConvention.Instance) - .IgnoreUnmatchedProperties() - .Build(); + .Build(); // No more IgnoreUnmatchedProperties — catch all unknown fields } public MahjongDslRoot Load(string path) @@ -134,8 +212,17 @@ public class DslLoader public MahjongDslRoot LoadString(string yaml, string source = "inline") { - var dsl = _deserializer.Deserialize(yaml) - ?? throw new InvalidOperationException($"Failed to parse DSL: {source}"); + 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);