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:
xiaoou
2026-07-04 20:16:47 +08:00
parent 55bdfd85a5
commit ed30351ad0
2 changed files with 108 additions and 11 deletions

View File

@ -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)
{

View File

@ -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);