namespace RuleEngine; using RuleEngine.AI; using RuleEngine.Core; using RuleEngine.Dsl; using RuleEngine.Patterns; using RuleEngine.Phase; using RuleEngine.Scoring; public class MahjongRoom { public MahjongGameState State { get; private set; } = new(); public bool IsFinished { get; private set; } private readonly MahjongDslRoot _rules; private readonly List _ais; private readonly MeldsSolver _solver; private readonly MahjongPhaseMachine _phaseMachine; private readonly MahjongScoreEngine _scoreEngine; private readonly bool _autoMode; private readonly Random _rng = Random.Shared; public MahjongRoom(MahjongDslRoot rules, string[] playerNames, bool autoMode = false) { _rules = rules; _autoMode = autoMode; _ais = playerNames.Select((n, i) => new RandomMahjongAI(n, new Random(i * 7919))).ToList(); var fanConfig = BuildFanConfig(); _solver = new MeldsSolver(fanConfig); var phases = BuildPhases(); _phaseMachine = new MahjongPhaseMachine(phases, _solver); _scoreEngine = new MahjongScoreEngine(new ScoringConfig { Mode = _rules.Scoring.Mode, MaxCap = _rules.Scoring.MaxCap }, _solver); // Init state State.PlayerOrder = playerNames.ToList(); State.AlivePlayers = playerNames.ToList(); State.Dealer = playerNames[0]; State.CurrentPlayer = playerNames[0]; State.Phase = "deal"; foreach (var p in playerNames) { State.Hands[p] = new List(); State.Exposed[p] = new List(); State.Scores[p] = 0; } } private Dictionary BuildFanConfig() { var config = new Dictionary(); foreach (var f in _rules.FanTypes) { config[f.Name] = new FanConfig { Name = f.Name, BaseFan = f.BaseFan, Level = f.Level, Excludes = f.Excludes ?? new(), Conflicts = f.Conflicts ?? new() }; } return config; } private List BuildPhases() { return _rules.Phases.Select(p => new PhaseConfig { Name = p.Name, Type = p.Type, Action = p.Action ?? "", Next = p.Next, TurnOrder = "counter_clockwise", ParallelElimination = p.ParallelElimination, SelfActions = p.SubPhases?.SelfAction?.Options?.Select(o => new ActionOption { Action = o.Action, Priority = o.Priority, Condition = o.Condition }).ToList() ?? new(), OthersReactions = p.SubPhases?.OthersReaction?.Options?.Select(o => new ActionOption { Action = o.Action, Priority = o.Priority, Condition = o.Condition }).ToList() ?? new(), PriorityPolicy = p.SubPhases?.OthersReaction?.PriorityPolicy ?? "highest_wins", OnWin = p.SubPhases?.OthersReaction?.OnWin }).ToList(); } public void Run() { Deal(); IsFinished = false; while (!IsFinished) { if (State.Phase == "deal") { State.Phase = "play"; continue; } if (State.Phase == "settle" || IsFinished) break; StepTurn(); } if (!IsFinished) Settle(); } public List StepTurn() { State.RecentEvents.Clear(); // Draw card if (State.Deck.Count > 0) { int drawn = State.Deck[^1]; State.Deck.RemoveAt(State.Deck.Count - 1); State.Hands[State.CurrentPlayer].Add(drawn); State.AddEvent("draw", State.CurrentPlayer, drawn, $"摸牌: {MahjongTile.ToString(drawn)}"); // Check flower if (MahjongTile.IsFlower(drawn)) { State.FlowerReplaced++; State.AddEvent("flower_drawn", State.CurrentPlayer, drawn, $"花牌 {MahjongTile.ToString(drawn)} → 补牌"); // Flower into flower pool and draw again if (!State.FlowerPool.ContainsKey(State.CurrentPlayer)) State.FlowerPool[State.CurrentPlayer] = new List(); State.FlowerPool[State.CurrentPlayer].Add(drawn); State.Hands[State.CurrentPlayer].Remove(drawn); if (State.Deck.Count > 0) { int extra = State.Deck[^1]; State.Deck.RemoveAt(State.Deck.Count - 1); State.Hands[State.CurrentPlayer].Add(extra); State.AddEvent("draw", State.CurrentPlayer, extra, $"补牌: {MahjongTile.ToString(extra)}"); } } } else { State.IsDeckExhausted = true; State.AddEvent("deck_exhausted", "", null, "牌墙耗尽"); // Blood war: check ting and hua zhu if (_rules.Phases.Any(p => p.ParallelElimination)) { CheckHuaZhu(); CheckTing(); } Settle(); return State.RecentEvents; } // Get legal actions for current player var player = State.CurrentPlayer; bool requireWinFan = _rules.WinMinFan > 0; var legalActions = _phaseMachine.GetLegalActions(State, player, requireWinFan); // AI decides var (action, tile) = _ais.First(a => a.Name == player).Decide(legalActions, State); if (action == "win") { var result = _solver.CheckWin(State.Hands[player]); State.HuPlayers.Add(player); State.AlivePlayers.Remove(player); State.AddEvent("win", player, null, $"自摸!番型: {string.Join(" + ", result?.Fans ?? new())}"); if (_rules.Phases.Any(p => p.ParallelElimination)) { // Blood war: continue without the winner if (State.AlivePlayers.Count <= 1) { Settle(); return State.RecentEvents; } } else { _scoreEngine.Settle(State, player, result!, isSelfDraw: true); Settle(); return State.RecentEvents; } } else if (action == "discard" && tile.HasValue) { State.Hands[player].Remove(tile.Value); State.LastDiscard = tile.Value; State.LastDiscardPlayer = player; State.DiscardPool.Add(tile.Value); State.AddEvent("discard", player, tile.Value, $"出牌: {MahjongTile.ToString(tile.Value)}"); // Check others' reactions (pung/kong/win) bool reactionTaken = CheckReactions(tile.Value); if (reactionTaken) { AdvancePlayer(); return State.RecentEvents; } } else if (action == "pung") { // Handle pung if (State.LastDiscard.HasValue) { int t = State.LastDiscard.Value; State.Hands[player].RemoveAll(x => x == t); State.Hands[player].RemoveAll(x => x == t); State.LastDiscard = null; State.Exposed[player].Add(new Meld { Type = "pung", Tiles = new List { t, t, t }, SourcePlayer = State.LastDiscardPlayer ?? "" }); State.AddEvent("pung", player, t, $"碰!{MahjongTile.ToString(t)}"); } } AdvancePlayer(); return State.RecentEvents; } private bool CheckReactions(int discardTile) { foreach (var p in State.AlivePlayers) { if (p == State.CurrentPlayer) continue; var handWithTile = new List(State.Hands[p]) { discardTile }; var winResult = _solver.CheckWin(handWithTile); if (winResult != null && winResult.IsWin) { // Win takes priority State.HuPlayers.Add(p); State.AlivePlayers.Remove(p); State.AddEvent("win", p, discardTile, $"胡!{MahjongTile.ToString(discardTile)}"); if (!_rules.Phases.Any(ph => ph.ParallelElimination)) { _scoreEngine.Settle(State, p, winResult, isSelfDraw: false); IsFinished = true; } return true; } // Check pung int sameCount = State.Hands[p].Count(t => t == discardTile); if (sameCount >= 2) { State.Hands[p].RemoveAll(t => t == discardTile); State.Hands[p].RemoveAll(t => t == discardTile); State.Exposed[p].Add(new Meld { Type = "pung", Tiles = new List { discardTile, discardTile, discardTile }, SourcePlayer = State.CurrentPlayer }); State.CurrentPlayer = p; State.LastDiscard = null; State.AddEvent("pung", p, discardTile, $"碰!"); return true; } } return false; } private void AdvancePlayer() { int idx = State.PlayerOrder.IndexOf(State.CurrentPlayer); for (int i = 0; i < State.PlayerOrder.Count; i++) { int next = (idx + 1 + i) % State.PlayerOrder.Count; if (State.AlivePlayers.Contains(State.PlayerOrder[next])) { State.CurrentPlayer = State.PlayerOrder[next]; State.RoundNumber++; return; } } // No alive players IsFinished = true; } private void Deal() { int perPlayer = _rules.Deal.CardsPerPlayer; bool includeFlowers = _rules.Deck.IncludeFlowers; bool includeHonors = _rules.Deck.IncludeHonors; int wildcardCount = _rules.WildcardRules != null ? 4 : 0; var deck = new MahjongDeck(includeHonors, includeFlowers, wildcardCount); deck.Shuffle(_rng); State.Deck = deck.Tiles; State.AddEvent("deal_start", "", null, $"发牌 — {deck.Count}张牌({(includeHonors ? "+字" : "")}{(includeFlowers ? "+花" : "")}{(wildcardCount > 0 ? "+癞子" : "")})"); foreach (var p in State.PlayerOrder) { int count = p == State.Dealer ? perPlayer + _rules.Deal.DealerExtra : perPlayer; for (int i = 0; i < count; i++) { int tile = State.Deck[^1]; State.Deck.RemoveAt(State.Deck.Count - 1); State.Hands[p].Add(tile); } } State.AddEvent("deal_done", "", null, "发牌完成"); } private void CheckHuaZhu() { foreach (var p in State.AlivePlayers) { var suits = State.Hands[p] .Where(t => MahjongTile.IsNumbered(t)) .Select(MahjongTile.Suit) .Distinct() .Count(); if (suits == 3) State.AddEvent("hua_zhu", p, null, "花猪!三色齐全"); else State.AddEvent("hua_zhu_ok", p, null, $"✓ {suits}种花色"); } } private void CheckTing() { foreach (var p in State.AlivePlayers) { bool ting = false; var hand = State.Hands[p]; for (int i = 0; i < hand.Count; i++) { var testHand = new List(hand); testHand.RemoveAt(i); var r = _solver.CheckWin(testHand); if (r != null && r.IsWin) { ting = true; break; } } if (ting) State.AddEvent("ting_checked", p, null, "✓ 听牌"); else { State.AddEvent("ting_failed", p, null, "❌ 未听牌"); State.AddEvent("pair_validated_258", p, null, "258将检查"); } } } private void Settle() { IsFinished = true; State.Phase = "settle"; State.AddEvent("settle", "", null, "结算"); } }