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 int FlowerNormalScore { get; set; } = 0; public int FlowerMatchScore { get; set; } = 0; public Dictionary>? FlowerMapping { get; set; } public int SelfDrawMultiplier { get; set; } = 1; public int DiscardWinMultiplier { get; set; } = 3; public int DealerMultiplier { get; set; } = 1; } public class MahjongScoreEngine { private readonly ScoringConfig _config; private readonly MeldsSolver _solver; private readonly Dictionary _fanConfig; public MahjongScoreEngine(ScoringConfig config, MeldsSolver solver, Dictionary 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: count ALL wildcards in winner's hand (not just gap-fill) if (_config.PerWildcardBonus > 0) { int totalWildInHand = 0; if (state.Hands.TryGetValue(winner, out var hand)) totalWildInHand += state.Wildcards.CountWildcards(hand); if (state.Exposed.TryGetValue(winner, out var exposed)) foreach (var m in exposed) totalWildInHand += state.Wildcards.CountWildcards(m.Tiles); if (totalWildInHand > 0) baseFan += totalWildInHand * _config.PerWildcardBonus; } // Flower bonus: e.g. Guangdong 每花牌+1分 + 配对花牌额外分 if (_config.FlowerNormalScore > 0 && state.FlowerPool.TryGetValue(winner, out var flowers) && flowers.Count > 0) { baseFan += flowers.Count * _config.FlowerNormalScore; if (_config.FlowerMatchScore > 0 && _config.FlowerMapping != null) { // Count matching position+season pairs var flowerNames = flowers.Select(f => FlowerName(f)).ToHashSet(); foreach (var (pos, names) in _config.FlowerMapping) { int matches = names.Count(n => flowerNames.Contains(n)); if (matches >= 2) baseFan += _config.FlowerMatchScore; } } } baseFan = Math.Min(baseFan, _config.MaxCap); if (isSelfDraw) { int perPlayerPay = baseFan * _config.SelfDrawMultiplier; bool dealerWon = (state.Dealer == winner); foreach (var p in state.AlivePlayers) { if (p == winner) continue; // 庄家赢→每家付倍数; 闲家赢→仅庄家付倍数 int mul = (dealerWon || p == state.Dealer) ? _config.DealerMultiplier : 1; int pay = perPlayerPay * mul; state.Scores[p] = (state.Scores.GetValueOrDefault(p) - pay); state.Scores[winner] = (state.Scores.GetValueOrDefault(winner) + pay); } } else { if (state.LastDiscardPlayer != null && state.LastDiscardPlayer != winner) { int discardPay = baseFan * _config.DiscardWinMultiplier; // 庄家赢了或庄家放炮→加倍 bool dealerInvolved = (state.Dealer == winner || state.Dealer == state.LastDiscardPlayer); int mul = dealerInvolved ? _config.DealerMultiplier : 1; int pay = discardPay * mul; state.Scores[state.LastDiscardPlayer] = (state.Scores.GetValueOrDefault(state.LastDiscardPlayer) - pay); state.Scores[winner] = (state.Scores.GetValueOrDefault(winner) + pay); } } } /// Compute total fan count applying stacking strategy from DSL. private int ComputeTotalFans(List 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(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; return Phase.MahjongPhaseMachine.DefaultFanValue(fan); } private static string FlowerName(int tile) => tile switch { 41 => "春", 42 => "夏", 43 => "秋", 44 => "冬", 45 => "梅", 46 => "兰", 47 => "竹", 48 => "菊", _ => "?" }; }