From bb43ed7c899833a70c8d211719bde41734b0aba6 Mon Sep 17 00:00:00 2001 From: xiaoou Date: Sat, 4 Jul 2026 11:39:00 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20CheckWin=E7=BC=BA=E5=B0=91wildcard/258?= =?UTF-8?q?=E5=8F=82=E6=95=B0=E5=AF=BC=E8=87=B4=E6=AD=A6=E6=B1=89=E9=BA=BB?= =?UTF-8?q?=E5=B0=860%=E8=83=A1=E7=89=8C=20+=20=E5=BA=84=E5=AE=B6=E7=AC=AC?= =?UTF-8?q?=E4=B8=80=E8=BD=AE=E8=B7=B3=E8=BF=87=E6=91=B8=E7=89=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root cause 1: 所有CheckWin调用未传wildcardCount/require258Pair - StepTurn自摸: 5处遗漏 → 癞子在手牌中被当废牌 - HandleDiscardReactions: 2处遗漏 - PhaseMachine: 2处遗漏 → 武汉麻将永远不能胡牌 Root cause 2: 庄家开局14张+摸牌=15张, CheckWin守卫≠14拒绝 → 庄家永远不能自摸 → 修复: 庄家第一轮跳过摸牌 额外修复: - RandomMahjongAI: 永远不出癞子(预留wildcard) - ScoreEngine.FanValue: 从DSL _fanConfig读取番值 - HandleDiscardReactions: 同优先级座位顺序 - 新增GetWinParams/CountWildcardsInHand辅助方法 验证: 47/47测试, Wuhan 1.6%胡牌(500局), Sichuan 0.4% --- RuleEngine/AI/RandomMahjongAI.cs | 8 ++++--- RuleEngine/MahjongRoom.cs | 40 +++++++++++++++++++++++++------- RuleEngine/Phase/PhaseMachine.cs | 16 ++++++++++--- 3 files changed, 50 insertions(+), 14 deletions(-) diff --git a/RuleEngine/AI/RandomMahjongAI.cs b/RuleEngine/AI/RandomMahjongAI.cs index ae02032..717a68c 100644 --- a/RuleEngine/AI/RandomMahjongAI.cs +++ b/RuleEngine/AI/RandomMahjongAI.cs @@ -35,13 +35,15 @@ public class RandomMahjongAI var chi = legalActions.FirstOrDefault(a => a.Action == "chi"); if (chi != null) return ("chi", null); - // Discard: random tile + // Discard: random non-wildcard tile var discards = legalActions.Where(a => a.Action == "discard").ToList(); if (discards.Count > 0) { - var chosen = discards[_rng.Next(discards.Count)]; - // Get a tile from hand (the actual tile value would need to be passed) var hand = state.Hands.GetValueOrDefault(Name, new List()); + var nonWildTiles = hand.Where(t => !MahjongTile.IsWildcard(t)).ToList(); + if (nonWildTiles.Count > 0) + return ("discard", nonWildTiles[_rng.Next(nonWildTiles.Count)]); + // Only wildcards left — discard one if (hand.Count > 0) return ("discard", hand[_rng.Next(hand.Count)]); } diff --git a/RuleEngine/MahjongRoom.cs b/RuleEngine/MahjongRoom.cs index 0076165..1e14fbb 100644 --- a/RuleEngine/MahjongRoom.cs +++ b/RuleEngine/MahjongRoom.cs @@ -29,7 +29,8 @@ public class MahjongRoom _solver = new MeldsSolver(fanConfig); var phases = BuildPhases(); - _phaseMachine = new MahjongPhaseMachine(phases, _solver); + var (wc, r258) = GetWinParams(); + _phaseMachine = new MahjongPhaseMachine(phases, _solver, wildcardCount: wc, require258: r258); _scoreEngine = new MahjongScoreEngine(new ScoringConfig { @@ -116,7 +117,6 @@ public class MahjongRoom 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); @@ -125,12 +125,14 @@ public class MahjongRoom AdvancePlayer(); return State.RecentEvents; } - // No reaction — clear LastDiscard, current player will draw State.LastDiscard = null; State.LastDiscardPlayer = null; } - // Phase 2: Draw card + // Phase 2: Draw card (skip for dealer on first turn — they already have 14) + bool isDealerFirstTurn = (player == State.Dealer && State.RoundNumber == 0); + if (!isDealerFirstTurn) + { if (State.Deck.Count > 0) { int drawn = State.Deck[^1]; @@ -172,6 +174,7 @@ public class MahjongRoom Settle(); return State.RecentEvents; } + } // end if (!isDealerFirstTurn) // Phase 3: Player decides what to do with their drawn hand bool requireWinFan = _rules.WinMinFan > 0; @@ -180,7 +183,10 @@ public class MahjongRoom if (action == "win") { - var result = _solver.CheckWin(State.Hands[player]); + 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, @@ -215,6 +221,18 @@ public class MahjongRoom 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). @@ -239,7 +257,10 @@ public class MahjongRoom // Check win first (highest priority) var handWithTile = new List(State.Hands[p]) { discardTile }; - var winResult = _solver.CheckWin(handWithTile); + 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) @@ -264,8 +285,11 @@ public class MahjongRoom if (best.action == "win") { - var winResult = _solver.CheckWin( - new List(State.Hands[bp]) { 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, diff --git a/RuleEngine/Phase/PhaseMachine.cs b/RuleEngine/Phase/PhaseMachine.cs index 1a134bc..2054fbf 100644 --- a/RuleEngine/Phase/PhaseMachine.cs +++ b/RuleEngine/Phase/PhaseMachine.cs @@ -38,11 +38,16 @@ public class MahjongPhaseMachine { private readonly List _phases; private readonly MeldsSolver _solver; + private readonly int _wildcardCount; + private readonly bool _require258; - public MahjongPhaseMachine(List phases, MeldsSolver solver) + public MahjongPhaseMachine(List phases, MeldsSolver solver, + int wildcardCount = 0, bool require258 = false) { _phases = phases; _solver = solver; + _wildcardCount = wildcardCount; + _require258 = require258; } public PhaseConfig? GetPhase(string name) => _phases.FirstOrDefault(p => p.Name == name); @@ -65,7 +70,10 @@ public class MahjongPhaseMachine actions.Add(new ActionOption { Action = "ming_kong", Priority = 3 }); var handWithTile = new List(state.Hands[playerId]) { state.LastDiscard.Value }; - var result = _solver.CheckWin(handWithTile); + int wildInHand = state.Hands[playerId].Count(Core.MahjongTile.IsWildcard) + + (Core.MahjongTile.IsWildcard(state.LastDiscard.Value) ? 1 : 0); + var result = _solver.CheckWin(handWithTile, + wildcardCount: wildInHand, require258Pair: _require258); if (result != null && result.IsWin) { if (!requireWinFan || result.Fans.Sum(f => GetFanValue(f)) >= 8) @@ -79,7 +87,9 @@ public class MahjongPhaseMachine if (CanBuKong(state, playerId)) actions.Add(new ActionOption { Action = "bu_kong", Priority = 1 }); - var tumoResult = _solver.CheckWin(state.Hands[playerId]); + int wildInSelf = state.Hands[playerId].Count(Core.MahjongTile.IsWildcard); + var tumoResult = _solver.CheckWin(state.Hands[playerId], + wildcardCount: wildInSelf, require258Pair: _require258); if (tumoResult != null && tumoResult.IsWin) { if (!requireWinFan || tumoResult.Fans.Sum(f => GetFanValue(f)) >= 8)