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; public MahjongPhaseMachine(List phases, MeldsSolver solver) { _phases = phases; _solver = solver; } 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(); // Always can discard foreach (var t in state.Hands[playerId].Distinct()) actions.Add(new ActionOption { Action = "discard", Priority = 0 }); // Check pung/kong/win for last discard 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 handWithTile = new List(state.Hands[playerId]) { state.LastDiscard.Value }; var result = _solver.CheckWin(handWithTile); if (result != null && result.IsWin) { 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 tumoResult = _solver.CheckWin(state.Hands[playerId]); if (tumoResult != null && tumoResult.IsWin) { if (!requireWinFan || tumoResult.Fans.Sum(f => GetFanValue(f)) >= 8) actions.Add(new ActionOption { Action = "win", Priority = 4 }); } // Chi if (state.LastDiscard.HasValue && state.LastDiscardPlayer != playerId) { if (CanChi(state, playerId, state.LastDiscard.Value)) actions.Add(new ActionOption { Action = "chi", Priority = 1 }); } actions.Add(new ActionOption { Action = "pass", Priority = 0 }); return actions; } private int GetFanValue(string fanName) { // Simple lookup — full implementation would read from DSL return fanName switch { "清一色" => 4, "对对胡" => 2, "暗七对" => 4, "带幺九" => 2, _ => 1 }; } 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) { return false; // Simplified: AI decides during draw phase } 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])); } private bool CanChi(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]; } }