feat: 吃(chi)反应支持 — HandleDiscardReactions + 人类/AI

- CanChi→CanChiCheck公开给MahjongRoom调用
- HandleDiscardReactions: offset=1(上家)且非字牌时检查chi
- 人类HUD: c(吃)选项
- ExecuteChi: 找到配对的两张牌→创建chi副露→出牌
- ExecutePungKongHuman: 抽取碰/杠人类交互逻辑
This commit is contained in:
xiaoou
2026-07-04 17:51:58 +08:00
parent a047877494
commit b6bdf064ff
2 changed files with 114 additions and 39 deletions

View File

@ -281,6 +281,11 @@ public class MahjongRoom
int sameCount = State.Hands[p].Count(t => t == discardTile);
bool canWin = false, canKong = sameCount >= 3, canPung = sameCount >= 2;
bool canChi = false;
// Chi: only available to the player immediately after the discarder (上家)
if (offset == 1 && !MahjongTile.IsHonor(discardTile))
canChi = _phaseMachine.CanChiCheck(State, p, discardTile);
// Check win
var fullHand = FullHand(State, p, extraTile: discardTile);
@ -292,7 +297,7 @@ public class MahjongRoom
canWin = true;
// Human player: ask what they want to do
if (_human != null && p == _human.Name && (canWin || canKong || canPung))
if (_human != null && p == _human.Name && (canWin || canKong || canPung || canChi))
{
Console.WriteLine();
Console.WriteLine($"=== {p} 可以反应 ===");
@ -303,6 +308,7 @@ public class MahjongRoom
if (canWin) ops.Add("h(胡)");
if (canKong) ops.Add("k(杠)");
if (canPung) ops.Add("p(碰)");
if (canChi) ops.Add("c(吃)");
ops.Add("Enter(过)");
Console.WriteLine(string.Join(" ", ops));
Console.Write("> ");
@ -326,42 +332,11 @@ public class MahjongRoom
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;
RecordDiscard(p, tileToDiscard);
State.AddEvent("discard", p, tileToDiscard,
$"出牌: {MahjongTile.ToString(tileToDiscard)}");
return true;
return ExecutePungKongHuman(p, discardTile, action);
}
if (input == "c" && canChi)
{
return ExecuteChi(p, discardTile);
}
// Human passed — continue to next player
continue;
@ -375,6 +350,106 @@ public class MahjongRoom
return false;
}
private bool ExecutePungKongHuman(string p, int discardTile, string action)
{
string discarder = State.LastDiscardPlayer ?? "";
State.DiscardPool.Remove(discardTile);
int toRemove = action == "ming_kong" ? 3 : 2;
for (int r = 0; r < toRemove; r++)
State.Hands[p].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" ? "碰!" : "明杠!");
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-{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;
RecordDiscard(p, tileToDiscard);
State.AddEvent("discard", p, tileToDiscard, $"出牌: {MahjongTile.ToString(tileToDiscard)}");
return true;
}
private bool ExecuteChi(string p, int discardTile)
{
string discarder = State.LastDiscardPlayer ?? "";
State.DiscardPool.Remove(discardTile);
int suit = MahjongTile.Suit(discardTile);
int rank = MahjongTile.Rank(discardTile);
var hand = State.Hands[p];
// Find which two tiles form the chi sequence
List<int> toRemove = new();
if (rank >= 3 && hand.Any(t => MahjongTile.Suit(t) == suit && MahjongTile.Rank(t) == rank - 2)
&& hand.Any(t => MahjongTile.Suit(t) == suit && MahjongTile.Rank(t) == rank - 1))
{
toRemove.Add(hand.First(t => MahjongTile.Suit(t) == suit && MahjongTile.Rank(t) == rank - 2));
toRemove.Add(hand.First(t => MahjongTile.Suit(t) == suit && MahjongTile.Rank(t) == rank - 1));
}
else if (rank >= 2 && rank <= 8 && hand.Any(t => MahjongTile.Suit(t) == suit && MahjongTile.Rank(t) == rank - 1)
&& hand.Any(t => MahjongTile.Suit(t) == suit && MahjongTile.Rank(t) == rank + 1))
{
toRemove.Add(hand.First(t => MahjongTile.Suit(t) == suit && MahjongTile.Rank(t) == rank - 1));
toRemove.Add(hand.First(t => MahjongTile.Suit(t) == suit && MahjongTile.Rank(t) == rank + 1));
}
else if (rank <= 7 && hand.Any(t => MahjongTile.Suit(t) == suit && MahjongTile.Rank(t) == rank + 1)
&& hand.Any(t => MahjongTile.Suit(t) == suit && MahjongTile.Rank(t) == rank + 2))
{
toRemove.Add(hand.First(t => MahjongTile.Suit(t) == suit && MahjongTile.Rank(t) == rank + 1));
toRemove.Add(hand.First(t => MahjongTile.Suit(t) == suit && MahjongTile.Rank(t) == rank + 2));
}
foreach (var t in toRemove) State.Hands[p].Remove(t);
var chiTiles = new List<int>(toRemove) { discardTile };
chiTiles.Sort();
State.Exposed[p].Add(new Meld
{
Type = "chi",
Tiles = chiTiles,
SourcePlayer = discarder
});
State.CurrentPlayer = p;
State.LastDiscard = null;
State.LastDiscardPlayer = null;
State.AddEvent("chi", p, discardTile, $"吃!{string.Join(",", chiTiles.Select(MahjongTile.ToString))}");
// Human must discard after chi
var remainingHand = State.Hands[p];
Console.WriteLine($" 吃后手牌 ({remainingHand.Count}张):");
var sorted = remainingHand.OrderBy(t => t).ToList();
for (int i = 0; i < sorted.Count; i++)
Console.WriteLine($" [{i + 1}] {MahjongTile.ToString(sorted[i])}");
Console.Write($" 请出牌 (1-{remainingHand.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;
RecordDiscard(p, tileToDiscard);
State.AddEvent("discard", p, tileToDiscard, $"出牌: {MahjongTile.ToString(tileToDiscard)}");
return true;
}
private bool ExecuteWinClaim(string player, int discardTile, MeldsResult winResult)
{
State.DiscardPool.Remove(discardTile);

View File

@ -99,7 +99,7 @@ public class MahjongPhaseMachine
// Chi
if (state.LastDiscard.HasValue && state.LastDiscardPlayer != playerId)
{
if (CanChi(state, playerId, state.LastDiscard.Value))
if (CanChiCheck(state, playerId, state.LastDiscard.Value))
actions.Add(new ActionOption { Action = "chi", Priority = 1 });
}
@ -147,7 +147,7 @@ public class MahjongPhaseMachine
state.Hands[playerId].Contains(m.Tiles[0]));
}
private bool CanChi(MahjongGameState state, string playerId, int tile)
public bool CanChiCheck(MahjongGameState state, string playerId, int tile)
{
if (MahjongTile.IsHonor(tile)) return false;
int suit = MahjongTile.Suit(tile);