using System; using System.Collections.Generic; using System.Linq; using GameFix.PaoDeKuaiF; using GameFix.Poker; using GameMessage; using GameMessage.PaoDeKuaiF; namespace PdkFriendServer.Logic { /// /// SO-ISMCTS v4 — 引擎级 rollout,支持全部牌型。 /// /// 核心改进:用 PdkCardAlgorithm.GetTipCard 替代字节级贪心模拟, /// 支持单张/对子/三张/三带二/顺子/连对/飞机/炸弹/四带二/飞机带对。 /// /// Rollout 策略:优先清牌效率(多牌优先),但保留大牌控制权。 /// public class SOIsmctsBot : IPdkBot { private readonly int _simBudget; private readonly Random _rng; private readonly CardTracker _tracker = new CardTracker(); // 静态牌池:52张牌,只创建一次,ID→card 快速查找 private static readonly TCardInfoPdkF[] FullDeck; static SOIsmctsBot() { FullDeck = new TCardInfoPdkF[53]; // 1-indexed int id = 1; foreach (int f in new[] { 1, 2, 3, 4 }) for (int n = 1; n <= 13; n++) FullDeck[id] = new TCardInfoPdkF { ID = (byte)id, Flower = (byte)f, GameNum = (byte)(n == 1 ? 14 : n == 2 ? 16 : n), GameState = 1 }; } public SOIsmctsBot(int simBudget = 800) { _simBudget = simBudget; _rng = new Random(); } public bool DecideBaoZhuang(PdkBotView view) { _tracker.Update(view); int minPlays = HandOptimizer.MinPlays(view.MyHand); if (minPlays >= 7) return false; double threshold = minPlays <= 3 ? 0.50 : minPlays <= 4 ? 0.60 : minPlays <= 5 ? 0.65 : 0.70; int wins = 0; for (int s = 0; s < 400; s++) if (Rollout(view.MyHand, view, null)) wins++; double wr = (double)wins / 400; Console.WriteLine($"[SOv4 pos{view.MyPos}] 包庄: min={minPlays} wr={wr:P0} → {(wr >= threshold ? "包" : "不")}"); return wr >= threshold; } public PlayOutCardPdkF DecidePlay(PdkBotView view) { _tracker.Update(view); var hand = view.MyHand; if (hand.Length == 0) return Pass(view); if (hand.Length <= 5 && _tracker.UnknownCount(hand) <= 15) { var eg = EndgameSolver.Solve(hand, view, _tracker, _rng); if (eg.HasValue) return eg.Value; } if (hand.Length <= 8) { var lockSeq = LockDetector.FindLockSequence(hand, _tracker, view.MyPos); if (lockSeq != null) { Console.WriteLine($"[SOv4 pos{view.MyPos}] LOCK {lockSeq.Count}步"); return new PlayOutCardPdkF { Pos = (byte)view.MyPos, GameNum = lockSeq[0].GameNum, Ids = lockSeq[0].Ids, Type = CardType1.None }; } } bool isFirstPlay = view.MaxPlayCard.GameNum <= 0; var candidates = BuildCandidates(hand, view, isFirstPlay); if (candidates.Count == 1) return candidates[0].play; var root = new SONode(candidates.Count); int myPos = view.MyPos; int o1c = view.ShenYuCard[myPos % 3]; int o2c = view.ShenYuCard[(myPos + 1) % 3]; // 预生成对手手牌 (TCardInfoPdkF) var knownIds = new HashSet(); foreach (var c in hand) knownIds.Add(c.ID); var poolCards = FullDeck.Where(c => c.ID > 0 && !knownIds.Contains(c.ID)).ToList(); int preGen = Math.Min(200, _simBudget); var preHands1 = new TCardInfoPdkF[preGen][]; var preHands2 = new TCardInfoPdkF[preGen][]; for (int g = 0; g < preGen; g++) { Shuffle(poolCards, _rng); preHands1[g] = poolCards.Take(o1c).ToArray(); preHands2[g] = poolCards.Skip(o1c).Take(o2c).ToArray(); } int genIdx = 0, totalSims = 0; for (int iter = 0; iter < _simBudget; iter++) { int ci = _rng.Next(candidates.Count); var (play, isMulti) = candidates[ci]; // 用预生成的对手手牌 genIdx = (genIdx + 1) % preGen; bool win = Rollout(hand, view, play, preHands1[genIdx], preHands2[genIdx]); root.Visit(ci, win); totalSims++; } int bestIdx = root.BestChild(candidates.Count); if (bestIdx < 0) bestIdx = 0; Console.WriteLine($"[SOv4 pos{view.MyPos}] {totalSims}s best={bestIdx}/{candidates.Count} wr={root.WinRate(bestIdx):P0}"); return candidates[bestIdx].play; } // ---- Rollout: 引擎级全牌型模拟 ---- /// /// Rollout 一局到终局。firstPlay=null 时不改状态直接从当前桌面开始。 /// private bool Rollout(TCardInfoPdkF[] myHand, PdkBotView view, PlayOutCardPdkF? myPlay = null, TCardInfoPdkF[] o1Hand = null, TCardInfoPdkF[] o2Hand = null) { int myPos = view.MyPos; var my = myHand.Where(c => c.GameState == 1).ToList(); if (myPlay.HasValue && myPlay.Value.Type != CardType1.None && myPlay.Value.Ids != null) { foreach (var id in myPlay.Value.Ids) my.RemoveAll(c => c.ID == id); if (my.Count == 0) return true; } // 构建或取样对手手牌 List o1, o2; if (o1Hand != null) { o1 = new List(o1Hand); o2 = new List(o2Hand); } else { var known = new HashSet(); foreach (var c in myHand) known.Add(c.ID); var pool = FullDeck.Where(c => c.ID > 0 && !known.Contains(c.ID)).ToList(); Shuffle(pool, _rng); int o1c = view.ShenYuCard[myPos % 3]; o1 = pool.Take(o1c).ToList(); o2 = pool.Skip(o1c).Take(view.ShenYuCard[(myPos + 1) % 3]).ToList(); } // 初始状态 PlayOutCardPdkF maxPlay; int maxPlayer, current; if (myPlay.HasValue && myPlay.Value.Type != CardType1.None) { maxPlay = ClonePlay(myPlay.Value); maxPlayer = myPos - 1; current = myPos % 3; } else { maxPlay = ClonePlay(view.MaxPlayCard); maxPlayer = maxPlay.Pos > 0 ? maxPlay.Pos - 1 : myPos - 1; current = myPos % 3; } return SimLoop(my, o1, o2, current, maxPlay, maxPlayer, 0); } private bool SimLoop(List my, List o1, List o2, int current, PlayOutCardPdkF maxPlay, int maxPlayer, int depth) { if (depth > 50) return my.Count <= Math.Min(o1.Count, o2.Count); if (o1.Count == 0) return false; if (o2.Count == 0) return false; if (my.Count == 0) return true; var cur = current == 0 ? o1 : current == 1 ? o2 : my; bool isNewRound = maxPlayer == current || maxPlay.GameNum <= 0; int next = (current + 1) % 3; // 用引擎获取合法出牌 var tips = PdkCardAlgorithm.GetTipCard( isNewRound ? new PlayOutCardPdkF() : maxPlay, cur.ToArray(), null, false); if (tips != null && tips.Count > 0) { // Rollout 策略:多牌优先 + 最小 rank 保留大牌 // isNewRound 时 GetTipCard 只返回单张 → 需要补充多牌先手 List> allPlays = new List>(tips); if (isNewRound) { var arr = cur.ToArray(); // 对子 (2张同rank) foreach (var g in arr.GroupBy(c => c.GameNum).Where(g => g.Count() >= 2)) allPlays.Add(g.Take(2).ToList()); // 三张 foreach (var g in arr.GroupBy(c => c.GameNum).Where(g => g.Count() >= 3)) allPlays.Add(g.Take(3).ToList()); // 顺子 (≥5张连续rank) var ranks = arr.Select(c => (int)c.GameNum).Where(r => r >= 3 && r <= 14).Distinct().OrderBy(r => r).ToList(); for (int len = Math.Min(ranks.Count, 12); len >= 5; len--) for (int si = 0; si <= ranks.Count - len; si++) { bool isConsecutive = true; for (int i = 1; i < len; i++) if (ranks[si + i] != ranks[si + i - 1] + 1) { isConsecutive = false; break; } if (!isConsecutive) continue; var strCards = ranks.Skip(si).Take(len) .SelectMany(r => arr.Where(c => c.GameNum == r).Take(1)).ToList(); if (strCards.Count == len) allPlays.Add(strCards); } } // 选择:优先多牌(×10权重),同长度选最小rank var best = allPlays .OrderByDescending(t => t.Count * 10) .ThenBy(t => t[0].GameNum) .First(); foreach (var c in best) cur.RemoveAll(x => x.ID == c.ID); var newMax = new PlayOutCardPdkF { GameNum = best[0].GameNum, Type = InferType(best), Ids = best.Select(c => c.ID).ToArray() }; return SimLoop(my, o1, o2, next, newMax, current, depth + 1); } // pass int newMaxPlayer = isNewRound ? current : maxPlayer; return SimLoop(my, o1, o2, next, maxPlay, newMaxPlayer, depth + 1); } // ---- 候选构建 ---- private List<(PlayOutCardPdkF play, bool isMulti)> BuildCandidates( TCardInfoPdkF[] hand, PdkBotView view, bool isFirstPlay) { var tips = PdkCardAlgorithm.GetTipCard(view.MaxPlayCard, hand, null, view.Rule.AAAIsZhaDan); var candidates = new List<(PlayOutCardPdkF, bool)>(); if (tips != null) foreach (var t in tips) candidates.Add((new PlayOutCardPdkF { Pos = (byte)view.MyPos, GameNum = t[0].GameNum, Ids = t.Select(c => c.ID).ToArray(), Type = InferType(t) }, t.Count > 1)); if (isFirstPlay) AddMultiCardLeads(hand, view.MyPos, candidates); if (!isFirstPlay) candidates.Add((Pass(view), false)); return candidates; } private static void AddMultiCardLeads(TCardInfoPdkF[] hand, int pos, List<(PlayOutCardPdkF, bool)> candidates) { void add(List cards) => candidates.Add(( new PlayOutCardPdkF { Pos = (byte)pos, GameNum = cards[0].GameNum, Ids = cards.Select(c => c.ID).ToArray(), Type = InferType(cards) }, true)); if (PokerLogic.GetPlayZhaDans(hand, out var bombs) && bombs != null) foreach (var b in bombs) add(b); foreach (var g in hand.GroupBy(c => c.GameNum).Where(g => g.Count() >= 2)) add(g.Take(2).ToList()); foreach (var g in hand.GroupBy(c => c.GameNum).Where(g => g.Count() >= 3)) { var t = g.Take(3).ToList(); var rest = hand.Where(c => c.GameNum != g.Key).Take(2).ToList(); add(rest.Count == 2 ? t.Concat(rest).ToList() : t); } var straights = PokerLogic.GetShunZi(hand); if (straights != null) foreach (var s in straights) add(s); } // ---- 辅助 ---- private static CardType1 InferType(List cards) { int n = cards.Count; if (n == 1) return CardType1.DanZhang; if (n == 2) return CardType1.DuiZi; var cnts = cards.GroupBy(c => c.GameNum).Select(g => g.Count()).OrderByDescending(x => x).ToList(); if (cnts[0] == 4) { if (n == 4) return CardType1.ZhaDan; if (n == 6) return CardType1.SiDai2; if (n == 7) return CardType1.SiDai3; } if (cnts[0] >= 3) return n == 3 ? CardType1.SanZhang : CardType1.SanDai2; if (cnts[0] == 2 && n % 2 == 0) return n == 4 ? CardType1.LianDui : CardType1.LianDui; return n >= 5 ? CardType1.ShunZi : CardType1.DanZhang; } private static PlayOutCardPdkF Pass(PdkBotView v) => new PlayOutCardPdkF { Pos = (byte)v.MyPos, Type = CardType1.None }; private static PlayOutCardPdkF ClonePlay(PlayOutCardPdkF s) => new PlayOutCardPdkF { Pos = s.Pos, GameNum = s.GameNum, Type = s.Type, Ids = s.Ids?.ToArray() }; private static void Shuffle(List list, Random rng) { for (int i = list.Count - 1; i > 0; i--) { int j = rng.Next(i + 1); (list[i], list[j]) = (list[j], list[i]); } } } internal class SONode { private readonly int[] _wins, _visits; public SONode(int n) { _wins = new int[n]; _visits = new int[n]; } public void Visit(int i, bool w) { if (i >= 0 && i < _visits.Length) { _visits[i]++; if (w) _wins[i]++; } } public double WinRate(int i) => i >= 0 && i < _visits.Length && _visits[i] > 0 ? (double)_wins[i] / _visits[i] : 0; public int VisitCount(int i) => i >= 0 && i < _visits.Length ? _visits[i] : 0; public int BestChild(int n) { int best = -1; double bestRate = -1; for (int i = 0; i < Math.Min(n, _visits.Length); i++) { double r = WinRate(i); if (r > bestRate) { bestRate = r; best = i; } } return best; } } }