using RuleEngine.Core; using RuleEngine.Patterns; namespace RuleEngine.Tests; public class MahjongTileTests { [Fact] public void Encode_万1_Returns1() => Assert.Equal(1, MahjongTile.Encode("万", 1)); [Fact] public void Encode_条9_Returns19() => Assert.Equal(19, MahjongTile.Encode("条", 9)); [Fact] public void Encode_筒3_Returns23() => Assert.Equal(23, MahjongTile.Encode("筒", 3)); [Fact] public void Suit_万1_Returns0() => Assert.Equal(0, MahjongTile.Suit(1)); [Fact] public void Suit_条19_Returns1() => Assert.Equal(1, MahjongTile.Suit(19)); [Fact] public void Rank_万9_Returns9() => Assert.Equal(9, MahjongTile.Rank(9)); [Fact] public void Rank_条11_Returns1() => Assert.Equal(1, MahjongTile.Rank(11)); [Fact] public void ToString_各类型正确() { Assert.Equal("5万", MahjongTile.ToString(5)); Assert.Equal("8条", MahjongTile.ToString(18)); Assert.Equal("3筒", MahjongTile.ToString(23)); Assert.Equal("东", MahjongTile.ToString(31)); Assert.Equal("春", MahjongTile.ToString(41)); Assert.Equal("🃏", MahjongTile.ToString(50)); } [Fact] public void AllTiles_108张() => Assert.Equal(27, MahjongTile.AllTiles().Length); [Fact] public void AllTiles_含字牌_34种() => Assert.Equal(34, MahjongTile.AllTiles(includeHonors: true).Length); [Fact] public void AllTiles_含花牌_35种() => Assert.Equal(35, MahjongTile.AllTiles(includeFlowers: true).Length); [Fact] public void IsWildcard_50_True() => Assert.True(MahjongTile.IsWildcard(50)); [Fact] public void IsWildcard_普通牌_False() => Assert.False(MahjongTile.IsWildcard(1)); [Fact] public void ToString_配置wildcard_显示癞子标记() { var wc = new WildcardRegistry(); wc.Add(35); Assert.Equal("🀫中", MahjongTile.ToString(35, wc)); } } public class DeckTests { [Fact] public void 四川牌库_108张() { var deck = new MahjongDeck(includeHonors: false, includeFlowers: false); Assert.Equal(108, deck.Tiles.Count); } [Fact] public void 广东牌库_136张() { // 108 numbered + 28 honors = 136 (no flowers) var deck = new MahjongDeck(includeHonors: true, includeFlowers: false); Assert.Equal(136, deck.Tiles.Count); } [Fact] public void 国标牌库_144张() { // 108 numbered + 28 honors + 8 flowers = 144 var deck = new MahjongDeck(includeHonors: true, includeFlowers: true); Assert.Equal(144, deck.Tiles.Count); } [Fact] public void 每张牌4份() { var deck = new MahjongDeck(); var groups = deck.Tiles.Where(t => !MahjongTile.IsFlower(t)).GroupBy(t => t); Assert.All(groups, g => Assert.Equal(4, g.Count())); } [Fact] public void 抽牌_减少计数() { var deck = new MahjongDeck(); int before = deck.Count; deck.Draw(); Assert.Equal(before - 1, deck.Count); } } public class MeldsSolverTests { private readonly MeldsSolver _solver = new(new Dictionary { ["清一色"] = new() { BaseFan = 24 }, ["混一色"] = new() { BaseFan = 6 }, ["字一色"] = new() { BaseFan = 64 }, ["对对胡"] = new() { BaseFan = 2 }, ["碰碰和"] = new() { BaseFan = 6 }, ["暗七对"] = new() { BaseFan = 8 }, ["带幺九"] = new() { BaseFan = 2 }, ["缺一门"] = new() { BaseFan = 0 }, ["平胡"] = new() { BaseFan = 0 }, ["断幺九"] = new() { BaseFan = 2 }, ["无字"] = new() { BaseFan = 0 }, ["全大"] = new() { BaseFan = 12 }, ["全中"] = new() { BaseFan = 12 }, ["全小"] = new() { BaseFan = 12 }, ["全双"] = new() { BaseFan = 24 }, ["鸡胡"] = new() { BaseFan = 1 }, }); // === 标准胡牌 === [Fact] public void 标准胡_4面子1对_ShouldWin() { // 123万 456万 789万 111条 99条 var hand = new List { 1, 2, 3, 4, 5, 6, 7, 8, 9, 11, 11, 11, 19, 19 }; var result = _solver.CheckWin(hand); Assert.True(result.IsWin); } [Fact] public void 标准胡_碰碰胡_ShouldWin() { // 111万 333万 555万 111条 99条 var hand = new List { 1, 1, 1, 3, 3, 3, 5, 5, 5, 11, 11, 11, 19, 19 }; var result = _solver.CheckWin(hand); Assert.True(result.IsWin); } [Fact] public void 不能胡_缺面子_ShouldNotWin() { // All sequential but wrong structure: 123 456 78? can't form 4 melds var hand = new List { 1, 2, 3, 4, 5, 6, 7, 8, 11, 12, 13, 14, 15, 16 }; var result = _solver.CheckWin(hand); Assert.False(result.IsWin); } [Fact] public void 不能胡_14张全不同_ShouldNotWin() { var hand = new List { 1, 2, 3, 4, 5, 6, 7, 8, 9, 11, 12, 21, 15, 19 }; var result = _solver.CheckWin(hand); Assert.False(result.IsWin); } // === 七对 === [Fact] public void 七对_ShouldWin() { var hand = new List { 1, 1, 3, 3, 5, 5, 7, 7, 11, 11, 13, 13, 21, 21 }; var result = _solver.CheckWin(hand); Assert.True(result.IsWin); } [Fact] public void 七对_缺一对_ShouldNotWin() { var hand = new List { 1, 1, 3, 3, 5, 5, 7, 7, 11, 11, 13, 13, 21, 5 }; var result = _solver.CheckWin(hand); Assert.False(result.IsWin); } // === 十三幺 === [Fact] public void 十三幺_ShouldWin() { var hand = new List { 1, 9, 11, 19, 21, 29, 31, 32, 33, 34, 35, 36, 37, 1 }; var result = _solver.CheckWin(hand); Assert.True(result.IsWin); } [Fact] public void 十三幺_缺字牌_ShouldNotWin() { var hand = new List { 1, 9, 11, 19, 21, 29, 2, 3, 4, 5, 6, 7, 8, 1 }; var result = _solver.CheckWin(hand); Assert.False(result.IsWin); } // === 番型识别 === [Fact] public void 清一色_ShouldIdentify() { // 全万: 111万 234万 567万 789万 99万 var hand = new List { 1, 1, 1, 2, 3, 4, 5, 6, 7, 7, 8, 9, 9, 9 }; var result = _solver.CheckWin(hand); Assert.True(result.IsWin); var fans = _solver.IdentifyFans(result!); Assert.Contains("清一色", fans); } [Fact] public void 对对胡_ShouldIdentify() { var hand = new List { 1, 1, 1, 3, 3, 3, 5, 5, 5, 11, 11, 11, 19, 19 }; var result = _solver.CheckWin(hand); var fans = _solver.IdentifyFans(result!); Assert.Contains("对对胡", fans); } // === Wildcard === [Fact] public void 鬼牌_1wildcard补刻子_ShouldWin() { // 111万 234万 567万 (3 melds) + 条11,条11 + wildcard = 刻子 (4th meld) + 筒9,筒9 = pair // 13 tiles + 1 wildcard = 14 var hand = new List { 1, 1, 1, 2, 3, 4, 5, 6, 7, 11, 11, 9, 9 }; var result = _solver.CheckWin(hand, wildcardCount: 1); Assert.True(result.IsWin); } [Fact] public void 鬼牌_2wildcard做对_ShouldWin() { // 111万 234万 555万 888万 + 2 wildcards as the pair var hand = new List { 1, 1, 1, 2, 3, 4, 5, 5, 5, 8, 8, 8 }; var result = _solver.CheckWin(hand, wildcardCount: 2); Assert.True(result.IsWin); } // === 258将 === [Fact] public void 武汉麻将_258将_2万_ShouldWin() { var hand = new List { 1, 2, 3, 4, 5, 6, 7, 8, 9, 11, 11, 11, 2, 2 }; var result = _solver.CheckWin(hand, require258Pair: true); Assert.True(result.IsWin); } [Fact] public void 武汉麻将_258将_7万将_ShouldFail() { var hand = new List { 1, 2, 3, 4, 5, 6, 7, 8, 9, 11, 11, 11, 7, 7 }; var result = _solver.CheckWin(hand, require258Pair: true); Assert.False(result.IsWin); } // === 全不靠 === // 全不靠: 14 tiles chosen from 16-position template (9 numbered gaps + 7 honors). // Two positions may be empty per rule — no wildcards needed. [Fact] public void 全不靠_14牌0wildcard_ShouldWin() { // 7 honors + 万147 + 条258 + 筒3 = 14 tiles from 16-position pool var hand = new List { 1, 4, 7, 12, 15, 18, 23, 31, 32, 33, 34, 35, 36, 37 }; var result = _solver.CheckWin(hand, wildcardCount: 0); Assert.True(result.IsWin, "全不靠(14牌从16位置模板选)应判定为胡牌"); } [Fact] public void 全不靠_3wildcard补位_ShouldWin() { // 5 honors + 万147 + 条258 = 11 tiles + 3 wildcards → fill 5 missing → 14 total var hand = new List { 1, 4, 7, 12, 15, 18, 31, 32, 33, 34, 35 }; var result = _solver.CheckWin(hand, wildcardCount: 3); Assert.True(result.IsWin, "全不靠(11牌+3wildcard补位)应判定为胡牌"); } [Fact] public void 全不靠_0wildcard不完整_ShouldNotWin() { // Only 6 honors + 万147 = 9 tiles, 5 positions missing, 0 wildcard → fail var hand = new List { 1, 4, 7, 31, 32, 33, 34, 35, 36 }; var result = _solver.CheckWin(hand, wildcardCount: 0); Assert.False(result.IsWin, "全不靠(9牌+0wildcard不完整)不应判定为胡牌"); } // === 一色双龙会 (需 wildcard: 9 ranks × 2 = 18 positions, 14 tiles + wildcards) === [Fact] public void 一色双龙会_4wildcard_ShouldWin() { // 5 ranks × 2 = 10 tiles + 4 wildcards for remaining 4 ranks = 14 total var hand = new List { 1, 1, 2, 2, 3, 3, 4, 4, 5, 5 }; var result = _solver.CheckWin(hand, wildcardCount: 4); Assert.True(result.IsWin, "一色双龙会(10牌+4wildcard)应判定为胡牌"); } [Fact] public void 一色双龙会_不足wildcard_ShouldNotWin() { // Same hand, only 2 wildcards → ranks 6-9 need 2 each → 6 missing, only 2 available var hand = new List { 1, 1, 2, 2, 3, 3, 4, 4, 5, 5 }; var result = _solver.CheckWin(hand, wildcardCount: 2); Assert.False(result.IsWin, "一色双龙会不足wildcard不应判定为胡牌"); } // === 新番型识别测试 === [Fact] public void 混一色_ShouldIdentify() { // 万牌 + 字牌对将: 111万 234万 567万 东东东 发发 var hand = new List { 1, 1, 1, 2, 3, 4, 5, 6, 7, 31, 31, 31, 36, 36 }; var result = _solver.CheckWin(hand); Assert.True(result.IsWin); var fans = _solver.IdentifyFans(result!); Assert.Contains("混一色", fans); } [Fact] public void 字一色_ShouldIdentify() { // All honors: 111 333 555 777 99 in encoding var hand = new List { 31, 31, 31, 33, 33, 33, 35, 35, 35, 37, 37, 37, 32, 32 }; var result = _solver.CheckWin(hand); Assert.True(result.IsWin); var fans = _solver.IdentifyFans(result!); Assert.Contains("字一色", fans); } [Fact] public void 缺一门_ShouldIdentify() { // Only 万 and 条, no 筒: 123万 456万 789万 111条 99条 var hand = new List { 1, 2, 3, 4, 5, 6, 7, 8, 9, 11, 11, 11, 19, 19 }; var result = _solver.CheckWin(hand); Assert.True(result.IsWin); var fans = _solver.IdentifyFans(result!); Assert.Contains("缺一门", fans); } [Fact] public void 平胡_全顺子_ShouldIdentify() { // All shunzi: 123万 456万 789万 234条 55条 var hand = new List { 1, 2, 3, 4, 5, 6, 7, 8, 9, 12, 13, 14, 15, 15 }; var result = _solver.CheckWin(hand); Assert.True(result.IsWin); var fans = _solver.IdentifyFans(result!); Assert.Contains("平胡", fans); } [Fact] public void 全大_7到9_ShouldIdentify() { // All ranks 7-9: 777万 888万 999万 777条 99条 var hand = new List { 7, 7, 7, 8, 8, 8, 9, 9, 9, 17, 17, 17, 19, 19 }; var result = _solver.CheckWin(hand); Assert.True(result.IsWin); var fans = _solver.IdentifyFans(result!); Assert.Contains("全大", fans); } [Fact] public void 全小_1到3_ShouldIdentify() { // All ranks 1-3: 111万 222万 333万 111条 33条 var hand = new List { 1, 1, 1, 2, 2, 2, 3, 3, 3, 11, 11, 11, 13, 13 }; var result = _solver.CheckWin(hand); Assert.True(result.IsWin); var fans = _solver.IdentifyFans(result!); Assert.Contains("全小", fans); } [Fact] public void 全双_偶数_ShouldIdentify() { // All even ranks: 222万 444万 666万 222条 88条 var hand = new List { 2, 2, 2, 4, 4, 4, 6, 6, 6, 12, 12, 12, 18, 18 }; var result = _solver.CheckWin(hand); Assert.True(result.IsWin); var fans = _solver.IdentifyFans(result!); Assert.Contains("全双", fans); } [Fact] public void 断幺九_ShouldIdentify() { // No terminals: 234万 456万 678万 234条 55条 (ranks 2-8 only) var hand = new List { 2, 3, 4, 4, 5, 6, 6, 7, 8, 12, 13, 14, 15, 15 }; var result = _solver.CheckWin(hand); Assert.True(result.IsWin); var fans = _solver.IdentifyFans(result!); Assert.Contains("断幺九", fans); } // === 听牌检查 === [Fact] public void 听牌_缺一张__ShouldDetectTing() { // 13 tiles, needs 9万 to complete 123万 456万 789万 111条 99条 var hand = new List { 1, 2, 3, 4, 5, 6, 7, 8, 11, 11, 11, 19, 19 }; var allTiles = MahjongTile.AllTiles(false, false); bool hasTing = false; foreach (var tile in allTiles) { if (hand.Count(t => t == tile) >= 4) continue; var r = _solver.CheckWin(hand, newTile: tile); if (r != null && r.IsWin) { hasTing = true; break; } } Assert.True(hasTing, "13张听牌应检测到至少一张能胡的牌"); } [Fact] public void 听牌_乱牌__ShouldNotDetectTing() { var hand = new List { 1, 3, 5, 7, 9, 11, 13, 15, 21, 23, 25, 31, 33 }; var allTiles = MahjongTile.AllTiles(false, false); bool hasTing = false; foreach (var tile in allTiles) { if (hand.Count(t => t == tile) >= 4) continue; var r = _solver.CheckWin(hand, newTile: tile); if (r != null && r.IsWin) { hasTing = true; break; } } Assert.False(hasTing, "乱牌不应检测到听牌"); } }