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

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