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)
{
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);
// 锁手检测:残局时跳过 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;
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));
}
// 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.12; // 最多 +12% 当 minPlays 减半
}
else if (am > beforeMin && m)
{
// 多牌出牌反而把手牌打散了 → 惩罚
wr -= 0.03;
}
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);
}
// 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}");
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 = GenerateDeck();
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);
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);
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;
}
return hands[myPos - 1].Count <= hands[nextPos - 1].Count && hands[myPos - 1].Count <= hands[oppPos - 1].Count;
}
// ---- 多牌先手枚举 ----
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.GameNum != t[0].GameNum).OrderBy(c => c.GameNum).ToList();
if (rem.Count >= 2)
AddCandidate(t.Concat(rem.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.GameNum != q[0].GameNum).OrderBy(c => c.GameNum).ToList();
if (rem.Count >= 3) AddCandidate(q.Concat(rem.Take(3)).ToList(), pos, candidates);
else if (rem.Count >= 2) AddCandidate(q.Concat(rem.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)
{
if (tips == null || tips.Count == 0) return null;
var best = tips[tips.Count - 1];
int bestScore = best.Count * 10 + best[0].GameNum;
for (int i = tips.Count - 2; i >= 0; i--)
{
int s = tips[i].Count * 10 + tips[i][0].GameNum;
if (s > bestScore) { bestScore = s; best = tips[i]; }
}
return best;
}
private static List GenerateDeck()
{
var d = new List(); int id = 1;
foreach (int f in new[] { 1, 2, 3, 4 })
for (int n = 1; n <= 13; n++)
d.Add(new TCardInfoPdkF { ID = (byte)id++, Flower = (byte)f, GameNum = (byte)(n == 1 ? 14 : n == 2 ? 16 : n), GameState = 1 });
return d;
}
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 };
}
}