Files
card-game-engine/RuleEngine/Scoring/ScoreEngine.cs
xiaoou 3c6748bf06 [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个通过
2026-07-03 17:51:59 +08:00

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
};
}