feat: DSL适配率报告+零静默丢弃YAML字段
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%加载,无字段被静默丢弃
This commit is contained in:
@ -37,6 +37,16 @@ MahjongDslRoot rules;
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
rules = loader.Load(dslPath);
|
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)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|||||||
@ -7,6 +7,24 @@ public class CapabilityRegistry
|
|||||||
{
|
{
|
||||||
private readonly HashSet<string> _capabilities = new();
|
private readonly HashSet<string> _capabilities = new();
|
||||||
|
|
||||||
|
// All known engine capabilities (what the engine CAN do)
|
||||||
|
private static readonly HashSet<string> 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 void Register(string id) => _capabilities.Add(id);
|
||||||
|
|
||||||
public bool Has(string id) => _capabilities.Contains(id);
|
public bool Has(string id) => _capabilities.Contains(id);
|
||||||
@ -18,12 +36,22 @@ public class CapabilityRegistry
|
|||||||
{
|
{
|
||||||
string list = string.Join("\n ", missing.Select(m =>
|
string list = string.Join("\n ", missing.Select(m =>
|
||||||
$"❌ {m} — 引擎尚未实现"));
|
$"❌ {m} — 引擎尚未实现"));
|
||||||
|
int matched = required.Count() - missing.Count;
|
||||||
throw new InvalidOperationException(
|
throw new InvalidOperationException(
|
||||||
$"DSL 需要的以下算法能力引擎尚未实现:\n {list}\n\n" +
|
$"DSL 需要的以下算法能力引擎尚未实现 ({matched}/{required.Count()} 匹配):\n {list}\n\n" +
|
||||||
$"请添加对应实现后注册 Capability。\n" +
|
$"请添加对应实现后注册 Capability。\n" +
|
||||||
$"当前引擎能力: {string.Join(", ", _capabilities)}");
|
$"当前引擎能力: {string.Join(", ", _capabilities)}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Compute how many DSL-required capabilities the engine has registered.</summary>
|
||||||
|
public (int required, int matched, int percent) ComputeCoverage(IEnumerable<string> 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 ===
|
// === DSL POCO types ===
|
||||||
@ -46,7 +74,14 @@ public class MahjongDslRoot
|
|||||||
public FlowerDslConfig? FlowerRules { get; set; }
|
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 class DeckConfig
|
||||||
{
|
{
|
||||||
public string Generator { get; set; } = "mahjong";
|
public string Generator { get; set; } = "mahjong";
|
||||||
@ -67,9 +102,18 @@ public class WildcardConfig
|
|||||||
public string FanCalculationPolicy { get; set; } = "optimal";
|
public string FanCalculationPolicy { get; set; } = "optimal";
|
||||||
}
|
}
|
||||||
public class WildcardScoringConfig { public int PerWildcardInWin { get; set; } }
|
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 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 class FanTypeConfig
|
||||||
{
|
{
|
||||||
public string Name { get; set; } = "";
|
public string Name { get; set; } = "";
|
||||||
@ -89,6 +133,15 @@ public class PhaseDslConfig
|
|||||||
public List<EndConditionDsl>? EndConditions { get; set; }
|
public List<EndConditionDsl>? EndConditions { get; set; }
|
||||||
public bool ParallelElimination { get; set; }
|
public bool ParallelElimination { get; set; }
|
||||||
public string? OnEliminate { 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 class SubPhasesConfig
|
||||||
{
|
{
|
||||||
@ -96,7 +149,13 @@ public class SubPhasesConfig
|
|||||||
public SelfActionSubPhase? SelfAction { get; set; }
|
public SelfActionSubPhase? SelfAction { get; set; }
|
||||||
public OthersReactionSubPhase? OthersReaction { 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<ActionOptionDsl> Options { get; set; } = new(); }
|
public class SelfActionSubPhase { public List<ActionOptionDsl> Options { get; set; } = new(); }
|
||||||
public class OthersReactionSubPhase
|
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 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 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 ScoringDslConfig
|
||||||
public class FlowerDslConfig { public string OnDraw { get; set; } = ""; }
|
{
|
||||||
|
public string Mode { get; set; } = "fan_table";
|
||||||
|
public int MaxCap { get; set; } = int.MaxValue;
|
||||||
|
public List<PreHookConfig> 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<int, List<string>>? Mapping { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
public class DslLoader
|
public class DslLoader
|
||||||
{
|
{
|
||||||
@ -119,8 +198,7 @@ public class DslLoader
|
|||||||
_caps = caps;
|
_caps = caps;
|
||||||
_deserializer = new DeserializerBuilder()
|
_deserializer = new DeserializerBuilder()
|
||||||
.WithNamingConvention(UnderscoredNamingConvention.Instance)
|
.WithNamingConvention(UnderscoredNamingConvention.Instance)
|
||||||
.IgnoreUnmatchedProperties()
|
.Build(); // No more IgnoreUnmatchedProperties — catch all unknown fields
|
||||||
.Build();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public MahjongDslRoot Load(string path)
|
public MahjongDslRoot Load(string path)
|
||||||
@ -134,8 +212,17 @@ public class DslLoader
|
|||||||
|
|
||||||
public MahjongDslRoot LoadString(string yaml, string source = "inline")
|
public MahjongDslRoot LoadString(string yaml, string source = "inline")
|
||||||
{
|
{
|
||||||
var dsl = _deserializer.Deserialize<MahjongDslRoot>(yaml)
|
MahjongDslRoot dsl;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
dsl = _deserializer.Deserialize<MahjongDslRoot>(yaml)
|
||||||
?? throw new InvalidOperationException($"Failed to parse DSL: {source}");
|
?? 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
|
// Check capabilities
|
||||||
_caps.Check(dsl.Requires);
|
_caps.Check(dsl.Requires);
|
||||||
|
|||||||
Reference in New Issue
Block a user