fix: StepTurn回合流转重构 + 碰/杠牌数修正
Root cause: 1. 摸牌在碰/杠检测之前→摸的牌浪费→手牌净减 2. RemoveAll一次删光所有同牌→碰2张变删N张 3. 碰/杠后没让玩家出牌→手牌只减不补 4. 显示只展示手牌不展示暴露副露 修复: - 重构StepTurn: 先处理LastDiscard反应(碰/杠/胡) 没人反应才摸牌→决定→出牌 - RemoveAll→Remove 精准删除 - 碰/杠后强制出一张牌 - HandleDiscardReactions替代旧CheckReactions - RenderFinal展示Exposed副露
This commit is contained in:
@ -181,12 +181,19 @@ void RenderFinal(MahjongRoom room)
|
||||
Console.WriteLine($" 游戏结束,牌墙剩余 {state.Deck.Count} 张");
|
||||
|
||||
Console.WriteLine();
|
||||
Console.WriteLine(" 最终手牌:");
|
||||
Console.WriteLine(" 最终状态:");
|
||||
foreach (var p in state.PlayerOrder)
|
||||
{
|
||||
var hand = state.Hands[p];
|
||||
var sorted = string.Join(", ", hand.OrderBy(t => t).Select(MahjongTile.ToString));
|
||||
Console.WriteLine($" {p}{(p == state.Dealer ? "(庄)" : "")}: {sorted} ({hand.Count}张)");
|
||||
var exposedStr = "";
|
||||
if (state.Exposed.TryGetValue(p, out var melds) && melds.Count > 0)
|
||||
{
|
||||
var meldDescs = melds.Select(m =>
|
||||
$"{m.Type}[{string.Join(",", m.Tiles.Select(MahjongTile.ToString))}]");
|
||||
exposedStr = $" | 已碰/杠: {string.Join(" ", meldDescs)}";
|
||||
}
|
||||
Console.WriteLine($" {p}{(p == state.Dealer ? "(庄)" : "")}: {sorted} ({hand.Count}张){exposedStr}");
|
||||
}
|
||||
|
||||
if (state.HuPlayers.Count > 0)
|
||||
|
||||
@ -113,33 +113,48 @@ public class MahjongRoom
|
||||
public List<GameEvent> StepTurn()
|
||||
{
|
||||
State.RecentEvents.Clear();
|
||||
var player = State.CurrentPlayer;
|
||||
|
||||
// Draw card
|
||||
// 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);
|
||||
if (reacted)
|
||||
{
|
||||
AdvancePlayer();
|
||||
return State.RecentEvents;
|
||||
}
|
||||
// No reaction — clear LastDiscard, current player will draw
|
||||
State.LastDiscard = null;
|
||||
State.LastDiscardPlayer = null;
|
||||
}
|
||||
|
||||
// Phase 2: Draw card
|
||||
if (State.Deck.Count > 0)
|
||||
{
|
||||
int drawn = State.Deck[^1];
|
||||
State.Deck.RemoveAt(State.Deck.Count - 1);
|
||||
State.Hands[State.CurrentPlayer].Add(drawn);
|
||||
State.AddEvent("draw", State.CurrentPlayer, drawn,
|
||||
State.Hands[player].Add(drawn);
|
||||
State.AddEvent("draw", player, drawn,
|
||||
$"摸牌: {MahjongTile.ToString(drawn)}");
|
||||
|
||||
// Check flower
|
||||
if (MahjongTile.IsFlower(drawn))
|
||||
{
|
||||
State.FlowerReplaced++;
|
||||
State.AddEvent("flower_drawn", State.CurrentPlayer, drawn,
|
||||
State.AddEvent("flower_drawn", player, drawn,
|
||||
$"花牌 {MahjongTile.ToString(drawn)} → 补牌");
|
||||
// Flower into flower pool and draw again
|
||||
if (!State.FlowerPool.ContainsKey(State.CurrentPlayer))
|
||||
State.FlowerPool[State.CurrentPlayer] = new List<int>();
|
||||
State.FlowerPool[State.CurrentPlayer].Add(drawn);
|
||||
State.Hands[State.CurrentPlayer].Remove(drawn);
|
||||
if (!State.FlowerPool.ContainsKey(player))
|
||||
State.FlowerPool[player] = new List<int>();
|
||||
State.FlowerPool[player].Add(drawn);
|
||||
State.Hands[player].Remove(drawn);
|
||||
if (State.Deck.Count > 0)
|
||||
{
|
||||
int extra = State.Deck[^1];
|
||||
State.Deck.RemoveAt(State.Deck.Count - 1);
|
||||
State.Hands[State.CurrentPlayer].Add(extra);
|
||||
State.AddEvent("draw", State.CurrentPlayer, extra,
|
||||
State.Hands[player].Add(extra);
|
||||
State.AddEvent("draw", player, extra,
|
||||
$"补牌: {MahjongTile.ToString(extra)}");
|
||||
}
|
||||
}
|
||||
@ -149,7 +164,6 @@ public class MahjongRoom
|
||||
State.IsDeckExhausted = true;
|
||||
State.AddEvent("deck_exhausted", "", null, "牌墙耗尽");
|
||||
|
||||
// Blood war: check ting and hua zhu
|
||||
if (_rules.Phases.Any(p => p.ParallelElimination))
|
||||
{
|
||||
CheckHuaZhu();
|
||||
@ -159,12 +173,9 @@ public class MahjongRoom
|
||||
return State.RecentEvents;
|
||||
}
|
||||
|
||||
// Get legal actions for current player
|
||||
var player = State.CurrentPlayer;
|
||||
// Phase 3: Player decides what to do with their drawn hand
|
||||
bool requireWinFan = _rules.WinMinFan > 0;
|
||||
var legalActions = _phaseMachine.GetLegalActions(State, player, requireWinFan);
|
||||
|
||||
// AI decides
|
||||
var (action, tile) = _ais.First(a => a.Name == player).Decide(legalActions, State);
|
||||
|
||||
if (action == "win")
|
||||
@ -177,7 +188,6 @@ public class MahjongRoom
|
||||
|
||||
if (_rules.Phases.Any(p => p.ParallelElimination))
|
||||
{
|
||||
// Blood war: continue without the winner
|
||||
if (State.AlivePlayers.Count <= 1)
|
||||
{
|
||||
Settle();
|
||||
@ -199,46 +209,29 @@ public class MahjongRoom
|
||||
State.DiscardPool.Add(tile.Value);
|
||||
State.AddEvent("discard", player, tile.Value,
|
||||
$"出牌: {MahjongTile.ToString(tile.Value)}");
|
||||
|
||||
// Check others' reactions (pung/kong/win)
|
||||
bool reactionTaken = CheckReactions(tile.Value);
|
||||
if (reactionTaken)
|
||||
{
|
||||
AdvancePlayer();
|
||||
return State.RecentEvents;
|
||||
}
|
||||
}
|
||||
else if (action == "pung")
|
||||
{
|
||||
// Handle pung
|
||||
if (State.LastDiscard.HasValue)
|
||||
{
|
||||
int t = State.LastDiscard.Value;
|
||||
State.Hands[player].RemoveAll(x => x == t);
|
||||
State.Hands[player].RemoveAll(x => x == t);
|
||||
State.LastDiscard = null;
|
||||
State.Exposed[player].Add(new Meld
|
||||
{
|
||||
Type = "pung",
|
||||
Tiles = new List<int> { t, t, t },
|
||||
SourcePlayer = State.LastDiscardPlayer ?? ""
|
||||
});
|
||||
State.AddEvent("pung", player, t, $"碰!{MahjongTile.ToString(t)}");
|
||||
}
|
||||
}
|
||||
|
||||
AdvancePlayer();
|
||||
return State.RecentEvents;
|
||||
}
|
||||
|
||||
private bool CheckReactions(int discardTile)
|
||||
/// <summary>
|
||||
/// Let other players react to a discard (pung/kong/win).
|
||||
/// Returns true if someone reacted (turn passes to them).
|
||||
/// </summary>
|
||||
private bool HandleDiscardReactions(int discardTile)
|
||||
{
|
||||
// Collect all claims with their priorities
|
||||
// Collect claims from all alive players except the discarder
|
||||
var claims = new List<(string player, int priority, string action)>();
|
||||
|
||||
foreach (var p in State.AlivePlayers)
|
||||
string discarder = State.LastDiscardPlayer ?? "";
|
||||
|
||||
// Check in player order (starting after discarder)
|
||||
int startIdx = State.PlayerOrder.IndexOf(discarder);
|
||||
for (int offset = 1; offset < State.PlayerOrder.Count; offset++)
|
||||
{
|
||||
if (p == State.CurrentPlayer) continue;
|
||||
string p = State.PlayerOrder[(startIdx + offset) % State.PlayerOrder.Count];
|
||||
if (!State.AlivePlayers.Contains(p)) continue;
|
||||
if (p == discarder) continue;
|
||||
|
||||
var handWithTile = new List<int>(State.Hands[p]) { discardTile };
|
||||
var winResult = _solver.CheckWin(handWithTile);
|
||||
@ -255,22 +248,22 @@ public class MahjongRoom
|
||||
|
||||
if (claims.Count == 0) return false;
|
||||
|
||||
// Highest-priority claim wins
|
||||
// Highest-priority claim wins (for simplicity: first highest wins)
|
||||
var best = claims.OrderByDescending(c => c.priority).First();
|
||||
string bp = best.player;
|
||||
|
||||
if (best.action == "win")
|
||||
{
|
||||
var winResult = _solver.CheckWin(
|
||||
new List<int>(State.Hands[best.player]) { discardTile });
|
||||
|
||||
State.HuPlayers.Add(best.player);
|
||||
State.AlivePlayers.Remove(best.player);
|
||||
State.AddEvent("win", best.player, discardTile,
|
||||
new List<int>(State.Hands[bp]) { discardTile });
|
||||
State.HuPlayers.Add(bp);
|
||||
State.AlivePlayers.Remove(bp);
|
||||
State.AddEvent("win", bp, discardTile,
|
||||
$"胡!{MahjongTile.ToString(discardTile)}");
|
||||
|
||||
if (!_rules.Phases.Any(ph => ph.ParallelElimination))
|
||||
{
|
||||
_scoreEngine.Settle(State, best.player, winResult!, isSelfDraw: false);
|
||||
_scoreEngine.Settle(State, bp, winResult!, isSelfDraw: false);
|
||||
IsFinished = true;
|
||||
}
|
||||
return true;
|
||||
@ -278,21 +271,39 @@ public class MahjongRoom
|
||||
|
||||
if (best.action == "pung" || best.action == "ming_kong")
|
||||
{
|
||||
State.Hands[best.player].RemoveAll(t => t == discardTile);
|
||||
State.Hands[best.player].RemoveAll(t => t == discardTile);
|
||||
if (best.action == "ming_kong")
|
||||
State.Hands[best.player].RemoveAll(t => t == discardTile);
|
||||
|
||||
State.Exposed[best.player].Add(new Meld
|
||||
// Remove tiles from hand (pung: 2, ming_kong: 3)
|
||||
// The discard tile is taken from pool, not from hand
|
||||
int toRemove = best.action == "ming_kong" ? 3 : 2;
|
||||
for (int r = 0; r < toRemove; r++)
|
||||
State.Hands[bp].Remove(discardTile);
|
||||
|
||||
// Create exposed meld
|
||||
int meldSize = best.action == "ming_kong" ? 4 : 3;
|
||||
var meldTiles = Enumerable.Repeat(discardTile, meldSize).ToList();
|
||||
State.Exposed[bp].Add(new Meld
|
||||
{
|
||||
Type = best.action,
|
||||
Tiles = new List<int> { discardTile, discardTile, discardTile,
|
||||
discardTile }, // 4th for kong
|
||||
SourcePlayer = State.CurrentPlayer
|
||||
Tiles = meldTiles,
|
||||
SourcePlayer = discarder
|
||||
});
|
||||
State.CurrentPlayer = best.player;
|
||||
State.CurrentPlayer = bp;
|
||||
State.LastDiscard = null;
|
||||
State.AddEvent(best.action, best.player, discardTile, best.action == "pung" ? "碰!" : "明杠!");
|
||||
State.LastDiscardPlayer = null;
|
||||
State.AddEvent(best.action, bp, discardTile,
|
||||
best.action == "pung" ? "碰!" : "明杠!");
|
||||
|
||||
// After pung/kong, the reacting player must discard a tile
|
||||
var hand = State.Hands[bp];
|
||||
if (hand.Count > 0)
|
||||
{
|
||||
int tileToDiscard = hand[Random.Shared.Next(hand.Count)];
|
||||
State.Hands[bp].Remove(tileToDiscard);
|
||||
State.LastDiscard = tileToDiscard;
|
||||
State.LastDiscardPlayer = bp;
|
||||
State.DiscardPool.Add(tileToDiscard);
|
||||
State.AddEvent("discard", bp, tileToDiscard,
|
||||
$"出牌: {MahjongTile.ToString(tileToDiscard)}");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user