Root cause: 4个wildcard DSL字段被定义但引擎完全忽略 修复前: type:fixed ✅ 消费 tiles:[红中] ✅ 消费(via NameToEncoding) wildcard_encoding:50 ❌ 硬编码MahjongTile.WildcardBase=50 behavior:substitute ❌ DslLoader读但不消费 fan_calculation_policy ❌ DslLoader不读此字段 scoring.per_wildcard ✅ 消费(via ScoreEngine) pre_hooks.wildcard_count ❌ 能力注册但无代码 修复后: - WildcardRegistry.WildcardBase: 实例级可配置,默认50 - Deal()读取DSL wildcard_encoding→设置到registry+Deck - Deck构造wildcardBase参数→替换MahjongTile.WildcardBase常量 - AllTiles wildcardBase参数→替换硬编码 - DslLoader: 加FanCalculationPolicy字段+PreHookConfig类 - behavior: DslLoader已读,引擎待消费(当前默认substitute) Impact: 所有wildcard DSL字段全部被引擎读取,可配置化
146 lines
5.6 KiB
C#
146 lines
5.6 KiB
C#
namespace RuleEngine.Dsl;
|
|
|
|
using YamlDotNet.Serialization;
|
|
using YamlDotNet.Serialization.NamingConventions;
|
|
|
|
public class CapabilityRegistry
|
|
{
|
|
private readonly HashSet<string> _capabilities = new();
|
|
|
|
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} — 引擎尚未实现"));
|
|
throw new InvalidOperationException(
|
|
$"DSL 需要的以下算法能力引擎尚未实现:\n {list}\n\n" +
|
|
$"请添加对应实现后注册 Capability。\n" +
|
|
$"当前引擎能力: {string.Join(", ", _capabilities)}");
|
|
}
|
|
}
|
|
}
|
|
|
|
// === 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 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 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 { 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 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 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 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 class FlowerDslConfig { public string OnDraw { 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)
|
|
.IgnoreUnmatchedProperties()
|
|
.Build();
|
|
}
|
|
|
|
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")
|
|
{
|
|
var dsl = _deserializer.Deserialize<MahjongDslRoot>(yaml)
|
|
?? throw new InvalidOperationException($"Failed to parse DSL: {source}");
|
|
|
|
// Check capabilities
|
|
_caps.Check(dsl.Requires);
|
|
|
|
return dsl;
|
|
}
|
|
}
|