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); // 终局精确求解:手牌≤5 + 未知牌少 → 替代 ISMCTS if (hand.Length <= 5 && _tracker.UnknownCount <= 15) { var endgameResult = EndgameSolver.Solve(hand, view, _tracker, _rng); if (endgameResult.HasValue) { var ep = endgameResult.Value; Console.WriteLine($"[ISMCTS pos{view.MyPos}] ENDGAME hand={hand.Length} unknown={_tracker.UnknownCount} → 精确求解"); return new PlayOutCardPdkF { Pos = (byte)view.MyPos, GameNum = ep.GameNum, Ids = ep.Ids, Type = CardType1.None }; } } // 锁手检测:残局时跳过 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.25; // max +25% } else if (am > beforeMin) { // 手牌被打散 → 惩罚 double degradation = (double)(am - beforeMin) / beforeMin; if (m) wr -= degradation * 0.30; // 多牌打散 → 严重(-30%) else wr -= degradation * 0.10; // 单张打散 → 中(-10%) } 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); } // 5.5. 领牌保留策略:有绝对最高牌(2)时,领牌优先出最小的 isFirstPlay = view.MaxPlayCard.GameNum <= 0; bool hasHighestCard = hand.Any(c => c.GameState == 1 && c.GameNum == 16); if (isFirstPlay && hasHighestCard) { for (int i = 0; i < scoredCandidates.Count; i++) { var (p, wr, am, m) = scoredCandidates[i]; if (!m && p.Ids != null && p.Ids.Length == 1) { int rank = p.GameNum; if (rank <= 6) wr += 0.15; else if (rank <= 10) wr += 0.05; else if (rank <= 13) wr -= 0.05; else wr -= 0.10; } 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 = GetDeckCopy(); 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; } // 静态牌池:52张牌,只创建一次 private static readonly TCardInfoPdkF[] FullDeck; static IsmctsBot() { 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 }; } private static List GetDeckCopy() { return FullDeck.Where(c => c.ID > 0).Select(c => new TCardInfoPdkF { ID = c.ID, Flower = c.Flower, GameNum = c.GameNum, GameState = 1 }).ToList(); } 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 }; } }