using RuleEngine; using RuleEngine.Core; using RuleEngine.Dsl; namespace RuleEngine.Tests; /// /// Integration tests for MahjongRoom — game flow, deal, win detection, reactions. /// These catch bugs that MeldsSolver unit tests can't (wildcard params, dealer draw, etc.) /// 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 { 31, 32, 33, 34, 35, 36, 37, 1, 2, 3, 4, 5, 6 }; room.State.Hands["北"] = new List { 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 { 3,4,5, 8,8,8, 13,14,15, 18,18 }; room.State.Exposed["东"] = new List { new() { Type = "pung", Tiles = new() { 1,1,1 } } }; room.State.DiscardPool = new List(); room.State.Deck = new List(); room.StepTurn(); // dealer first turn, no draw, checks win with full hand Assert.Contains("东", room.State.HuPlayers); } }