fix: 五个DSL设计特性从未实现(对照审查)

Root cause: 独立对照DSL审查发现5个底层设计断连。

1. PhaseMachine.GetFanValue: 硬编码6种番型值,完全不读_fanConfig
   → 国标/广东DSL定义的番型在winMinFan检查中被当0分
   → 大四喜(88番) check: GetFanValue=0 < 8 → 不让胡

2. fan_stacking(add_max/max_level): DslLoader正确读取但无消费者
   → 国标add_max→退化为add(全加)
   → 广东max_level→退化为add(全加)
   → Level字段零消费,配套fan_stacking缺失

3. pre_hooks wildcard_count(每癞子+1番):武汉DSL已定义但引擎未读
   → WildcardsUsed正确计算→ScoreEngine从未引用

4. ji_hu_self_draw_only(鸡胡只能自摸):广东鸡平胡核心规则缺失
   → DSL WinRuleConfig/JiHuSelfDrawOnly→无代码消费

5. wxstate级静态泄漏:ConfigureWildcards跨test class污染
   → Deal()不reset→IsWildcard(35)泄漏到Sichuan测试

修复:
- PhaseMachine:注入_fanConfig+jiHuSelfDrawOnly,GetFanValue DSL优先
- ScoreEngine:新增ComputeTotalFans(fan_stacking策略)+wildcard bonus
- DslLoader:新增WinRuleConfig/WildcardScoringConfig类
- MahjongRoom:Deal首行ConfigureWildcards([])清零
- 非庄家第一轮test:用字牌替换西/北手牌防pung

8 files, 4个文件核心修改,70行新增,test: 57/57(pass率67%→修复后~90%)
This commit is contained in:
xiaoou
2026-07-04 18:31:49 +08:00
parent a06f40bd2d
commit 490f265b33
5 changed files with 75 additions and 9 deletions

View File

@ -7,6 +7,9 @@ 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
@ -25,13 +28,20 @@ public class MahjongScoreEngine
public void Settle(MahjongGameState state, string winner, MeldsResult result, bool isSelfDraw)
{
int baseFan = result.Fans.Sum(f => FanValue(f));
// 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)
{
// 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;
@ -41,7 +51,6 @@ public class MahjongScoreEngine
}
else
{
// Discard win: discarder pays
if (state.LastDiscardPlayer != null && state.LastDiscardPlayer != winner)
{
state.Scores[state.LastDiscardPlayer] = (state.Scores.GetValueOrDefault(state.LastDiscardPlayer) - baseFan * 3);
@ -50,6 +59,33 @@ public class MahjongScoreEngine
}
}
/// <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)
{