LockDetector.cs (新文件, 170行): - BFS搜索: 手牌≤8张时寻找无人能压制的出牌序列 - 硬锁: 每步绝对无人可挡→跳过ISMCTS,直接走必胜路径 - 支持5种牌型: 炸弹/对子/三张/三带二/顺子/单张 - 利用CardTracker.CanAnyoneBeat()判断压制可能性 - 优先大牌型(炸弹>顺子>三带二>对子>单张) IsmctsBot集成: - DecidePlay开头: hand≤8→调用LockDetector - 发现锁手→跳过ISMCTS模拟,直接出牌(Console打LOG) - Type=CardType1.None交给引擎GetOutCard重新算 生效条件: 残局+CardTracker累积足够已知牌信息+手牌有压制力
167 lines
6.3 KiB
C#
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
|
|
};
|
|
}
|
|
}
|
|
} |