namespace RuleEngine.Phase; using RuleEngine.Core; using RuleEngine.Patterns; public class PhaseConfig { public string Name { get; set; } = ""; public string Type { get; set; } = ""; // "auto", "mahjong_turn" public string Action { get; set; } = ""; public string? Next { get; set; } public string TurnOrder { get; set; } = "counter_clockwise"; public string? FirstPlayer { get; set; } public List SelfActions { get; set; } = new(); public List OthersReactions { get; set; } = new(); public string PriorityPolicy { get; set; } = "highest_wins"; public string? OnWin { get; set; } public List EndConditions { get; set; } = new(); public bool ParallelElimination { get; set; } public string? OnEliminate { get; set; } public int MaxFan { get; set; } = int.MaxValue; } public class ActionOption { public string Action { get; set; } = ""; public int Priority { get; set; } public string? Condition { get; set; } } public class EndCondition { public string Type { get; set; } = ""; public string Action { get; set; } = ""; } public class MahjongPhaseMachine { private readonly List _phases; private readonly MeldsSolver _solver; private readonly int _wildcardCount; private readonly bool _require258; private readonly Dictionary _fanConfig; private readonly bool _jiHuSelfDrawOnly; private readonly WildcardRegistry _wildcards; private readonly bool _chiAllowed; private readonly int _winMinFan; public bool ChiAllowed => _chiAllowed; public MahjongPhaseMachine(List phases, MeldsSolver solver, int wildcardCount = 0, bool require258 = false, Dictionary? fanConfig = null, bool jiHuSelfDrawOnly = false, WildcardRegistry? wildcards = null, int winMinFan = 0) { _phases = phases; _solver = solver; _wildcardCount = wildcardCount; _require258 = require258; _fanConfig = fanConfig ?? new(); _jiHuSelfDrawOnly = jiHuSelfDrawOnly; _wildcards = wildcards ?? new(); _winMinFan = winMinFan; _chiAllowed = phases.Any(p => p.OthersReactions.Any(o => o.Action == "chi")); } public PhaseConfig? GetPhase(string name) => _phases.FirstOrDefault(p => p.Name == name); public List GetLegalActions(MahjongGameState state, string playerId, bool requireWinFan = false) { var actions = new List(); // 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; // 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 >= _winMinFan, "can_win_and_fan_ge_8" => canSelfWin && fanSum >= _winMinFan, _ => 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 (allowPung && CanPung(state, playerId, state.LastDiscard.Value)) { 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) { // 鸡胡只能自摸 — 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)) >= _winMinFan) actions.Add(new ActionOption { Action = "win", Priority = 4 }); } } } // Self actions: only if DSL allows them if (allowAnKong && CanAnKong(state, playerId)) { 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)) >= _winMinFan) actions.Add(new ActionOption { Action = "win", Priority = 4 }); } } actions.Add(new ActionOption { Action = "pass", Priority = 0 }); return actions; } /// Shared fan value fallback — single source of truth for both PhaseMachine and ScoreEngine. public static int DefaultFanValue(string fanName) => fanName switch { "清一色" => 24, "混一色" => 6, "对对胡" => 2, "碰碰和" => 6, "暗七对" => 8, "七对" => 8, "带幺九" => 2, "十三幺" => 88, "鸡胡" => 1, _ => 0 }; private int GetFanValue(string fanName) { if (_fanConfig.TryGetValue(fanName, out var def)) return def.BaseFan; return DefaultFanValue(fanName); } private bool CanPung(MahjongGameState state, string playerId, int tile) { int count = state.Hands[playerId].Count(t => t == tile); return count >= 2; } private bool CanMingKong(MahjongGameState state, string playerId, int tile) { int count = state.Hands[playerId].Count(t => t == tile); return count >= 3; } private bool CanAnKong(MahjongGameState state, string playerId) { // Check if player has 4 identical tiles in hand (暗杠) var hand = state.Hands[playerId]; return hand.GroupBy(t => t).Any(g => g.Count() >= 4 && !_wildcards.IsWildcard(g.Key)); } private bool CanBuKong(MahjongGameState state, string playerId) { // Check if player has an exposed pung and drew the 4th tile if (!state.Exposed.ContainsKey(playerId)) return false; return state.Exposed[playerId].Any(m => m.Type == "pung" && state.Hands[playerId].Contains(m.Tiles[0])); } public bool CanChiCheck(MahjongGameState state, string playerId, int tile) { if (MahjongTile.IsHonor(tile)) return false; int suit = MahjongTile.Suit(tile); int rank = MahjongTile.Rank(tile); var hand = state.Hands[playerId]; // Check two sequential combinations bool hasLower = rank >= 3 && hand.Count(t => MahjongTile.Suit(t) == suit && MahjongTile.Rank(t) == rank - 2) > 0 && hand.Count(t => MahjongTile.Suit(t) == suit && MahjongTile.Rank(t) == rank - 1) > 0; bool hasMiddle = rank >= 2 && rank <= 8 && hand.Count(t => MahjongTile.Suit(t) == suit && MahjongTile.Rank(t) == rank - 1) > 0 && hand.Count(t => MahjongTile.Suit(t) == suit && MahjongTile.Rank(t) == rank + 1) > 0; bool hasUpper = rank <= 7 && hand.Count(t => MahjongTile.Suit(t) == suit && MahjongTile.Rank(t) == rank + 1) > 0 && hand.Count(t => MahjongTile.Suit(t) == suit && MahjongTile.Rank(t) == rank + 2) > 0; return hasLower || hasMiddle || hasUpper; } public string GetNextPlayer(MahjongGameState state, string currentPlayer) { var alive = state.AlivePlayers; int idx = alive.IndexOf(currentPlayer); if (idx < 0) return alive[0]; int next = (idx + 1) % alive.Count; return alive[next]; } /// Combine hand + exposed melds into full virtual hand for CheckWin. private static List FullHand(MahjongGameState state, string player, int? extraTile = null) { var tiles = new List(state.Hands[player]); if (extraTile.HasValue) tiles.Add(extraTile.Value); if (state.Exposed.TryGetValue(player, out var melds)) foreach (var m in melds) tiles.AddRange(m.Tiles.Where(t => t != -1)); return tiles; } }