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:
@ -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]);
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user