From 3c381f2758d9c66b63d69dddd971e443fc76039f Mon Sep 17 00:00:00 2001 From: xiaoou Date: Sat, 4 Jul 2026 11:52:15 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BA=BA=E7=B1=BB=E7=8E=A9=E5=AE=B6?= =?UTF-8?q?=E5=90=83=E7=A2=B0=E6=9D=A0=E8=83=A1=20=E2=80=94=20HandleDiscar?= =?UTF-8?q?dReactions=E6=9A=82=E5=81=9C=E8=AF=A2=E9=97=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - HandleDiscardReactions重写: 按座位顺序逐个询问 - 人类玩家在可反应列表中时暂停(h=胡/p=碰/k=杠/Enter=过) - AI玩家自动决策(先胡后碰杠) - ExecuteWinClaim/ExecutePungKong抽取公共逻辑 - 碰/杠后让人类选择出哪张牌 --- RuleEngine/MahjongRoom.cs | 214 +++++++++++++++++++++++--------------- 1 file changed, 129 insertions(+), 85 deletions(-) diff --git a/RuleEngine/MahjongRoom.cs b/RuleEngine/MahjongRoom.cs index 88c4de9..c388999 100644 --- a/RuleEngine/MahjongRoom.cs +++ b/RuleEngine/MahjongRoom.cs @@ -248,12 +248,10 @@ public class MahjongRoom /// 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 ?? ""; + int startIdx = State.PlayerOrder.IndexOf(discarder); // 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]; @@ -261,104 +259,150 @@ public class MahjongRoom if (p == discarder) continue; int sameCount = State.Hands[p].Count(t => t == discardTile); - string? claim = null; - int priority = 0; + bool canWin = false, canKong = sameCount >= 3, canPung = sameCount >= 2; - // Check win first (highest priority) + // 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) - claim = "win"; - else if (sameCount >= 3) - claim = "ming_kong"; - else if (sameCount >= 2) - claim = "pung"; + canWin = true; - 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") - { - // Remove the winning tile from pool (it's now part of the winning hand) - State.DiscardPool.Remove(discardTile); - - var handWithTile = new List(State.Hands[bp]) { discardTile }; - var (wc2, r2582) = GetWinParams(); - int wildInHand2 = CountWildcardsInHand(handWithTile); - var winResult = _solver.CheckWin(handWithTile, - wildcardCount: wildInHand2, require258Pair: r2582); - State.HuPlayers.Add(bp); - State.AlivePlayers.Remove(bp); - State.AddEvent("win", bp, discardTile, - $"胡!{MahjongTile.ToString(discardTile)}"); - - if (!_rules.Phases.Any(ph => ph.ParallelElimination)) + // Human player: ask what they want to do + if (_human != null && p == _human.Name && (canWin || canKong || canPung)) { - _scoreEngine.Settle(State, bp, winResult!, isSelfDraw: false); - IsFinished = true; + 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; } - return true; - } - if (best.action == "pung" || best.action == "ming_kong") - { - // Remove the discarded tile from pool (it's now in the meld) - State.DiscardPool.Remove(discardTile); - - // 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; + // 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);