diff --git a/PdkFriendServer/Logic/IsmctsBot.cs b/PdkFriendServer/Logic/IsmctsBot.cs index 6ad64e1e..f37c5941 100644 --- a/PdkFriendServer/Logic/IsmctsBot.cs +++ b/PdkFriendServer/Logic/IsmctsBot.cs @@ -82,7 +82,26 @@ namespace PdkFriendServer.Logic { _tracker.Update(view); var hand = view.MyHand; - if (hand.Length == 0) return Pass(view); + if (hand.Length == 0) return Pass(view); + + // 锁手检测:残局时跳过 ISMCTS,走必胜序列 + if (hand.Length <= 8) + { + var lockSeq = LockDetector.FindLockSequence(hand, _tracker, view.MyPos); + if (lockSeq != null) + { + var firstPlay = lockSeq[0]; + Console.WriteLine($"[ISMCTS pos{view.MyPos}] LOCK {lockSeq.Count}步必胜 → 跳过模拟"); + // 通过引擎的出牌验证 + return new PlayOutCardPdkF + { + Pos = (byte)view.MyPos, + GameNum = firstPlay.GameNum, + Ids = firstPlay.Ids, + Type = CardType1.None + }; + } + } bool isFirstPlay = view.MaxPlayCard.GameNum <= 0; diff --git a/PdkFriendServer/Logic/LockDetector.cs b/PdkFriendServer/Logic/LockDetector.cs new file mode 100644 index 00000000..0c7d4ad0 --- /dev/null +++ b/PdkFriendServer/Logic/LockDetector.cs @@ -0,0 +1,167 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using GameMessage; +using GameMessage.PaoDeKuaiF; + +namespace PdkFriendServer.Logic +{ + /// + /// 锁手检测器 — BFS 搜索是否存在必胜出牌序列。 + /// + /// 硬锁: 找到一个出牌序列,每一步都绝对无人能压制 → 100% 必胜。 + /// 软锁: 找到一个出牌序列,每一步大概率无人能压制 → 高置信度。 + /// + /// 发现硬锁时跳过 ISMCTS 模拟,直接走必胜路径。 + /// + public static class LockDetector + { + /// + /// 搜索锁手序列。返回 null = 无锁手,返回列表 = 必胜出牌序列。 + /// + public static List FindLockSequence( + TCardInfoPdkF[] hand, CardTracker tracker, int pos) + { + // 仅在手牌 ≤8 张时搜索(搜索空间可控) + if (hand.Length > 8 || hand.Length == 0) return null; + + var lockSeq = new List(); + var remaining = hand.Where(c => c.GameState == 1).ToList(); + + // BFS 搜索:每次选一种必无人压制的出牌方案,直到手牌空 + while (remaining.Count > 0 && lockSeq.Count < 10) + { + var candidate = FindUnbeatablePlay(remaining, tracker, hand); + if (!candidate.HasValue) return null; + + var c = candidate.Value; + if (c.Ids != null) + foreach (var id in c.Ids) + remaining.RemoveAll(ca => ca.ID == id); + + lockSeq.Add(c); + } + + return remaining.Count == 0 ? lockSeq : null; + } + + public static bool IsLocked(TCardInfoPdkF[] hand, CardTracker tracker) + { + return FindLockSequence(hand, tracker, 1) != null; + } + + // ---- 内部 ---- + + private static PlayOutCardPdkF? FindUnbeatablePlay( + List hand, CardTracker tracker, TCardInfoPdkF[] fullHand) + { + var candidates = new List<(PlayOutCardPdkF play, int score)>(); + + // 1. 炸弹(最高优先级) + var byRank = hand.GroupBy(c => c.GameNum).Where(g => g.Count() >= 4); + foreach (var g in byRank) + { + var bomb = g.Take(4).ToList(); + var play = MakePlay(bomb, CardType1.ZhaDan); + if (!tracker.CanAnyoneBeat(fullHand, play)) + candidates.Add((play, 1000)); + } + + // 2. 对子 + 顺子 + 连对 + var pairs = hand.GroupBy(c => c.GameNum).Where(g => g.Count() >= 2).ToList(); + foreach (var g in pairs) + { + var pair = g.Take(2).ToList(); + var play = MakePlay(pair, CardType1.DuiZi); + if (!tracker.CanAnyoneBeat(fullHand, play)) + candidates.Add((play, pair[0].GameNum * 10 + 2)); + } + + // 3. 三张 + 三带二 + var triples = hand.GroupBy(c => c.GameNum).Where(g => g.Count() >= 3).ToList(); + foreach (var g in triples) + { + var trip = g.Take(3).ToList(); + var play = MakePlay(trip, CardType1.SanZhang); + if (!tracker.CanAnyoneBeat(fullHand, play)) + candidates.Add((play, trip[0].GameNum * 10 + 3)); + + // 三带二 + var rest = hand.Where(c => c.GameNum != g.Key).Take(2).ToList(); + if (rest.Count == 2) + { + var all = trip.Concat(rest).ToList(); + var p2 = MakePlay(all, CardType1.SanDai2); + if (!tracker.CanAnyoneBeat(fullHand, p2)) + candidates.Add((p2, all[0].GameNum * 10 + 5)); + } + } + + var bestStraight = FindLongestUnbeatableStraight(hand, tracker, fullHand); + if (bestStraight != null) + candidates.Add(bestStraight.Value); + + // 5. 单张(最后选择,出最大的无人压制的单张) + var singles = hand.OrderByDescending(c => c.GameNum) + .Select(c => new List { c }); + foreach (var s in singles) + { + var play = MakePlay(s, CardType1.DanZhang); + if (!tracker.CanAnyoneBeat(fullHand, play)) + { + candidates.Add((play, s[0].GameNum)); + break; // 只取最大的一张 + } + } + + if (candidates.Count == 0) return null; + + // 优先出牌最多的方案 + candidates.Sort((a, b) => b.score.CompareTo(a.score)); + return candidates[0].play; + } + + private static (PlayOutCardPdkF play, int score)? FindLongestUnbeatableStraight( + List hand, CardTracker tracker, TCardInfoPdkF[] fullHand) + { + var ranks = hand.Select(c => (int)c.GameNum).Distinct() + .Where(r => r >= 3 && r <= 14).OrderBy(r => r).ToList(); + + // 找最长的连续段 + for (int len = Math.Min(ranks.Count, 12); len >= 5; len--) + { + for (int i = 0; i <= ranks.Count - len; i++) + { + if (!IsConsecutive(ranks, i, len)) continue; + + var strCards = ranks.Skip(i).Take(len) + .SelectMany(r => hand.Where(c => c.GameNum == r).Take(1)) + .ToList(); + var play = MakePlay(strCards, CardType1.ShunZi); + + if (!tracker.CanAnyoneBeat(fullHand, play)) + return (play, len * 10); + } + } + return null; + } + + private static bool IsConsecutive(List ranks, int start, int len) + { + for (int i = 1; i < len; i++) + if (ranks[start + i] != ranks[start + i - 1] + 1) + return false; + return true; + } + + private static PlayOutCardPdkF MakePlay(List cards, CardType1 type) + { + return new PlayOutCardPdkF + { + GameNum = cards[0].GameNum, + Ids = cards.Select(c => c.ID).ToArray(), + Type = type + }; + } + } +} \ No newline at end of file