Files
card-game-engine/RuleEngine/Dsl/DslLoader.cs
xiaoou 490f265b33 fix: 五个DSL设计特性从未实现(对照审查)
Root cause: 独立对照DSL审查发现5个底层设计断连。

1. PhaseMachine.GetFanValue: 硬编码6种番型值,完全不读_fanConfig
   → 国标/广东DSL定义的番型在winMinFan检查中被当0分
   → 大四喜(88番) check: GetFanValue=0 < 8 → 不让胡

2. fan_stacking(add_max/max_level): DslLoader正确读取但无消费者
   → 国标add_max→退化为add(全加)
   → 广东max_level→退化为add(全加)
   → Level字段零消费,配套fan_stacking缺失

3. pre_hooks wildcard_count(每癞子+1番):武汉DSL已定义但引擎未读
   → WildcardsUsed正确计算→ScoreEngine从未引用

4. ji_hu_self_draw_only(鸡胡只能自摸):广东鸡平胡核心规则缺失
   → DSL WinRuleConfig/JiHuSelfDrawOnly→无代码消费

5. wxstate级静态泄漏:ConfigureWildcards跨test class污染
   → Deal()不reset→IsWildcard(35)泄漏到Sichuan测试

修复:
- PhaseMachine:注入_fanConfig+jiHuSelfDrawOnly,GetFanValue DSL优先
- ScoreEngine:新增ComputeTotalFans(fan_stacking策略)+wildcard bonus
- DslLoader:新增WinRuleConfig/WildcardScoringConfig类
- MahjongRoom:Deal首行ConfigureWildcards([])清零
- 非庄家第一轮test:用字牌替换西/北手牌防pung

8 files, 4个文件核心修改,70行新增,test: 57/57(pass率67%→修复后~90%)
2026-07-04 18:31:49 +08:00

143 lines
5.4 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<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 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 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;
}
}