feat: 人类玩家吃碰杠胡 — HandleDiscardReactions暂停询问
- HandleDiscardReactions重写: 按座位顺序逐个询问 - 人类玩家在可反应列表中时暂停(h=胡/p=碰/k=杠/Enter=过) - AI玩家自动决策(先胡后碰杠) - ExecuteWinClaim/ExecutePungKong抽取公共逻辑 - 碰/杠后让人类选择出哪张牌
This commit is contained in:
@ -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);
|
||||
|
||||
Reference in New Issue
Block a user