diff --git a/PdkFriendServer/Logic/CardTracker.cs b/PdkFriendServer/Logic/CardTracker.cs
index 4948fd79..195faa1d 100644
--- a/PdkFriendServer/Logic/CardTracker.cs
+++ b/PdkFriendServer/Logic/CardTracker.cs
@@ -299,6 +299,70 @@ namespace PdkFriendServer.Logic
return Math.Min(score, 1.0);
}
+ ///
+ /// 预测对手手牌组成。
+ /// 基于:未知牌池中各 rank 剩余张数 + 对手手牌张数 → 期望持有量。
+ /// 用于修正 ISMCTS 随机采样,使模拟更接近真实分布。
+ ///
+ public OpponentPrediction PredictOpponent(int oppIdx, TCardInfoPdkF[] myHand, PdkBotView view)
+ {
+ var pred = new OpponentPrediction();
+ int oppNeed = view.ShenYuCard[oppIdx];
+ if (oppNeed == 0) return pred;
+
+ var pool = GetUnknownPool(myHand);
+ double totalUnknown = Math.Max(pool.Count, 1);
+
+ foreach (int rank in new[] { 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 16 })
+ {
+ int remaining = RemainingCount(rank, myHand);
+ if (remaining == 0) continue;
+
+ double expected = remaining * oppNeed / totalUnknown;
+ double prob = Math.Min(1.0, expected);
+
+ pred.RankProbabilities[rank] = prob;
+
+ if (expected >= 1.5 && remaining >= 2)
+ pred.LikelyPairs.Add(rank);
+ if (expected >= 2.5 && remaining >= 3)
+ pred.LikelyTriples.Add(rank);
+ if (prob >= 0.3 && rank > pred.MaxRank)
+ pred.MaxRank = rank;
+ }
+
+ // 炸弹概率
+ double bombProb = 0;
+ foreach (int rank in new[] { 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 16 })
+ {
+ int remaining = RemainingCount(rank, myHand);
+ if (remaining == 4)
+ {
+ double p = oppNeed / totalUnknown;
+ bombProb += Math.Pow(p, 4);
+ }
+ }
+ pred.BombProbability = Math.Min(bombProb, 1.0);
+
+ // 顺子长度
+ int maxCons = 0, cur = 0;
+ foreach (int rank in new[] { 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14 })
+ {
+ if (pred.RankProbabilities.GetValueOrDefault(rank, 0) >= 0.5) cur++;
+ else { maxCons = Math.Max(maxCons, cur); cur = 0; }
+ }
+ pred.MaxStraightLength = Math.Max(maxCons, cur);
+
+ // Pass 约束强化
+ foreach (var c in _opponentConstraints[oppIdx])
+ {
+ if (c.Type == CardType1.DanZhang && c.GameNum > 0 && pred.MaxRank > c.GameNum)
+ pred.MaxRank = c.GameNum;
+ }
+
+ return pred;
+ }
+
/// 在外面可能存在的炸弹数(按 rank 计)
public int BombsOutside(TCardInfoPdkF[] myHand)
{
@@ -557,4 +621,34 @@ namespace PdkFriendServer.Logic
public int Count;
}
}
+
+ ///
+ /// 对手手牌预测结果 — 基于剩余牌计数 + pass 约束的确定性推断
+ ///
+ public class OpponentPrediction
+ {
+ /// 每个 rank 对手至少持有一张的概率 (0.0-1.0)
+ public Dictionary RankProbabilities = new Dictionary();
+
+ /// 对手最可能的最高牌 rank (概率>30%)
+ public int MaxRank;
+
+ /// 对手可能有炸弹的概率
+ public double BombProbability;
+
+ /// 对手很可能持有对子的 rank 列表
+ public List LikelyPairs = new List();
+
+ /// 对手很可能持有三张的 rank 列表
+ public List LikelyTriples = new List();
+
+ /// 对手可能持有的最大顺子长度
+ public int MaxStraightLength;
+
+ /// 对手是否很可能没有高于指定 rank 的单牌
+ public bool LikelyNoHigherThan(int rank) => MaxRank > 0 && MaxRank <= rank;
+
+ /// 对手是否很可能没有高于指定 rank 的对子
+ public bool LikelyNoPairHigherThan(int rank) => !LikelyPairs.Any(r => r > rank);
+ }
}
\ No newline at end of file
diff --git a/PdkFriendServer/Logic/IsmctsBot.cs b/PdkFriendServer/Logic/IsmctsBot.cs
index 7042bc5c..bf0c9104 100644
--- a/PdkFriendServer/Logic/IsmctsBot.cs
+++ b/PdkFriendServer/Logic/IsmctsBot.cs
@@ -278,6 +278,40 @@ namespace PdkFriendServer.Logic
}
}
+ // 5.7. 对手牌型预测加权:基于剩余牌计数推断对手能压制我的概率
+ var opp1Pred = _tracker.PredictOpponent(view.MyPos % 3, hand, view);
+ var opp2Pred = _tracker.PredictOpponent((view.MyPos + 1) % 3, hand, view);
+ int oppMaxRank = Math.Max(opp1Pred.MaxRank, opp2Pred.MaxRank);
+
+ for (int i = 0; i < scoredCandidates.Count; i++)
+ {
+ var (p, wr, am, m) = scoredCandidates[i];
+ if (p.Type == CardType1.None || p.Ids == null || p.Ids.Length == 0)
+ continue;
+
+ int myRank = p.GameNum;
+
+ // 我的出牌 rank 高于两对手最高牌 → 他们很可能压不了
+ if (myRank > oppMaxRank && oppMaxRank > 0)
+ {
+ wr += 0.08;
+ if (opp1Pred.LikelyNoHigherThan(myRank) && opp2Pred.LikelyNoHigherThan(myRank))
+ wr += 0.05; // 两个对手都可能压不了 → 更像无敌
+ }
+ // 我的出牌低于对手最高牌 → 容易被压制
+ else if (myRank < oppMaxRank && oppMaxRank > 0)
+ {
+ wr -= 0.05;
+ }
+
+ // 对手可能有炸弹 → 大牌型谨慎
+ double maxBombProb = Math.Max(opp1Pred.BombProbability, opp2Pred.BombProbability);
+ if (maxBombProb > 0.3 && (p.Type == CardType1.ZhaDan || p.Type == CardType1.ShunZi))
+ wr -= 0.03;
+
+ scoredCandidates[i] = (p, wr, am, m);
+ }
+
// 6. 两阶段选择:多牌优先
scoredCandidates.Sort((a, b) => b.winRate.CompareTo(a.winRate));
var best = scoredCandidates[0];
diff --git a/exe/PdkFriendServer/PdkFriendServer.dll b/exe/PdkFriendServer/PdkFriendServer.dll
index de535739..ae0e15ff 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 06491c6c..d9abc03e 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 de535739..ae0e15ff 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 06491c6c..d9abc03e 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 b38bea20..ea636c67 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 85b2e1b6..cf672b8f 100644
Binary files a/exe/hjha-console/hjha-console.pdb and b/exe/hjha-console/hjha-console.pdb differ