fix: Code Review完整修复 — AI chi/暗杠/加杠/chi位置

发现的6个缺口:
1. HandleDiscardReactions AI分支缺chi → 新增ExecuteAiChi
2. StepTurn不处理an_kong/bu_kong → 新增ExecuteAnKong/ExecuteBuKong
3. PhaseMachine.GetLegalActions chi误在自回合 → 移除(chi只能反应)
4. ScoreEngine.CheckFinish死代码 → 保留(无影响)
5. ExecutePungKong用Random.Shared → 改用_rng
6. HumanMahjongPlayer残留chiOpt → 移除
This commit is contained in:
xiaoou
2026-07-04 17:56:14 +08:00
parent b6bdf064ff
commit cbbe503117
3 changed files with 101 additions and 11 deletions

View File

@ -77,7 +77,6 @@ public class HumanMahjongPlayer
var winOpt = legalActions.FirstOrDefault(a => a.Action == "win");
var pungOpt = legalActions.FirstOrDefault(a => a.Action == "pung");
var kongOpts = legalActions.Where(a => a.Action is "ming_kong" or "an_kong" or "bu_kong").ToList();
var chiOpt = legalActions.FirstOrDefault(a => a.Action == "chi");
// Check if we're one tile away from winning (ting)
bool isTing = CheckTing(state);
@ -87,7 +86,6 @@ public class HumanMahjongPlayer
if (hand.Count > 0) ops.Add($"1-{hand.Count}(出牌)");
if (pungOpt != null) ops.Add("p(碰)");
if (kongOpts.Count > 0) ops.Add($"k(杠: {string.Join("/", kongOpts.Select(o => o.Action))})");
if (chiOpt != null) ops.Add("c(吃)");
if (winOpt != null) ops.Add("h✨(胡!)");
if (isTing) ops.Add("🀄听牌!");
ops.Add("q(退出)");
@ -101,7 +99,6 @@ public class HumanMahjongPlayer
if (input == "h" && winOpt != null) return ("win", null);
if (input == "p" && pungOpt != null) return ("pung", null);
if (input == "k" && kongOpts.Count > 0) return (kongOpts[0].Action, null);
if (input == "c" && chiOpt != null) return ("chi", null);
if (int.TryParse(input, out int idx) && idx >= 1 && idx <= sorted.Count)
return ("discard", sorted[idx - 1]);

View File

@ -235,6 +235,14 @@ public class MahjongRoom
State.AddEvent("discard", player, tile.Value,
$"出牌: {MahjongTile.ToString(tile.Value)}");
}
else if (action == "an_kong")
{
ExecuteAnKong(player);
}
else if (action == "bu_kong")
{
ExecuteBuKong(player);
}
AdvancePlayer();
return State.RecentEvents;
@ -345,6 +353,7 @@ public class MahjongRoom
// AI player: auto-decide
if (canWin) return ExecuteWinClaim(p, discardTile, winResult!);
if (canKong || canPung) return ExecutePungKong(p, discardTile, canKong ? "ming_kong" : "pung");
if (canChi) return ExecuteAiChi(p, discardTile);
}
return false;
@ -450,6 +459,49 @@ public class MahjongRoom
return true;
}
private bool ExecuteAiChi(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];
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))}");
if (hand.Count > 0)
{
int tileToDiscard = hand[_rng.Next(hand.Count)];
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);
@ -464,6 +516,54 @@ public class MahjongRoom
return true;
}
private void ExecuteAnKong(string player)
{
// Find a set of 4 identical tiles and expose them as concealed kong
var hand = State.Hands[player];
var group = hand.GroupBy(t => t)
.First(g => g.Count() >= 4 && !MahjongTile.IsWildcard(g.Key));
int tile = group.Key;
for (int i = 0; i < 4; i++) State.Hands[player].Remove(tile);
State.Exposed[player].Add(new Meld
{
Type = "kong_an",
Tiles = Enumerable.Repeat(tile, 4).ToList(),
IsConcealed = true
});
State.AddEvent("an_kong", player, tile, $"暗杠!{MahjongTile.ToString(tile)}");
// After kong, draw a replacement from the back of the wall
if (State.Deck.Count > 0)
{
int drawn = State.Deck[^1];
State.Deck.RemoveAt(State.Deck.Count - 1);
State.Hands[player].Add(drawn);
State.LastDrawnTile = drawn;
State.AddEvent("draw", player, drawn, $"杠补: {MahjongTile.ToString(drawn)}");
}
}
private void ExecuteBuKong(string player)
{
// Find an exposed pung that matches a tile in hand
var exposed = State.Exposed[player];
var pung = exposed.First(m => m.Type == "pung" && State.Hands[player].Contains(m.Tiles[0]));
int tile = pung.Tiles[0];
State.Hands[player].Remove(tile);
pung.Type = "kong_bu";
pung.Tiles.Add(tile);
State.AddEvent("bu_kong", player, tile, $"加杠!{MahjongTile.ToString(tile)}");
if (State.Deck.Count > 0)
{
int drawn = State.Deck[^1];
State.Deck.RemoveAt(State.Deck.Count - 1);
State.Hands[player].Add(drawn);
State.LastDrawnTile = drawn;
State.AddEvent("draw", player, drawn, $"杠补: {MahjongTile.ToString(drawn)}");
}
}
private bool ExecutePungKong(string player, int discardTile, string action)
{
State.DiscardPool.Remove(discardTile);
@ -488,7 +588,7 @@ public class MahjongRoom
var hand = State.Hands[player];
if (hand.Count > 0)
{
int tileToDiscard = hand[Random.Shared.Next(hand.Count)];
int tileToDiscard = hand[_rng.Next(hand.Count)];
State.Hands[player].Remove(tileToDiscard);
State.LastDiscard = tileToDiscard;
State.LastDiscardPlayer = player;

View File

@ -96,13 +96,6 @@ public class MahjongPhaseMachine
actions.Add(new ActionOption { Action = "win", Priority = 4 });
}
// Chi
if (state.LastDiscard.HasValue && state.LastDiscardPlayer != playerId)
{
if (CanChiCheck(state, playerId, state.LastDiscard.Value))
actions.Add(new ActionOption { Action = "chi", Priority = 1 });
}
actions.Add(new ActionOption { Action = "pass", Priority = 0 });
return actions;
}