fix: ISMCTS包庄决策——600次模拟估算P(win), 阈值45%

替换占位逻辑(大牌≥6张)为真正的蒙特卡洛评估:
- EstimateWinProbability(): 模拟N局完整对局, 统计我第一个出完的概率
- 阈值45%: 包庄收益翻倍, 略微倾斜于有利局势
- 日志: [ISMCTS posX] 包庄评估: P(win)=XX% → 包庄/不包

原理: 包庄不改变牌局流程(不像斗地主叫地主), 只翻倍输赢。
因此决策简化为 P(win|我的手牌) ≥ 阈值 → 包庄。
This commit is contained in:
2026-07-07 13:29:36 +08:00
parent a75733e4ae
commit d23e0b7616
9 changed files with 163 additions and 80 deletions

View File

@ -26,10 +26,86 @@ namespace PdkFriendServer.Logic
public bool DecideBaoZhuang(PdkBotView view)
{
// 包庄决策:用快速模拟评估——手牌质量高就包
// 简化:手牌中 >10 的大牌J/Q/K/A/2多就包
int bigCards = view.MyHand.Count(c => c.GameNum >= 11);
return bigCards >= 6; // ≥6 张大牌 → 有一定控制力
// ISMCTS 包庄决策:模拟 N 局,估算自己第一个出完的概率
// 包庄不改变牌局流程只翻倍输赢P(win)*2*gain - P(lose)*2*loss
// 阈值 45%——因为包庄成功收益翻倍,略微倾斜有利于局势占优
double winRate = EstimateWinProbability(view);
bool shouldBao = winRate >= 0.45;
Console.WriteLine($"[ISMCTS pos{view.MyPos}] 包庄评估: P(win)={winRate:P0} → {(shouldBao ? "" : "")}");
return shouldBao;
}
/// <summary>模拟 N 局随机对局,统计我第一个出完的概率</summary>
private double EstimateWinProbability(PdkBotView view)
{
int myPos = view.MyPos;
var hand = view.MyHand;
int sims = Math.Min(_simulations, 600); // 包庄模拟用 600 次,比出牌少
int wins = 0;
for (int s = 0; s < sims; s++)
{
if (SimulateWin(hand, view))
wins++;
}
return (double)wins / sims;
}
/// <summary>模拟一局只看胜负——不指定第一步出什么</summary>
private bool SimulateWin(TCardInfoPdkF[] myHand, PdkBotView view)
{
int myPos = view.MyPos;
int nextPos = myPos % 3 + 1;
int oppPos = (myPos + 1) % 3 + 1;
var hands = new List<TCardInfoPdkF>[3];
hands[myPos - 1] = new List<TCardInfoPdkF>(myHand);
hands[nextPos - 1] = new List<TCardInfoPdkF>();
hands[oppPos - 1] = new List<TCardInfoPdkF>();
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));
// 模拟到有人出完
int current = myPos;
var maxPlay = new PlayOutCardPdkF(); // GameNum=0 → 新一轮
int maxPlayer = 0;
for (int move = 0; move < 400; move++)
{
var curHand = hands[current - 1];
if (curHand.Count == 0) return current == myPos;
bool isNewRound = maxPlayer == current || maxPlay.GameNum == 0;
var curMax = isNewRound ? new PlayOutCardPdkF() : maxPlay;
var tips = PdkCardAlgorithm.GetTipCard(curMax, curHand.ToArray(), null, false);
if (tips != null && tips.Count > 0)
{
var pick = tips[0];
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;
foreach (var id in maxPlay.Ids)
curHand.RemoveAll(c => c.ID == id);
}
current = current % 3 + 1;
}
return hands[myPos - 1].Count <= hands[nextPos - 1].Count
&& hands[myPos - 1].Count <= hands[oppPos - 1].Count;
}
public PlayOutCardPdkF DecidePlay(PdkBotView view)