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