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, fanConfig); // 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(), Condition = f.Condition }; } 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(); var player = State.CurrentPlayer; // Phase 1: Check reactions to last discard BEFORE drawing // (order: starting from next player after discarder, check each alive player) if (State.LastDiscard.HasValue && State.LastDiscardPlayer != null) { bool reacted = HandleDiscardReactions(State.LastDiscard.Value); if (reacted) { AdvancePlayer(); return State.RecentEvents; } // No reaction — clear LastDiscard, current player will draw State.LastDiscard = null; State.LastDiscardPlayer = null; } // Phase 2: Draw card if (State.Deck.Count > 0) { int drawn = State.Deck[^1]; State.Deck.RemoveAt(State.Deck.Count - 1); State.Hands[player].Add(drawn); State.AddEvent("draw", player, drawn, $"摸牌: {MahjongTile.ToString(drawn)}"); // Check flower if (MahjongTile.IsFlower(drawn)) { State.FlowerReplaced++; State.AddEvent("flower_drawn", player, drawn, $"花牌 {MahjongTile.ToString(drawn)} → 补牌"); if (!State.FlowerPool.ContainsKey(player)) State.FlowerPool[player] = new List(); State.FlowerPool[player].Add(drawn); State.Hands[player].Remove(drawn); if (State.Deck.Count > 0) { int extra = State.Deck[^1]; State.Deck.RemoveAt(State.Deck.Count - 1); State.Hands[player].Add(extra); State.AddEvent("draw", player, extra, $"补牌: {MahjongTile.ToString(extra)}"); } } } else { State.IsDeckExhausted = true; State.AddEvent("deck_exhausted", "", null, "牌墙耗尽"); if (_rules.Phases.Any(p => p.ParallelElimination)) { CheckHuaZhu(); CheckTing(); } Settle(); return State.RecentEvents; } // Phase 3: Player decides what to do with their drawn hand bool requireWinFan = _rules.WinMinFan > 0; var legalActions = _phaseMachine.GetLegalActions(State, player, requireWinFan); 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)) { 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)}"); } AdvancePlayer(); return State.RecentEvents; } /// /// Let other players react to a discard (pung/kong/win). /// Returns true if someone reacted (turn passes to them). /// private bool HandleDiscardReactions(int discardTile) { // Collect claims from all alive players except the discarder var claims = new List<(string player, int priority, string action)>(); string discarder = State.LastDiscardPlayer ?? ""; // Check in player order (starting after discarder) int startIdx = State.PlayerOrder.IndexOf(discarder); for (int offset = 1; offset < State.PlayerOrder.Count; offset++) { string p = State.PlayerOrder[(startIdx + offset) % State.PlayerOrder.Count]; if (!State.AlivePlayers.Contains(p)) continue; if (p == discarder) continue; int sameCount = State.Hands[p].Count(t => t == discardTile); string? claim = null; int priority = 0; // Check win first (highest priority) var handWithTile = new List(State.Hands[p]) { discardTile }; var winResult = _solver.CheckWin(handWithTile); if (winResult != null && winResult.IsWin) claim = "win"; else if (sameCount >= 3) claim = "ming_kong"; else if (sameCount >= 2) claim = "pung"; if (claim == null) continue; priority = claim switch { "win" => 4, "ming_kong" => 3, "pung" => 2, _ => 0 }; // For same priority, closer seat wins (first claim in order) // For higher priority, it overrides all previous claims if (claims.Count == 0 || priority > claims[0].priority) claims.Add((p, priority, claim)); // Same priority: first claimant wins (closer to discarder in seat order) // Different (lower) priority: ignored — higher priority already claimed } if (claims.Count == 0) return false; var best = claims[0]; string bp = best.player; if (best.action == "win") { var winResult = _solver.CheckWin( new List(State.Hands[bp]) { discardTile }); State.HuPlayers.Add(bp); State.AlivePlayers.Remove(bp); State.AddEvent("win", bp, discardTile, $"胡!{MahjongTile.ToString(discardTile)}"); if (!_rules.Phases.Any(ph => ph.ParallelElimination)) { _scoreEngine.Settle(State, bp, winResult!, isSelfDraw: false); IsFinished = true; } return true; } if (best.action == "pung" || best.action == "ming_kong") { // Remove tiles from hand (pung: 2, ming_kong: 3) // The discard tile is taken from pool, not from hand int toRemove = best.action == "ming_kong" ? 3 : 2; for (int r = 0; r < toRemove; r++) State.Hands[bp].Remove(discardTile); // Create exposed meld int meldSize = best.action == "ming_kong" ? 4 : 3; var meldTiles = Enumerable.Repeat(discardTile, meldSize).ToList(); State.Exposed[bp].Add(new Meld { Type = best.action, Tiles = meldTiles, SourcePlayer = discarder }); State.CurrentPlayer = bp; State.LastDiscard = null; State.LastDiscardPlayer = null; State.AddEvent(best.action, bp, discardTile, best.action == "pung" ? "碰!" : "明杠!"); // After pung/kong, the reacting player must discard a tile var hand = State.Hands[bp]; if (hand.Count > 0) { int tileToDiscard = hand[Random.Shared.Next(hand.Count)]; State.Hands[bp].Remove(tileToDiscard); State.LastDiscard = tileToDiscard; State.LastDiscardPlayer = bp; State.DiscardPool.Add(tileToDiscard); State.AddEvent("discard", bp, tileToDiscard, $"出牌: {MahjongTile.ToString(tileToDiscard)}"); } 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; } public void Deal() { int perPlayer = _rules.Deal.CardsPerPlayer; bool includeFlowers = _rules.Deck.IncludeFlowers; bool includeHonors = _rules.Deck.IncludeHonors; int wildcardCount = _rules.WildcardRules != null ? _rules.Deck.WildcardCount : 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() { bool hasWildcard = _rules.WildcardRules != null; int wildcardCount = hasWildcard ? _rules.Deck.WildcardCount : 0; bool require258 = _rules.WinCondition?.PairMustBe258 ?? false; // All possible tiles that could be drawn (numbered 1-9 across 3 suits + 7 honors) var allPossibleTiles = MahjongTile.AllTiles( includeHonors: _rules.Deck.IncludeHonors, includeFlowers: false); foreach (var p in State.AlivePlayers) { bool ting = false; var hand = State.Hands[p]; // For each possible tile we could draw, check if hand + tile is a win foreach (var tile in allPossibleTiles) { // Count how many of this tile exist (4 max per tile type) int alreadyHave = hand.Count(t => t == tile); if (alreadyHave >= 4) continue; // can't draw more of this tile var r = _solver.CheckWin(hand, newTile: tile, wildcardCount: wildcardCount, require258Pair: require258); if (r != null && r.IsWin) { ting = true; break; } } if (ting) State.AddEvent("ting_checked", p, null, "✓ 听牌"); else State.AddEvent("ting_failed", p, null, "❌ 未听牌"); } } private void Settle() { IsFinished = true; State.Phase = "settle"; State.AddEvent("settle", "", null, "结算"); } }