fix: CheckWin守卫!=14拒绝副露后手牌 + FullHand合并副露

根因: 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张手牌自摸
This commit is contained in:
xiaoou
2026-07-04 17:31:31 +08:00
parent d416450e56
commit bf041b7e50
5 changed files with 54 additions and 18 deletions

View File

@ -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<int> { 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<int> { 3,4,5, 8,8,8, 13,14,15, 18,18 };
room.State.Exposed["东"] = new List<Meld> { new() { Type = "pung", Tiles = new() { 1,1,1 } } };
room.State.DiscardPool = new List<int>();
room.State.Deck = new List<int>();
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);
}
}

View File

@ -91,12 +91,18 @@ public class HumanMahjongPlayer
private bool CheckTing(MahjongGameState state)
{
var hand = state.Hands.GetValueOrDefault(Name, new List<int>());
// Combine with exposed melds
var fullHand = new List<int>(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;

View File

@ -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<int> hand)
=> hand.Count(MahjongTile.IsWildcard);
/// <summary>Combine hand + exposed melds into full virtual hand for CheckWin.</summary>
private static List<int> FullHand(MahjongGameState state, string player, int? extraTile = null)
{
var tiles = new List<int>(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;
}
/// <summary>
/// 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<int>(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;

View File

@ -27,7 +27,12 @@ public class MeldsSolver
{
var tiles = new List<int>(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();

View File

@ -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<int>(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];
}
/// <summary>Combine hand + exposed melds into full virtual hand for CheckWin.</summary>
private static List<int> FullHand(MahjongGameState state, string player, int? extraTile = null)
{
var tiles = new List<int>(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;
}
}