子代理审查(DSL+Core)发现26个问题,本轮修复3个: 1. fan_stacking值验证: 非法值→警告(之前静默以add计算) 2. OnEliminate传播: BuildPhases补映射(PhaseConfig已有属性) 3. TurnOrder读DSL: 之前硬编码counter_clockwise,现读YAML REVIEW_REPORT.md: 子代理审查完整报告(保留作为参考)
319 lines
13 KiB
C#
319 lines
13 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");
|
||
|
||
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 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 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;
|
||
}
|
||
}
|