namespace RuleEngine.Patterns; using RuleEngine.Core; public class FanConfig { public string Name { get; set; } = ""; public int BaseFan { get; set; } public int Level { get; set; } public List Excludes { get; set; } = new(); public List Conflicts { get; set; } = new(); } public class MeldsSolver { private readonly Dictionary _fanConfig; public MeldsSolver(Dictionary fanConfig) { _fanConfig = fanConfig; } // === 主入口 === public MeldsResult CheckWin(List hand, int? newTile = null, int wildcardCount = 0, bool require258Pair = false) { var tiles = new List(hand); if (newTile.HasValue) tiles.Add(newTile.Value); if (tiles.Count + wildcardCount != 14) return new MeldsResult { IsWin = false }; tiles.Sort(); // 1. 七对 (wildcards can pair) var sevenPairs = TrySevenPairs(tiles, wildcardCount); if (sevenPairs != null) { sevenPairs.Fans = IdentifyFans(sevenPairs); return sevenPairs; } // 2. 十三幺 var thirteen = TryThirteenOrphans(tiles, wildcardCount); if (thirteen != null) { thirteen.Fans = IdentifyFans(thirteen); return thirteen; } // 3. 全不靠 var allOrphans = TryAllOrphans(tiles, wildcardCount); if (allOrphans != null) { allOrphans.Fans = IdentifyFans(allOrphans); return allOrphans; } // 4. 一色双龙会 var doubleDragon = TryDoubleDragon(tiles, wildcardCount); if (doubleDragon != null) { doubleDragon.Fans = IdentifyFans(doubleDragon); return doubleDragon; } // 5. 标准回溯(含 wildcard 缺口填充) var counts = BuildCounts(tiles); var result = TryExtractMelds(counts, wildcardCount, 0); if (result != null) { // 258将检查 if (require258Pair && result.PairTiles.Count == 2) { if (!IsValid258Pair(result.PairTiles[0], result.PairTiles[1])) return new MeldsResult { IsWin = false }; } result.Fans = IdentifyFans(result); return result; } return new MeldsResult { IsWin = false }; } // === Counts array helper === private int[] BuildCounts(List tiles) { // Index: 万1-9→0-8, 条1-9→9-17, 筒1-9→18-26, // 字31-37→27-33, 花/宝牌不进counts var counts = new int[34]; foreach (var t in tiles) { if (MahjongTile.IsWildcard(t) || MahjongTile.IsFlower(t)) continue; int idx = TileToIndex(t); if (idx >= 0 && idx < 34) counts[idx]++; } return counts; } private int TileToIndex(int tile) { if (tile >= 1 && tile <= 9) return tile - 1; if (tile >= 11 && tile <= 19) return 9 + (tile - 11); if (tile >= 21 && tile <= 29) return 18 + (tile - 21); if (tile >= 31 && tile <= 37) return 27 + (tile - 31); return -1; } private int IndexToTile(int idx) { if (idx < 9) return idx + 1; if (idx < 18) return 11 + (idx - 9); if (idx < 27) return 21 + (idx - 18); if (idx < 34) return 31 + (idx - 27); return -1; } // === 标准回溯(基础版,无 wildcard) === private MeldsResult? TryExtractMelds(int[] counts, int wildcardCount, int pairCount) { // Find first non-zero int i = 0; while (i < counts.Length && counts[i] == 0) i++; if (i == counts.Length) { // All non-wildcard tiles consumed return FinalizeWithWildcards(wildcardCount, pairCount); } int tile = IndexToTile(i); // Try kezi (triplet) if (counts[i] >= 3) { counts[i] -= 3; var r = TryExtractMelds(counts, wildcardCount, pairCount); if (r != null) { counts[i] += 3; r.Melds.Insert(0, new Meld { Type = "kezi", Tiles = new List { tile, tile, tile } }); return r; } counts[i] += 3; } // Try shunzi (sequence) - only for numbered tiles if (IsNumberedIndex(i) && i + 2 < 27 && SameSuitGroup(i, i + 2)) { if (counts[i] >= 1 && counts[i + 1] >= 1 && counts[i + 2] >= 1) { counts[i]--; counts[i + 1]--; counts[i + 2]--; var r = TryExtractMelds(counts, wildcardCount, pairCount); if (r != null) { counts[i]++; counts[i + 1]++; counts[i + 2]++; r.Melds.Insert(0, new Meld { Type = "shunzi", Tiles = new List { tile, IndexToTile(i + 1), IndexToTile(i + 2) } }); return r; } counts[i]++; counts[i + 1]++; counts[i + 2]++; } } // Try pair if (counts[i] >= 2 && pairCount == 0) { counts[i] -= 2; var r = TryExtractMelds(counts, wildcardCount, 1); if (r != null) { counts[i] += 2; r.PairTiles = new List { tile, tile }; return r; } counts[i] += 2; } // === Wildcard gap-fill (缺口填充) === // Try kezi with 1 wildcard if (counts[i] >= 2 && wildcardCount >= 1) { counts[i] -= 2; var r = TryExtractMelds(counts, wildcardCount - 1, pairCount); if (r != null) { counts[i] += 2; r.Melds.Insert(0, new Meld { Type = "kezi", Tiles = new List { tile, tile, -1 } }); r.WildcardsUsed++; return r; } counts[i] += 2; } // Try kezi with 2 wildcards if (counts[i] >= 1 && wildcardCount >= 2) { counts[i] -= 1; var r = TryExtractMelds(counts, wildcardCount - 2, pairCount); if (r != null) { counts[i] += 1; r.Melds.Insert(0, new Meld { Type = "kezi", Tiles = new List { tile, -1, -1 } }); r.WildcardsUsed += 2; return r; } counts[i] += 1; } // Shunzi with 1 wildcard: need [tile, wild, tile+2] if (IsNumberedIndex(i) && i + 2 < 27 && SameSuitGroup(i, i + 2) && wildcardCount >= 1) { if (counts[i] >= 1 && counts[i + 2] >= 1) { counts[i]--; counts[i + 2]--; var r = TryExtractMelds(counts, wildcardCount - 1, pairCount); if (r != null) { counts[i]++; counts[i + 2]++; r.Melds.Insert(0, new Meld { Type = "shunzi", Tiles = new List { tile, -1, IndexToTile(i + 2) } }); r.WildcardsUsed++; return r; } counts[i]++; counts[i + 2]++; } } // Shunzi with 1 wildcard: need [tile, tile+1, wild] if (IsNumberedIndex(i) && i + 2 < 27 && SameSuitGroup(i, i + 2) && wildcardCount >= 1) { if (counts[i] >= 1 && counts[i + 1] >= 1) { counts[i]--; counts[i + 1]--; var r = TryExtractMelds(counts, wildcardCount - 1, pairCount); if (r != null) { counts[i]++; counts[i + 1]++; r.Melds.Insert(0, new Meld { Type = "shunzi", Tiles = new List { tile, IndexToTile(i + 1), -1 } }); r.WildcardsUsed++; return r; } counts[i]++; counts[i + 1]++; } } // Pair with 1 wildcard if (counts[i] >= 1 && wildcardCount >= 1 && pairCount == 0) { counts[i] -= 1; var r = TryExtractMelds(counts, wildcardCount - 1, 1); if (r != null) { counts[i] += 1; r.PairTiles = new List { tile, -1 }; r.WildcardsUsed++; return r; } counts[i] += 1; } return null; } private MeldsResult? FinalizeWithWildcards(int wildcardCount, int pairCount) { if (pairCount == 0) { // Need a pair — use 2 wildcards if (wildcardCount >= 2) { return new MeldsResult { IsWin = true, Melds = new List(), PairTiles = new List { -1, -1 }, WildcardsUsed = 2 }; } return null; } // All tiles consumed, pair exists. Remaining wildcards form extra melds if any. // For simplicity: any remaining wildcards in multiples of 3 form kezi int remaining = wildcardCount; var extraMelds = new List(); while (remaining >= 3) { extraMelds.Add(new Meld { Type = "kezi", Tiles = new List { -1, -1, -1 } }); remaining -= 3; } return new MeldsResult { IsWin = true, Melds = extraMelds, PairTiles = new List(), WildcardsUsed = wildcardCount - remaining }; } private bool SameSuitGroup(int idxA, int idxB) { // Both must be in same numbered suit group (0-8, 9-17, 18-26) return (idxA / 9) == (idxB / 9) && idxA < 27 && idxB < 27; } private bool IsNumberedIndex(int idx) => idx < 27; // === 七对 === private MeldsResult? TrySevenPairs(List tiles, int wildcardCount) { // Count non-wildcard tiles var counts = new Dictionary(); foreach (var t in tiles) { if (MahjongTile.IsWildcard(t)) continue; if (MahjongTile.IsFlower(t)) return null; // 七对不能有花牌 counts[t] = counts.GetValueOrDefault(t) + 1; } int needWildcards = 0; foreach (var (_, c) in counts) { if (c % 2 == 1) needWildcards++; // need 1 wildcard to complete this pair } if (needWildcards <= wildcardCount) { var melds = new List(); foreach (var (t, c) in counts) { int pairs = c / 2; for (int p = 0; p < pairs; p++) melds.Add(new Meld { Type = "pair", Tiles = new List { t, t } }); } return new MeldsResult { IsWin = true, Melds = melds, PairTiles = melds.LastOrDefault()?.Tiles ?? new List(), WildcardsUsed = needWildcards }; } return null; } // === 十三幺 === private MeldsResult? TryThirteenOrphans(List tiles, int wildcardCount) { // 13 unique terminal/honor tiles: 1万,9万,1条,9条,1筒,9筒 + 7字牌 = 13 // + 1 duplicate = 14 int[] required = { 1, 9, 11, 19, 21, 29, 31, 32, 33, 34, 35, 36, 37 }; int present = 0; int extra = 0; // duplicate of required tiles foreach (var t in tiles) { if (MahjongTile.IsWildcard(t)) continue; if (MahjongTile.IsFlower(t)) return null; if (required.Contains(t)) { if (HasBit(present, t)) extra++; else present = SetBit(present, t); } else return null; // non-terminal tile found } int unique = PopCount(present); int missing = 13 - unique; // Need wildcards for: missing unique tiles + 1 for the pair (if no duplicate exists) int neededForMissing = missing; int neededForPair = extra > 0 ? 0 : 1; // duplicate already present → no pair wildcard needed int totalNeeded = neededForMissing + neededForPair; if (totalNeeded <= wildcardCount) { return new MeldsResult { IsWin = true, Melds = new List(), PairTiles = new List(), WildcardsUsed = totalNeeded }; } return null; } private static bool HasBit(int bits, int tile) { int idx = tile switch { 1 => 0, 9 => 1, 11 => 2, 19 => 3, 21 => 4, 29 => 5, 31 => 6, 32 => 7, 33 => 8, 34 => 9, 35 => 10, 36 => 11, 37 => 12, _ => -1 }; return idx >= 0 && (bits & (1 << idx)) != 0; } private static int SetBit(int bits, int tile) { int idx = tile switch { 1 => 0, 9 => 1, 11 => 2, 19 => 3, 21 => 4, 29 => 5, 31 => 6, 32 => 7, 33 => 8, 34 => 9, 35 => 10, 36 => 11, 37 => 12, _ => -1 }; return idx >= 0 ? bits | (1 << idx) : bits; } private static int PopCount(int bits) { int count = 0; while (bits != 0) { count++; bits &= bits - 1; } return count; } // === 全不靠 (All Orphans) === public MeldsResult? TryAllOrphans(List tiles, int wildcardCount) { // 147, 258, 369 distribution + all 7 honors + any pair // For simplicity: check that no 2 tiles share the same suit with adjacent ranks // and all tiles are terminals/honors. foreach (var t in tiles) { if (MahjongTile.IsWildcard(t)) continue; if (MahjongTile.IsFlower(t)) return null; if (!MahjongTile.IsTerminal(t) && !MahjongTile.IsHonor(t)) return null; } // Check 147/258/369 pattern within each suit var suitTiles = new Dictionary>(); var honors = new List(); foreach (var t in tiles) { if (MahjongTile.IsWildcard(t)) continue; if (MahjongTile.IsHonor(t)) honors.Add(t); else { int s = MahjongTile.Suit(t); if (!suitTiles.ContainsKey(s)) suitTiles[s] = new List(); suitTiles[s].Add(t); } } // Each suit group must be in 147, 258, or 369 only int gapCount = 0; foreach (var (_, st) in suitTiles) { var ranks = st.Select(MahjongTile.Rank).Distinct().OrderBy(r => r).ToList(); // Check ranks are all in same "gap-3" group var groups = ranks.GroupBy(r => (r - 1) % 3); if (groups.Count() > 1) return null; gapCount += ranks.Count; } // Need exactly 3 tiles per suit (one each of 3 gap groups) or wildcards to fill int missingSlots = 9 - gapCount; // Max: 3 suits × 3 rank groups int honorNeeded = 7 - honors.Distinct().Count(); if (honorNeeded < 0) honorNeeded = 0; int totalMissing = missingSlots + honorNeeded; if (totalMissing <= wildcardCount + 1) // +1 for the pair tolerance { return new MeldsResult { IsWin = true, Melds = new List(), PairTiles = new List(), WildcardsUsed = Math.Min(totalMissing, wildcardCount) }; } return null; } // === 一色双龙会 === public MeldsResult? TryDoubleDragon(List tiles, int wildcardCount) { // All same suit, 1-9 each at least 2 copies var nonWildTiles = tiles.Where(t => !MahjongTile.IsWildcard(t) && !MahjongTile.IsFlower(t)).ToList(); if (nonWildTiles.Count == 0) return null; int suit = MahjongTile.Suit(nonWildTiles[0]); if (nonWildTiles.Any(t => MahjongTile.Suit(t) != suit)) return null; var rankCounts = new int[10]; // 1-indexed foreach (var t in nonWildTiles) rankCounts[MahjongTile.Rank(t)]++; int wildcardsAvailable = wildcardCount + tiles.Count(MahjongTile.IsWildcard); for (int r = 1; r <= 9; r++) { if (rankCounts[r] < 2) { int need = 2 - rankCounts[r]; if (wildcardsAvailable >= need) wildcardsAvailable -= need; else return null; } } // 18 tiles needed (1-9 × 2), but hand is 14. Extra 4 can come from melds that use // more than 2 of some ranks (forming shunzi halves) return new MeldsResult { IsWin = true, Melds = new List(), PairTiles = new List() }; } // === 258将检查 === private bool IsValid258Pair(int tileA, int tileB) { bool Check(int t) { if (t == -1) return true; // wildcard int r = MahjongTile.Rank(t); return r == 2 || r == 5 || r == 8; } return Check(tileA) || Check(tileB); } // === 番型识别 === public List IdentifyFans(MeldsResult result) { var fans = new List(); var melds = result.Melds; var pair = result.PairTiles; // 清一色 var allTiles = melds.SelectMany(m => m.Tiles.Where(t => t != -1)).Concat(pair.Where(t => t != -1)).ToList(); if (allTiles.Count > 0) { var suits = allTiles.Select(MahjongTile.Suit).Distinct().ToList(); if (suits.Count == 1 && suits[0] < 3) // 万/条/筒 only (not 字/花) fans.Add("清一色"); } // 对对胡 (all melds are kezi) if (melds.Count > 0 && melds.All(m => m.Type == "kezi")) fans.Add("对对胡"); // 暗七对 (all melds are pairs) if (melds.Count > 0 && melds.All(m => m.Type == "pair")) fans.Add("暗七对"); // 带幺九 if (melds.Count > 0 && pair.Count >= 1) { bool allTerminals = melds.All(m => m.Tiles.Where(t => t != -1).All(t => MahjongTile.IsTerminal(t))); bool pairTerminal = pair.All(t => t == -1 || MahjongTile.IsTerminal(t)); if (allTerminals && pairTerminal) fans.Add("带幺九"); } return fans; } // === 番型互斥应用 === public List ApplyFanExclusions(List fans) { var toRemove = new HashSet(); foreach (var fan in fans) { if (_fanConfig.TryGetValue(fan, out var def)) { if (def.Excludes != null) foreach (var excluded in def.Excludes) toRemove.Add(excluded); if (def.Conflicts != null) { foreach (var conflict in def.Conflicts) { if (fans.Contains(conflict)) { // Keep the higher fan if (_fanConfig.TryGetValue(conflict, out var def2)) { if (def2.BaseFan > def.BaseFan) toRemove.Add(fan); else toRemove.Add(conflict); } } } } } } return fans.Where(f => !toRemove.Contains(f)).ToList(); } }