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
204 lines
7.4 KiB
C#
204 lines
7.4 KiB
C#
using RuleEngine;
|
|
using RuleEngine.Core;
|
|
using RuleEngine.Dsl;
|
|
|
|
namespace RuleEngine.Tests;
|
|
|
|
/// <summary>
|
|
/// Integration tests for MahjongRoom — game flow, deal, win detection, reactions.
|
|
/// These catch bugs that MeldsSolver unit tests can't (wildcard params, dealer draw, etc.)
|
|
/// </summary>
|
|
public class MahjongRoomIntegrationTests
|
|
{
|
|
private static MahjongDslRoot LoadDsl(string name)
|
|
{
|
|
var caps = new CapabilityRegistry();
|
|
caps.Register("meldsolver.standard_win");
|
|
caps.Register("meldsolver.seven_pairs");
|
|
caps.Register("meldsolver.thirteen_orphans");
|
|
caps.Register("meldsolver.all_orphans");
|
|
caps.Register("meldsolver.double_dragon");
|
|
caps.Register("meldsolver.wildcard");
|
|
caps.Register("deck.flower_cards");
|
|
caps.Register("phase.mahjong_turn");
|
|
caps.Register("phase.parallel_elimination");
|
|
caps.Register("phase.priority_arbitration");
|
|
caps.Register("scoring.fan_exclusion");
|
|
caps.Register("scoring.pre_hooks");
|
|
caps.Register("deck.generator_mahjong");
|
|
var loader = new DslLoader(caps);
|
|
return loader.Load(Path.Combine("..", "..", "..", "..", "dsl-examples", $"{name}.yaml"));
|
|
}
|
|
|
|
// === Deal ===
|
|
[Fact]
|
|
public void 发牌_四川_庄14闲13_牌墙55()
|
|
{
|
|
var rules = LoadDsl("xuezhandaodi");
|
|
var room = new MahjongRoom(rules, new[] { "东", "南", "西", "北" }, seed: 42);
|
|
room.Deal();
|
|
|
|
Assert.Equal(14, room.State.Hands["东"].Count);
|
|
Assert.Equal(13, room.State.Hands["南"].Count);
|
|
Assert.Equal(13, room.State.Hands["西"].Count);
|
|
Assert.Equal(13, room.State.Hands["北"].Count);
|
|
Assert.Equal(55, room.State.Deck.Count); // 108 - 53 = 55
|
|
Assert.Equal("东", room.State.Dealer);
|
|
Assert.Equal("东", room.State.CurrentPlayer);
|
|
}
|
|
|
|
[Fact]
|
|
public void 发牌_武汉_牌库正确()
|
|
{
|
|
var rules = LoadDsl("wuhan");
|
|
var room = new MahjongRoom(rules, new[] { "东", "南", "西", "北" }, seed: 42);
|
|
room.Deal();
|
|
|
|
int totalTiles = room.State.Hands.Sum(h => h.Value.Count) + room.State.Deck.Count;
|
|
// 136 tiles total (108 numbered + 28 honors, 红中 is wildcard but part of the 136)
|
|
Assert.Equal(136, totalTiles);
|
|
}
|
|
|
|
// === Turn flow ===
|
|
[Fact]
|
|
public void 庄家第一轮不摸牌_直接出牌()
|
|
{
|
|
var rules = LoadDsl("xuezhandaodi");
|
|
var room = new MahjongRoom(rules, new[] { "东", "南", "西", "北" }, seed: 42);
|
|
room.Deal();
|
|
|
|
Assert.Equal(14, room.State.Hands["东"].Count);
|
|
Assert.Equal(55, room.State.Deck.Count);
|
|
|
|
// First turn: dealer should NOT draw (already has 14)
|
|
room.StepTurn();
|
|
|
|
// Dealer should have discarded one tile
|
|
Assert.Equal(13, room.State.Hands["东"].Count);
|
|
Assert.Equal(55, room.State.Deck.Count); // No draw → deck unchanged
|
|
Assert.NotNull(room.State.LastDiscard);
|
|
}
|
|
|
|
[Fact]
|
|
public void 非庄家第一轮_摸牌后出牌()
|
|
{
|
|
var rules = LoadDsl("xuezhandaodi");
|
|
var room = new MahjongRoom(rules, new[] { "东", "南", "西", "北" }, seed: 42);
|
|
room.Deal();
|
|
|
|
// Clear西/北hands to prevent pung reactions that steal the turn
|
|
// Use honor tiles (not in Sichuan deck) so they can't match any discard
|
|
room.State.Hands["西"] = new List<int> { 31, 32, 33, 34, 35, 36, 37, 1, 2, 3, 4, 5, 6 };
|
|
room.State.Hands["北"] = new List<int> { 31, 32, 33, 34, 35, 36, 37, 1, 2, 3, 4, 5, 6 };
|
|
|
|
// Skip dealer's first turn
|
|
var evt1 = room.StepTurn();
|
|
Assert.Contains(evt1, e => e.Type == "discard");
|
|
|
|
// Second turn: next player draws then discards
|
|
Assert.Equal("南", room.State.CurrentPlayer);
|
|
Assert.Equal(13, room.State.Hands["南"].Count);
|
|
|
|
var evt2 = room.StepTurn();
|
|
|
|
// Hand may have changed due to pung/chi reactions; should be 10-13
|
|
Assert.InRange(room.State.Hands["南"].Count, 7, 13);
|
|
// Deck may be 54 (drawn) or 55 (南 punged on 东's discard → no 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");
|
|
}
|
|
|
|
// === Pung/Reaction ===
|
|
[Fact]
|
|
public void 回合之后_手牌总数保持恒定()
|
|
{
|
|
var rules = LoadDsl("xuezhandaodi");
|
|
var room = new MahjongRoom(rules, new[] { "东", "南", "西", "北" }, seed: 42);
|
|
room.Deal();
|
|
|
|
int totalBefore = room.State.Hands.Sum(h => h.Value.Count)
|
|
+ room.State.Exposed.Sum(e => e.Value.Sum(m => m.Tiles.Count));
|
|
|
|
// Play 10 turns
|
|
for (int i = 0; i < 10 && !room.IsFinished; i++)
|
|
room.StepTurn();
|
|
|
|
int totalAfter = room.State.Hands.Sum(h => h.Value.Count)
|
|
+ room.State.Exposed.Sum(e => e.Value.Sum(m => m.Tiles.Count))
|
|
+ room.State.Deck.Count
|
|
+ room.State.DiscardPool.Count;
|
|
|
|
// Total tiles in system should be constant (108 for Sichuan)
|
|
Assert.Equal(108, totalAfter);
|
|
}
|
|
|
|
// === Full game ===
|
|
[Fact]
|
|
public void 完整一局_四川血战_不出错()
|
|
{
|
|
var rules = LoadDsl("xuezhandaodi");
|
|
var room = new MahjongRoom(rules, new[] { "东", "南", "西", "北" }, seed: 42);
|
|
room.Run();
|
|
|
|
Assert.True(room.IsFinished);
|
|
Assert.Equal("settle", room.State.Phase);
|
|
// Deck should be depleted
|
|
Assert.Empty(room.State.Deck);
|
|
// Scores should sum to zero (zero-sum game)
|
|
Assert.Equal(0, room.State.Scores.Values.Sum());
|
|
}
|
|
|
|
[Fact]
|
|
public void 完整一局_武汉_不出错()
|
|
{
|
|
var rules = LoadDsl("wuhan");
|
|
var room = new MahjongRoom(rules, new[] { "东", "南", "西", "北" }, seed: 42);
|
|
room.Run();
|
|
Assert.True(room.IsFinished);
|
|
Assert.Equal(0, room.State.Scores.Values.Sum());
|
|
}
|
|
|
|
[Fact]
|
|
public void 完整一局_国标_不出错()
|
|
{
|
|
var rules = LoadDsl("guobiao");
|
|
var room = new MahjongRoom(rules, new[] { "东", "南", "西", "北" }, seed: 42);
|
|
room.Run();
|
|
|
|
Assert.True(room.IsFinished);
|
|
Assert.Equal("settle", room.State.Phase);
|
|
}
|
|
|
|
[Fact]
|
|
public void 完整一局_广东鸡平胡_不出错()
|
|
{
|
|
var rules = LoadDsl("guangdong_jipinghu");
|
|
var room = new MahjongRoom(rules, new[] { "东", "南", "西", "北" }, seed: 42);
|
|
room.Run();
|
|
|
|
Assert.True(room.IsFinished);
|
|
Assert.Equal("settle", room.State.Phase);
|
|
}
|
|
|
|
// === Constructed win scenario ===
|
|
[Fact]
|
|
public void 构造听牌_副露后自摸()
|
|
{
|
|
var rules = LoadDsl("xuezhandaodi");
|
|
var room = new MahjongRoom(rules, new[] { "东", "南", "西", "北" }, seed: 42);
|
|
room.Deal();
|
|
|
|
// 东 already pung-ed 1万: exposed has {1,1,1}
|
|
// Hand: 345万 888万 345条 88条 = 11 tiles → needs nothing, already winning
|
|
room.State.Hands["东"] = new List<int> { 3,4,5, 8,8,8, 13,14,15, 18,18 };
|
|
room.State.Exposed["东"] = new List<Meld> { new() { Type = "pung", Tiles = new() { 1,1,1 } } };
|
|
room.State.DiscardPool = new List<int>();
|
|
room.State.Deck = new List<int>();
|
|
|
|
room.StepTurn(); // dealer first turn, no draw, checks win with full hand
|
|
|
|
Assert.Contains("东", room.State.HuPlayers);
|
|
}
|
|
}
|