using System;
using System.Collections.Generic;
using System.Linq;
using GameFix.PaoDeKuaiF;
using GameFix.Poker;
using GameMessage;
using GameMessage.PaoDeKuaiF;
namespace PdkFriendServer.Logic
{
///
/// ISMCTS 机器人 v2
/// P0修复: 包庄阈值 70%(从 45% 校准),rollout 用最强方案而非最小
/// P2修复: 优先多牌组合(对子/顺子 > 单张),提高清牌效率
///
public class IsmctsBot : IPdkBot
{
private readonly int _simulations;
private readonly Random _rng;
private const double BaoThreshold = 0.60; // 校准自1000局压测:70%→0人包,45%→5.2%成功
public IsmctsBot(int simulations = 800)
{
_simulations = simulations;
_rng = new Random();
}
// ---- 包庄决策 ----
public bool DecideBaoZhuang(PdkBotView view)
{
double winRate = EstimateWinProbability(view);
bool shouldBao = winRate >= BaoThreshold;
Console.WriteLine($"[ISMCTS pos{view.MyPos}] 包庄评估: P(win)={winRate:P0} "
+ $"(阈值{BaoThreshold:P0}) → {(shouldBao ? "包庄" : "不包")}");
return shouldBao;
}
private double EstimateWinProbability(PdkBotView view)
{
var hand = view.MyHand;
int sims = Math.Min(_simulations, 600);
int wins = 0;
for (int s = 0; s < sims; s++)
{
if (SimulateWin(hand, view))
wins++;
}
return (double)wins / sims;
}
private bool SimulateWin(TCardInfoPdkF[] myHand, PdkBotView view)
{
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();
Shuffle(unknown);
hands[nextPos - 1].AddRange(unknown.Take(view.ShenYuCard[nextPos - 1]));
hands[oppPos - 1].AddRange(unknown.Skip(view.ShenYuCard[nextPos - 1])
.Take(view.ShenYuCard[oppPos - 1]));
int current = myPos;
var maxPlay = new PlayOutCardPdkF();
int maxPlayer = 0;
int passStreak = 0;
for (int move = 0; move < 400; move++)
{
var curHand = hands[current - 1];
if (curHand.Count == 0) return current == myPos;
bool isNewRound = maxPlayer == current || maxPlay.GameNum == 0;
var curMax = isNewRound ? new PlayOutCardPdkF() : maxPlay;
var tips = PdkCardAlgorithm.GetTipCard(curMax, curHand.ToArray(), null, false);
if (tips != null && tips.Count > 0)
{
// P2修复: 优先多牌组合,其次选最强(最大GameNum)
var pick = PickBestTip(tips, isNewRound);
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
{
passStreak++;
// 防止死循环: 三轮全 pass 则强制最大出牌
if (passStreak >= 2)
{
var forceTips = PdkCardAlgorithm.GetTipCard(
new PlayOutCardPdkF(), curHand.ToArray(), null, false);
if (forceTips != null && forceTips.Count > 0)
{
var pick = PickBestTip(forceTips, 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;
}
return hands[myPos - 1].Count <= hands[nextPos - 1].Count
&& hands[myPos - 1].Count <= hands[oppPos - 1].Count;
}
// ---- 出牌决策 ----
public PlayOutCardPdkF DecidePlay(PdkBotView view)
{
var hand = view.MyHand;
if (hand.Length == 0) return Pass(view);
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)>();
var multiCount = 0;
if (tips != null)
foreach (var tip in tips)
{
var isMulti = tip.Count > 1;
if (isMulti) multiCount++;
allCandidates.Add((MakePlay(tip, view.MyPos), isMulti));
}
// 2. 新轮次:补充多牌先手方案(GetTipCard只返回单张)
if (isFirstPlay)
{
AddMultiCardLeads(hand, view.MyPos, allCandidates, ref multiCount);
}
if (!isFirstPlay)
allCandidates.Add((Pass(view), false));
var scoredCandidates = new List<(PlayOutCardPdkF play, double winRate, bool isMulti)>();
if (allCandidates.Count == 1)
return allCandidates[0].play;
int unknownCount = 48 - hand.Length - CountPlayed(hand, view);
int simsPerCandidate = Math.Max(50, _simulations / allCandidates.Count);
for (int ci = 0; ci < allCandidates.Count; ci++)
{
int wins = 0;
var play = allCandidates[ci].play;
for (int s = 0; s < simsPerCandidate; s++)
{
if (SimulateOneGame(hand, play, view, unknownCount))
wins++;
}
double wr = (double)wins / simsPerCandidate;
scoredCandidates.Add((play, wr, allCandidates[ci].isMulti));
}
// 两阶段选择:多牌组合胜率≥10%就优用
scoredCandidates.Sort((a, b) => b.winRate.CompareTo(a.winRate));
var best = scoredCandidates[0];
if (multiCount > 0)
{
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)
best = bestMulti;
}
string desc = best.play.Ids == null || best.play.Ids.Length == 0
? "pass"
: $"{best.play.Ids.Length}c";
Console.WriteLine($"[ISMCTS pos{view.MyPos}] {simsPerCandidate * allCandidates.Count}sims "
+ $"winRate:{best.winRate:P0} => {desc}");
return best.play;
}
// ---- 模拟引擎 ----
private bool SimulateOneGame(
TCardInfoPdkF[] myHand, PlayOutCardPdkF myPlay,
PdkBotView view, int unknownCount)
{
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();
Shuffle(unknown);
hands[nextPos - 1].AddRange(unknown.Take(view.ShenYuCard[nextPos - 1]));
hands[oppPos - 1].AddRange(unknown.Skip(view.ShenYuCard[nextPos - 1])
.Take(view.ShenYuCard[oppPos - 1]));
var maxPlay = new PlayOutCardPdkF
{
Pos = (byte)myPos, GameNum = myPlay.GameNum,
Type = myPlay.Type, Ids = myPlay.Ids?.ToArray()
};
if (myPlay.Type != CardType1.None && myPlay.Ids != null)
foreach (var id in myPlay.Ids)
hands[myPos - 1].RemoveAll(c => c.ID == id);
int current = myPos % 3 + 1;
int maxPlayer = myPlay.Type == CardType1.None ? view.MaxPlayCard.Pos : myPos;
if (myPlay.Type == CardType1.None)
{
maxPlayer = view.MaxPlayCard.Pos;
maxPlay = CloneCard(view.MaxPlayCard);
}
int moveCount = 0;
int passStreak = 0;
while (moveCount++ < 500)
{
var curHand = hands[current - 1];
if (curHand.Count == 0) return current == myPos;
bool isNewRound = maxPlayer == current || maxPlay.GameNum == 0;
var curMax = isNewRound ? new PlayOutCardPdkF() : maxPlay;
var curTips = PdkCardAlgorithm.GetTipCard(curMax, curHand.ToArray(), null, false);
if (curTips != null && curTips.Count > 0)
{
var pick = PickBestTip(curTips, isNewRound);
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
{
passStreak++;
if (passStreak >= 2)
{
var forceTips = PdkCardAlgorithm.GetTipCard(
new PlayOutCardPdkF(), curHand.ToArray(), null, false);
if (forceTips != null && forceTips.Count > 0)
{
var pick = PickBestTip(forceTips, 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;
}
return hands[myPos - 1].Count <= hands[nextPos - 1].Count
&& hands[myPos - 1].Count <= hands[oppPos - 1].Count;
}
// ---- Rollout 策略 ----
///
/// 新轮次先手时,补充多牌组合候选(GetTipCard 只返回单张)
/// 枚举:对子、三张、顺子、连对、炸弹
///
private static void AddMultiCardLeads(TCardInfoPdkF[] hand, int pos,
List<(PlayOutCardPdkF play, bool isMulti)> candidates, ref int multiCount)
{
// 炸弹
if (PokerLogic.GetPlayZhaDans(hand, out var bombs) && bombs != null)
foreach (var bomb in bombs)
AddCandidate(bomb, pos, candidates, ref multiCount);
// 对子(2张相同)
foreach (var pair in GetMultiGroups(hand, 2))
AddCandidate(pair, pos, candidates, ref multiCount);
// 三张 / 三带二
var triples = GetMultiGroups(hand, 3);
foreach (var t in triples)
{
// 三带二: 三张 + 附带最小的两张其他牌
var remainder = hand.Where(c => c.GameNum != t[0].GameNum).OrderBy(c => c.GameNum).ToList();
if (remainder.Count >= 2)
{
var sd2 = t.Concat(remainder.Take(2)).ToList();
AddCandidate(sd2, pos, candidates, ref multiCount);
}
else
{
AddCandidate(t, pos, candidates, ref multiCount); // 手牌不够带,纯三张
}
}
// 四带二 / 四带三
var quads = GetMultiGroups(hand, 4);
foreach (var q in quads)
{
var remainder = hand.Where(c => c.GameNum != q[0].GameNum).OrderBy(c => c.GameNum).ToList();
if (remainder.Count >= 3)
{
var sd3 = q.Concat(remainder.Take(3)).ToList();
AddCandidate(sd3, pos, candidates, ref multiCount);
}
else if (remainder.Count >= 2)
{
var sd2 = q.Concat(remainder.Take(2)).ToList();
AddCandidate(sd2, pos, candidates, ref multiCount);
}
// else: 炸弹已在上面处理
}
// 顺子(≥5 张连续)
var straights = PokerLogic.GetShunZi(hand);
if (straights != null)
foreach (var s in straights)
AddCandidate(s, pos, candidates, ref multiCount);
}
private static void AddCandidate(List cards, int pos,
List<(PlayOutCardPdkF play, bool isMulti)> candidates, ref int multiCount)
{
candidates.Add((new PlayOutCardPdkF
{
Pos = (byte)pos, GameNum = cards[0].GameNum,
Ids = cards.Select(c => c.ID).ToArray(),
Type = CardType1.None
}, true));
multiCount++;
}
/// 获取指定数量的同面值组合(对子=2,三张=3)
private static List> GetMultiGroups(TCardInfoPdkF[] hand, int count)
{
var result = new List>();
var groups = hand.GroupBy(c => c.GameNum).Where(g => g.Count() >= count);
foreach (var g in groups)
{
var cards = g.Take(count).ToList();
result.Add(cards);
}
return result;
}
///
/// 从合法方案中选最优:优先出多张牌的组合,其次出最大的
/// P2修复: 之前只取 tips[0](最小单张),改为优先多牌组合
/// P0修复: 选最大(最后一项),模拟更强对手
///
private static List PickBestTip(List> tips, bool isNewRound)
{
if (tips == null || tips.Count == 0) return null;
// 优先多牌组合(2张>1张,3张>2张等),同数量选最大的
var best = tips[tips.Count - 1]; // 默认最大
int bestScore = best.Count * 10 + best[0].GameNum;
for (int i = tips.Count - 2; i >= 0; i--)
{
int score = tips[i].Count * 10 + tips[i][0].GameNum;
if (score > bestScore)
{
bestScore = score;
best = tips[i];
}
}
return best;
}
// ---- 工具 ----
private static int CountPlayed(TCardInfoPdkF[] myHand, PdkBotView view)
{
int total = 48 - myHand.Length;
foreach (var c in view.ShenYuCard) total -= c;
return Math.Max(0, total);
}
private static List GenerateDeck()
{
var deck = new List();
int id = 1;
foreach (int flower in new[] { 1, 2, 3, 4 })
for (int num = 1; num <= 13; num++)
{
int gameNum = num switch { 1 => 14, 2 => 16, _ => num };
deck.Add(new TCardInfoPdkF
{
ID = (byte)id++, Flower = (byte)flower,
GameNum = (byte)gameNum, GameState = 1
});
}
return deck;
}
private void Shuffle(List list)
{
for (int i = list.Count - 1; i > 0; i--)
{
int j = _rng.Next(i + 1);
(list[i], list[j]) = (list[j], list[i]);
}
}
private static PlayOutCardPdkF CloneCard(PlayOutCardPdkF src)
{
return new PlayOutCardPdkF
{
Pos = src.Pos, GameNum = src.GameNum,
Type = src.Type, Ids = src.Ids?.ToArray()
};
}
private static PlayOutCardPdkF MakePlay(List cards, int pos)
{
return new PlayOutCardPdkF
{
Pos = (byte)pos, GameNum = cards[0].GameNum,
Ids = cards.Select(x => x.ID).ToArray(),
Type = CardType1.None // 不给假类型,让引擎GetOutCard赋予真值
};
}
private static PlayOutCardPdkF Pass(PdkBotView view)
{
return new PlayOutCardPdkF { Pos = (byte)view.MyPos, Type = CardType1.None };
}
}
}