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个通过
90 lines
2.8 KiB
C#
90 lines
2.8 KiB
C#
namespace RuleEngine.Scoring;
|
|
|
|
using RuleEngine.Core;
|
|
using RuleEngine.Patterns;
|
|
|
|
public class ScoringConfig
|
|
{
|
|
public string Mode { get; set; } = "fan_table";
|
|
public int MaxCap { get; set; } = int.MaxValue;
|
|
}
|
|
|
|
public class MahjongScoreEngine
|
|
{
|
|
private readonly ScoringConfig _config;
|
|
private readonly MeldsSolver _solver;
|
|
|
|
public MahjongScoreEngine(ScoringConfig config, MeldsSolver solver)
|
|
{
|
|
_config = config;
|
|
_solver = solver;
|
|
}
|
|
|
|
public void Settle(MahjongGameState state, string winner, MeldsResult result, bool isSelfDraw)
|
|
{
|
|
int baseFan = result.Fans.Sum(f => FanValue(f));
|
|
baseFan = Math.Min(baseFan, _config.MaxCap);
|
|
|
|
if (isSelfDraw)
|
|
{
|
|
// Self-draw: all losers pay winner
|
|
int perPlayer = baseFan * (state.HuPlayers.Contains(winner) ? 1 : 1);
|
|
foreach (var p in state.AlivePlayers)
|
|
{
|
|
if (p == winner) continue;
|
|
state.Scores[p] = (state.Scores.GetValueOrDefault(p) - baseFan);
|
|
state.Scores[winner] = (state.Scores.GetValueOrDefault(winner) + baseFan);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Discard win: discarder pays
|
|
if (state.LastDiscardPlayer != null && state.LastDiscardPlayer != winner)
|
|
{
|
|
state.Scores[state.LastDiscardPlayer] = (state.Scores.GetValueOrDefault(state.LastDiscardPlayer) - baseFan * 3);
|
|
state.Scores[winner] = (state.Scores.GetValueOrDefault(winner) + baseFan * 3);
|
|
}
|
|
}
|
|
}
|
|
|
|
public void CheckFinish(MahjongGameState state, MeldsSolver solver,
|
|
bool require258Pair, int wildcardCount)
|
|
{
|
|
// Check each alive player for ting
|
|
foreach (var p in state.AlivePlayers)
|
|
{
|
|
var hand = state.Hands[p];
|
|
if (hand.Count <= 14)
|
|
{
|
|
// Simple: check if removing any one tile leads to win
|
|
bool hasTing = false;
|
|
foreach (var t in hand)
|
|
{
|
|
var testHand = new List<int>(hand);
|
|
testHand.Remove(t);
|
|
var r = solver.CheckWin(testHand, wildcardCount: wildcardCount, require258Pair: require258Pair);
|
|
if (r != null && r.IsWin)
|
|
{
|
|
hasTing = true;
|
|
break;
|
|
}
|
|
}
|
|
if (hasTing)
|
|
state.AddEvent("ting_checked", p, null, "听牌");
|
|
else
|
|
state.AddEvent("ting_failed", p, null, "不听牌 — 罚分");
|
|
}
|
|
}
|
|
}
|
|
|
|
private int FanValue(string fan) => fan switch
|
|
{
|
|
"清一色" => 4,
|
|
"对对胡" => 2,
|
|
"暗七对" => 4,
|
|
"带幺九" => 2,
|
|
"十三幺" => 88,
|
|
_ => 1
|
|
};
|
|
}
|