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:
@ -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<int>());
|
||||
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)]);
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -38,11 +38,16 @@ public class MahjongPhaseMachine
|
||||
{
|
||||
private readonly List<PhaseConfig> _phases;
|
||||
private readonly MeldsSolver _solver;
|
||||
private readonly int _wildcardCount;
|
||||
private readonly bool _require258;
|
||||
|
||||
public MahjongPhaseMachine(List<PhaseConfig> phases, MeldsSolver solver)
|
||||
public MahjongPhaseMachine(List<PhaseConfig> 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<int>(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)
|
||||
|
||||
Reference in New Issue
Block a user