feat: 吃(chi)反应支持 — HandleDiscardReactions + 人类/AI
- CanChi→CanChiCheck公开给MahjongRoom调用 - HandleDiscardReactions: offset=1(上家)且非字牌时检查chi - 人类HUD: c(吃)选项 - ExecuteChi: 找到配对的两张牌→创建chi副露→出牌 - ExecutePungKongHuman: 抽取碰/杠人类交互逻辑
This commit is contained in:
@ -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);
|
||||
|
||||
@ -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);
|
||||
|
||||
Reference in New Issue
Block a user