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:
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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.
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user