feat: 人类玩家吃碰杠胡 — HandleDiscardReactions暂停询问

- HandleDiscardReactions重写: 按座位顺序逐个询问
- 人类玩家在可反应列表中时暂停(h=胡/p=碰/k=杠/Enter=过)
- AI玩家自动决策(先胡后碰杠)
- ExecuteWinClaim/ExecutePungKong抽取公共逻辑
- 碰/杠后让人类选择出哪张牌
This commit is contained in:
xiaoou
2026-07-04 11:52:15 +08:00
parent f9242cf965
commit 3c381f2758

View File

@ -248,12 +248,10 @@ public class MahjongRoom
/// </summary>
private bool HandleDiscardReactions(int discardTile)
{
// Collect claims from all alive players except the discarder
var claims = new List<(string player, int priority, string action)>();
string discarder = State.LastDiscardPlayer ?? "";
int startIdx = State.PlayerOrder.IndexOf(discarder);
// Check in player order (starting after discarder)
int startIdx = State.PlayerOrder.IndexOf(discarder);
for (int offset = 1; offset < State.PlayerOrder.Count; offset++)
{
string p = State.PlayerOrder[(startIdx + offset) % State.PlayerOrder.Count];
@ -261,104 +259,150 @@ public class MahjongRoom
if (p == discarder) continue;
int sameCount = State.Hands[p].Count(t => t == discardTile);
string? claim = null;
int priority = 0;
bool canWin = false, canKong = sameCount >= 3, canPung = sameCount >= 2;
// Check win first (highest priority)
// Check win
var handWithTile = new List<int>(State.Hands[p]) { discardTile };
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)
claim = "ming_kong";
else if (sameCount >= 2)
claim = "pung";
canWin = true;
if (claim == null) continue;
priority = claim switch { "win" => 4, "ming_kong" => 3, "pung" => 2, _ => 0 };
// For same priority, closer seat wins (first claim in order)
// For higher priority, it overrides all previous claims
if (claims.Count == 0 || priority > claims[0].priority)
claims.Add((p, priority, claim));
// Same priority: first claimant wins (closer to discarder in seat order)
// Different (lower) priority: ignored — higher priority already claimed
}
if (claims.Count == 0) return false;
var best = claims[0];
string bp = best.player;
if (best.action == "win")
{
// Remove the winning tile from pool (it's now part of the winning hand)
State.DiscardPool.Remove(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,
$"胡!{MahjongTile.ToString(discardTile)}");
if (!_rules.Phases.Any(ph => ph.ParallelElimination))
// Human player: ask what they want to do
if (_human != null && p == _human.Name && (canWin || canKong || canPung))
{
_scoreEngine.Settle(State, bp, winResult!, isSelfDraw: false);
IsFinished = true;
Console.WriteLine();
Console.WriteLine($"=== {p} 可以反应 ===");
Console.WriteLine($" 对手 {discarder} 出了: {MahjongTile.ToString(discardTile)}");
Console.WriteLine($" 你的手牌: {string.Join(" ", State.Hands[p].OrderBy(t => t).Select(MahjongTile.ToString))}");
Console.Write(" 操作: ");
var ops = new List<string>();
if (canWin) ops.Add("h(胡)");
if (canKong) ops.Add("k(杠)");
if (canPung) ops.Add("p(碰)");
ops.Add("Enter(过)");
Console.WriteLine(string.Join(" ", ops));
Console.Write("> ");
string? input = Console.ReadLine()?.Trim().ToLower();
if (input == "h" && canWin)
{
State.DiscardPool.Remove(discardTile);
State.HuPlayers.Add(p);
State.AlivePlayers.Remove(p);
State.AddEvent("win", p, discardTile,
$"胡!{MahjongTile.ToString(discardTile)}");
if (!_rules.Phases.Any(ph => ph.ParallelElimination))
{
_scoreEngine.Settle(State, p, winResult!, isSelfDraw: false);
IsFinished = true;
}
return true;
}
if ((input == "k" && canKong) || (input == "p" && canPung))
{
string action = input == "k" ? "ming_kong" : "pung";
int toRemove = action == "ming_kong" ? 3 : 2;
for (int r = 0; r < toRemove; r++)
State.Hands[p].Remove(discardTile);
State.DiscardPool.Remove(discardTile);
int meldSize = action == "ming_kong" ? 4 : 3;
State.Exposed[p].Add(new Meld
{
Type = action,
Tiles = Enumerable.Repeat(discardTile, meldSize).ToList(),
SourcePlayer = discarder
});
State.CurrentPlayer = p;
State.LastDiscard = null;
State.LastDiscardPlayer = null;
State.AddEvent(action, p, discardTile,
action == "pung" ? "碰!" : "明杠!");
// Human must discard after pung/kong
var hand = State.Hands[p];
Console.WriteLine($" 碰/杠后手牌 ({hand.Count}张):");
var sorted = hand.OrderBy(t => t).ToList();
for (int i = 0; i < sorted.Count; i++)
Console.WriteLine($" [{i + 1}] {MahjongTile.ToString(sorted[i])}");
Console.Write(" 请出牌 (1-{0}): ".Replace("{0}", hand.Count.ToString()), hand.Count);
int discardIdx;
while (!int.TryParse(Console.ReadLine(), out discardIdx) || discardIdx < 1 || discardIdx > sorted.Count)
Console.Write(" 无效,重试: ");
int tileToDiscard = sorted[discardIdx - 1];
State.Hands[p].Remove(tileToDiscard);
State.LastDiscard = tileToDiscard;
State.LastDiscardPlayer = p;
State.DiscardPool.Add(tileToDiscard);
State.AddEvent("discard", p, tileToDiscard,
$"出牌: {MahjongTile.ToString(tileToDiscard)}");
return true;
}
// Human passed — continue to next player
continue;
}
return true;
}
if (best.action == "pung" || best.action == "ming_kong")
{
// Remove the discarded tile from pool (it's now in the meld)
State.DiscardPool.Remove(discardTile);
// 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 = meldTiles,
SourcePlayer = discarder
});
State.CurrentPlayer = bp;
State.LastDiscard = null;
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;
// AI player: auto-decide
if (canWin) return ExecuteWinClaim(p, discardTile, winResult!);
if (canKong || canPung) return ExecutePungKong(p, discardTile, canKong ? "ming_kong" : "pung");
}
return false;
}
private bool ExecuteWinClaim(string player, int discardTile, MeldsResult winResult)
{
State.DiscardPool.Remove(discardTile);
State.HuPlayers.Add(player);
State.AlivePlayers.Remove(player);
State.AddEvent("win", player, discardTile, $"胡!{MahjongTile.ToString(discardTile)}");
if (!_rules.Phases.Any(ph => ph.ParallelElimination))
{
_scoreEngine.Settle(State, player, winResult, isSelfDraw: false);
IsFinished = true;
}
return true;
}
private bool ExecutePungKong(string player, int discardTile, string action)
{
State.DiscardPool.Remove(discardTile);
int toRemove = action == "ming_kong" ? 3 : 2;
for (int r = 0; r < toRemove; r++)
State.Hands[player].Remove(discardTile);
int meldSize = action == "ming_kong" ? 4 : 3;
State.Exposed[player].Add(new Meld
{
Type = action,
Tiles = Enumerable.Repeat(discardTile, meldSize).ToList(),
SourcePlayer = State.LastDiscardPlayer ?? ""
});
State.CurrentPlayer = player;
State.LastDiscard = null;
State.LastDiscardPlayer = null;
State.AddEvent(action, player, discardTile,
action == "pung" ? "碰!" : "明杠!");
// AI discards after pung/kong
var hand = State.Hands[player];
if (hand.Count > 0)
{
int tileToDiscard = hand[Random.Shared.Next(hand.Count)];
State.Hands[player].Remove(tileToDiscard);
State.LastDiscard = tileToDiscard;
State.LastDiscardPlayer = player;
State.DiscardPool.Add(tileToDiscard);
State.AddEvent("discard", player, tileToDiscard,
$"出牌: {MahjongTile.ToString(tileToDiscard)}");
}
return true;
}
private void AdvancePlayer()
{
int idx = State.PlayerOrder.IndexOf(State.CurrentPlayer);