namespace RuleEngine.AI; using RuleEngine.Core; using RuleEngine.Phase; using RuleEngine.Patterns; /// /// Human player — reads commands from console stdin. /// Commands: h=hu, p=pung, k=kong (an_kong/ming_kong/bu_kong), c=chi, 1-N=discard by index, q=quit /// public class HumanMahjongPlayer { public string Name { get; } private readonly MeldsSolver _solver; private readonly bool _includeHonors; public HumanMahjongPlayer(string name, MeldsSolver solver, bool includeHonors = false) { Name = name; _solver = solver; _includeHonors = includeHonors; } public (string action, int? tile) Decide(List legalActions, MahjongGameState state) { var hand = state.Hands.GetValueOrDefault(Name, new List()); var sorted = hand.OrderBy(t => t).ToList(); int? drawnTile = state.LastDrawnTile; int drawnIdx = drawnTile.HasValue ? sorted.IndexOf(drawnTile.Value) : -1; while (true) { Console.WriteLine(); Console.WriteLine($"=== {Name} 的回合 ==="); Console.WriteLine($" 牌墙: {state.Deck.Count}张 | 弃牌堆: {state.DiscardPool.Count}张"); // Show wildcard info var wcTiles = state.Wildcards.ConfiguredWildcards; if (wcTiles.Count > 0) { var wcDisplay = string.Join(" ", wcTiles.Select(t => MahjongTile.ToString(t, state.Wildcards))); if (state.Wildcards.RevealedTile.HasValue) Console.WriteLine($" 🀫 精: {wcDisplay} (翻牌: {MahjongTile.ToString(state.Wildcards.RevealedTile.Value)})"); else Console.WriteLine($" 🀫 宝牌(癞子): {wcDisplay}"); } // Show opponent summary Console.Write(" 对手: "); var parts = new List(); foreach (var op in state.PlayerOrder) { if (op == Name || !state.AlivePlayers.Contains(op)) continue; var opHand = state.Hands[op]; var opExposed = ""; if (state.Exposed.TryGetValue(op, out var melds) && melds.Count > 0) opExposed = $" 已{melds.Count}副"; var opDiscards = ""; if (state.DiscardHistory.TryGetValue(op, out var dh) && dh.Count > 0) opDiscards = $" 出:{string.Join("", dh.TakeLast(5).Select(MahjongTile.ToString))}"; parts.Add($"{op}:{opHand.Count}张{opExposed}{opDiscards}"); } Console.WriteLine(string.Join(" | ", parts)); // 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) Console.WriteLine($" 🀫 刚刚摸到: {MahjongTile.ToString(drawnTile.Value)}"); Console.WriteLine($" 手牌 ({hand.Count}张):"); for (int i = 0; i < sorted.Count; i++) { bool isDrawn = (i == drawnIdx); Console.WriteLine($" [{i + 1}] {(isDrawn ? "→ " : " ")}{MahjongTile.ToString(sorted[i])}{(isDrawn ? " ← 新摸" : "")}"); } Console.WriteLine(); // Show available actions 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(); // Check if we're one tile away from winning (ting) bool isTing = CheckTing(state); Console.Write(" 可选操作: "); var ops = new List(); 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 (winOpt != null) ops.Add("h✨(胡!)"); if (isTing) ops.Add("🀄听牌!"); ops.Add("q(退出)"); Console.WriteLine(string.Join(" ", ops)); Console.Write("> "); string? input = Console.ReadLine()?.Trim().ToLower(); if (string.IsNullOrEmpty(input)) continue; if (input == "q") return ("pass", null); 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 (int.TryParse(input, out int idx) && idx >= 1 && idx <= sorted.Count) return ("discard", sorted[idx - 1]); Console.WriteLine(" 无效输入,请重试"); } } private bool CheckTing(MahjongGameState state) { var hand = state.Hands.GetValueOrDefault(Name, new List()); // Combine with exposed melds var fullHand = new List(hand); if (state.Exposed.TryGetValue(Name, out var melds)) foreach (var m in melds) fullHand.AddRange(m.Tiles.Where(t => t != -1)); // Check all possible tiles for win potential var allTiles = MahjongTile.AllTiles(_includeHonors, false); int wc = state.Wildcards.CountWildcards(fullHand); foreach (var tile in allTiles) { if (hand.Count(t => t == tile) >= 4) continue; var r = _solver.CheckWin(fullHand, newTile: tile, wildcardCount: wc); if (r != null && r.IsWin) return true; } return false; } }