feat: 精分独立计分模式(independent_bonus)

引擎原算法: (底分+精分)×倍数 → 精分被庄家/自摸放大
南昌实际: 底分×倍数 + 精分 → 精分不参与倍数

新增DSL字段 wildcard_rules.scoring.independent_bonus:
- false(默认): 精分混入底分(现有行为,向后兼容)
- true(南昌): 精分独立加在倍数之后

代码: ScoreEngine.Settle → multiplierPart(底分×倍数) + addPart(精分)
自摸: 精分只加给赢家一次(不计入倍数)
点炮: 精分计入pay总额(放炮者全额支付)
This commit is contained in:
xiaoou
2026-07-05 01:37:57 +08:00
parent 0c489b48c9
commit d5329e5b0c
4 changed files with 21 additions and 9 deletions

View File

@ -319,7 +319,11 @@ public class WildcardConfig
public WildcardScoringConfig? Scoring { get; set; }
public string FanCalculationPolicy { get; set; } = "optimal";
}
public class WildcardScoringConfig { public int PerWildcardInWin { get; set; } }
public class WildcardScoringConfig
{
public int PerWildcardInWin { get; set; }
public bool IndependentBonus { get; set; } = false; // true=精分不参与庄家/自摸倍数(南昌麻将)
}
public class WinConditionConfig
{
[YamlMember(Alias = "pair_must_be_258")]

View File

@ -49,6 +49,7 @@ public class MahjongRoom
MaxCap = _rules.Scoring.MaxCap,
FanStacking = _rules.FanStacking,
PerWildcardBonus = _rules.WildcardRules?.Scoring?.PerWildcardInWin ?? 0,
WildcardIndependent = _rules.WildcardRules?.Scoring?.IndependentBonus ?? false,
JiHuSelfDrawOnly = _rules.WinRule?.JiHuSelfDrawOnly ?? false,
FlowerNormalScore = _rules.FlowerRules?.Scoring?.Normal ?? 0,
FlowerMatchScore = _rules.FlowerRules?.Scoring?.Matching?.Value ?? 0,

View File

@ -9,6 +9,7 @@ public class ScoringConfig
public int MaxCap { get; set; } = int.MaxValue;
public string FanStacking { get; set; } = "add";
public int PerWildcardBonus { get; set; } = 0;
public bool WildcardIndependent { get; set; } = false; // 精分不参与倍数(南昌麻将)
public bool JiHuSelfDrawOnly { get; set; } = false;
public int FlowerNormalScore { get; set; } = 0;
public int FlowerMatchScore { get; set; } = 0;
@ -39,6 +40,7 @@ public class MahjongScoreEngine
return;
int baseFan = ComputeTotalFans(result.Fans);
int wildcardBonus = 0;
// Wildcard bonus: count ALL wildcards in winner's hand (not just gap-fill)
if (_config.PerWildcardBonus > 0)
@ -50,16 +52,15 @@ public class MahjongScoreEngine
foreach (var m in exposed)
totalWildInHand += state.Wildcards.CountWildcards(m.Tiles);
if (totalWildInHand > 0)
baseFan += totalWildInHand * _config.PerWildcardBonus;
wildcardBonus = totalWildInHand * _config.PerWildcardBonus;
}
// Flower bonus: e.g. Guangdong 每花牌+1分 + 配对花牌额外分
// Flower bonus
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)
{
@ -72,29 +73,34 @@ public class MahjongScoreEngine
baseFan = Math.Min(baseFan, _config.MaxCap);
// Apply self-draw/discard multipliers to baseFan (not wildcardBonus when independent)
int multiplierPart = baseFan;
int addPart = _config.WildcardIndependent ? wildcardBonus : 0;
if (!_config.WildcardIndependent)
multiplierPart += wildcardBonus; // classic: wildcardBonus included in multiplier
if (isSelfDraw)
{
int perPlayerPay = baseFan * _config.SelfDrawMultiplier;
int perPlayerPay = multiplierPart * _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);
}
state.Scores[winner] = state.Scores.GetValueOrDefault(winner) + addPart;
}
else
{
if (state.LastDiscardPlayer != null && state.LastDiscardPlayer != winner)
{
int discardPay = baseFan * _config.DiscardWinMultiplier;
// 庄家赢了或庄家放炮→加倍
int discardPay = multiplierPart * _config.DiscardWinMultiplier;
bool dealerInvolved = (state.Dealer == winner || state.Dealer == state.LastDiscardPlayer);
int mul = dealerInvolved ? _config.DealerMultiplier : 1;
int pay = discardPay * mul;
int pay = discardPay * mul + addPart; // 精分也要付
state.Scores[state.LastDiscardPlayer] = (state.Scores.GetValueOrDefault(state.LastDiscardPlayer) - pay);
state.Scores[winner] = (state.Scores.GetValueOrDefault(winner) + pay);
}

View File

@ -35,6 +35,7 @@ wildcard_rules:
fan_calculation_policy: optimal
scoring:
per_wildcard_in_win: 2
independent_bonus: true
fan_types:
# 基础番型