using System;
using System.Collections.Generic;
using System.Linq;
using GameFix.PaoDeKuaiF;
using GameFix.Poker;
using GameMessage;
using GameMessage.PaoDeKuaiF;
namespace PdkFriendServer.Logic
{
///
/// ISMCTS 机器人 v3
/// - 包庄阈值 60%(校准自1000局压测)
/// - 全牌型枚举:对子/三带二/四带二/四带三/顺子/连对/飞机/炸弹/单张
/// - SimulateWin + SimulateOneGame 合并为 SimulateToEnd
///
public class IsmctsBot : IPdkBot
{
private readonly int _simulations;
private readonly Random _rng;
private readonly CardTracker _tracker = new CardTracker();
private const double BaoThreshold = 0.70;
private const double MultiBonusPerCard = 0.10;
private const int FastSims = 40;
public IsmctsBot(int simulations = 800)
{
_simulations = simulations;
_rng = new Random();
}
// ---- 自适应模拟次数 ----
private int AdaptiveSims(int handSize)
{
if (handSize <= 3) return 2000;
if (handSize <= 6) return 1500;
if (handSize <= 10) return 1200;
if (handSize <= 13) return 1000;
return 800;
}
// ---- 包庄决策 ----
public bool DecideBaoZhuang(PdkBotView view)
{
_tracker.Update(view);
int minPlays = HandOptimizer.MinPlays(view.MyHand);
double handEfficiency = view.MyHand.Length > 0
? (double)(view.MyHand.Length - minPlays) / view.MyHand.Length : 0;
// 手牌散 → 不包
if (minPlays >= 7 || handEfficiency < 0.3) return false;
// 手牌极好 → 阈值降低
double adjustedThreshold = BaoThreshold;
if (minPlays <= 3) adjustedThreshold = 0.50; // 手牌极好
else if (minPlays <= 4) adjustedThreshold = 0.60; // 手牌好
else if (minPlays <= 5) adjustedThreshold = 0.65; // 手牌还行
double winRate = EstimateWinProbability(view);
bool shouldBao = winRate >= adjustedThreshold;
Console.WriteLine($"[ISMCTS pos{view.MyPos}] 包庄评估: minPlays={minPlays} eff={handEfficiency:P0} "
+ $"P(win)={winRate:P0} (阈值{adjustedThreshold:P0}) → {(shouldBao ? "包庄" : "不包")}");
return shouldBao;
}
private double EstimateWinProbability(PdkBotView view)
{
// 确定性安全检查:手牌极弱 → 直接否定包庄
double detStrength = _tracker.AbsoluteHandStrength(view.MyHand);
if (detStrength < 0.15) return 0.0;
// ISMCTS 模拟(确定性只做否决,不混入加成以避免虚假信心)
int sims = Math.Min(_simulations, 600);
int wins = 0;
for (int s = 0; s < sims; s++)
if (SimulateToEnd(view.MyHand, view, null))
wins++;
return (double)wins / sims;
}
// ---- 出牌决策 ----
public PlayOutCardPdkF DecidePlay(PdkBotView view)
{
_tracker.Update(view);
var hand = view.MyHand;
if (hand.Length == 0) return Pass(view);
// 终局精确求解:手牌≤5 + 未知牌少 → 替代 ISMCTS
if (hand.Length <= 5 && _tracker.UnknownCount(hand) <= 15)
{
var endgameResult = EndgameSolver.Solve(hand, view, _tracker, _rng);
if (endgameResult.HasValue)
{
var ep = endgameResult.Value;
Console.WriteLine($"[ISMCTS pos{view.MyPos}] ENDGAME hand={hand.Length} unknown={_tracker.UnknownCount(hand)} → 精确求解");
return new PlayOutCardPdkF
{
Pos = (byte)view.MyPos,
GameNum = ep.GameNum,
Ids = ep.Ids,
Type = CardType1.None
};
}
}
// 锁手检测:残局时跳过 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;
// 1. 合法出牌方案
var tips = PdkCardAlgorithm.GetTipCard(view.MaxPlayCard, hand, null, view.Rule.AAAIsZhaDan);
var allCandidates = new List<(PlayOutCardPdkF play, bool isMulti)>();
if (tips != null)
foreach (var tip in tips)
{
allCandidates.Add((MakePlay(tip, view.MyPos), tip.Count > 1));
}
// 2. 新轮次:补充多牌先手方案
if (isFirstPlay)
AddMultiCardLeads(hand, view.MyPos, allCandidates);
if (!isFirstPlay)
allCandidates.Add((Pass(view), false));
if (allCandidates.Count == 1)
return allCandidates[0].play;
// 3. 自适应模拟次数 + 快速初筛
int simsPerCandidate = Math.Max(40, AdaptiveSims(hand.Length) / allCandidates.Count);
int fastSims = Math.Min(FastSims, simsPerCandidate / 2);
var scoredCandidates = new List<(PlayOutCardPdkF play, double winRate, int afterMinPlays, bool isMulti)>();
for (int ci = 0; ci < allCandidates.Count; ci++)
{
int wins = 0;
var play = allCandidates[ci].play;
// 快速评估(少量模拟定位好候选)
for (int s = 0; s < fastSims; s++)
if (SimulateToEnd(hand, view, play))
wins++;
double fastRate = (double)wins / fastSims;
// 胜率太低 → 剪枝,不参与后续评估
if (fastRate < 0.15 && allCandidates.Count > 3)
continue;
// 有前途的候选多模拟
int extraSims = fastRate > 0.3 ? simsPerCandidate - fastSims : 0;
for (int s = 0; s < extraSims; s++)
if (SimulateToEnd(hand, view, play))
wins++;
double totalRate = (double)wins / (fastSims + extraSims);
int afterMin = HandOptimizer.MinPlays(
hand.Where(c =>
play.Ids == null || !play.Ids.Contains(c.ID)).ToArray());
scoredCandidates.Add((play, totalRate, afterMin, allCandidates[ci].isMulti));
}
// 如果所有候选都被剪枝,回退到不做剪枝,全部重新评估
if (scoredCandidates.Count == 0)
{
for (int ci = 0; ci < allCandidates.Count; ci++)
{
int wins = 0;
var play = allCandidates[ci].play;
for (int s = 0; s < fastSims; s++)
if (SimulateToEnd(hand, view, play)) wins++;
double rate = (double)wins / fastSims;
int am = HandOptimizer.MinPlays(
hand.Where(c => play.Ids == null || !play.Ids.Contains(c.ID)).ToArray());
scoredCandidates.Add((play, rate, am, allCandidates[ci].isMulti));
}
}
// 4. 多牌加权
for (int i = 0; i < scoredCandidates.Count; i++)
{
if (scoredCandidates[i].isMulti)
{
int cardCount = scoredCandidates[i].play.Ids?.Length ?? 1;
var (p, wr, am, m) = scoredCandidates[i];
scoredCandidates[i] = (p, wr + cardCount * MultiBonusPerCard, am, m);
}
}
// 4.5. afterMin 牌效率加权(强化版)
int beforeMin = HandOptimizer.MinPlays(hand);
for (int i = 0; i < scoredCandidates.Count; i++)
{
var (p, wr, am, m) = scoredCandidates[i];
if (am < beforeMin)
{
// 手牌结构变好 → 大幅加分
double improvement = (double)(beforeMin - am) / beforeMin;
wr += improvement * 0.25; // max +25%
}
else if (am > beforeMin)
{
// 手牌被打散 → 惩罚
double degradation = (double)(am - beforeMin) / beforeMin;
if (m)
wr -= degradation * 0.30; // 多牌打散 → 严重(-30%)
else
wr -= degradation * 0.10; // 单张打散 → 中(-10%)
}
scoredCandidates[i] = (p, wr, am, m);
}
// 5. 炸弹感知加权
int bombsOutside = _tracker.BombsOutside(hand);
for (int i = 0; i < scoredCandidates.Count; i++)
{
var (p, wr, am, m) = scoredCandidates[i];
bool isBomb = p.Ids != null && p.Ids.Length == 4
&& hand.Count(c => c.GameNum == p.GameNum && c.GameState == 1) == 4;
if (isBomb)
{
// 我方炸弹
if (bombsOutside == 0) wr += 0.25; // 外面没炸弹 → 我方炸弹无敌
else if (am <= 3) wr += 0.15; // 快赢了,果断炸
else wr -= 0.05; // 先留着
}
else if (bombsOutside > 0 && am <= 3)
{
// 外面有炸弹但我快赢了 → 出牌保守,少加多牌分
if (m) wr -= 0.05;
}
scoredCandidates[i] = (p, wr, am, m);
}
// 5.5. 领牌保留策略:有绝对最高牌(2)时,领牌优先出最小的
isFirstPlay = view.MaxPlayCard.GameNum <= 0;
bool hasHighestCard = hand.Any(c => c.GameState == 1 && c.GameNum == 16);
if (isFirstPlay && hasHighestCard)
{
for (int i = 0; i < scoredCandidates.Count; i++)
{
var (p, wr, am, m) = scoredCandidates[i];
if (!m && p.Ids != null && p.Ids.Length == 1)
{
int rank = p.GameNum;
if (rank <= 6) wr += 0.15;
else if (rank <= 10) wr += 0.05;
else if (rank <= 13) wr -= 0.05;
else wr -= 0.10;
}
scoredCandidates[i] = (p, wr, am, m);
}
}
// 5.6. 确定性无敌出牌:基于牌计数,如果某出牌绝对无人能压制 → 大幅加分
for (int i = 0; i < scoredCandidates.Count; i++)
{
var (p, wr, am, m) = scoredCandidates[i];
if (p.Type != CardType1.None && p.Ids != null && p.Ids.Length > 0)
{
if (_tracker.IsAbsolutelyHighest(hand, p.GameNum, p.Ids.Length, p.Type))
{
// 确定性无敌 → 大幅加分(相当于省掉 ISMCTS 模拟噪声)
double bonus = 0.15;
if (am <= 2) bonus = 0.25; // 马上就要赢
else if (m) bonus = 0.20; // 多牌无敌
wr += bonus;
scoredCandidates[i] = (p, wr, am, m);
}
}
}
// 5.7. 对手牌型预测加权:按牌型区分压制条件
var opp1Pred = _tracker.PredictOpponent(view.MyPos % 3, hand, view);
var opp2Pred = _tracker.PredictOpponent((view.MyPos + 1) % 3, hand, view);
for (int i = 0; i < scoredCandidates.Count; i++)
{
var (p, wr, am, m) = scoredCandidates[i];
if (p.Type == CardType1.None || p.Ids == null || p.Ids.Length == 0)
continue;
int myRank = p.GameNum;
// 按牌型判定:对手是否有能力压制
bool oppLikelyCanBeat = false;
bool oppLikelyCantBeat = false;
switch (p.Type)
{
case CardType1.DanZhang:
// 单张:对手有更高 rank 的单张就能压制
oppLikelyCanBeat = opp1Pred.MaxRank > myRank || opp2Pred.MaxRank > myRank;
oppLikelyCantBeat = opp1Pred.MaxRank > 0 && opp2Pred.MaxRank > 0
&& opp1Pred.MaxRank <= myRank && opp2Pred.MaxRank <= myRank;
break;
case CardType1.DuiZi:
// 对子:对手需要有更高的对子(LikelyPairs ≥ 2张同rank)
oppLikelyCanBeat = opp1Pred.LikelyPairs.Any(r => r > myRank)
|| opp2Pred.LikelyPairs.Any(r => r > myRank);
oppLikelyCantBeat = opp1Pred.LikelyNoPairHigherThan(myRank)
&& opp2Pred.LikelyNoPairHigherThan(myRank)
&& opp1Pred.MaxRank > 0 && opp2Pred.MaxRank > 0;
break;
case CardType1.SanZhang:
case CardType1.SanDai2:
// 三张/三带二:对手需要有更高的三张(LikelyTriples ≥ 3张同rank)
oppLikelyCanBeat = opp1Pred.LikelyTriples.Any(r => r > myRank)
|| opp2Pred.LikelyTriples.Any(r => r > myRank);
oppLikelyCantBeat = opp1Pred.LikelyTriples.Count == 0
&& opp2Pred.LikelyTriples.Count == 0
&& opp1Pred.MaxRank > 0 && opp2Pred.MaxRank > 0;
break;
case CardType1.ShunZi:
// 顺子:对手需要有更长的同长顺子
int myLen = p.Ids.Length;
oppLikelyCanBeat = (opp1Pred.MaxStraightLength >= myLen
&& opp1Pred.MaxRank > myRank)
|| (opp2Pred.MaxStraightLength >= myLen
&& opp2Pred.MaxRank > myRank);
break;
default:
// 连对/飞机/炸弹:用 rank 近似
oppLikelyCanBeat = opp1Pred.MaxRank > myRank || opp2Pred.MaxRank > myRank;
oppLikelyCantBeat = opp1Pred.MaxRank > 0 && opp2Pred.MaxRank > 0
&& opp1Pred.MaxRank <= myRank && opp2Pred.MaxRank <= myRank;
break;
}
if (oppLikelyCantBeat)
wr += 0.10; // 两个对手都可能压不了 → 近似无敌
else if (oppLikelyCanBeat)
wr -= 0.04; // 对手大概率能压制 → 轻微惩罚
// 对手可能有炸弹 → 大牌型谨慎
double maxBombProb = Math.Max(opp1Pred.BombProbability, opp2Pred.BombProbability);
if (maxBombProb > 0.3 && (p.Type == CardType1.ZhaDan || p.Type == CardType1.ShunZi
|| p.Type == CardType1.SanDai2 || p.Type == CardType1.LianDui))
wr -= 0.03;
scoredCandidates[i] = (p, wr, am, m);
}
// 6. 两阶段选择:多牌优先
scoredCandidates.Sort((a, b) => b.winRate.CompareTo(a.winRate));
var best = scoredCandidates[0];
// 多牌候选中有胜率≥top 90%的 → 优先多牌
if (allCandidates.Any(c => c.isMulti))
{
var bestMulti = scoredCandidates.Where(x => x.isMulti)
.OrderByDescending(x => x.play.Ids?.Length ?? 0)
.ThenByDescending(x => x.winRate)
.FirstOrDefault();
if (bestMulti.play.Ids != null && bestMulti.play.Ids.Length > 0)
{
double topRate = scoredCandidates[0].winRate;
if (bestMulti.winRate >= topRate * 0.85)
best = bestMulti;
}
}
string desc = best.play.Ids == null || best.play.Ids.Length == 0
? "pass" : $"{best.play.Ids.Length}c winRate:{best.winRate:P0}";
int totalSims = allCandidates.Count * fastSims
+ scoredCandidates.Count(c => c.winRate > 0.3) * (simsPerCandidate - fastSims);
Console.WriteLine($"[ISMCTS pos{view.MyPos}] {totalSims}sims => {desc}");
TrainingCollector.Record(view, _tracker, TrainingCollector.ActionToIdx(best.play), view.MyPos-1, (float)best.winRate);
return best.play;
}
// ---- 模拟引擎 ----
private bool SimulateToEnd(TCardInfoPdkF[] myHand, PdkBotView view, PlayOutCardPdkF? firstPlay = null)
{
int myPos = view.MyPos;
int nextPos = myPos % 3 + 1;
int oppPos = (myPos + 1) % 3 + 1;
var hands = new List[3];
hands[myPos - 1] = new List(myHand);
hands[nextPos - 1] = new List();
hands[oppPos - 1] = new List();
var allCards = GetDeckCopy();
var unknown = allCards.Where(c => !myHand.Any(h => h.ID == c.ID)).ToList();
// 使用 CardTracker 进行约束采样
var sampled = _tracker.SampleOpponentHands(myHand, view, _rng);
hands[nextPos - 1].AddRange(sampled[0]);
hands[oppPos - 1].AddRange(sampled[1]);
// 如果约束采样没填满,用随机采样补齐
var usedIds = new HashSet(myHand.Select(c => c.ID));
foreach (var c in hands[nextPos - 1]) usedIds.Add(c.ID);
foreach (var c in hands[oppPos - 1]) usedIds.Add(c.ID);
var remaining = unknown.Where(c => !usedIds.Contains(c.ID)).ToList();
Shuffle(remaining);
int n1Need = view.ShenYuCard[nextPos - 1] - hands[nextPos - 1].Count;
int n2Need = view.ShenYuCard[oppPos - 1] - hands[oppPos - 1].Count;
if (n1Need > 0) hands[nextPos - 1].AddRange(remaining.Take(n1Need));
if (n2Need > 0) hands[oppPos - 1].AddRange(remaining.Skip(n1Need).Take(n2Need));
int current, maxPlayer;
PlayOutCardPdkF maxPlay;
if (firstPlay.HasValue)
{
var fp = firstPlay.Value;
maxPlay = new PlayOutCardPdkF { Pos = (byte)myPos, GameNum = fp.GameNum, Type = fp.Type, Ids = fp.Ids?.ToArray() };
if (fp.Type != CardType1.None && fp.Ids != null)
foreach (var id in fp.Ids) hands[myPos - 1].RemoveAll(c => c.ID == id);
current = myPos % 3 + 1;
maxPlayer = fp.Type == CardType1.None ? view.MaxPlayCard.Pos : myPos;
if (fp.Type == CardType1.None) { maxPlayer = view.MaxPlayCard.Pos; maxPlay = CloneCard(view.MaxPlayCard); }
}
else
{
current = myPos; maxPlay = new PlayOutCardPdkF(); maxPlayer = 0;
}
int passStreak = 0;
for (int move = 0; move < 500; move++)
{
var curHand = hands[current - 1];
if (curHand.Count == 0) return current == myPos;
bool isNewRound = maxPlayer == current || maxPlay.GameNum == 0;
var curTips = PdkCardAlgorithm.GetTipCard(isNewRound ? new PlayOutCardPdkF() : maxPlay, curHand.ToArray(), null, false);
if (curTips != null && curTips.Count > 0)
{
var pick = PickBestTip(curTips, curHand, false);
maxPlay = new PlayOutCardPdkF { Pos = (byte)current, GameNum = pick[0].GameNum, Type = pick.Count == 1 ? CardType1.DanZhang : CardType1.DuiZi, Ids = pick.Select(c => c.ID).ToArray() };
maxPlayer = current; passStreak = 0;
foreach (var id in maxPlay.Ids) curHand.RemoveAll(c => c.ID == id);
}
else
{
if (++passStreak >= 2)
{
var ft = PdkCardAlgorithm.GetTipCard(new PlayOutCardPdkF(), curHand.ToArray(), null, false);
if (ft != null && ft.Count > 0)
{
var pick = PickBestTip(ft, curHand, true);
maxPlay = new PlayOutCardPdkF { Pos = (byte)current, GameNum = pick[0].GameNum, Type = CardType1.DanZhang, Ids = pick.Select(c => c.ID).ToArray() };
maxPlayer = current;
foreach (var id in maxPlay.Ids) curHand.RemoveAll(c => c.ID == id);
}
passStreak = 0;
}
}
current = current % 3 + 1;
}
// 截断:用 MinPlays 评估而非比较张数
int myMin = HandOptimizer.MinPlays(hands[myPos - 1].ToArray());
int n1Min = HandOptimizer.MinPlays(hands[nextPos - 1].ToArray());
int n2Min = HandOptimizer.MinPlays(hands[oppPos - 1].ToArray());
return myMin <= n1Min && myMin <= n2Min;
}
// ---- 多牌先手枚举 ----
private static void AddMultiCardLeads(TCardInfoPdkF[] hand, int pos,
List<(PlayOutCardPdkF play, bool isMulti)> candidates)
{
// 炸弹
if (PokerLogic.GetPlayZhaDans(hand, out var bombs) && bombs != null)
foreach (var bomb in bombs) AddCandidate(bomb, pos, candidates);
// 对子 + 三张(缓存)
var pairs = GetMultiGroups(hand, 2);
foreach (var p in pairs) AddCandidate(p, pos, candidates);
var triples = GetMultiGroups(hand, 3);
foreach (var t in triples)
{
var rem = hand.Where(c => c.GameState == 1 && c.GameNum != t[0].GameNum).ToList();
if (rem.Count >= 2)
{
// 三带二:优先带对子(对子本身是一手牌),找不到对子才带两张最小散牌
var pair = rem.GroupBy(c => c.GameNum)
.Where(g => g.Count() >= 2)
.OrderBy(g => g.Key)
.FirstOrDefault();
if (pair != null)
AddCandidate(t.Concat(pair.Take(2)).ToList(), pos, candidates);
else
AddCandidate(t.Concat(rem.OrderBy(c => c.GameNum).Take(2)).ToList(), pos, candidates);
}
else
AddCandidate(t, pos, candidates);
}
// 四带二/三:优先带对子
var quads = GetMultiGroups(hand, 4);
foreach (var q in quads)
{
var rem = hand.Where(c => c.GameState == 1 && c.GameNum != q[0].GameNum).ToList();
var pair = rem.GroupBy(c => c.GameNum)
.Where(g => g.Count() >= 2)
.OrderBy(g => g.Key)
.FirstOrDefault();
if (rem.Count >= 3)
{
if (pair != null)
AddCandidate(q.Concat(pair.Take(2)).Concat(rem
.Where(c => c.GameNum != pair.Key).OrderBy(c => c.GameNum).Take(1))
.ToList(), pos, candidates);
else
AddCandidate(q.Concat(rem.OrderBy(c => c.GameNum).Take(3)).ToList(), pos, candidates);
}
else if (rem.Count >= 2)
{
if (pair != null)
AddCandidate(q.Concat(pair.Take(2)).ToList(), pos, candidates);
else
AddCandidate(q.Concat(rem.OrderBy(c => c.GameNum).Take(2)).ToList(), pos, candidates);
}
}
// 连对(复用 pairs,不重复 GetMultiGroups)
SortByGameNum(pairs);
AddConsecutiveGroups(pairs, 3, pos, candidates);
// 飞机
SortByGameNum(triples);
AddConsecutiveGroups(triples, 2, pos, candidates);
// 飞机带对
var runs = GetConsecutiveRuns(triples, 2);
foreach (var run in runs)
{
var runCards = run.SelectMany(t => t).ToList();
var rp = GetMultiGroups(hand.Where(c => !runCards.Any(r => r.ID == c.ID)).ToArray(), 2);
if (rp.Count >= run.Count)
{
SortByGameNum(rp);
runCards.AddRange(rp.Take(run.Count).SelectMany(p => p));
AddCandidate(runCards, pos, candidates);
}
}
// 顺子
var straights = PokerLogic.GetShunZi(hand);
if (straights != null) foreach (var s in straights) AddCandidate(s, pos, candidates);
}
// ---- 辅助方法 ----
private static void AddCandidate(List cards, int pos,
List<(PlayOutCardPdkF play, bool isMulti)> candidates)
{
candidates.Add((new PlayOutCardPdkF { Pos = (byte)pos, GameNum = cards[0].GameNum, Ids = cards.Select(c => c.ID).ToArray(), Type = InferType(cards) }, true));
}
private static CardType1 InferType(List cards)
{
int n = cards.Count;
if (n == 1) return CardType1.DanZhang;
if (n == 2) return CardType1.DuiZi;
var counts = cards.GroupBy(c => c.GameNum).Select(g => g.Count()).OrderByDescending(x => x).ToList();
if (counts[0] == 4) { if (n == 4) return CardType1.ZhaDan; if (n == 6) return CardType1.SiDai2; if (n == 7) return CardType1.SiDai3; }
if (counts[0] >= 3) return CardType1.SanDai2;
if (counts[0] == 2 && n % 2 == 0) return CardType1.LianDui;
return CardType1.ShunZi;
}
private static PlayOutCardPdkF MakePlay(List cards, int pos)
=> new PlayOutCardPdkF { Pos = (byte)pos, GameNum = cards[0].GameNum, Ids = cards.Select(x => x.ID).ToArray(), Type = InferType(cards) };
private static List> GetMultiGroups(TCardInfoPdkF[] hand, int count)
{
var r = new List>();
foreach (var g in hand.GroupBy(c => c.GameNum).Where(g => g.Count() >= count))
r.Add(g.Take(count).ToList());
return r;
}
private static void SortByGameNum(List> g) => g.Sort((a, b) => a[0].GameNum.CompareTo(b[0].GameNum));
private static void AddConsecutiveGroups(List> groups, int minLen, int pos,
List<(PlayOutCardPdkF, bool)> candidates)
{ foreach (var run in GetConsecutiveRuns(groups, minLen)) AddCandidate(run.SelectMany(g => g).ToList(), pos, candidates); }
private static List>> GetConsecutiveRuns(List> groups, int minLen)
{
var runs = new List>>();
if (groups.Count < minLen) return runs;
var cur = new List> { groups[0] };
int prev = groups[0][0].GameNum;
for (int i = 1; i < groups.Count; i++)
{
int n = groups[i][0].GameNum;
if (n == prev + 1) cur.Add(groups[i]);
else { if (cur.Count >= minLen) runs.Add(new List>(cur)); cur.Clear(); cur.Add(groups[i]); }
prev = n;
}
if (cur.Count >= minLen) runs.Add(cur);
return runs;
}
///
/// 智能出牌选择:模拟中的虚拟对手应该合理出牌,而非纯贪心。
/// - 跟牌时:出刚好能压制的最小牌(保留大牌)
/// - 领牌时:优先多牌组合(提高清牌效率)
///
private static List PickBestTip(List> tips,
List curHand, bool isNewRound)
{
if (tips == null || tips.Count == 0) return null;
if (!isNewRound)
{
// 跟牌:出刚好能压制的最小牌
// 策略:出最小 rank 的有效牌,保留大牌用于后续压制
var best = tips[0];
int bestScore = int.MaxValue;
foreach (var tip in tips)
{
// 同数量优先选最小 rank;不同数量优先选少的
int score = tip.Count * 1000 + tip[0].GameNum;
if (score < bestScore) { bestScore = score; best = tip; }
}
return best;
}
else
{
// 领牌:优先高效清牌
var best = tips[0];
int bestScore = int.MinValue;
foreach (var tip in tips)
{
// 优先多牌;同数量出最小 rank(保留大牌控制权)
int score = tip.Count * 100 - tip[0].GameNum;
if (score > bestScore) { bestScore = score; best = tip; }
}
return best;
}
}
// 静态牌池:52张牌,只创建一次
private static readonly TCardInfoPdkF[] FullDeck;
static IsmctsBot()
{
FullDeck = new TCardInfoPdkF[52];
int id = 1;
foreach (int f in new[] { 1, 2, 3, 4 })
for (int n = 1; n <= 13; n++)
FullDeck[id - 1] = new TCardInfoPdkF
{
ID = (byte)id,
Flower = (byte)f,
GameNum = (byte)(n == 1 ? 14 : n == 2 ? 16 : n),
GameState = 1
};
}
private static List GetDeckCopy()
{
return FullDeck.Where(c => c.ID > 0).Select(c => new TCardInfoPdkF
{
ID = c.ID, Flower = c.Flower, GameNum = c.GameNum, GameState = 1
}).ToList();
}
private void Shuffle(List l) { for (int i = l.Count - 1; i > 0; i--) { int j = _rng.Next(i + 1); (l[i], l[j]) = (l[j], l[i]); } }
private static PlayOutCardPdkF CloneCard(PlayOutCardPdkF s)
=> new PlayOutCardPdkF { Pos = s.Pos, GameNum = s.GameNum, Type = s.Type, Ids = s.Ids?.ToArray() };
private static PlayOutCardPdkF Pass(PdkBotView v)
=> new PlayOutCardPdkF { Pos = (byte)v.MyPos, Type = CardType1.None };
}
}