fix: Code Review — CheckTing完全断裂 + 座位顺序 + FanValue硬编码

🔴 Critical: CheckTing逻辑完全错误
- 旧: testHand.RemoveAt(i) → 13张 → CheckWin守卫拒绝(≠14) → 听牌检测永远false
- 新: 遍历所有可摸牌(27-34种), CheckWin(hand, newTile:tile) → 正确检测听牌
- 新增2个听牌测试

🟡 Medium: HandleDiscardReactions 同优先级顺序
- 旧: OrderByDescending取第一个,不保证座位顺序
- 新: 按座位顺序遍历,高优先级覆盖,同优先级先到先得

🟡 Medium: ScoreEngine.FanValue 硬编码番值
- 旧: switch expression 仅5种番型
- 新: 优先从 _fanConfig(DSL)读取,fallback到硬编码

🟢 Low: 删除硬编码的258将事件(非武汉也输出)
This commit is contained in:
xiaoou
2026-07-04 11:33:13 +08:00
parent c902482d4b
commit 55447d6b05
3 changed files with 87 additions and 28 deletions

View File

@ -35,7 +35,7 @@ public class MahjongRoom
{
Mode = _rules.Scoring.Mode,
MaxCap = _rules.Scoring.MaxCap
}, _solver);
}, _solver, fanConfig);
// Init state
State.PlayerOrder = playerNames.ToList();
@ -233,23 +233,33 @@ public class MahjongRoom
if (!State.AlivePlayers.Contains(p)) continue;
if (p == discarder) continue;
int sameCount = State.Hands[p].Count(t => t == discardTile);
string? claim = null;
int priority = 0;
// Check win first (highest priority)
var handWithTile = new List<int>(State.Hands[p]) { discardTile };
var winResult = _solver.CheckWin(handWithTile);
if (winResult != null && winResult.IsWin)
claims.Add((p, 4, "win"));
int sameCount = State.Hands[p].Count(t => t == discardTile);
if (sameCount >= 3)
claims.Add((p, 3, "ming_kong"));
claim = "win";
else if (sameCount >= 3)
claim = "ming_kong";
else if (sameCount >= 2)
claims.Add((p, 2, "pung"));
claim = "pung";
if (claim == null) continue;
priority = claim switch { "win" => 4, "ming_kong" => 3, "pung" => 2, _ => 0 };
// For same priority, closer seat wins (first claim in order)
// For higher priority, it overrides all previous claims
if (claims.Count == 0 || priority > claims[0].priority)
claims.Add((p, priority, claim));
// Same priority: first claimant wins (closer to discarder in seat order)
// Different (lower) priority: ignored — higher priority already claimed
}
if (claims.Count == 0) return false;
// Highest-priority claim wins (for simplicity: first highest wins)
var best = claims.OrderByDescending(c => c.priority).First();
var best = claims[0];
string bp = best.player;
if (best.action == "win")
@ -376,25 +386,33 @@ public class MahjongRoom
bool hasWildcard = _rules.WildcardRules != null;
int wildcardCount = hasWildcard ? _rules.Deck.WildcardCount : 0;
bool require258 = _rules.WinCondition?.PairMustBe258 ?? false;
// All possible tiles that could be drawn (numbered 1-9 across 3 suits + 7 honors)
var allPossibleTiles = MahjongTile.AllTiles(
includeHonors: _rules.Deck.IncludeHonors,
includeFlowers: false);
foreach (var p in State.AlivePlayers)
{
bool ting = false;
var hand = State.Hands[p];
for (int i = 0; i < hand.Count; i++)
// For each possible tile we could draw, check if hand + tile is a win
foreach (var tile in allPossibleTiles)
{
var testHand = new List<int>(hand);
testHand.RemoveAt(i);
var r = _solver.CheckWin(testHand, wildcardCount: wildcardCount, require258Pair: require258);
// Count how many of this tile exist (4 max per tile type)
int alreadyHave = hand.Count(t => t == tile);
if (alreadyHave >= 4) continue; // can't draw more of this tile
var r = _solver.CheckWin(hand, newTile: tile,
wildcardCount: wildcardCount, require258Pair: require258);
if (r != null && r.IsWin) { ting = true; break; }
}
if (ting)
State.AddEvent("ting_checked", p, null, "✓ 听牌");
else
{
State.AddEvent("ting_failed", p, null, "❌ 未听牌");
State.AddEvent("pair_validated_258", p, null, "258将检查");
}
}
}