From b6bdf064ff83cf212eeda5f780b9ea133a4e3f3b Mon Sep 17 00:00:00 2001 From: xiaoou Date: Sat, 4 Jul 2026 17:51:58 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=90=83(chi)=E5=8F=8D=E5=BA=94?= =?UTF-8?q?=E6=94=AF=E6=8C=81=20=E2=80=94=20HandleDiscardReactions=20+=20?= =?UTF-8?q?=E4=BA=BA=E7=B1=BB/AI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - CanChi→CanChiCheck公开给MahjongRoom调用 - HandleDiscardReactions: offset=1(上家)且非字牌时检查chi - 人类HUD: c(吃)选项 - ExecuteChi: 找到配对的两张牌→创建chi副露→出牌 - ExecutePungKongHuman: 抽取碰/杠人类交互逻辑 --- RuleEngine/MahjongRoom.cs | 149 +++++++++++++++++++++++-------- RuleEngine/Phase/PhaseMachine.cs | 4 +- 2 files changed, 114 insertions(+), 39 deletions(-) diff --git a/RuleEngine/MahjongRoom.cs b/RuleEngine/MahjongRoom.cs index 5369f53..595f436 100644 --- a/RuleEngine/MahjongRoom.cs +++ b/RuleEngine/MahjongRoom.cs @@ -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 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(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); diff --git a/RuleEngine/Phase/PhaseMachine.cs b/RuleEngine/Phase/PhaseMachine.cs index 9ad3848..1fb6d87 100644 --- a/RuleEngine/Phase/PhaseMachine.cs +++ b/RuleEngine/Phase/PhaseMachine.cs @@ -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);