From bf041b7e500ae2a51d44f038d7bf613a01515b71 Mon Sep 17 00:00:00 2001 From: xiaoou Date: Sat, 4 Jul 2026 17:31:31 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20CheckWin=E5=AE=88=E5=8D=AB!=3D14?= =?UTF-8?q?=E6=8B=92=E7=BB=9D=E5=89=AF=E9=9C=B2=E5=90=8E=E6=89=8B=E7=89=8C?= =?UTF-8?q?=20+=20FullHand=E5=90=88=E5=B9=B6=E5=89=AF=E9=9C=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 根因: CheckWin要求tiles.Count==14,碰/杠后手牌只有11/8/5/2张 修复: - CheckWin守卫: !=14 → total%3!=2 (接受2,5,8,11,14) - FullHand(): 合并手牌+Exposed副露→虚拟完整手牌 - 所有CheckWin调用点改用FullHand (MahjongRoom+PhaseMachine+HumanMahjongPlayer) - 新增集成测试: 副露后11张手牌自摸 --- .../MahjongRoomIntegrationTests.cs | 12 ++++++---- RuleEngine/AI/HumanMahjongPlayer.cs | 8 ++++++- RuleEngine/MahjongRoom.cs | 22 ++++++++++++++---- RuleEngine/Patterns/MeldsSolver.cs | 7 +++++- RuleEngine/Phase/PhaseMachine.cs | 23 ++++++++++++++----- 5 files changed, 54 insertions(+), 18 deletions(-) diff --git a/RuleEngine.Tests/MahjongRoomIntegrationTests.cs b/RuleEngine.Tests/MahjongRoomIntegrationTests.cs index a2855b9..18176b9 100644 --- a/RuleEngine.Tests/MahjongRoomIntegrationTests.cs +++ b/RuleEngine.Tests/MahjongRoomIntegrationTests.cs @@ -180,19 +180,21 @@ public class MahjongRoomIntegrationTests // === Constructed win scenario === [Fact] - public void 构造听牌_庄家第一轮自摸() + public void 构造听牌_副露后自摸() { var rules = LoadDsl("xuezhandaodi"); var room = new MahjongRoom(rules, new[] { "东", "南", "西", "北" }); room.Deal(); - // 东 has a complete win: 123万 456万 789万 111条 99条 - room.State.Hands["东"] = new List { 1,2,3,4,5,6,7,8,9,11,11,11,19,19 }; + // 东 already pung-ed 1万: exposed has {1,1,1} + // Hand: 345万 888万 345条 88条 = 11 tiles → needs nothing, already winning + room.State.Hands["东"] = new List { 3,4,5, 8,8,8, 13,14,15, 18,18 }; + room.State.Exposed["东"] = new List { new() { Type = "pung", Tiles = new() { 1,1,1 } } }; + room.State.DiscardPool = new List(); room.State.Deck = new List(); - room.StepTurn(); // dealer first turn, no draw, checks win + room.StepTurn(); // dealer first turn, no draw, checks win with full hand Assert.Contains("东", room.State.HuPlayers); - Assert.True(room.State.AlivePlayers.Count < 4); } } diff --git a/RuleEngine/AI/HumanMahjongPlayer.cs b/RuleEngine/AI/HumanMahjongPlayer.cs index 02805bd..84d1cba 100644 --- a/RuleEngine/AI/HumanMahjongPlayer.cs +++ b/RuleEngine/AI/HumanMahjongPlayer.cs @@ -91,12 +91,18 @@ public class HumanMahjongPlayer private bool CheckTing(MahjongGameState state) { var hand = state.Hands.GetValueOrDefault(Name, new List()); + // Combine with exposed melds + var fullHand = new List(hand); + if (state.Exposed.TryGetValue(Name, out var melds)) + foreach (var m in melds) + fullHand.AddRange(m.Tiles.Where(t => t != -1)); + // Check all possible tiles for win potential var allTiles = MahjongTile.AllTiles(false, false); foreach (var tile in allTiles) { if (hand.Count(t => t == tile) >= 4) continue; - var r = _solver.CheckWin(hand, newTile: tile); + var r = _solver.CheckWin(fullHand, newTile: tile); if (r != null && r.IsWin) return true; } return false; diff --git a/RuleEngine/MahjongRoom.cs b/RuleEngine/MahjongRoom.cs index 87535d2..0fe0a6d 100644 --- a/RuleEngine/MahjongRoom.cs +++ b/RuleEngine/MahjongRoom.cs @@ -194,8 +194,9 @@ public class MahjongRoom if (action == "win") { var (wc, r258) = GetWinParams(); - int wildInHand = CountWildcardsInHand(State.Hands[player]); - var result = _solver.CheckWin(State.Hands[player], + var fullHand = FullHand(State, player); + int wildInHand = CountWildcardsInHand(fullHand); + var result = _solver.CheckWin(fullHand, wildcardCount: wildInHand, require258Pair: r258); State.HuPlayers.Add(player); State.AlivePlayers.Remove(player); @@ -243,6 +244,17 @@ public class MahjongRoom private static int CountWildcardsInHand(List hand) => hand.Count(MahjongTile.IsWildcard); + /// Combine hand + exposed melds into full virtual hand for CheckWin. + private static List FullHand(MahjongGameState state, string player, int? extraTile = null) + { + var tiles = new List(state.Hands[player]); + if (extraTile.HasValue) tiles.Add(extraTile.Value); + if (state.Exposed.TryGetValue(player, out var melds)) + foreach (var m in melds) + tiles.AddRange(m.Tiles.Where(t => t != -1)); + return tiles; + } + /// /// Let other players react to a discard (pung/kong/win). /// Returns true if someone reacted (turn passes to them). @@ -263,10 +275,10 @@ public class MahjongRoom bool canWin = false, canKong = sameCount >= 3, canPung = sameCount >= 2; // Check win - var handWithTile = new List(State.Hands[p]) { discardTile }; + var fullHand = FullHand(State, p, extraTile: discardTile); var (wc, r258) = GetWinParams(); - int wildInHand = CountWildcardsInHand(handWithTile); - var winResult = _solver.CheckWin(handWithTile, + int wildInHand = CountWildcardsInHand(fullHand); + var winResult = _solver.CheckWin(fullHand, wildcardCount: wildInHand, require258Pair: r258); if (winResult != null && winResult.IsWin) canWin = true; diff --git a/RuleEngine/Patterns/MeldsSolver.cs b/RuleEngine/Patterns/MeldsSolver.cs index 89d9d19..93db37f 100644 --- a/RuleEngine/Patterns/MeldsSolver.cs +++ b/RuleEngine/Patterns/MeldsSolver.cs @@ -27,7 +27,12 @@ public class MeldsSolver { var tiles = new List(hand); if (newTile.HasValue) tiles.Add(newTile.Value); - if (tiles.Count + wildcardCount != 14) return new MeldsResult { IsWin = false }; + + // Standard mahjong hand: 4 melds (3 tiles each) + 1 pair (2 tiles) = 14 tiles + // With exposed melds, hand has fewer tiles. Valid counts: 2,5,8,11,14 + int total = tiles.Count + wildcardCount; + if (total < 2 || total % 3 != 2 || total > 14) + return new MeldsResult { IsWin = false }; tiles.Sort(); diff --git a/RuleEngine/Phase/PhaseMachine.cs b/RuleEngine/Phase/PhaseMachine.cs index cbb62c8..9ad3848 100644 --- a/RuleEngine/Phase/PhaseMachine.cs +++ b/RuleEngine/Phase/PhaseMachine.cs @@ -69,10 +69,9 @@ public class MahjongPhaseMachine if (CanMingKong(state, playerId, state.LastDiscard.Value)) actions.Add(new ActionOption { Action = "ming_kong", Priority = 3 }); - var handWithTile = new List(state.Hands[playerId]) { state.LastDiscard.Value }; - int wildInHand = state.Hands[playerId].Count(Core.MahjongTile.IsWildcard) - + (Core.MahjongTile.IsWildcard(state.LastDiscard.Value) ? 1 : 0); - var result = _solver.CheckWin(handWithTile, + var fullHand = FullHand(state, playerId, extraTile: state.LastDiscard.Value); + int wildInHand = fullHand.Count(Core.MahjongTile.IsWildcard); + var result = _solver.CheckWin(fullHand, wildcardCount: wildInHand, require258Pair: _require258); if (result != null && result.IsWin) { @@ -87,8 +86,9 @@ public class MahjongPhaseMachine if (CanBuKong(state, playerId)) actions.Add(new ActionOption { Action = "bu_kong", Priority = 1 }); - int wildInSelf = state.Hands[playerId].Count(Core.MahjongTile.IsWildcard); - var tumoResult = _solver.CheckWin(state.Hands[playerId], + var fullSelfHand = FullHand(state, playerId); + int wildInSelf = fullSelfHand.Count(Core.MahjongTile.IsWildcard); + var tumoResult = _solver.CheckWin(fullSelfHand, wildcardCount: wildInSelf, require258Pair: _require258); if (tumoResult != null && tumoResult.IsWin) { @@ -176,4 +176,15 @@ public class MahjongPhaseMachine int next = (idx + 1) % alive.Count; return alive[next]; } + + /// Combine hand + exposed melds into full virtual hand for CheckWin. + private static List FullHand(MahjongGameState state, string player, int? extraTile = null) + { + var tiles = new List(state.Hands[player]); + if (extraTile.HasValue) tiles.Add(extraTile.Value); + if (state.Exposed.TryGetValue(player, out var melds)) + foreach (var m in melds) + tiles.AddRange(m.Tiles.Where(t => t != -1)); + return tiles; + } }