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 v2 — 优化版,静态牌池+数组交换替代对象分配。 /// /// 性能关键: /// - 48张牌静态数组,模拟时只交换索引不创建新对象 /// - 预生成N份对手手牌分布,分摊 determinization 成本 /// - 贪心 rollout 直接操作数组切片 /// public class SOIsmctsBot : IPdkBot { private readonly int _simBudget; private readonly Random _rng; private readonly CardTracker _tracker = new CardTracker(); // 静态牌池:52张牌(同 IsmctsBot.GenerateDeck),只创建一次 private static readonly TCardInfoPdkF[] FullDeck; static SOIsmctsBot() { 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 }; } 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 (SimulateToEnd(view.MyHand, view, null)) wins++; double wr = (double)wins / 400; Console.WriteLine($"[SOv2 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 <= 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($"[SOv2 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; // SO-ISMCTS 主循环:单树 + 预生成手牌 var root = new SONode(candidates.Count); int myPos = view.MyPos; // 预生成 N 份对手手牌分布 int preGen = Math.Min(300, _simBudget); var hands1 = new byte[preGen][]; var hands2 = new byte[preGen][]; // 建立已知牌ID集合 var knownIds = new HashSet(); foreach (var c in hand) knownIds.Add(c.ID); int o1c = view.ShenYuCard[myPos % 3]; int o2c = view.ShenYuCard[(myPos + 1) % 3]; var pool = new byte[52 - knownIds.Count]; int pi = 0; foreach (var c in FullDeck) if (!knownIds.Contains(c.ID)) pool[pi++] = c.ID; for (int g = 0; g < preGen; g++) { Shuffle(pool, _rng); hands1[g] = new byte[o1c]; hands2[g] = new byte[o2c]; Array.Copy(pool, 0, hands1[g], 0, o1c); Array.Copy(pool, o1c, hands2[g], 0, o2c); } int genIdx = 0; int totalSims = 0; for (int iter = 0; iter < _simBudget; iter++) { int ci = _rng.Next(candidates.Count); var play = candidates[ci]; // 取一份预生成手牌 var o1 = hands1[genIdx % preGen]; var o2 = hands2[genIdx % preGen]; genIdx++; bool win; if (play.play.Type == CardType1.None) // pass { win = SimPass(hand, view, o1, o2); } else { var myRem = hand.Where(c => c.GameState == 1 && !play.play.Ids.Contains(c.ID)).ToArray(); if (myRem.Length == 0) win = true; else win = SimAfterPlay(myRem, view, o1, o2, play.play.GameNum); } root.Visit(ci, win); totalSims++; if (totalSims % 150 == 0) { int converged = root.BestChild(candidates.Count); if (converged >= 0 && root.VisitCount(converged) > _simBudget / candidates.Count / 2 && root.WinRate(converged) > 0.85) break; } } int bestIdx = root.BestChild(candidates.Count); if (bestIdx < 0) bestIdx = 0; Console.WriteLine($"[SOv2 pos{view.MyPos}] {totalSims}s best={bestIdx} wr={root.WinRate(bestIdx):P0}"); return candidates[bestIdx].play; } // ---- 快速模拟(用数组而非对象) ---- private bool SimPass(TCardInfoPdkF[] myHand, PdkBotView view, byte[] o1Ids, byte[] o2Ids) { int myPos = view.MyPos; var maxPlay = view.MaxPlayCard; int maxPlayer = maxPlay.Pos > 0 ? maxPlay.Pos - 1 : myPos - 1; int current = myPos % 3; // 从 ID 数组构建手牌 GameNum 数组 var mg = new List(myHand.Where(c => c.GameState == 1).Select(c => c.GameNum)); var o1g = new List(IdsToGameNums(o1Ids)); var o2g = new List(IdsToGameNums(o2Ids)); return FastSim(o1g, o2g, mg, current, maxPlay.GameNum, maxPlay.Type, maxPlayer, 0); } private bool SimAfterPlay(TCardInfoPdkF[] myRem, PdkBotView view, byte[] o1Ids, byte[] o2Ids, byte myGameNum) { int myPos = view.MyPos; int current = myPos % 3; var mg = new List(myRem.Where(c => c.GameState == 1).Select(c => c.GameNum)); var o1g = new List(IdsToGameNums(o1Ids)); var o2g = new List(IdsToGameNums(o2Ids)); return FastSim(o1g, o2g, mg, current, myGameNum, CardType1.DanZhang, myPos - 1, 0); } // 快速模拟核心:全部用 byte[]。支持单张+对子。 private bool FastSim(List h1, List h2, List my, int current, byte maxGameNum, CardType1 maxType, int maxPlayer, int depth) { if (depth > 30) return my.Count <= Math.Min(h1.Count, h2.Count); if (h1.Count == 0) return false; if (h2.Count == 0) return false; if (my.Count == 0) return true; var cur = current == 0 ? h1 : current == 1 ? h2 : my; bool isNewRound = maxPlayer == current || maxGameNum == 0; if (isNewRound) { if (cur.Count == 0) return false; // 找对子(贪心:出最大的对子) var groups = cur.GroupBy(g => g).Where(g => g.Count() >= 2).OrderByDescending(g => g.Key).ToList(); if (groups.Count > 0) { var pair = groups[0]; var toRemove = pair.Take(2).ToList(); foreach (var g in toRemove) cur.Remove(g); return FastSim(h1, h2, my, (current + 1) % 3, pair.Key, CardType1.DuiZi, current, depth + 1); } // 单张 var sorted = cur.OrderByDescending(g => g).ToList(); byte play = sorted[0]; cur.Remove(play); return FastSim(h1, h2, my, (current + 1) % 3, play, CardType1.DanZhang, current, depth + 1); } else { // 跟牌:找能压制的同类型牌 var bigger = cur.Where(g => g > maxGameNum).OrderBy(g => g).ToList(); if (maxType == CardType1.DuiZi) { // 对子:找 ≥2 张且大于 maxGameNum var pairGroups = cur.GroupBy(g => g).Where(g => g.Count() >= 2 && g.Key > maxGameNum).OrderBy(g => g.Key).ToList(); if (pairGroups.Count > 0) { var pair = pairGroups[0]; var toRemove = pair.Take(2).ToList(); foreach (var g in toRemove) cur.Remove(g); return FastSim(h1, h2, my, (current + 1) % 3, pair.Key, CardType1.DuiZi, current, depth + 1); } } if (bigger.Count > 0) { byte play = bigger[0]; cur.Remove(play); return FastSim(h1, h2, my, (current + 1) % 3, play, CardType1.DanZhang, current, depth + 1); } // pass return FastSim(h1, h2, my, (current + 1) % 3, maxGameNum, maxType, isNewRound ? current : maxPlayer, depth + 1); } } // ---- 旧的 SimulateToEnd (保留兼容) ---- private bool SimulateToEnd(TCardInfoPdkF[] myHand, PdkBotView view, PlayOutCardPdkF? fp) { int myPos = view.MyPos; var myList = myHand.Where(c => c.GameState == 1).Select(c => c.GameNum).ToList(); int o1c = view.ShenYuCard[myPos % 3]; int o2c = view.ShenYuCard[(myPos + 1) % 3]; var knownIds = new HashSet(); foreach (var c in myHand) knownIds.Add(c.ID); var pool = FullDeck.Where(c => !knownIds.Contains(c.ID)).Select(c => c.GameNum).ToList(); Shuffle(pool, _rng); var o1 = pool.Take(o1c).ToList(); var o2 = pool.Skip(o1c).Take(o2c).ToList(); byte maxGn = 0; int maxP = 0; int cur = myPos % 3; if (fp.HasValue && fp.Value.Type != CardType1.None) { maxGn = fp.Value.GameNum; maxP = myPos - 1; foreach (var id in fp.Value.Ids ?? Array.Empty()) myList.RemoveAll(g => g == GameNumOf(id)); } else if (fp.HasValue) // pass { maxGn = view.MaxPlayCard.GameNum; maxP = view.MaxPlayCard.Pos > 0 ? view.MaxPlayCard.Pos - 1 : myPos - 1; } return FastSim(o1, o2, myList, cur, maxGn, CardType1.DanZhang, maxP, 0); } // ---- 辅助 ---- 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((MakePlay(t, view.MyPos), t.Count > 1)); if (isFirstPlay) AddMultiCardLeads(hand, view.MyPos, candidates); if (!isFirstPlay) candidates.Add((Pass(view), false)); return candidates; } // 复用 IsmctsBot 的多牌枚举(对子/三张/顺子/飞机/炸弹/连对) private static void AddMultiCardLeads(TCardInfoPdkF[] hand, int pos, List<(PlayOutCardPdkF, bool)> candidates) { var make = (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) make(b); foreach (var g in hand.GroupBy(c => c.GameNum).Where(g => g.Count() >= 2)) make(g.Take(2).ToList()); foreach (var g in hand.GroupBy(c => c.GameNum).Where(g => g.Count() >= 3)) make(g.Take(3).ToList()); var straights = PokerLogic.GetShunZi(hand); if (straights != null) foreach (var s in straights) make(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 CardType1.SanZhang; if (cnts[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 = CardType1.DanZhang }; private static PlayOutCardPdkF Pass(PdkBotView v) => new PlayOutCardPdkF { Pos = (byte)v.MyPos, Type = CardType1.None }; private static List IdsToGameNums(byte[] ids) { var result = new List(); foreach (var id in ids) { var card = FullDeck.FirstOrDefault(c => c.ID == id); if (card.GameNum > 0) result.Add(card.GameNum); } return result; } private static byte GameNumOf(byte id) { var card = FullDeck.FirstOrDefault(c => c.ID == id); return card.GameNum; } private static void Shuffle(T[] arr, Random rng) { for (int i = arr.Length - 1; i > 0; i--) { int j = rng.Next(i + 1); (arr[i], arr[j]) = (arr[j], arr[i]); } } 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; private readonly int[] _visits; public SONode(int numChildren) { _wins = new int[numChildren]; _visits = new int[numChildren]; } public void Visit(int idx, bool win) { if (idx < 0 || idx >= _visits.Length) return; _visits[idx]++; if (win) _wins[idx]++; } public double WinRate(int idx) => idx >= 0 && idx < _visits.Length && _visits[idx] > 0 ? (double)_wins[idx] / _visits[idx] : 0; public int VisitCount(int idx) => idx >= 0 && idx < _visits.Length ? _visits[idx] : 0; public int BestChild(int n) { int best = -1; double bestRate = -1; for (int i = 0; i < Math.Min(n, _visits.Length); i++) { double rate = WinRate(i); if (rate > bestRate) { bestRate = rate; best = i; } } return best; } } }