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