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 HumanMahjongPlayer? _human; 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, string? humanPlayer = null) { _rules = rules; _autoMode = autoMode; _ais = playerNames.Select((n, i) => new RandomMahjongAI(n, new Random(i * 7919))).ToList(); var fanConfig = BuildFanConfig(); _solver = new MeldsSolver(fanConfig); if (humanPlayer != null) _human = new HumanMahjongPlayer(humanPlayer, _solver); var phases = BuildPhases(); var (wc, r258) = GetWinParams(); _phaseMachine = new MahjongPhaseMachine(phases, _solver, wildcardCount: wc, require258: r258); _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 if (State.LastDiscard.HasValue && State.LastDiscardPlayer != null) { bool reacted = HandleDiscardReactions(State.LastDiscard.Value); if (reacted) { AdvancePlayer(); return State.RecentEvents; } State.LastDiscard = null; State.LastDiscardPlayer = null; } // Phase 2: Draw card (skip for dealer on first turn — they already have 14) bool isDealerFirstTurn = (player == State.Dealer && State.RoundNumber == 0); if (isDealerFirstTurn) State.LastDrawnTile = null; else { if (State.Deck.Count > 0) { int drawn = State.Deck[^1]; State.Deck.RemoveAt(State.Deck.Count - 1); State.Hands[player].Add(drawn); State.LastDrawnTile = 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; } } // end if (!isDealerFirstTurn) // 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) = _human != null && player == _human.Name ? _human.Decide(legalActions, State) : _ais.First(a => a.Name == player).Decide(legalActions, State); if (action == "win") { var (wc, r258) = GetWinParams(); int wildInHand = CountWildcardsInHand(State.Hands[player]); var result = _solver.CheckWin(State.Hands[player], wildcardCount: wildInHand, require258Pair: r258); 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; } /// Get wildcard count and 258-pair requirement for current rules. private (int wildcardCount, bool require258) GetWinParams() { int wc = _rules.WildcardRules != null ? _rules.Deck.WildcardCount : 0; bool r258 = _rules.WinCondition?.PairMustBe258 ?? false; return (wc, r258); } /// Count wildcard tiles in a hand (encoding 50-59). private static int CountWildcardsInHand(List hand) => hand.Count(MahjongTile.IsWildcard); /// /// Let other players react to a discard (pung/kong/win). /// Returns true if someone reacted (turn passes to them). /// private bool HandleDiscardReactions(int discardTile) { string discarder = State.LastDiscardPlayer ?? ""; int startIdx = State.PlayerOrder.IndexOf(discarder); // Check in player order (starting after 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); bool canWin = false, canKong = sameCount >= 3, canPung = sameCount >= 2; // Check win var handWithTile = new List(State.Hands[p]) { discardTile }; var (wc, r258) = GetWinParams(); int wildInHand = CountWildcardsInHand(handWithTile); var winResult = _solver.CheckWin(handWithTile, wildcardCount: wildInHand, require258Pair: r258); if (winResult != null && winResult.IsWin) canWin = true; // Human player: ask what they want to do if (_human != null && p == _human.Name && (canWin || canKong || canPung)) { Console.WriteLine(); Console.WriteLine($"=== {p} 可以反应 ==="); Console.WriteLine($" 对手 {discarder} 出了: {MahjongTile.ToString(discardTile)}"); Console.WriteLine($" 你的手牌: {string.Join(" ", State.Hands[p].OrderBy(t => t).Select(MahjongTile.ToString))}"); Console.Write(" 操作: "); var ops = new List(); if (canWin) ops.Add("h(胡)"); if (canKong) ops.Add("k(杠)"); if (canPung) ops.Add("p(碰)"); ops.Add("Enter(过)"); Console.WriteLine(string.Join(" ", ops)); Console.Write("> "); string? input = Console.ReadLine()?.Trim().ToLower(); if (input == "h" && canWin) { State.DiscardPool.Remove(discardTile); 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; } if ((input == "k" && canKong) || (input == "p" && canPung)) { string action = input == "k" ? "ming_kong" : "pung"; int toRemove = action == "ming_kong" ? 3 : 2; for (int r = 0; r < toRemove; r++) State.Hands[p].Remove(discardTile); State.DiscardPool.Remove(discardTile); int meldSize = action == "ming_kong" ? 4 : 3; State.Exposed[p].Add(new Meld { Type = action, Tiles = Enumerable.Repeat(discardTile, meldSize).ToList(), SourcePlayer = discarder }); State.CurrentPlayer = p; State.LastDiscard = null; State.LastDiscardPlayer = null; State.AddEvent(action, p, discardTile, action == "pung" ? "碰!" : "明杠!"); // Human must discard after pung/kong var hand = State.Hands[p]; Console.WriteLine($" 碰/杠后手牌 ({hand.Count}张):"); var sorted = hand.OrderBy(t => t).ToList(); for (int i = 0; i < sorted.Count; i++) Console.WriteLine($" [{i + 1}] {MahjongTile.ToString(sorted[i])}"); Console.Write(" 请出牌 (1-{0}): ".Replace("{0}", hand.Count.ToString()), hand.Count); int discardIdx; while (!int.TryParse(Console.ReadLine(), out discardIdx) || discardIdx < 1 || discardIdx > sorted.Count) Console.Write(" 无效,重试: "); int tileToDiscard = sorted[discardIdx - 1]; State.Hands[p].Remove(tileToDiscard); State.LastDiscard = tileToDiscard; State.LastDiscardPlayer = p; State.DiscardPool.Add(tileToDiscard); State.AddEvent("discard", p, tileToDiscard, $"出牌: {MahjongTile.ToString(tileToDiscard)}"); return true; } // Human passed — continue to next player continue; } // AI player: auto-decide if (canWin) return ExecuteWinClaim(p, discardTile, winResult!); if (canKong || canPung) return ExecutePungKong(p, discardTile, canKong ? "ming_kong" : "pung"); } return false; } private bool ExecuteWinClaim(string player, int discardTile, MeldsResult winResult) { State.DiscardPool.Remove(discardTile); State.HuPlayers.Add(player); State.AlivePlayers.Remove(player); State.AddEvent("win", player, discardTile, $"胡!{MahjongTile.ToString(discardTile)}"); if (!_rules.Phases.Any(ph => ph.ParallelElimination)) { _scoreEngine.Settle(State, player, winResult, isSelfDraw: false); IsFinished = true; } return true; } private bool ExecutePungKong(string player, int discardTile, string action) { State.DiscardPool.Remove(discardTile); int toRemove = action == "ming_kong" ? 3 : 2; for (int r = 0; r < toRemove; r++) State.Hands[player].Remove(discardTile); int meldSize = action == "ming_kong" ? 4 : 3; State.Exposed[player].Add(new Meld { Type = action, Tiles = Enumerable.Repeat(discardTile, meldSize).ToList(), SourcePlayer = State.LastDiscardPlayer ?? "" }); State.CurrentPlayer = player; State.LastDiscard = null; State.LastDiscardPlayer = null; State.AddEvent(action, player, discardTile, action == "pung" ? "碰!" : "明杠!"); // AI discards after pung/kong var hand = State.Hands[player]; if (hand.Count > 0) { int tileToDiscard = hand[Random.Shared.Next(hand.Count)]; State.Hands[player].Remove(tileToDiscard); State.LastDiscard = tileToDiscard; State.LastDiscardPlayer = player; State.DiscardPool.Add(tileToDiscard); State.AddEvent("discard", player, tileToDiscard, $"出牌: {MahjongTile.ToString(tileToDiscard)}"); } return true; } 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, "结算"); } }