From cbbe503117b480d218ee5dfb5ab348c563844516 Mon Sep 17 00:00:00 2001 From: xiaoou Date: Sat, 4 Jul 2026 17:56:14 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20Code=20Review=E5=AE=8C=E6=95=B4=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=20=E2=80=94=20AI=20chi/=E6=9A=97=E6=9D=A0/=E5=8A=A0?= =?UTF-8?q?=E6=9D=A0/chi=E4=BD=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 发现的6个缺口: 1. HandleDiscardReactions AI分支缺chi → 新增ExecuteAiChi 2. StepTurn不处理an_kong/bu_kong → 新增ExecuteAnKong/ExecuteBuKong 3. PhaseMachine.GetLegalActions chi误在自回合 → 移除(chi只能反应) 4. ScoreEngine.CheckFinish死代码 → 保留(无影响) 5. ExecutePungKong用Random.Shared → 改用_rng 6. HumanMahjongPlayer残留chiOpt → 移除 --- RuleEngine/AI/HumanMahjongPlayer.cs | 3 - RuleEngine/MahjongRoom.cs | 102 +++++++++++++++++++++++++++- RuleEngine/Phase/PhaseMachine.cs | 7 -- 3 files changed, 101 insertions(+), 11 deletions(-) diff --git a/RuleEngine/AI/HumanMahjongPlayer.cs b/RuleEngine/AI/HumanMahjongPlayer.cs index ea078fe..a776169 100644 --- a/RuleEngine/AI/HumanMahjongPlayer.cs +++ b/RuleEngine/AI/HumanMahjongPlayer.cs @@ -77,7 +77,6 @@ public class HumanMahjongPlayer var winOpt = legalActions.FirstOrDefault(a => a.Action == "win"); var pungOpt = legalActions.FirstOrDefault(a => a.Action == "pung"); var kongOpts = legalActions.Where(a => a.Action is "ming_kong" or "an_kong" or "bu_kong").ToList(); - var chiOpt = legalActions.FirstOrDefault(a => a.Action == "chi"); // Check if we're one tile away from winning (ting) bool isTing = CheckTing(state); @@ -87,7 +86,6 @@ public class HumanMahjongPlayer if (hand.Count > 0) ops.Add($"1-{hand.Count}(出牌)"); if (pungOpt != null) ops.Add("p(碰)"); if (kongOpts.Count > 0) ops.Add($"k(杠: {string.Join("/", kongOpts.Select(o => o.Action))})"); - if (chiOpt != null) ops.Add("c(吃)"); if (winOpt != null) ops.Add("h✨(胡!)"); if (isTing) ops.Add("🀄听牌!"); ops.Add("q(退出)"); @@ -101,7 +99,6 @@ public class HumanMahjongPlayer if (input == "h" && winOpt != null) return ("win", null); if (input == "p" && pungOpt != null) return ("pung", null); if (input == "k" && kongOpts.Count > 0) return (kongOpts[0].Action, null); - if (input == "c" && chiOpt != null) return ("chi", null); if (int.TryParse(input, out int idx) && idx >= 1 && idx <= sorted.Count) return ("discard", sorted[idx - 1]); diff --git a/RuleEngine/MahjongRoom.cs b/RuleEngine/MahjongRoom.cs index 595f436..de60e17 100644 --- a/RuleEngine/MahjongRoom.cs +++ b/RuleEngine/MahjongRoom.cs @@ -235,6 +235,14 @@ public class MahjongRoom State.AddEvent("discard", player, tile.Value, $"出牌: {MahjongTile.ToString(tile.Value)}"); } + else if (action == "an_kong") + { + ExecuteAnKong(player); + } + else if (action == "bu_kong") + { + ExecuteBuKong(player); + } AdvancePlayer(); return State.RecentEvents; @@ -345,6 +353,7 @@ public class MahjongRoom // AI player: auto-decide if (canWin) return ExecuteWinClaim(p, discardTile, winResult!); if (canKong || canPung) return ExecutePungKong(p, discardTile, canKong ? "ming_kong" : "pung"); + if (canChi) return ExecuteAiChi(p, discardTile); } return false; @@ -450,6 +459,49 @@ public class MahjongRoom return true; } + private bool ExecuteAiChi(string p, int discardTile) + { + string discarder = State.LastDiscardPlayer ?? ""; + State.DiscardPool.Remove(discardTile); + int suit = MahjongTile.Suit(discardTile); + int rank = MahjongTile.Rank(discardTile); + var hand = State.Hands[p]; + + List toRemove = new(); + if (rank >= 3 && hand.Any(t => MahjongTile.Suit(t) == suit && MahjongTile.Rank(t) == rank - 2) + && hand.Any(t => MahjongTile.Suit(t) == suit && MahjongTile.Rank(t) == rank - 1)) + { toRemove.Add(hand.First(t => MahjongTile.Suit(t) == suit && MahjongTile.Rank(t) == rank - 2)); + toRemove.Add(hand.First(t => MahjongTile.Suit(t) == suit && MahjongTile.Rank(t) == rank - 1)); } + else if (rank >= 2 && rank <= 8 && hand.Any(t => MahjongTile.Suit(t) == suit && MahjongTile.Rank(t) == rank - 1) + && hand.Any(t => MahjongTile.Suit(t) == suit && MahjongTile.Rank(t) == rank + 1)) + { toRemove.Add(hand.First(t => MahjongTile.Suit(t) == suit && MahjongTile.Rank(t) == rank - 1)); + toRemove.Add(hand.First(t => MahjongTile.Suit(t) == suit && MahjongTile.Rank(t) == rank + 1)); } + else if (rank <= 7 && hand.Any(t => MahjongTile.Suit(t) == suit && MahjongTile.Rank(t) == rank + 1) + && hand.Any(t => MahjongTile.Suit(t) == suit && MahjongTile.Rank(t) == rank + 2)) + { toRemove.Add(hand.First(t => MahjongTile.Suit(t) == suit && MahjongTile.Rank(t) == rank + 1)); + toRemove.Add(hand.First(t => MahjongTile.Suit(t) == suit && MahjongTile.Rank(t) == rank + 2)); } + foreach (var t in toRemove) State.Hands[p].Remove(t); + + var chiTiles = new List(toRemove) { discardTile }; + chiTiles.Sort(); + State.Exposed[p].Add(new Meld { Type = "chi", Tiles = chiTiles, SourcePlayer = discarder }); + State.CurrentPlayer = p; + State.LastDiscard = null; + State.LastDiscardPlayer = null; + State.AddEvent("chi", p, discardTile, $"吃!{string.Join(",", chiTiles.Select(MahjongTile.ToString))}"); + + if (hand.Count > 0) + { + int tileToDiscard = hand[_rng.Next(hand.Count)]; + State.Hands[p].Remove(tileToDiscard); + State.LastDiscard = tileToDiscard; + State.LastDiscardPlayer = p; + RecordDiscard(p, tileToDiscard); + State.AddEvent("discard", p, tileToDiscard, $"出牌: {MahjongTile.ToString(tileToDiscard)}"); + } + return true; + } + private bool ExecuteWinClaim(string player, int discardTile, MeldsResult winResult) { State.DiscardPool.Remove(discardTile); @@ -464,6 +516,54 @@ public class MahjongRoom return true; } + private void ExecuteAnKong(string player) + { + // Find a set of 4 identical tiles and expose them as concealed kong + var hand = State.Hands[player]; + var group = hand.GroupBy(t => t) + .First(g => g.Count() >= 4 && !MahjongTile.IsWildcard(g.Key)); + int tile = group.Key; + for (int i = 0; i < 4; i++) State.Hands[player].Remove(tile); + State.Exposed[player].Add(new Meld + { + Type = "kong_an", + Tiles = Enumerable.Repeat(tile, 4).ToList(), + IsConcealed = true + }); + State.AddEvent("an_kong", player, tile, $"暗杠!{MahjongTile.ToString(tile)}"); + + // After kong, draw a replacement from the back of the wall + 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)}"); + } + } + + private void ExecuteBuKong(string player) + { + // Find an exposed pung that matches a tile in hand + var exposed = State.Exposed[player]; + var pung = exposed.First(m => m.Type == "pung" && State.Hands[player].Contains(m.Tiles[0])); + int tile = pung.Tiles[0]; + State.Hands[player].Remove(tile); + pung.Type = "kong_bu"; + pung.Tiles.Add(tile); + State.AddEvent("bu_kong", player, tile, $"加杠!{MahjongTile.ToString(tile)}"); + + 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)}"); + } + } + private bool ExecutePungKong(string player, int discardTile, string action) { State.DiscardPool.Remove(discardTile); @@ -488,7 +588,7 @@ public class MahjongRoom var hand = State.Hands[player]; if (hand.Count > 0) { - int tileToDiscard = hand[Random.Shared.Next(hand.Count)]; + int tileToDiscard = hand[_rng.Next(hand.Count)]; State.Hands[player].Remove(tileToDiscard); State.LastDiscard = tileToDiscard; State.LastDiscardPlayer = player; diff --git a/RuleEngine/Phase/PhaseMachine.cs b/RuleEngine/Phase/PhaseMachine.cs index 1fb6d87..b2330f9 100644 --- a/RuleEngine/Phase/PhaseMachine.cs +++ b/RuleEngine/Phase/PhaseMachine.cs @@ -96,13 +96,6 @@ public class MahjongPhaseMachine actions.Add(new ActionOption { Action = "win", Priority = 4 }); } - // Chi - if (state.LastDiscard.HasValue && state.LastDiscardPlayer != playerId) - { - if (CanChiCheck(state, playerId, state.LastDiscard.Value)) - actions.Add(new ActionOption { Action = "chi", Priority = 1 }); - } - actions.Add(new ActionOption { Action = "pass", Priority = 0 }); return actions; }