diff --git a/PdkFriendServer/Logic/IsmctsBot.cs b/PdkFriendServer/Logic/IsmctsBot.cs index 260de9c7..62fb6c9e 100644 --- a/PdkFriendServer/Logic/IsmctsBot.cs +++ b/PdkFriendServer/Logic/IsmctsBot.cs @@ -9,14 +9,15 @@ using GameMessage.PaoDeKuaiF; namespace PdkFriendServer.Logic { /// - /// ISMCTS (Information Set Monte Carlo Tree Search) 机器人 - /// 对每个候选出牌方案模拟 N 局随机对局,选胜率最高的方案 - /// 只看到自己手牌+桌面信息——对手手牌从未知牌池随机采样 + /// 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) { @@ -24,24 +25,22 @@ namespace PdkFriendServer.Logic _rng = new Random(); } + // ---- 包庄决策 ---- + public bool DecideBaoZhuang(PdkBotView view) { - // ISMCTS 包庄决策:模拟 N 局,估算自己第一个出完的概率 - // 包庄不改变牌局流程,只翻倍输赢:P(win)*2*gain - P(lose)*2*loss - // 阈值 45%——因为包庄成功收益翻倍,略微倾斜有利于局势占优 double winRate = EstimateWinProbability(view); - bool shouldBao = winRate >= 0.45; + bool shouldBao = winRate >= BaoThreshold; - Console.WriteLine($"[ISMCTS pos{view.MyPos}] 包庄评估: P(win)={winRate:P0} → {(shouldBao ? "包庄" : "不包")}"); + Console.WriteLine($"[ISMCTS pos{view.MyPos}] 包庄评估: P(win)={winRate:P0} " + + $"(阈值{BaoThreshold:P0}) → {(shouldBao ? "包庄" : "不包")}"); return shouldBao; } - /// 模拟 N 局随机对局,统计我第一个出完的概率 private double EstimateWinProbability(PdkBotView view) { - int myPos = view.MyPos; var hand = view.MyHand; - int sims = Math.Min(_simulations, 600); // 包庄模拟用 600 次,比出牌少 + int sims = Math.Min(_simulations, 600); int wins = 0; for (int s = 0; s < sims; s++) @@ -52,7 +51,6 @@ namespace PdkFriendServer.Logic return (double)wins / sims; } - /// 模拟一局只看胜负——不指定第一步出什么 private bool SimulateWin(TCardInfoPdkF[] myHand, PdkBotView view) { int myPos = view.MyPos; @@ -68,15 +66,14 @@ namespace PdkFriendServer.Logic var unknown = allCards.Where(c => !myHand.Any(h => h.ID == c.ID)).ToList(); Shuffle(unknown); - int nextCnt = view.ShenYuCard[nextPos - 1]; - int oppCnt = view.ShenYuCard[oppPos - 1]; - hands[nextPos - 1].AddRange(unknown.Take(nextCnt)); - hands[oppPos - 1].AddRange(unknown.Skip(nextCnt).Take(oppCnt)); + 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(); // GameNum=0 → 新一轮 + var maxPlay = new PlayOutCardPdkF(); int maxPlayer = 0; + int passStreak = 0; for (int move = 0; move < 400; move++) { @@ -89,7 +86,8 @@ namespace PdkFriendServer.Logic if (tips != null && tips.Count > 0) { - var pick = tips[0]; + // P2修复: 优先多牌组合,其次选最强(最大GameNum) + var pick = PickBestTip(tips, isNewRound); maxPlay = new PlayOutCardPdkF { Pos = (byte)current, @@ -98,9 +96,35 @@ namespace PdkFriendServer.Logic 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; } @@ -108,15 +132,15 @@ namespace PdkFriendServer.Logic && hands[myPos - 1].Count <= hands[oppPos - 1].Count; } + // ---- 出牌决策 ---- + public PlayOutCardPdkF DecidePlay(PdkBotView view) { var hand = view.MyHand; - if (hand.Length == 0) - return Pass(view); + if (hand.Length == 0) return Pass(view); bool isFirstPlay = view.MaxPlayCard.GameNum <= 0; - // 枚举所有合法出牌 var tips = PdkCardAlgorithm.GetTipCard( view.MaxPlayCard, hand, null, view.Rule.AAAIsZhaDan); @@ -131,7 +155,6 @@ namespace PdkFriendServer.Logic if (candidates.Count == 1) return candidates[0].play; - // 对每个候选做蒙特卡洛评估 int unknownCount = 48 - hand.Length - CountPlayed(hand, view); int simsPerCandidate = Math.Max(50, _simulations / candidates.Count); @@ -170,38 +193,29 @@ namespace PdkFriendServer.Logic int nextPos = myPos % 3 + 1; int oppPos = (myPos + 1) % 3 + 1; - // 构建 3 人手牌:我已知,对手随机分配 var hands = new List[3]; hands[myPos - 1] = new List(myHand); hands[nextPos - 1] = new List(); hands[oppPos - 1] = new List(); - // 生成 48 张标准牌,排除我的手牌 var allCards = GenerateDeck(); var unknown = allCards.Where(c => !myHand.Any(h => h.ID == c.ID)).ToList(); Shuffle(unknown); - int nextCnt = view.ShenYuCard[nextPos - 1]; - int oppCnt = view.ShenYuCard[oppPos - 1]; - hands[nextPos - 1].AddRange(unknown.Take(nextCnt)); - hands[oppPos - 1].AddRange(unknown.Skip(nextCnt).Take(oppCnt)); + 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() + 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 current = myPos % 3 + 1; int maxPlayer = myPlay.Type == CardType1.None ? view.MaxPlayCard.Pos : myPos; if (myPlay.Type == CardType1.None) { @@ -210,60 +224,92 @@ namespace PdkFriendServer.Logic } int moveCount = 0; - int maxMoves = 500; + int passStreak = 0; - while (moveCount++ < maxMoves) + while (moveCount++ < 500) { var curHand = hands[current - 1]; - if (curHand.Count == 0) - return current == myPos; // 我赢了 + if (curHand.Count == 0) return current == myPos; - bool isNewRound = maxPlayer == current; + bool isNewRound = maxPlayer == current || maxPlay.GameNum == 0; + var curMax = isNewRound ? new PlayOutCardPdkF() : maxPlay; + var curTips = PdkCardAlgorithm.GetTipCard(curMax, curHand.ToArray(), null, false); - // 获取当前玩家的合法出牌 - var curMax = isNewRound - ? new PlayOutCardPdkF() // GameNum=0 means new round - : maxPlay; - var curTips = PdkCardAlgorithm.GetTipCard( - curMax, curHand.ToArray(), null, false); - - if (isNewRound || (curTips != null && curTips.Count > 0)) + if (curTips != null && curTips.Count > 0) { - // 选第一个方案(最小牌) - var pick = isNewRound - ? curHand.OrderBy(c => c.GameNum).First() - : curTips[0][0]; - var ids = isNewRound - ? new[] { pick.ID } - : curTips[0].Select(c => c.ID).ToArray(); - + var pick = PickBestTip(curTips, isNewRound); maxPlay = new PlayOutCardPdkF { Pos = (byte)current, - GameNum = pick.GameNum, - Type = curTips?[0].Count == 1 ? CardType1.DanZhang : CardType1.DuiZi, - Ids = ids + GameNum = pick[0].GameNum, + Type = pick.Count == 1 ? CardType1.DanZhang : CardType1.DuiZi, + Ids = pick.Select(c => c.ID).ToArray() }; maxPlayer = current; - - foreach (var id in ids) + passStreak = 0; + foreach (var id in maxPlay.Ids) curHand.RemoveAll(c => c.ID == id); } - // else: pass, keep maxPlay unchanged - + 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 策略 ---- + + /// + /// 从合法方案中选最优:优先出多张牌的组合,其次出最大的 + /// 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) { - // 粗略估计已出牌:总牌 48 - 我的手牌 - 所有剩余牌 int total = 48 - myHand.Length; foreach (var c in view.ShenYuCard) total -= c; return Math.Max(0, total); @@ -273,17 +319,15 @@ namespace PdkFriendServer.Logic { var deck = new List(); int id = 1; - // 花色 1=黑桃 2=红桃 3=梅花 4=方块, 大小 1-13 (A=14, 2=16) foreach (int flower in new[] { 1, 2, 3, 4 }) for (int num = 1; num <= 13; num++) { - int gameNum = num switch + int gameNum = num switch { 1 => 14, 2 => 16, _ => num }; + deck.Add(new TCardInfoPdkF { - 1 => 14, // A - 2 => 16, // 2 - _ => num - }; - deck.Add(new TCardInfoPdkF { ID = (byte)id++, Flower = (byte)flower, GameNum = (byte)gameNum, GameState = 1 }); + ID = (byte)id++, Flower = (byte)flower, + GameNum = (byte)gameNum, GameState = 1 + }); } return deck; } @@ -301,10 +345,8 @@ namespace PdkFriendServer.Logic { return new PlayOutCardPdkF { - Pos = src.Pos, - GameNum = src.GameNum, - Type = src.Type, - Ids = src.Ids?.ToArray() + Pos = src.Pos, GameNum = src.GameNum, + Type = src.Type, Ids = src.Ids?.ToArray() }; } @@ -312,8 +354,7 @@ namespace PdkFriendServer.Logic { return new PlayOutCardPdkF { - Pos = (byte)pos, - GameNum = cards[0].GameNum, + Pos = (byte)pos, GameNum = cards[0].GameNum, Ids = cards.Select(x => x.ID).ToArray(), Type = CardType1.DanZhang }; diff --git a/exe/PdkFriendServer/PdkFriendServer.dll b/exe/PdkFriendServer/PdkFriendServer.dll index e16b7922..13b5317e 100644 Binary files a/exe/PdkFriendServer/PdkFriendServer.dll and b/exe/PdkFriendServer/PdkFriendServer.dll differ diff --git a/exe/PdkFriendServer/PdkFriendServer.pdb b/exe/PdkFriendServer/PdkFriendServer.pdb index 42b9bdba..2f72231b 100644 Binary files a/exe/PdkFriendServer/PdkFriendServer.pdb and b/exe/PdkFriendServer/PdkFriendServer.pdb differ diff --git a/exe/hjha-console/PdkFriendServer.dll b/exe/hjha-console/PdkFriendServer.dll index e16b7922..13b5317e 100644 Binary files a/exe/hjha-console/PdkFriendServer.dll and b/exe/hjha-console/PdkFriendServer.dll differ diff --git a/exe/hjha-console/PdkFriendServer.pdb b/exe/hjha-console/PdkFriendServer.pdb index 42b9bdba..2f72231b 100644 Binary files a/exe/hjha-console/PdkFriendServer.pdb and b/exe/hjha-console/PdkFriendServer.pdb differ diff --git a/exe/hjha-console/hjha-console.dll b/exe/hjha-console/hjha-console.dll index 427a8460..dd7550d9 100644 Binary files a/exe/hjha-console/hjha-console.dll and b/exe/hjha-console/hjha-console.dll differ diff --git a/exe/hjha-console/hjha-console.pdb b/exe/hjha-console/hjha-console.pdb index 31e3a1bf..fe8e83d9 100644 Binary files a/exe/hjha-console/hjha-console.pdb and b/exe/hjha-console/hjha-console.pdb differ