diff --git a/RuleEngine/Core/Deck.cs b/RuleEngine/Core/Deck.cs index 46b86ca..fb50ee0 100644 --- a/RuleEngine/Core/Deck.cs +++ b/RuleEngine/Core/Deck.cs @@ -5,7 +5,7 @@ public class MahjongDeck public List Tiles { get; private set; } = new(); public MahjongDeck(bool includeHonors = false, bool includeFlowers = false, int wildcardCount = 0, - int? expectedTotal = null, int wildcardBase = 50) + int? expectedTotal = null, int wildcardBase = 50) // default matches MahjongTile.WildcardBase { var allTiles = MahjongTile.AllTiles(includeHonors, includeFlowers, wildcardCount); foreach (var t in allTiles) diff --git a/RuleEngine/Core/WildcardRegistry.cs b/RuleEngine/Core/WildcardRegistry.cs index 05890f1..46f9a95 100644 --- a/RuleEngine/Core/WildcardRegistry.cs +++ b/RuleEngine/Core/WildcardRegistry.cs @@ -20,8 +20,8 @@ public class WildcardRegistry /// The tile revealed from deck to determine random wildcards (骰子翻牌定精). public int? RevealedTile { get; set; } - public WildcardRegistry() { _wildcardBase = 50; } - public WildcardRegistry(int wildcardBase) { _wildcardBase = wildcardBase > 0 ? wildcardBase : 50; } + public WildcardRegistry() { _wildcardBase = MahjongTile.WildcardBase; } + public WildcardRegistry(int wildcardBase) { _wildcardBase = wildcardBase > 0 ? wildcardBase : MahjongTile.WildcardBase; } /// Non-extra wildcard tiles — normal tiles (e.g. 红中=35) that ALSO act as wildcards. public IReadOnlySet ConfiguredWildcards => _wildcards; diff --git a/RuleEngine/Dsl/DslLoader.cs b/RuleEngine/Dsl/DslLoader.cs index c066f9e..e287ea1 100644 --- a/RuleEngine/Dsl/DslLoader.cs +++ b/RuleEngine/Dsl/DslLoader.cs @@ -132,6 +132,62 @@ public class CapabilityRegistry if (recognized < rules.FanTypes.Count) warnings.Add($"番型识别率: {recognized}/{rules.FanTypes.Count}→{rules.FanTypes.Count - recognized} 个番型得分为0"); + // Excludes/Conflicts 引用完整性: 引用的番型名必须存在于 fan_types + 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 (!fanNames.Contains(excluded)) + warnings.Add($"番型「{f.Name}」excludes 引用了不存在的番型「{excluded}」→排除规则失效"); + if (f.Conflicts != null) + foreach (var conflict in f.Conflicts) + if (!fanNames.Contains(conflict)) + 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 { "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 { "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 { "discard", "an_kong", "bu_kong", "win", "chi", "pung", "ming_kong", "pass" }; + foreach (var p in rules.Phases) + { + var allActions = new List(); + 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 { "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生效"); + return warnings; } } diff --git a/RuleEngine/MahjongRoom.cs b/RuleEngine/MahjongRoom.cs index 282bc42..1d90e40 100644 --- a/RuleEngine/MahjongRoom.cs +++ b/RuleEngine/MahjongRoom.cs @@ -102,6 +102,7 @@ public class MahjongRoom TurnOrder = p.TurnOrder ?? "counter_clockwise", ParallelElimination = p.ParallelElimination, OnEliminate = p.OnEliminate, + FuFlag = p.FuFlag, SelfActions = p.SubPhases?.SelfAction?.Options?.Select(o => new ActionOption { Action = o.Action, Priority = o.Priority, Condition = o.Condition diff --git a/RuleEngine/Phase/PhaseMachine.cs b/RuleEngine/Phase/PhaseMachine.cs index 879ba26..3c12520 100644 --- a/RuleEngine/Phase/PhaseMachine.cs +++ b/RuleEngine/Phase/PhaseMachine.cs @@ -1,6 +1,7 @@ namespace RuleEngine.Phase; using RuleEngine.Core; +using RuleEngine.Dsl; using RuleEngine.Patterns; public class PhaseConfig @@ -18,6 +19,7 @@ public class PhaseConfig public List EndConditions { get; set; } = new(); public bool ParallelElimination { get; set; } public string? OnEliminate { get; set; } + public FuFlagConfig? FuFlag { get; set; } public int MaxFan { get; set; } = int.MaxValue; }