feat: 对手牌型预测 — PredictOpponent + ISMCTS 候选加权

CardTracker:
- 新增 OpponentPrediction 类: RankProbabilities/MaxRank/BombProbability/LikelyPairs/LikelyTriples/MaxStraightLength
- 新增 PredictOpponent: 基于剩余rank计数+对手手牌数计算期望持有量
  核心公式: expected = remainingCount × (oppNeed / totalUnknown)
  推断: 对子(>=1.5期望)/三张(>=2.5)/炸弹概率/顺子长度/MaxRank
- Pass约束强化: 对手pass过某单张 → 降低MaxRank

IsmctsBot:
- 新增步骤5.7 对手牌型预测加权:
  我的出牌rank > 对手MaxRank → +8-13%
  我的出牌rank < 对手MaxRank → -5%
  对手可能有炸弹 → 大牌型-3%

验证: 100局0错误 0.8s/局
This commit is contained in:
2026-07-13 01:05:14 +08:00
parent 1b1404f38e
commit 73c8783276
8 changed files with 128 additions and 0 deletions

View File

@ -299,6 +299,70 @@ namespace PdkFriendServer.Logic
return Math.Min(score, 1.0);
}
/// <summary>
/// 预测对手手牌组成。
/// 基于:未知牌池中各 rank 剩余张数 + 对手手牌张数 → 期望持有量。
/// 用于修正 ISMCTS 随机采样,使模拟更接近真实分布。
/// </summary>
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;
}
/// <summary>在外面可能存在的炸弹数(按 rank 计)</summary>
public int BombsOutside(TCardInfoPdkF[] myHand)
{
@ -557,4 +621,34 @@ namespace PdkFriendServer.Logic
public int Count;
}
}
/// <summary>
/// 对手手牌预测结果 — 基于剩余牌计数 + pass 约束的确定性推断
/// </summary>
public class OpponentPrediction
{
/// <summary>每个 rank 对手至少持有一张的概率 (0.0-1.0)</summary>
public Dictionary<int, double> RankProbabilities = new Dictionary<int, double>();
/// <summary>对手最可能的最高牌 rank (概率>30%)</summary>
public int MaxRank;
/// <summary>对手可能有炸弹的概率</summary>
public double BombProbability;
/// <summary>对手很可能持有对子的 rank 列表</summary>
public List<int> LikelyPairs = new List<int>();
/// <summary>对手很可能持有三张的 rank 列表</summary>
public List<int> LikelyTriples = new List<int>();
/// <summary>对手可能持有的最大顺子长度</summary>
public int MaxStraightLength;
/// <summary>对手是否很可能没有高于指定 rank 的单牌</summary>
public bool LikelyNoHigherThan(int rank) => MaxRank > 0 && MaxRank <= rank;
/// <summary>对手是否很可能没有高于指定 rank 的对子</summary>
public bool LikelyNoPairHigherThan(int rank) => !LikelyPairs.Any(r => r > rank);
}
}

View File

@ -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];

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.