Root cause: guobiao.yaml只列了11个高番型(≥24番),缺少Level 1-6的常见番型。 add_max每level只取最大值→缺中低番型拼不出8番门槛。 修复: - DSL补全:全大/全中/全小(12番),大于五/小于五(8番),碰碰和/混一色/五门齐/全求人(6番), 全带幺(4番),断幺九/平和/门前清(2番),缺一门/无字(1番) - ScoreEngine/PhaseMachine fallback加'碰碰和'(国标名称) - 随机AI 国标胡牌率: 0%→30%(纯运气,合理) 注: 完整国标有81种番型,Demo阶段补到28种够覆盖常见组合
138 lines
4.6 KiB
C#
138 lines
4.6 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 string FanStacking { get; set; } = "add";
|
|
public int PerWildcardBonus { get; set; } = 0;
|
|
public bool JiHuSelfDrawOnly { get; set; } = false;
|
|
}
|
|
|
|
public class MahjongScoreEngine
|
|
{
|
|
private readonly ScoringConfig _config;
|
|
private readonly MeldsSolver _solver;
|
|
private readonly Dictionary<string, FanConfig> _fanConfig;
|
|
|
|
public MahjongScoreEngine(ScoringConfig config, MeldsSolver solver,
|
|
Dictionary<string, FanConfig> fanConfig)
|
|
{
|
|
_config = config;
|
|
_solver = solver;
|
|
_fanConfig = fanConfig;
|
|
}
|
|
|
|
public void Settle(MahjongGameState state, string winner, MeldsResult result, bool isSelfDraw)
|
|
{
|
|
// JiHu self-draw only: 鸡胡不能吃胡
|
|
if (_config.JiHuSelfDrawOnly && !isSelfDraw && result.Fans.Contains("鸡胡"))
|
|
return;
|
|
|
|
int baseFan = ComputeTotalFans(result.Fans);
|
|
|
|
// Wildcard bonus: e.g. Wuhan 每癞子+1番
|
|
if (_config.PerWildcardBonus > 0 && result.WildcardsUsed > 0)
|
|
baseFan += result.WildcardsUsed * _config.PerWildcardBonus;
|
|
|
|
baseFan = Math.Min(baseFan, _config.MaxCap);
|
|
|
|
if (isSelfDraw)
|
|
{
|
|
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
|
|
{
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>Compute total fan count applying stacking strategy from DSL.</summary>
|
|
private int ComputeTotalFans(List<string> fans)
|
|
{
|
|
switch (_config.FanStacking)
|
|
{
|
|
case "max_level":
|
|
{
|
|
// Only count fans from the highest level present
|
|
int maxLevel = fans.Max(f =>
|
|
_fanConfig.TryGetValue(f, out var fc) ? fc.Level : 0);
|
|
return fans
|
|
.Where(f => (_fanConfig.TryGetValue(f, out var fc) ? fc.Level : 0) == maxLevel)
|
|
.Sum(f => FanValue(f));
|
|
}
|
|
case "add_max":
|
|
{
|
|
// Sum across levels, taking max fan per level
|
|
return fans
|
|
.GroupBy(f => _fanConfig.TryGetValue(f, out var fc) ? fc.Level : 0)
|
|
.Select(g => g.Max(f => FanValue(f)))
|
|
.Sum();
|
|
}
|
|
default: // "add"
|
|
return fans.Sum(f => FanValue(f));
|
|
}
|
|
}
|
|
|
|
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)
|
|
{
|
|
if (_fanConfig.TryGetValue(fan, out var def))
|
|
return def.BaseFan;
|
|
// Hardcoded values for well-known fans not in every DSL
|
|
return fan switch
|
|
{
|
|
"清一色" => 24,
|
|
"混一色" => 6,
|
|
"对对胡" => 2,
|
|
"碰碰和" => 6,
|
|
"暗七对" => 8,
|
|
"七对" => 8,
|
|
"带幺九" => 2,
|
|
"十三幺" => 88,
|
|
_ => 0
|
|
};
|
|
}
|
|
}
|