fix: CanAnKong硬编码false + 人类HUD显示听牌/杠/对手状态
修复:
- CanAnKong: 硬编码false → 检查手牌中4张相同(暗杠)
- HumanMahjongPlayer: 添加MeldsSolver依赖,实时检测听牌
- 人类HUD: 显示牌墙/副露/听牌🀄/杠类型(暗/明/加)
- 交互模式人类: 对手显示'X张手牌 | 已pung[..]'不暴露手牌
This commit is contained in:
@ -146,17 +146,43 @@ else
|
|||||||
// Show current hands
|
// Show current hands
|
||||||
if (!room.IsFinished)
|
if (!room.IsFinished)
|
||||||
{
|
{
|
||||||
|
// In human mode, show only opponent hands (human HUD shows their own)
|
||||||
Console.WriteLine();
|
Console.WriteLine();
|
||||||
Console.WriteLine(" 当前手牌:");
|
if (humanMode)
|
||||||
foreach (var p in room.State.PlayerOrder)
|
|
||||||
{
|
{
|
||||||
if (!room.State.AlivePlayers.Contains(p)) continue;
|
Console.WriteLine(" 对手状态:");
|
||||||
var hand = room.State.Hands[p];
|
foreach (var p in room.State.PlayerOrder)
|
||||||
Console.WriteLine($" {p}: {string.Join(" ", hand.OrderBy(t => t).Select(MahjongTile.ToString))} ({hand.Count}张)");
|
{
|
||||||
|
if (p == "AI-东") continue; // skip human
|
||||||
|
if (!room.State.AlivePlayers.Contains(p)) continue;
|
||||||
|
var hand = room.State.Hands[p];
|
||||||
|
var exposedStr = "";
|
||||||
|
if (room.State.Exposed.TryGetValue(p, out var melds) && melds.Count > 0)
|
||||||
|
{
|
||||||
|
var descs = melds.Select(m =>
|
||||||
|
$"{m.Type}[{string.Join(",", m.Tiles.Where(t => t != -1).Select(MahjongTile.ToString))}]");
|
||||||
|
exposedStr = $" | 已{string.Join(" ", descs)}";
|
||||||
|
}
|
||||||
|
Console.WriteLine($" {p}: {hand.Count}张手牌{exposedStr}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Console.WriteLine(" 当前手牌:");
|
||||||
|
foreach (var p in room.State.PlayerOrder)
|
||||||
|
{
|
||||||
|
if (!room.State.AlivePlayers.Contains(p)) continue;
|
||||||
|
var hand = room.State.Hands[p];
|
||||||
|
Console.WriteLine($" {p}: {string.Join(" ", hand.OrderBy(t => t).Select(MahjongTile.ToString))} ({hand.Count}张)");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Console.Write("\n[按 Enter 继续]");
|
// In human mode, don't wait for Enter — human interactions already pause
|
||||||
Console.ReadLine();
|
if (!humanMode)
|
||||||
|
{
|
||||||
|
Console.Write("\n[按 Enter 继续]");
|
||||||
|
Console.ReadLine();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -2,18 +2,21 @@ namespace RuleEngine.AI;
|
|||||||
|
|
||||||
using RuleEngine.Core;
|
using RuleEngine.Core;
|
||||||
using RuleEngine.Phase;
|
using RuleEngine.Phase;
|
||||||
|
using RuleEngine.Patterns;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Human player — reads commands from console stdin.
|
/// Human player — reads commands from console stdin.
|
||||||
/// Commands: h=hu, p=pung, k=kong, c=chi, 1-9=discard tile by index, q=quit
|
/// Commands: h=hu, p=pung, k=kong (an_kong/ming_kong/bu_kong), c=chi, 1-N=discard by index, q=quit
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class HumanMahjongPlayer
|
public class HumanMahjongPlayer
|
||||||
{
|
{
|
||||||
public string Name { get; }
|
public string Name { get; }
|
||||||
|
private readonly MeldsSolver _solver;
|
||||||
|
|
||||||
public HumanMahjongPlayer(string name)
|
public HumanMahjongPlayer(string name, MeldsSolver solver)
|
||||||
{
|
{
|
||||||
Name = name;
|
Name = name;
|
||||||
|
_solver = solver;
|
||||||
}
|
}
|
||||||
|
|
||||||
public (string action, int? tile) Decide(List<ActionOption> legalActions, MahjongGameState state)
|
public (string action, int? tile) Decide(List<ActionOption> legalActions, MahjongGameState state)
|
||||||
@ -21,7 +24,6 @@ public class HumanMahjongPlayer
|
|||||||
var hand = state.Hands.GetValueOrDefault(Name, new List<int>());
|
var hand = state.Hands.GetValueOrDefault(Name, new List<int>());
|
||||||
var sorted = hand.OrderBy(t => t).ToList();
|
var sorted = hand.OrderBy(t => t).ToList();
|
||||||
|
|
||||||
// Highlight last drawn tile
|
|
||||||
int? drawnTile = state.LastDrawnTile;
|
int? drawnTile = state.LastDrawnTile;
|
||||||
int drawnIdx = drawnTile.HasValue ? sorted.IndexOf(drawnTile.Value) : -1;
|
int drawnIdx = drawnTile.HasValue ? sorted.IndexOf(drawnTile.Value) : -1;
|
||||||
|
|
||||||
@ -29,29 +31,43 @@ public class HumanMahjongPlayer
|
|||||||
{
|
{
|
||||||
Console.WriteLine();
|
Console.WriteLine();
|
||||||
Console.WriteLine($"=== {Name} 的回合 ===");
|
Console.WriteLine($"=== {Name} 的回合 ===");
|
||||||
|
Console.WriteLine($" 牌墙: {state.Deck.Count}张 | 弃牌堆: {state.DiscardPool.Count}张");
|
||||||
|
|
||||||
|
// Show exposed melds
|
||||||
|
if (state.Exposed.TryGetValue(Name, out var exposed) && exposed.Count > 0)
|
||||||
|
{
|
||||||
|
var meldStr = string.Join(" ", exposed.Select(m =>
|
||||||
|
$"{m.Type}[{string.Join(",", m.Tiles.Where(t => t != -1).Select(MahjongTile.ToString))}]"));
|
||||||
|
Console.WriteLine($" 已碰/杠: {meldStr}");
|
||||||
|
}
|
||||||
|
|
||||||
if (drawnTile.HasValue && drawnIdx >= 0)
|
if (drawnTile.HasValue && drawnIdx >= 0)
|
||||||
Console.WriteLine($" 🀫 刚刚摸到: {MahjongTile.ToString(drawnTile.Value)}");
|
Console.WriteLine($" 🀫 刚刚摸到: {MahjongTile.ToString(drawnTile.Value)}");
|
||||||
Console.WriteLine($"手牌 ({hand.Count}张):");
|
Console.WriteLine($" 手牌 ({hand.Count}张):");
|
||||||
for (int i = 0; i < sorted.Count; i++)
|
for (int i = 0; i < sorted.Count; i++)
|
||||||
{
|
{
|
||||||
bool isDrawn = (i == drawnIdx);
|
bool isDrawn = (i == drawnIdx);
|
||||||
Console.WriteLine($" [{i + 1}] {(isDrawn ? "→ " : " ")}{MahjongTile.ToString(sorted[i])}{(isDrawn ? " ← 新摸" : "")}");
|
Console.WriteLine($" [{i + 1}] {(isDrawn ? "→ " : " ")}{MahjongTile.ToString(sorted[i])}{(isDrawn ? " ← 新摸" : "")}");
|
||||||
}
|
}
|
||||||
Console.WriteLine();
|
Console.WriteLine();
|
||||||
|
|
||||||
// Show available actions
|
// Show available actions
|
||||||
var winOpt = legalActions.FirstOrDefault(a => a.Action == "win");
|
var winOpt = legalActions.FirstOrDefault(a => a.Action == "win");
|
||||||
var pungOpt = legalActions.FirstOrDefault(a => a.Action == "pung");
|
var pungOpt = legalActions.FirstOrDefault(a => a.Action == "pung");
|
||||||
var kongOpt = legalActions.FirstOrDefault(a => a.Action == "ming_kong" || a.Action == "an_kong");
|
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");
|
var chiOpt = legalActions.FirstOrDefault(a => a.Action == "chi");
|
||||||
|
|
||||||
Console.Write("可选操作: ");
|
// Check if we're one tile away from winning (ting)
|
||||||
|
bool isTing = CheckTing(state);
|
||||||
|
|
||||||
|
Console.Write(" 可选操作: ");
|
||||||
var ops = new List<string>();
|
var ops = new List<string>();
|
||||||
if (hand.Count > 0) ops.Add("1-{0}(出牌)".Replace("{0}", hand.Count.ToString()));
|
if (hand.Count > 0) ops.Add($"1-{hand.Count}(出牌)");
|
||||||
if (pungOpt != null) ops.Add("p(碰)");
|
if (pungOpt != null) ops.Add("p(碰)");
|
||||||
if (kongOpt != null) ops.Add("k(杠)");
|
if (kongOpts.Count > 0) ops.Add($"k(杠: {string.Join("/", kongOpts.Select(o => o.Action))})");
|
||||||
if (chiOpt != null) ops.Add("c(吃)");
|
if (chiOpt != null) ops.Add("c(吃)");
|
||||||
if (winOpt != null) ops.Add("h(胡)");
|
if (winOpt != null) ops.Add("h✨(胡!)");
|
||||||
|
if (isTing) ops.Add("🀄听牌!");
|
||||||
ops.Add("q(退出)");
|
ops.Add("q(退出)");
|
||||||
Console.WriteLine(string.Join(" ", ops));
|
Console.WriteLine(string.Join(" ", ops));
|
||||||
Console.Write("> ");
|
Console.Write("> ");
|
||||||
@ -62,16 +78,27 @@ public class HumanMahjongPlayer
|
|||||||
if (input == "q") return ("pass", null);
|
if (input == "q") return ("pass", null);
|
||||||
if (input == "h" && winOpt != null) return ("win", null);
|
if (input == "h" && winOpt != null) return ("win", null);
|
||||||
if (input == "p" && pungOpt != null) return ("pung", null);
|
if (input == "p" && pungOpt != null) return ("pung", null);
|
||||||
if (input == "k" && kongOpt != null) return (kongOpt.Action, null);
|
if (input == "k" && kongOpts.Count > 0) return (kongOpts[0].Action, null);
|
||||||
if (input == "c" && chiOpt != null) return ("chi", null);
|
if (input == "c" && chiOpt != null) return ("chi", null);
|
||||||
|
|
||||||
// Discard by index
|
|
||||||
if (int.TryParse(input, out int idx) && idx >= 1 && idx <= sorted.Count)
|
if (int.TryParse(input, out int idx) && idx >= 1 && idx <= sorted.Count)
|
||||||
{
|
|
||||||
return ("discard", sorted[idx - 1]);
|
return ("discard", sorted[idx - 1]);
|
||||||
}
|
|
||||||
|
|
||||||
Console.WriteLine("无效输入,请重试");
|
Console.WriteLine(" 无效输入,请重试");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private bool CheckTing(MahjongGameState state)
|
||||||
|
{
|
||||||
|
var hand = state.Hands.GetValueOrDefault(Name, new List<int>());
|
||||||
|
// Check all possible tiles for win potential
|
||||||
|
var allTiles = MahjongTile.AllTiles(false, false);
|
||||||
|
foreach (var tile in allTiles)
|
||||||
|
{
|
||||||
|
if (hand.Count(t => t == tile) >= 4) continue;
|
||||||
|
var r = _solver.CheckWin(hand, newTile: tile);
|
||||||
|
if (r != null && r.IsWin) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -26,12 +26,13 @@ public class MahjongRoom
|
|||||||
_rules = rules;
|
_rules = rules;
|
||||||
_autoMode = autoMode;
|
_autoMode = autoMode;
|
||||||
_ais = playerNames.Select((n, i) => new RandomMahjongAI(n, new Random(i * 7919))).ToList();
|
_ais = playerNames.Select((n, i) => new RandomMahjongAI(n, new Random(i * 7919))).ToList();
|
||||||
if (humanPlayer != null)
|
|
||||||
_human = new HumanMahjongPlayer(humanPlayer);
|
|
||||||
|
|
||||||
var fanConfig = BuildFanConfig();
|
var fanConfig = BuildFanConfig();
|
||||||
_solver = new MeldsSolver(fanConfig);
|
_solver = new MeldsSolver(fanConfig);
|
||||||
|
|
||||||
|
if (humanPlayer != null)
|
||||||
|
_human = new HumanMahjongPlayer(humanPlayer, _solver);
|
||||||
|
|
||||||
var phases = BuildPhases();
|
var phases = BuildPhases();
|
||||||
var (wc, r258) = GetWinParams();
|
var (wc, r258) = GetWinParams();
|
||||||
_phaseMachine = new MahjongPhaseMachine(phases, _solver, wildcardCount: wc, require258: r258);
|
_phaseMachine = new MahjongPhaseMachine(phases, _solver, wildcardCount: wc, require258: r258);
|
||||||
|
|||||||
@ -134,7 +134,9 @@ public class MahjongPhaseMachine
|
|||||||
|
|
||||||
private bool CanAnKong(MahjongGameState state, string playerId)
|
private bool CanAnKong(MahjongGameState state, string playerId)
|
||||||
{
|
{
|
||||||
return false; // Simplified: AI decides during draw phase
|
// Check if player has 4 identical tiles in hand (暗杠)
|
||||||
|
var hand = state.Hands[playerId];
|
||||||
|
return hand.GroupBy(t => t).Any(g => g.Count() >= 4 && !MahjongTile.IsWildcard(g.Key));
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool CanBuKong(MahjongGameState state, string playerId)
|
private bool CanBuKong(MahjongGameState state, string playerId)
|
||||||
|
|||||||
Reference in New Issue
Block a user