diff --git a/RuleEngine/Phase/PhaseMachine.cs b/RuleEngine/Phase/PhaseMachine.cs index 6e34dae..526a724 100644 --- a/RuleEngine/Phase/PhaseMachine.cs +++ b/RuleEngine/Phase/PhaseMachine.cs @@ -69,50 +69,102 @@ public class MahjongPhaseMachine { var actions = new List(); - // Always can discard - foreach (var t in state.Hands[playerId].Distinct()) - actions.Add(new ActionOption { Action = "discard", Priority = 0 }); + // Read action lists from DSL play phase (instead of hardcoding) + var playPhase = _phases.FirstOrDefault(p => p.Name == "play"); + var selfActs = playPhase?.SelfActions ?? new(); + var reactActs = playPhase?.OthersReactions ?? new(); + bool allowDiscard = selfActs.Any(a => a.Action == "discard"); + bool allowAnKong = selfActs.Any(a => a.Action == "an_kong"); + bool allowBuKong = selfActs.Any(a => a.Action == "bu_kong"); + bool allowSelfDrawWin = selfActs.Any(a => a.Action == "win"); + bool allowPung = reactActs.Any(a => a.Action == "pung"); + bool allowMingKong = reactActs.Any(a => a.Action == "ming_kong"); + bool allowReactionWin = reactActs.Any(a => a.Action == "win"); + bool chiAllowed = _chiAllowed; - // Check pung/kong/win for last discard + // Helper: evaluate DSL condition string + bool CheckCondition(ActionOption opt, bool canPung, bool canMingKong, bool canSelfWin, int fanSum) + { + if (string.IsNullOrEmpty(opt.Condition)) return true; + return opt.Condition switch + { + "has_punged_pair" => CanBuKong(state, playerId), + "has_two_same" => canPung, + "has_three_same" => canMingKong, + "can_chi" => chiAllowed, + "can_win" => canSelfWin, + "can_win_tumo" => canSelfWin, + "can_win_tumo_and_fan_ge_8" => canSelfWin && fanSum >= 8, + "can_win_and_fan_ge_8" => canSelfWin && fanSum >= 8, + _ => true // unknown condition → allow (fail-open for safety) + }; + } + + // Always can discard (every variant supports this) + if (allowDiscard) + { + foreach (var t in state.Hands[playerId].Distinct()) + actions.Add(new ActionOption { Action = "discard", Priority = 0 }); + } + + // Check pung/kong/win for last discard (only if DSL allows them) if (state.LastDiscard.HasValue && state.LastDiscardPlayer != playerId) { - if (CanPung(state, playerId, state.LastDiscard.Value)) - actions.Add(new ActionOption { Action = "pung", Priority = 2 }); - - if (CanMingKong(state, playerId, state.LastDiscard.Value)) - actions.Add(new ActionOption { Action = "ming_kong", Priority = 3 }); - - var fullHand = FullHand(state, playerId, extraTile: state.LastDiscard.Value); - int wildInHand = _wildcards.CountWildcards(fullHand); - var result = _solver.CheckWin(fullHand, - wildcardCount: wildInHand, require258Pair: _require258); - if (result != null && result.IsWin) + if (allowPung && CanPung(state, playerId, state.LastDiscard.Value)) { - // 鸡胡只能自摸 — block discard win if 鸡胡 is the only/lowest fan - if (_jiHuSelfDrawOnly && result.Fans.Count > 0 - && (result.Fans.Contains("鸡胡") || result.Fans.All(f => GetFanValue(f) <= 1))) + if (CheckCondition(reactActs.First(a => a.Action == "pung"), true, false, false, 0)) + actions.Add(new ActionOption { Action = "pung", Priority = 2 }); + } + + if (allowMingKong && CanMingKong(state, playerId, state.LastDiscard.Value)) + { + if (CheckCondition(reactActs.First(a => a.Action == "ming_kong"), false, true, false, 0)) + actions.Add(new ActionOption { Action = "ming_kong", Priority = 3 }); + } + + if (allowReactionWin) + { + var fullHand = FullHand(state, playerId, extraTile: state.LastDiscard.Value); + int wildInHand = _wildcards.CountWildcards(fullHand); + var result = _solver.CheckWin(fullHand, + wildcardCount: wildInHand, require258Pair: _require258); + if (result != null && result.IsWin) { - // Don't add win action; 鸡胡 can only self-draw + // 鸡胡只能自摸 — block discard win if 鸡胡 is the only/lowest fan + if (_jiHuSelfDrawOnly && result.Fans.Count > 0 + && (result.Fans.Contains("鸡胡") || result.Fans.All(f => GetFanValue(f) <= 1))) + { + // Don't add win action; 鸡胡 can only self-draw + } + else if (!requireWinFan || result.Fans.Sum(f => GetFanValue(f)) >= 8) + actions.Add(new ActionOption { Action = "win", Priority = 4 }); } - else if (!requireWinFan || result.Fans.Sum(f => GetFanValue(f)) >= 8) - actions.Add(new ActionOption { Action = "win", Priority = 4 }); } } - // Self actions: an_kong, bu_kong, win (tumo) - if (CanAnKong(state, playerId)) - actions.Add(new ActionOption { Action = "an_kong", Priority = 1 }); - if (CanBuKong(state, playerId)) - actions.Add(new ActionOption { Action = "bu_kong", Priority = 1 }); - - var fullSelfHand = FullHand(state, playerId); - int wildInSelf = fullSelfHand.Count(Core.MahjongTile.IsWildcard); - var tumoResult = _solver.CheckWin(fullSelfHand, - wildcardCount: wildInSelf, require258Pair: _require258); - if (tumoResult != null && tumoResult.IsWin) + // Self actions: only if DSL allows them + if (allowAnKong && CanAnKong(state, playerId)) { - if (!requireWinFan || tumoResult.Fans.Sum(f => GetFanValue(f)) >= 8) - actions.Add(new ActionOption { Action = "win", Priority = 4 }); + if (CheckCondition(selfActs.First(a => a.Action == "an_kong"), false, false, false, 0)) + actions.Add(new ActionOption { Action = "an_kong", Priority = 1 }); + } + if (allowBuKong && CanBuKong(state, playerId)) + { + if (CheckCondition(selfActs.First(a => a.Action == "bu_kong"), false, false, false, 0)) + actions.Add(new ActionOption { Action = "bu_kong", Priority = 1 }); + } + + if (allowSelfDrawWin) + { + var fullSelfHand = FullHand(state, playerId); + int wildInSelf = _wildcards.CountWildcards(fullSelfHand); + var tumoResult = _solver.CheckWin(fullSelfHand, + wildcardCount: wildInSelf, require258Pair: _require258); + if (tumoResult != null && tumoResult.IsWin) + { + if (!requireWinFan || tumoResult.Fans.Sum(f => GetFanValue(f)) >= 8) + actions.Add(new ActionOption { Action = "win", Priority = 4 }); + } } actions.Add(new ActionOption { Action = "pass", Priority = 0 }); diff --git a/RuleEngine/Scoring/ScoreEngine.cs b/RuleEngine/Scoring/ScoreEngine.cs index 2e0be6d..b0f81db 100644 --- a/RuleEngine/Scoring/ScoreEngine.cs +++ b/RuleEngine/Scoring/ScoreEngine.cs @@ -37,9 +37,18 @@ public class MahjongScoreEngine int baseFan = ComputeTotalFans(result.Fans); - // Wildcard bonus: e.g. Wuhan 每癞子+1番 - if (_config.PerWildcardBonus > 0 && result.WildcardsUsed > 0) - baseFan += result.WildcardsUsed * _config.PerWildcardBonus; + // Wildcard bonus: count ALL wildcards in winner's hand (not just gap-fill) + if (_config.PerWildcardBonus > 0) + { + int totalWildInHand = 0; + if (state.Hands.TryGetValue(winner, out var hand)) + totalWildInHand += state.Wildcards.CountWildcards(hand); + if (state.Exposed.TryGetValue(winner, out var exposed)) + foreach (var m in exposed) + totalWildInHand += state.Wildcards.CountWildcards(m.Tiles); + if (totalWildInHand > 0) + baseFan += totalWildInHand * _config.PerWildcardBonus; + } // Flower bonus: e.g. Guangdong 每花牌+1分 + 配对花牌额外分 if (_config.FlowerNormalScore > 0 && state.FlowerPool.TryGetValue(winner, out var flowers) && flowers.Count > 0)