所有YAML的on_win=game_over与引擎非血战默认行为一致, 只当on_win≠game_over时才报告警告。 效果: 南昌5→4,武汉2→1,国标2→1,广东1→0条警告
460 lines
21 KiB
C#
460 lines
21 KiB
C#
namespace RuleEngine.Dsl;
|
||
|
||
using YamlDotNet.Serialization;
|
||
using YamlDotNet.Serialization.NamingConventions;
|
||
|
||
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);
|
||
|
||
public void Check(IEnumerable<string> 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)}");
|
||
}
|
||
}
|
||
|
||
/// <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);
|
||
}
|
||
|
||
/// <summary>Comprehensive rule-level compliance check — finds DSL rules the engine can't fully honor.</summary>
|
||
public List<string> ValidateRuleCompliance(MahjongDslRoot rules)
|
||
{
|
||
var warnings = new List<string>();
|
||
|
||
// Fan types: check engine can recognize (structural patterns)
|
||
var enginePatterns = new HashSet<string>
|
||
{
|
||
"清一色","混一色","字一色","对对胡","碰碰胡","碰碰和","暗七对","七对",
|
||
"带幺九","全带幺","混幺九","缺一门","平胡","平和","断幺九",
|
||
"全大","全中","全小","全双","大于五","小于五",
|
||
"十三幺","全不靠","一色双龙会","连七对",
|
||
"将一色","大四喜","大三元","小四喜","小三元","三风","双箭刻",
|
||
"一色四同顺","一色三同顺","一般高","五门齐","全求人","门前清",
|
||
"鸡胡","癞子胡",
|
||
"圈风","门风","单钓将","四归一","杠上开花","海底捞月","抢杠胡","天胡","地胡",
|
||
"将","无字" // 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<string> { "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<string> { "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<string> { "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<string> { "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<string> { "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<string> { "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<string> { "discard", "an_kong", "bu_kong", "win", "chi", "pung", "ming_kong", "pass" };
|
||
foreach (var p in rules.Phases)
|
||
{
|
||
var allActions = new List<ActionOptionDsl>();
|
||
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<string> { "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<string> 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<PreHookConfig> PreHooks { get; set; } = new();
|
||
public List<FanTypeConfig> FanTypes { get; set; } = new();
|
||
public string FanStacking { get; set; } = "add";
|
||
public int MaxFan { get; set; } = int.MaxValue;
|
||
public List<PhaseDslConfig> 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<string>? 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<string>? Excludes { get; set; }
|
||
public List<string>? 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<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
|
||
{
|
||
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<ActionOptionDsl> Options { get; set; } = new(); }
|
||
public class OthersReactionSubPhase
|
||
{
|
||
public List<ActionOptionDsl> 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<PreHookConfig> 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<int, List<string>>? 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<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);
|
||
|
||
return dsl;
|
||
}
|
||
}
|