feat: LockDetector — 残局锁手必胜序列检测
LockDetector.cs (新文件, 170行): - BFS搜索: 手牌≤8张时寻找无人能压制的出牌序列 - 硬锁: 每步绝对无人可挡→跳过ISMCTS,直接走必胜路径 - 支持5种牌型: 炸弹/对子/三张/三带二/顺子/单张 - 利用CardTracker.CanAnyoneBeat()判断压制可能性 - 优先大牌型(炸弹>顺子>三带二>对子>单张) IsmctsBot集成: - DecidePlay开头: hand≤8→调用LockDetector - 发现锁手→跳过ISMCTS模拟,直接出牌(Console打LOG) - Type=CardType1.None交给引擎GetOutCard重新算 生效条件: 残局+CardTracker累积足够已知牌信息+手牌有压制力
This commit is contained in:
@ -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;
|
||||
|
||||
|
||||
167
PdkFriendServer/Logic/LockDetector.cs
Normal file
167
PdkFriendServer/Logic/LockDetector.cs
Normal file
@ -0,0 +1,167 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using GameMessage;
|
||||
using GameMessage.PaoDeKuaiF;
|
||||
|
||||
namespace PdkFriendServer.Logic
|
||||
{
|
||||
/// <summary>
|
||||
/// 锁手检测器 — BFS 搜索是否存在必胜出牌序列。
|
||||
///
|
||||
/// 硬锁: 找到一个出牌序列,每一步都绝对无人能压制 → 100% 必胜。
|
||||
/// 软锁: 找到一个出牌序列,每一步大概率无人能压制 → 高置信度。
|
||||
///
|
||||
/// 发现硬锁时跳过 ISMCTS 模拟,直接走必胜路径。
|
||||
/// </summary>
|
||||
public static class LockDetector
|
||||
{
|
||||
/// <summary>
|
||||
/// 搜索锁手序列。返回 null = 无锁手,返回列表 = 必胜出牌序列。
|
||||
/// </summary>
|
||||
public static List<PlayOutCardPdkF> FindLockSequence(
|
||||
TCardInfoPdkF[] hand, CardTracker tracker, int pos)
|
||||
{
|
||||
// 仅在手牌 ≤8 张时搜索(搜索空间可控)
|
||||
if (hand.Length > 8 || hand.Length == 0) return null;
|
||||
|
||||
var lockSeq = new List<PlayOutCardPdkF>();
|
||||
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<TCardInfoPdkF> 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<TCardInfoPdkF> { 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<TCardInfoPdkF> 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<int> 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<TCardInfoPdkF> cards, CardType1 type)
|
||||
{
|
||||
return new PlayOutCardPdkF
|
||||
{
|
||||
GameNum = cards[0].GameNum,
|
||||
Ids = cards.Select(c => c.ID).ToArray(),
|
||||
Type = type
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user