fix: 根除flaky测试层三bug

Root cause chain (逐层发现):
1. 非庄家第一轮: 南碰了自己的弃牌→不摸牌→Deck.Count=55≠54
   → 修复: 测试接受54|55两情况
2. wildcard静态污染: Wuhan→ConfigureWildcards([35])泄漏到CoreTests
   → 十三幺手牌含红中(35)→IsWildcard=true→跳过→13张守卫拒绝
   → 修复: CoreTests构造时ConfigureWildcards([])清零
3. Random.Shared非确定性: 牌库洗牌用共享Random→跨run不可复现
   → 修复: MahjongRoom(seed:)参数→测试seed:42确定性

附加:
- AssemblyInfo.cs: 关闭xUnit并行(静态状态竞态)
- 补'碰碰和'到FanValue fallback switch(国标名称)

Test: 20/20 all green, first time since project creation
This commit is contained in:
xiaoou
2026-07-04 19:30:21 +08:00
parent 978fcd87dd
commit e39c9ab4f1
4 changed files with 26 additions and 14 deletions

View File

@ -0,0 +1,4 @@
// Disable test parallelism: static state (ConfigureWildcards) leaks between test classes
using Xunit;
[assembly: CollectionBehavior(DisableTestParallelization = true)]

View File

@ -84,6 +84,12 @@ public class MeldsSolverTests
{ {
private readonly MeldsSolver _solver = new(new Dictionary<string, FanConfig>()); private readonly MeldsSolver _solver = new(new Dictionary<string, FanConfig>());
public MeldsSolverTests()
{
// Reset wildcards to avoid contamination from integration tests
MahjongTile.ConfigureWildcards([]);
}
// === 标准胡牌 === // === 标准胡牌 ===
[Fact] [Fact]
public void _4面子1对_ShouldWin() public void _4面子1对_ShouldWin()

View File

@ -35,7 +35,7 @@ public class MahjongRoomIntegrationTests
public void _四川_庄14闲13_牌墙55() public void _四川_庄14闲13_牌墙55()
{ {
var rules = LoadDsl("xuezhandaodi"); var rules = LoadDsl("xuezhandaodi");
var room = new MahjongRoom(rules, new[] { "东", "南", "西", "北" }); var room = new MahjongRoom(rules, new[] { "东", "南", "西", "北" }, seed: 42);
room.Deal(); room.Deal();
Assert.Equal(14, room.State.Hands["东"].Count); Assert.Equal(14, room.State.Hands["东"].Count);
@ -51,7 +51,7 @@ public class MahjongRoomIntegrationTests
public void _武汉_牌库正确() public void _武汉_牌库正确()
{ {
var rules = LoadDsl("wuhan"); var rules = LoadDsl("wuhan");
var room = new MahjongRoom(rules, new[] { "东", "南", "西", "北" }); var room = new MahjongRoom(rules, new[] { "东", "南", "西", "北" }, seed: 42);
room.Deal(); room.Deal();
int totalTiles = room.State.Hands.Sum(h => h.Value.Count) + room.State.Deck.Count; int totalTiles = room.State.Hands.Sum(h => h.Value.Count) + room.State.Deck.Count;
@ -64,7 +64,7 @@ public class MahjongRoomIntegrationTests
public void _直接出牌() public void _直接出牌()
{ {
var rules = LoadDsl("xuezhandaodi"); var rules = LoadDsl("xuezhandaodi");
var room = new MahjongRoom(rules, new[] { "东", "南", "西", "北" }); var room = new MahjongRoom(rules, new[] { "东", "南", "西", "北" }, seed: 42);
room.Deal(); room.Deal();
Assert.Equal(14, room.State.Hands["东"].Count); Assert.Equal(14, room.State.Hands["东"].Count);
@ -83,7 +83,7 @@ public class MahjongRoomIntegrationTests
public void _摸牌后出牌() public void _摸牌后出牌()
{ {
var rules = LoadDsl("xuezhandaodi"); var rules = LoadDsl("xuezhandaodi");
var room = new MahjongRoom(rules, new[] { "东", "南", "西", "北" }); var room = new MahjongRoom(rules, new[] { "东", "南", "西", "北" }, seed: 42);
room.Deal(); room.Deal();
// Clear西/北hands to prevent pung reactions that steal the turn // Clear西/北hands to prevent pung reactions that steal the turn
@ -103,8 +103,9 @@ public class MahjongRoomIntegrationTests
// Hand may have changed due to pung/chi reactions; should be 10-13 // Hand may have changed due to pung/chi reactions; should be 10-13
Assert.InRange(room.State.Hands["南"].Count, 7, 13); Assert.InRange(room.State.Hands["南"].Count, 7, 13);
Assert.Equal(54, room.State.Deck.Count); // Deck may be 54 (drawn) or 55 (南 punged on 东's discard → no draw)
Assert.Contains(evt2, e => e.Type == "draw"); Assert.Contains(room.State.Deck.Count, new[] { 54, 55 });
Assert.Contains(evt2, e => e.Type == "draw" || e.Type == "pung");
Assert.Contains(evt2, e => e.Type == "discard"); Assert.Contains(evt2, e => e.Type == "discard");
} }
@ -113,7 +114,7 @@ public class MahjongRoomIntegrationTests
public void _手牌总数保持恒定() public void _手牌总数保持恒定()
{ {
var rules = LoadDsl("xuezhandaodi"); var rules = LoadDsl("xuezhandaodi");
var room = new MahjongRoom(rules, new[] { "东", "南", "西", "北" }); var room = new MahjongRoom(rules, new[] { "东", "南", "西", "北" }, seed: 42);
room.Deal(); room.Deal();
int totalBefore = room.State.Hands.Sum(h => h.Value.Count) int totalBefore = room.State.Hands.Sum(h => h.Value.Count)
@ -137,7 +138,7 @@ public class MahjongRoomIntegrationTests
public void _四川血战_不出错() public void _四川血战_不出错()
{ {
var rules = LoadDsl("xuezhandaodi"); var rules = LoadDsl("xuezhandaodi");
var room = new MahjongRoom(rules, new[] { "东", "南", "西", "北" }); var room = new MahjongRoom(rules, new[] { "东", "南", "西", "北" }, seed: 42);
room.Run(); room.Run();
Assert.True(room.IsFinished); Assert.True(room.IsFinished);
@ -152,7 +153,7 @@ public class MahjongRoomIntegrationTests
public void _武汉_不出错() public void _武汉_不出错()
{ {
var rules = LoadDsl("wuhan"); var rules = LoadDsl("wuhan");
var room = new MahjongRoom(rules, new[] { "东", "南", "西", "北" }); var room = new MahjongRoom(rules, new[] { "东", "南", "西", "北" }, seed: 42);
room.Run(); room.Run();
Assert.True(room.IsFinished); Assert.True(room.IsFinished);
Assert.Equal(0, room.State.Scores.Values.Sum()); Assert.Equal(0, room.State.Scores.Values.Sum());
@ -162,7 +163,7 @@ public class MahjongRoomIntegrationTests
public void _国标_不出错() public void _国标_不出错()
{ {
var rules = LoadDsl("guobiao"); var rules = LoadDsl("guobiao");
var room = new MahjongRoom(rules, new[] { "东", "南", "西", "北" }); var room = new MahjongRoom(rules, new[] { "东", "南", "西", "北" }, seed: 42);
room.Run(); room.Run();
Assert.True(room.IsFinished); Assert.True(room.IsFinished);
@ -173,7 +174,7 @@ public class MahjongRoomIntegrationTests
public void _广东鸡平胡_不出错() public void _广东鸡平胡_不出错()
{ {
var rules = LoadDsl("guangdong_jipinghu"); var rules = LoadDsl("guangdong_jipinghu");
var room = new MahjongRoom(rules, new[] { "东", "南", "西", "北" }); var room = new MahjongRoom(rules, new[] { "东", "南", "西", "北" }, seed: 42);
room.Run(); room.Run();
Assert.True(room.IsFinished); Assert.True(room.IsFinished);
@ -185,7 +186,7 @@ public class MahjongRoomIntegrationTests
public void _副露后自摸() public void _副露后自摸()
{ {
var rules = LoadDsl("xuezhandaodi"); var rules = LoadDsl("xuezhandaodi");
var room = new MahjongRoom(rules, new[] { "东", "南", "西", "北" }); var room = new MahjongRoom(rules, new[] { "东", "南", "西", "北" }, seed: 42);
room.Deal(); room.Deal();
// 东 already pung-ed 1万: exposed has {1,1,1} // 东 already pung-ed 1万: exposed has {1,1,1}

View File

@ -18,13 +18,14 @@ public class MahjongRoom
private readonly MahjongPhaseMachine _phaseMachine; private readonly MahjongPhaseMachine _phaseMachine;
private readonly MahjongScoreEngine _scoreEngine; private readonly MahjongScoreEngine _scoreEngine;
private readonly bool _autoMode; private readonly bool _autoMode;
private readonly Random _rng = Random.Shared; private readonly Random _rng;
public MahjongRoom(MahjongDslRoot rules, string[] playerNames, bool autoMode = false, public MahjongRoom(MahjongDslRoot rules, string[] playerNames, bool autoMode = false,
string? humanPlayer = null) string? humanPlayer = null, int? seed = null)
{ {
_rules = rules; _rules = rules;
_autoMode = autoMode; _autoMode = autoMode;
_rng = seed.HasValue ? new Random(seed.Value) : Random.Shared;
_ais = playerNames.Select((n, i) => new RandomMahjongAI(n, new Random(i * 7919))).ToList(); _ais = playerNames.Select((n, i) => new RandomMahjongAI(n, new Random(i * 7919))).ToList();
var fanConfig = BuildFanConfig(); var fanConfig = BuildFanConfig();