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