Files
card-game-engine/RuleEngine/Scoring/ScoreEngine.cs
xiaoou 978fcd87dd fix: 国标fan_types缺常见番型→0%胡牌率(补全28种)
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种够覆盖常见组合
2026-07-04 19:16:16 +08:00

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