diff --git a/RuleEngine.Tests/MahjongRoomIntegrationTests.cs b/RuleEngine.Tests/MahjongRoomIntegrationTests.cs index b26c72f..aa262b2 100644 --- a/RuleEngine.Tests/MahjongRoomIntegrationTests.cs +++ b/RuleEngine.Tests/MahjongRoomIntegrationTests.cs @@ -55,8 +55,8 @@ public class MahjongRoomIntegrationTests room.Deal(); int totalTiles = room.State.Hands.Sum(h => h.Value.Count) + room.State.Deck.Count; - // 136 tiles + 4 wildcards = 140 - Assert.Equal(140, totalTiles); + // 136 tiles total (108 numbered + 28 honors, 红中 is wildcard but part of the 136) + Assert.Equal(136, totalTiles); } // === Turn flow === diff --git a/RuleEngine/AI/HumanMahjongPlayer.cs b/RuleEngine/AI/HumanMahjongPlayer.cs index 84d1cba..c8e42b6 100644 --- a/RuleEngine/AI/HumanMahjongPlayer.cs +++ b/RuleEngine/AI/HumanMahjongPlayer.cs @@ -33,6 +33,11 @@ public class HumanMahjongPlayer Console.WriteLine($"=== {Name} 的回合 ==="); Console.WriteLine($" 牌墙: {state.Deck.Count}张 | 弃牌堆: {state.DiscardPool.Count}张"); + // Show wildcard info + var wcTiles = MahjongTile.ConfiguredWildcards; + if (wcTiles.Count > 0) + Console.WriteLine($" 🀫 宝牌(癞子): {string.Join(" ", wcTiles.Select(MahjongTile.ToString))}"); + // Show exposed melds if (state.Exposed.TryGetValue(Name, out var exposed) && exposed.Count > 0) { diff --git a/RuleEngine/Core/Deck.cs b/RuleEngine/Core/Deck.cs index 00059bd..4e5a3f2 100644 --- a/RuleEngine/Core/Deck.cs +++ b/RuleEngine/Core/Deck.cs @@ -10,12 +10,13 @@ public class MahjongDeck foreach (var t in allTiles) { int count = MahjongTile.IsFlower(t) ? 1 : 4; - // Wildcards are handled by AllTiles already - if (MahjongTile.IsWildcard(t)) continue; + // Skip extra encoding-50+ wildcards (they're added separately below) + // But include fixed wildcards (e.g. 红中) as normal tiles + if (t >= MahjongTile.WildcardBase) continue; for (int i = 0; i < count; i++) Tiles.Add(t); } - // Add wildcards separately + // Add extra wildcard tiles for (int i = 0; i < wildcardCount; i++) Tiles.Add(MahjongTile.WildcardBase + i); } diff --git a/RuleEngine/Core/MahjongTile.cs b/RuleEngine/Core/MahjongTile.cs index 7682002..81059a2 100644 --- a/RuleEngine/Core/MahjongTile.cs +++ b/RuleEngine/Core/MahjongTile.cs @@ -6,7 +6,19 @@ public static class MahjongTile // Encoding: 万1-9=1-9, 条1-9=11-19, 筒1-9=21-29 // 字: 东=31,南=32,西=33,北=34,中=35,发=36,白=37 // 花: 春=41..菊=48 - // 宝牌: 50-59 + // 宝牌: 50-59 (extra wildcards added to deck) + + private static readonly HashSet _wildcards = new(); + + /// Configure which normal tiles (e.g. 红中=35) also act as wildcards. + public static void ConfigureWildcards(IEnumerable tiles) + { + _wildcards.Clear(); + foreach (var t in tiles) _wildcards.Add(t); + } + + /// Get the set of normal tiles configured as wildcards. + public static IReadOnlySet ConfiguredWildcards => _wildcards; public static int Encode(string suit, int rank) { @@ -24,6 +36,7 @@ public static class MahjongTile if (tile >= 1 && tile <= 9) return $"{tile}万"; if (tile >= 11 && tile <= 19) return $"{tile - 10}条"; if (tile >= 21 && tile <= 29) return $"{tile - 20}筒"; + if (_wildcards.Contains(tile)) return $"🀫{TileName(tile)}"; return tile switch { 31 => "东", 32 => "南", 33 => "西", 34 => "北", @@ -35,6 +48,13 @@ public static class MahjongTile }; } + private static string TileName(int tile) => tile switch + { + 31 => "东", 32 => "南", 33 => "西", 34 => "北", + 35 => "中", 36 => "发", 37 => "白", + _ => $"?{tile}" + }; + /// 0=万, 1=条, 2=筒, 3=字, 4=花, 5=宝牌 public static int Suit(int tile) { @@ -64,7 +84,7 @@ public static class MahjongTile public static bool IsHonor(int tile) => tile >= 31 && tile <= 37; public static bool IsFlower(int tile) => tile >= 41 && tile <= 48; - public static bool IsWildcard(int tile) => tile >= 50 && tile <= 59; + public static bool IsWildcard(int tile) => tile >= 50 && tile <= 59 || _wildcards.Contains(tile); public static bool IsNumbered(int tile) => tile >= 1 && tile <= 29; public static bool IsTerminal(int tile) { diff --git a/RuleEngine/MahjongRoom.cs b/RuleEngine/MahjongRoom.cs index 0fe0a6d..b032b4a 100644 --- a/RuleEngine/MahjongRoom.cs +++ b/RuleEngine/MahjongRoom.cs @@ -438,7 +438,25 @@ public class MahjongRoom int perPlayer = _rules.Deal.CardsPerPlayer; bool includeFlowers = _rules.Deck.IncludeFlowers; bool includeHonors = _rules.Deck.IncludeHonors; - int wildcardCount = _rules.WildcardRules != null ? _rules.Deck.WildcardCount : 0; + + // Configure fixed wildcards (e.g. Wuhan 红中) + int wildcardCount = 0; + if (_rules.WildcardRules != null) + { + if (_rules.WildcardRules.Type == "fixed" && _rules.WildcardRules.Tiles != null) + { + // Fixed wildcards: specified tiles ARE the wildcards, not extra tiles + var wcTiles = _rules.WildcardRules.Tiles + .Select(t => t switch { "红中" => 35, "发财" => 36, "白板" => 37, _ => -1 }) + .Where(t => t > 0).ToArray(); + MahjongTile.ConfigureWildcards(wcTiles); + wildcardCount = 0; // No extra tiles + } + else + { + wildcardCount = _rules.Deck.WildcardCount; + } + } var deck = new MahjongDeck(includeHonors, includeFlowers, wildcardCount); deck.Shuffle(_rng); @@ -479,8 +497,6 @@ public class MahjongRoom private void CheckTing() { - 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) @@ -500,8 +516,10 @@ public class MahjongRoom int alreadyHave = hand.Count(t => t == tile); if (alreadyHave >= 4) continue; // can't draw more of this tile + var handWithTile = new List(hand) { tile }; + int wc = CountWildcardsInHand(handWithTile); var r = _solver.CheckWin(hand, newTile: tile, - wildcardCount: wildcardCount, require258Pair: require258); + wildcardCount: wc, require258Pair: require258); if (r != null && r.IsWin) { ting = true; break; } } diff --git a/dsl-examples/wuhan.yaml b/dsl-examples/wuhan.yaml index 077caff..a3c685a 100644 --- a/dsl-examples/wuhan.yaml +++ b/dsl-examples/wuhan.yaml @@ -17,8 +17,6 @@ deck: generator: mahjong include_honors: true include_flowers: false - wildcard_tile: 红中 - wildcard_count: 4 total: 136 deal: