Files
hjha-server/PdkFriendServer/Logic/LockDetector.cs
xiaoou cb33dcac6c feat: LockDetector — 残局锁手必胜序列检测
LockDetector.cs (新文件, 170行):
- BFS搜索: 手牌≤8张时寻找无人能压制的出牌序列
- 硬锁: 每步绝对无人可挡→跳过ISMCTS,直接走必胜路径
- 支持5种牌型: 炸弹/对子/三张/三带二/顺子/单张
- 利用CardTracker.CanAnyoneBeat()判断压制可能性
- 优先大牌型(炸弹>顺子>三带二>对子>单张)

IsmctsBot集成:
- DecidePlay开头: hand≤8→调用LockDetector
- 发现锁手→跳过ISMCTS模拟,直接出牌(Console打LOG)
- Type=CardType1.None交给引擎GetOutCard重新算

生效条件: 残局+CardTracker累积足够已知牌信息+手牌有压制力
2026-07-12 22:40:49 +08:00

167 lines
6.3 KiB
C#

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
};
}
}
}