144 lines
6.3 KiB
C#
144 lines
6.3 KiB
C#
namespace RuleEngine.AI;
|
|
|
|
using RuleEngine.Core;
|
|
using RuleEngine.Phase;
|
|
using RuleEngine.Patterns;
|
|
|
|
/// <summary>
|
|
/// 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
|
|
/// </summary>
|
|
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<ActionOption> legalActions, MahjongGameState state)
|
|
{
|
|
var hand = state.Hands.GetValueOrDefault(Name, new List<int>());
|
|
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<string>();
|
|
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, state.Wildcards)}");
|
|
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], state.Wildcards)}{(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) — returns list of winning tiles
|
|
var tingTiles = GetTingTiles(state);
|
|
|
|
Console.Write(" 可选操作: ");
|
|
var ops = new List<string>();
|
|
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 (tingTiles.Count > 0)
|
|
{
|
|
var names = tingTiles.Select(t => MahjongTile.ToString(t, state.Wildcards));
|
|
ops.Add($"🀄听 {tingTiles.Count}张({string.Join(" ", names)})");
|
|
}
|
|
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 List<int> GetTingTiles(MahjongGameState state)
|
|
{
|
|
var result = new List<int>();
|
|
var hand = state.Hands.GetValueOrDefault(Name, new List<int>());
|
|
// Combine with exposed melds
|
|
var fullHand = new List<int>(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) result.Add(tile);
|
|
}
|
|
return result;
|
|
}
|
|
}
|