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:
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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();
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user