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
|
||||
{
|
||||
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)
|
||||
{
|
||||
|
||||
@ -7,6 +7,24 @@ public class CapabilityRegistry
|
||||
{
|
||||
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 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)}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <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 ===
|
||||
@ -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<EndConditionDsl>? 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<ActionOptionDsl> 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<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
|
||||
{
|
||||
@ -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<MahjongDslRoot>(yaml)
|
||||
?? throw new InvalidOperationException($"Failed to parse DSL: {source}");
|
||||
MahjongDslRoot dsl;
|
||||
try
|
||||
{
|
||||
dsl = _deserializer.Deserialize<MahjongDslRoot>(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);
|
||||
|
||||
Reference in New Issue
Block a user