Files
card-game-engine/Demo/Program.cs
xiaoou d83494e669 feat: demo显示听牌状态+可胡张数
引擎: MahjongRoom.GetTingTiles(player) 返回所有可胡的unique tile
Demo: 每回合后显示'听牌: X: 🀄听N张(tile1 tile2...) | Y: 未听...'
包含已胡/副露中的tile计数,兼容wildcard和258规则

示例: 听牌: AI-东: 🀄听 1张(中) | AI-南: 未听 | AI-西: 未听 | AI-北: 未听
2026-07-05 00:20:53 +08:00

303 lines
11 KiB
C#

using RuleEngine;
using RuleEngine.Core;
using RuleEngine.Dsl;
// Parse arguments
bool autoMode = args.Contains("--auto");
bool humanMode = args.Contains("--human");
bool checkOnly = args.Contains("--check");
string dslPath = "dsl-examples/xuezhandaodi.yaml";
int dslIdx = Array.IndexOf(args, "--dsl");
if (dslIdx >= 0 && dslIdx + 1 < args.Length)
dslPath = $"dsl-examples/{args[dslIdx + 1]}.yaml";
int count = 1;
int countIdx = Array.IndexOf(args, "--count");
if (countIdx >= 0 && countIdx + 1 < args.Length)
int.TryParse(args[countIdx + 1], out count);
// Init engine
var caps = new CapabilityRegistry();
caps.Register("meldsolver.standard_win");
caps.Register("meldsolver.seven_pairs");
caps.Register("meldsolver.thirteen_orphans");
caps.Register("meldsolver.all_orphans");
caps.Register("meldsolver.double_dragon");
caps.Register("meldsolver.wildcard");
caps.Register("deck.flower_cards");
caps.Register("phase.mahjong_turn");
caps.Register("phase.parallel_elimination");
caps.Register("phase.priority_arbitration");
caps.Register("scoring.fan_exclusion");
caps.Register("scoring.pre_hooks");
caps.Register("deck.generator_mahjong");
var loader = new DslLoader(caps);
MahjongDslRoot rules;
try
{
rules = loader.Load(dslPath);
var (r, m, pct) = caps.ComputeCoverage(rules.Requires);
string status = pct == 100 ? "✅" : "⚠";
Console.WriteLine($"{status} 能力注册: {m}/{r} ({pct}%) — {rules.Game.Name}");
// Rule-level compliance check (finer granularity than capability check)
var warnings = caps.ValidateRuleCompliance(rules);
if (warnings.Count > 0)
{
Console.WriteLine($" ⚠ {warnings.Count} 条规则警告:");
foreach (var w in warnings)
Console.WriteLine($"{w}");
}
else
{
Console.WriteLine($" ✅ 规则合规检查通过");
}
if (pct < 100)
{
var missing = rules.Requires.Where(req => !caps.Has(req));
foreach (var miss in missing)
Console.WriteLine($" ❌ {miss} — 引擎尚未实现,相关规则将被跳过");
}
Console.WriteLine();
}
catch (Exception ex)
{
Console.WriteLine($"❌ DSL 加载失败: {ex.Message}");
return 1;
}
if (checkOnly)
{
Console.WriteLine($"\n✅ 仅验证模式 — 未运行牌局\n 注意: 能力注册≠完整规则覆盖, 需逐条验证玩法规则");
return 0;
}
if (autoMode)
{
// Batch mode: run N rounds silently
Console.WriteLine($"=== 麻将规则引擎 Demo — {rules.Game.Name} === (自动模式 {count}局)");
int wins = 0, errors = 0;
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
for (int i = 0; i < count; i++)
{
try
{
var room = new MahjongRoom(rules, new[] { "AI-东", "AI-南", "AI-西", "AI-北" }, autoMode: true,
humanPlayer: humanMode ? "AI-东" : null);
room.Run();
if (room.State.HuPlayers.Count > 0)
wins++;
var elapsed = stopwatch.ElapsedMilliseconds;
if ((i + 1) % 100 == 0 || i == count - 1)
Console.WriteLine($"[局 {i + 1}/{count}] ✅ {(room.State.HuPlayers.Count > 0 ? room.State.HuPlayers[0] + "" : "")} | 总耗时 {(elapsed / 1000.0):F1}s");
}
catch (Exception ex)
{
errors++;
Console.WriteLine($"[局 {i + 1}/{count}] ❌ 错误: {ex.Message}");
}
}
stopwatch.Stop();
Console.WriteLine("\n========================================");
Console.WriteLine($"统计: 总对局 {count} | 胡牌率 {(double)wins / count:P1} | 出错 {errors} | 平均 {(double)stopwatch.ElapsedMilliseconds / count:F0}ms/局");
}
else
{
// Interactive mode: step-by-step with key-press advancement
Console.WriteLine($"=== 麻将规则引擎 Demo — {rules.Game.Name} === (交互模式,按 Enter 继续下一步)");
Console.WriteLine();
var room = new MahjongRoom(rules, new[] { "AI-东", "AI-南", "AI-西", "AI-北" }, autoMode: false,
humanPlayer: humanMode ? "AI-东" : null);
room.Deal();
// Show dealt hands
RenderDeal(room);
Console.Write("\n[按 Enter 开始对局]");
Console.ReadLine();
// Step through each turn
int turnNumber = 0;
while (!room.IsFinished)
{
turnNumber++;
var events = room.StepTurn();
// Skip deal phase transition
if (room.State.Phase == "deal") continue;
Console.WriteLine($"\n--- 第 {turnNumber} 回合 | 牌墙剩余 {room.State.Deck.Count} 张 ---");
// Show events for this turn
foreach (var evt in events)
{
switch (evt.Type)
{
case "draw":
Console.WriteLine($" [{evt.Player}] 摸牌: {MahjongTile.ToString(evt.Tile ?? 0)}");
break;
case "discard":
Console.WriteLine($" [{evt.Player}] 出牌: {MahjongTile.ToString(evt.Tile ?? 0)}");
break;
case "pung":
Console.WriteLine($" → {evt.Player}: 碰!{MahjongTile.ToString(evt.Tile ?? 0)}");
break;
case "ming_kong":
Console.WriteLine($" → {evt.Player}: 明杠!{MahjongTile.ToString(evt.Tile ?? 0)}");
break;
case "win":
Console.WriteLine($" ✅ {evt.Player}: 胡!{evt.Description}");
break;
case "deck_exhausted":
Console.WriteLine($" ⚠ 牌墙耗尽!");
break;
case "hua_zhu":
Console.WriteLine($" ⚠ {evt.Player}: 花猪(三色齐全)");
break;
case "hua_zhu_ok":
break;
case "ting_checked":
Console.WriteLine($" ✓ {evt.Player}: 听牌");
break;
case "ting_failed":
Console.WriteLine($" ✗ {evt.Player}: 未听牌");
break;
case "settle":
Console.WriteLine($" 💰 结算");
break;
}
}
// 听牌检测: 每回合后显示各玩家听牌状态
var tingInfo = new List<string>();
foreach (var p in room.State.AlivePlayers)
{
var tingTiles = room.GetTingTiles(p);
if (tingTiles.Count > 0)
{
var tileNames = tingTiles.Select(MahjongTile.ToString);
tingInfo.Add($"{p}: 🀄听 {tingTiles.Count}张({string.Join(" ", tileNames)})");
}
else
tingInfo.Add($"{p}: 未听");
}
Console.WriteLine($" 听牌: {string.Join(" | ", tingInfo)}");
// Show current hands
if (!room.IsFinished)
{
Console.WriteLine();
if (humanMode)
{
Console.WriteLine(" 对手状态:");
foreach (var p in room.State.PlayerOrder)
{
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)}";
}
var discardStr = "";
if (room.State.DiscardHistory.TryGetValue(p, out var discards) && discards.Count > 0)
{
var recent = discards.TakeLast(6).Select(MahjongTile.ToString);
discardStr = $" | 出: {string.Join(" ", recent)}";
}
Console.WriteLine($" {p}: {hand.Count}张手牌{exposedStr}{discardStr}");
}
}
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}张)");
}
}
// In human mode, don't wait for Enter — human interactions already pause
if (!humanMode)
{
Console.Write("\n[按 Enter 继续]");
Console.ReadLine();
}
}
}
// Final render
Console.WriteLine("\n══════════════════════════════════════");
RenderFinal(room);
}
return 0;
void RenderDeal(MahjongRoom room)
{
var state = room.State;
Console.WriteLine("═══ 发牌 ═══");
foreach (var p in state.PlayerOrder)
Console.WriteLine($" {p}{(p == state.Dealer ? " ()" : "")}: {state.Hands[p].Count}张");
Console.WriteLine($" 牌墙: {state.Deck.Count} 张");
// Show wildcard info if configured
var wc = state.Wildcards;
if (wc.ConfiguredWildcards.Count > 0)
{
var wcStr = string.Join(" ", wc.ConfiguredWildcards.Select(t => MahjongTile.ToString(t, wc)));
if (wc.RevealedTile.HasValue)
Console.WriteLine($" 🀫 精: {wcStr} (翻牌: {MahjongTile.ToString(wc.RevealedTile.Value)})");
else
Console.WriteLine($" 🀫 宝牌: {wcStr}");
}
Console.WriteLine($" (手牌已洗牌,按 Enter 查看牌局)");
}
void RenderFinal(MahjongRoom room)
{
var state = room.State;
Console.WriteLine("[最终状态]");
Console.WriteLine($" 游戏结束,牌墙剩余 {state.Deck.Count} 张");
Console.WriteLine();
Console.WriteLine(" 最终状态:");
foreach (var p in state.PlayerOrder)
{
var hand = state.Hands[p];
var sorted = string.Join(", ", hand.OrderBy(t => t).Select(MahjongTile.ToString));
var exposedStr = "";
if (state.Exposed.TryGetValue(p, out var melds) && melds.Count > 0)
{
var meldDescs = melds.Select(m =>
$"{m.Type}[{string.Join(",", m.Tiles.Select(MahjongTile.ToString))}]");
exposedStr = $" | 已碰/杠: {string.Join(" ", meldDescs)}";
}
Console.WriteLine($" {p}{(p == state.Dealer ? "()" : "")}: {sorted} ({hand.Count}张){exposedStr}");
}
if (state.HuPlayers.Count > 0)
{
Console.WriteLine();
Console.WriteLine($" 胡牌: {string.Join(", ", state.HuPlayers)}");
}
Console.WriteLine();
Console.WriteLine("[最终结算]");
foreach (var (p, score) in state.Scores)
Console.WriteLine($" {p}: {(score >= 0 ? "+" : "")}{score}分");
int total = state.Scores.Values.Sum();
Console.WriteLine($" 总分: {(total >= 0 ? "+" : "")}{total} {(total == 0 ? "" : " ")}");
Console.WriteLine("══════════════════════════════════════");
}