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