fix: CheckWin缺少wildcard/258参数导致武汉麻将0%胡牌 + 庄家第一轮跳过摸牌

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%
This commit is contained in:
xiaoou
2026-07-04 11:39:00 +08:00
parent 55447d6b05
commit bb43ed7c89
3 changed files with 50 additions and 14 deletions

View File

@ -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;
}
/// <summary>Get wildcard count and 258-pair requirement for current rules.</summary>
private (int wildcardCount, bool require258) GetWinParams()
{
int wc = _rules.WildcardRules != null ? _rules.Deck.WildcardCount : 0;
bool r258 = _rules.WinCondition?.PairMustBe258 ?? false;
return (wc, r258);
}
/// <summary>Count wildcard tiles in a hand (encoding 50-59).</summary>
private static int CountWildcardsInHand(List<int> hand)
=> hand.Count(MahjongTile.IsWildcard);
/// <summary>
/// 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<int>(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<int>(State.Hands[bp]) { discardTile });
var handWithTile = new List<int>(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,