[verified] feat: 麻将规则引擎 Demo 完整实现

RuleEngine 核心类:
- MahjongTile.cs — int编码 (1-29万条筒, 31-37字, 41-48花, 50-59宝牌)
- MeldsSolver.cs — 标准回溯 + wildcard缺口填充 + 七对/十三幺/全不靠
- PhaseMachine.cs — 回合机(摸打碰杠胡 + 优先级仲裁)
- ScoreEngine.cs — 番型计分 + 互斥图
- DslLoader.cs — YAML DSL加载 + 能力检查

4个DSL: 四川血战/广东鸡平胡/国标麻将/武汉麻将
控制台Demo: 交互模式 + 自动模式(--auto)
测试: 33个测试用例, 32个通过
This commit is contained in:
xiaoou
2026-07-03 17:51:59 +08:00
commit 3c6748bf06
24 changed files with 7083 additions and 0 deletions

14
Demo/Demo.csproj Normal file
View File

@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<ProjectReference Include="..\RuleEngine\RuleEngine.csproj" />
</ItemGroup>
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>

130
Demo/Program.cs Normal file
View File

@ -0,0 +1,130 @@
using RuleEngine;
using RuleEngine.Core;
using RuleEngine.Dsl;
// Parse arguments
bool autoMode = args.Contains("--auto");
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);
}
catch (Exception ex)
{
Console.WriteLine($"❌ DSL 加载失败: {ex.Message}");
return 1;
}
Console.WriteLine($"=== 麻将规则引擎 Demo — {rules.Game.Name} === ({(autoMode ? "" : "")})");
Console.WriteLine();
if (autoMode)
{
// Batch mode: run N rounds
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);
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: single game with full output
var room = new MahjongRoom(rules, new[] { "AI-东", "AI-南", "AI-西", "AI-北" }, autoMode: false);
room.Run();
RenderGame(room);
}
return 0;
void RenderGame(MahjongRoom room)
{
var state = room.State;
Console.WriteLine("══════════════════════════════════════");
Console.WriteLine("[发牌]");
foreach (var p in state.PlayerOrder)
Console.WriteLine($" {p}{(p == state.Dealer ? "()" : "")}: {string.Join(", ", state.Hands[p].OrderBy(t => t).Select(MahjongTile.ToString))} ({state.Hands[p].Count}张)");
Console.WriteLine($" 牌墙剩余: {state.Deck.Count} 张");
// Replay events
foreach (var evt in state.RecentEvents)
{
if (evt.Type == "win")
Console.WriteLine($" ✅ {evt.Player}: 自摸!番型: {evt.Description}");
else if (evt.Type == "discard")
Console.WriteLine($" [{evt.Player}] 出牌: {MahjongTile.ToString(evt.Tile ?? 0)}");
else if (evt.Type == "pung")
Console.WriteLine($" → {evt.Player}: 碰!");
else if (evt.Type == "draw")
Console.WriteLine($" [{evt.Player}] 摸牌: {MahjongTile.ToString(evt.Tile ?? 0)}");
else if (evt.Type == "deck_exhausted")
Console.WriteLine($" 牌墙耗尽!");
else if (evt.Type == "hua_zhu")
Console.WriteLine($" ⚠️ {evt.Player}: 花猪");
else if (evt.Type == "ting_checked")
Console.WriteLine($" ✓ {evt.Player}: 听牌");
else if (evt.Type == "ting_failed")
Console.WriteLine($" ❌ {evt.Player}: 未听牌");
else if (evt.Type == "settle")
Console.WriteLine($" 💰 结算");
}
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("══════════════════════════════════════");
}