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);
|
int sameCount = State.Hands[p].Count(t => t == discardTile);
|
||||||
bool canWin = false, canKong = sameCount >= 3, canPung = sameCount >= 2;
|
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
|
// Check win
|
||||||
var fullHand = FullHand(State, p, extraTile: discardTile);
|
var fullHand = FullHand(State, p, extraTile: discardTile);
|
||||||
@ -292,7 +297,7 @@ public class MahjongRoom
|
|||||||
canWin = true;
|
canWin = true;
|
||||||
|
|
||||||
// Human player: ask what they want to do
|
// 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();
|
||||||
Console.WriteLine($"=== {p} 可以反应 ===");
|
Console.WriteLine($"=== {p} 可以反应 ===");
|
||||||
@ -303,6 +308,7 @@ public class MahjongRoom
|
|||||||
if (canWin) ops.Add("h(胡)");
|
if (canWin) ops.Add("h(胡)");
|
||||||
if (canKong) ops.Add("k(杠)");
|
if (canKong) ops.Add("k(杠)");
|
||||||
if (canPung) ops.Add("p(碰)");
|
if (canPung) ops.Add("p(碰)");
|
||||||
|
if (canChi) ops.Add("c(吃)");
|
||||||
ops.Add("Enter(过)");
|
ops.Add("Enter(过)");
|
||||||
Console.WriteLine(string.Join(" ", ops));
|
Console.WriteLine(string.Join(" ", ops));
|
||||||
Console.Write("> ");
|
Console.Write("> ");
|
||||||
@ -326,42 +332,11 @@ public class MahjongRoom
|
|||||||
if ((input == "k" && canKong) || (input == "p" && canPung))
|
if ((input == "k" && canKong) || (input == "p" && canPung))
|
||||||
{
|
{
|
||||||
string action = input == "k" ? "ming_kong" : "pung";
|
string action = input == "k" ? "ming_kong" : "pung";
|
||||||
int toRemove = action == "ming_kong" ? 3 : 2;
|
return ExecutePungKongHuman(p, discardTile, action);
|
||||||
for (int r = 0; r < toRemove; r++)
|
}
|
||||||
State.Hands[p].Remove(discardTile);
|
if (input == "c" && canChi)
|
||||||
|
{
|
||||||
State.DiscardPool.Remove(discardTile);
|
return ExecuteChi(p, 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;
|
|
||||||
}
|
}
|
||||||
// Human passed — continue to next player
|
// Human passed — continue to next player
|
||||||
continue;
|
continue;
|
||||||
@ -375,6 +350,106 @@ public class MahjongRoom
|
|||||||
return false;
|
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)
|
private bool ExecuteWinClaim(string player, int discardTile, MeldsResult winResult)
|
||||||
{
|
{
|
||||||
State.DiscardPool.Remove(discardTile);
|
State.DiscardPool.Remove(discardTile);
|
||||||
|
|||||||
@ -99,7 +99,7 @@ public class MahjongPhaseMachine
|
|||||||
// Chi
|
// Chi
|
||||||
if (state.LastDiscard.HasValue && state.LastDiscardPlayer != playerId)
|
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 });
|
actions.Add(new ActionOption { Action = "chi", Priority = 1 });
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -147,7 +147,7 @@ public class MahjongPhaseMachine
|
|||||||
state.Hands[playerId].Contains(m.Tiles[0]));
|
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;
|
if (MahjongTile.IsHonor(tile)) return false;
|
||||||
int suit = MahjongTile.Suit(tile);
|
int suit = MahjongTile.Suit(tile);
|
||||||
|
|||||||
Reference in New Issue
Block a user