Files
hjha-server/PdkFriendServer/Logic/CardTracker.cs
xiaoou 73c8783276 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/局
2026-07-13 01:05:14 +08:00

654 lines
25 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System;
using System.Collections.Generic;
using System.Linq;
using GameMessage;
using GameMessage.PaoDeKuaiF;
namespace PdkFriendServer.Logic
{
/// <summary>
/// 出牌追踪器 — 累积已知牌信息,推断对手手牌约束。
///
/// 原理48 张牌去掉我的 16 张和已出现的牌 = 未知牌池。
/// 对手 pass 某种牌型 → 该对手手上没有能压制此牌型的牌。
/// 随着牌局进行,未知牌池不断缩小,推断精度不断提高。
///
/// 用法IsmctsBot 持有一个 CardTracker 实例,每次 DecidePlay 时调用 Update()。
/// </summary>
public class CardTracker
{
// 48 位 bitset已确认出现的牌ID 级别1-52 但 跑得快只用到其中 48 张)
private readonly HashSet<byte> _seenCardIds = new HashSet<byte>();
// 上次看到的 MaxPlayCard用于检测新出牌
private PlayOutCardPdkF _lastMaxPlay;
// 上次我的手牌 ID 集合
private HashSet<byte> _lastMyHandIds;
// 对手约束:[pos-2] = 该对手 pass 过的牌型列表
private readonly List<PassConstraint>[] _opponentConstraints;
public CardTracker()
{
_opponentConstraints = new List<PassConstraint>[3];
for (int i = 0; i < 3; i++)
_opponentConstraints[i] = new List<PassConstraint>();
}
/// <summary>
/// 每次 bot 决策前调用,更新已知信息。
/// myPos 从 1 开始。
/// </summary>
public void Update(PdkBotView view)
{
int myPos = view.MyPos;
var handIds = new HashSet<byte>(view.MyHand.Where(c => c.GameState == 1).Select(c => c.ID));
// 1. 追踪我的手牌变化
if (_lastMyHandIds != null)
{
var played = _lastMyHandIds.Where(id => !handIds.Contains(id));
foreach (var id in played) _seenCardIds.Add(id);
}
_lastMyHandIds = handIds;
// 2. 追踪桌面出牌
var currentMax = view.MaxPlayCard;
if (currentMax.Ids != null && currentMax.Ids.Length > 0)
{
// 新出牌 → 记录
if (_lastMaxPlay.Ids == null ||
!_lastMaxPlay.Ids.SequenceEqual(currentMax.Ids))
{
foreach (var id in currentMax.Ids) _seenCardIds.Add(id);
}
else
{
// 同一手牌还在桌上 → 有人 pass 了
int passer = currentMax.Pos > 0
? (currentMax.Pos % 3 + 1)
: 0;
if (passer > 0 && passer != myPos)
RecordPassConstraint(passer, currentMax);
}
}
_lastMaxPlay = new PlayOutCardPdkF
{
Pos = currentMax.Pos,
GameNum = currentMax.GameNum,
Type = currentMax.Type,
Ids = currentMax.Ids?.ToArray()
};
}
/// <summary>
/// 重置追踪器(每局开始时调用)
/// </summary>
public void Reset()
{
_seenCardIds.Clear();
_lastMyHandIds = null;
_lastMaxPlay = new PlayOutCardPdkF();
foreach (var c in _opponentConstraints) c.Clear();
}
// ---- 查询接口 ----
/// <summary>
/// 返回外面还有多少未知卡48 张发牌去掉我的手牌和已见牌)
/// </summary>
public int UnknownCount(TCardInfoPdkF[] myHand)
{
int myCount = myHand.Count(c => c.GameState == 1);
return 48 - myCount - _seenCardIds.Count;
}
/// <summary>返回指定 rank 剩余未知张数 (0-4)</summary>
public int RemainingCount(int gameNum, TCardInfoPdkF[] myHand)
{
int myCount = myHand.Count(c => c.GameState == 1 && c.GameNum == gameNum);
int seenCount = CountSeenByRank(gameNum);
return 4 - myCount - seenCount;
}
/// <summary>返回未知牌池(排除我的手牌和已见牌后的所有未出现牌)</summary>
public List<TCardInfoPdkF> GetUnknownPool(TCardInfoPdkF[] myHand)
{
var knownIds = new HashSet<byte>(_seenCardIds);
foreach (var c in myHand) knownIds.Add(c.ID);
var pool = new List<TCardInfoPdkF>();
for (byte suit = 1; suit <= 4; suit++)
for (byte n = 1; n <= 13; n++)
{
byte id = (byte)((suit - 1) * 13 + n);
if (!knownIds.Contains(id))
{
int g = n == 1 ? 14 : n == 2 ? 16 : n;
pool.Add(new TCardInfoPdkF { ID = id, Flower = suit, GameNum = (byte)g, GameState = 1 });
}
}
return pool;
}
/// <summary>
/// 检查指定牌型的出牌是否绝对无法被压制。
/// 基于确定性信息:我的手牌 + 已见牌 = 对手可能持有的牌池完全已知。
/// </summary>
public bool IsAbsolutelyHighest(TCardInfoPdkF[] myHand, int gameNum, int count, CardType1 type)
{
// 炸弹:检查是否有更大的炸弹 rank 在外面
if (type == CardType1.ZhaDan)
return !HasHigherRank(gameNum, 4, myHand);
// 单张/对子/三张:检查是否有同类型更大的
if (type == CardType1.DanZhang || type == CardType1.DuiZi || type == CardType1.SanZhang)
return !HasHigherRank(gameNum, count, myHand);
// 三带二 / 四带二:比较主牌 rank
if (type == CardType1.SanDai2)
{
// 三带二:主三张需要不被更大的三张压制,且不被炸弹压制
if (HasHigherRank(gameNum, 3, myHand)) return false;
return !HasBombOutside(myHand);
}
if (type == CardType1.SiDai2 || type == CardType1.SiDai3)
{
// 四带二/四带三:已经是最大牌型(除非有大炸弹)
return !HasHigherRank(gameNum, 4, myHand);
}
// 顺子:检查更大起点的同长顺子
if (type == CardType1.ShunZi)
return !HasHigherStraightEx(gameNum, count, myHand);
// 连对
if (type == CardType1.LianDui)
return !HasHigherLianDuiEx(gameNum, count / 2, myHand);
// 飞机/飞机带对
if (type == CardType1.FeiJi || type == CardType1.FeijiDai2)
return !HasHigherPlaneEx(gameNum, count / 3, myHand);
return false;
}
/// <summary>外面是否存在能压制该 rank 的炸弹</summary>
private bool HasBombOutside(TCardInfoPdkF[] myHand)
{
return BombsOutside(myHand) > 0;
}
/// <summary>检查未知牌池中是否存在更大 rank 的 count 张同 rank 牌</summary>
private bool HasHigherRank(int gameNum, int needPerRank, TCardInfoPdkF[] myHand)
{
foreach (int rank in new[] { 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 16 })
{
if (rank <= gameNum) continue;
if (RemainingCount(rank, myHand) >= needPerRank)
return true;
}
return false;
}
/// <summary>检查是否存在更大起点的同长顺子count=张数)</summary>
private bool HasHigherStraightEx(int startRank, int length, TCardInfoPdkF[] myHand)
{
int maxStart = 14 - length + 1;
for (int s = startRank + 1; s <= maxStart; s++)
{
bool possible = true;
for (int r = s; r < s + length; r++)
{
if (RemainingCount(r, myHand) < 1) { possible = false; break; }
}
if (possible) return true;
}
return false;
}
/// <summary>检查是否存在更大起点的连对pairs=对数)</summary>
private bool HasHigherLianDuiEx(int startRank, int pairs, TCardInfoPdkF[] myHand)
{
int maxStart = 14 - pairs;
for (int s = startRank + 1; s <= maxStart; s++)
{
bool possible = true;
for (int r = s; r < s + pairs; r++)
{
if (RemainingCount(r, myHand) < 2) { possible = false; break; }
}
if (possible) return true;
}
return false;
}
/// <summary>检查是否存在更大起点的飞机triples=三连数)</summary>
private bool HasHigherPlaneEx(int startRank, int triples, TCardInfoPdkF[] myHand)
{
int maxStart = 14 - triples;
for (int s = startRank + 1; s <= maxStart; s++)
{
bool possible = true;
for (int r = s; r < s + triples; r++)
{
if (RemainingCount(r, myHand) < 3) { possible = false; break; }
}
if (possible) return true;
}
return false;
}
/// <summary>
/// 确定性手牌强度基于绝对牌计数评估手牌实力0.0-1.0。
/// 核心思想:我的手牌 + 已见牌 = 对手的牌池完全确定,
/// 可以精确计算哪些 rank 已被我控制、哪些出牌无敌。
/// 用于修正 ISMCTS 模拟噪声。
/// </summary>
public double AbsoluteHandStrength(TCardInfoPdkF[] myHand)
{
if (myHand.Length == 0) return 1.0;
double score = 0.0;
// 1. Rank 绝对控制率:我持有该 rank 所有剩余牌的百分比
int controlledRanks = 0;
int[] ranks = { 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 16 };
foreach (int r in ranks)
{
int remaining = RemainingCount(r, myHand);
int myCount = myHand.Count(c => c.GameState == 1 && c.GameNum == r);
if (remaining == 0 && myCount > 0)
{
// 我持有该 rank 所有剩余牌 → 绝对控制
controlledRanks++;
if (myCount >= 3) controlledRanks++; // 三张以上价值更高
}
}
score += controlledRanks * 0.04; // 每个控制 rank +4%
// 2. 绝对最高牌:持有 ♠2 或所有2
int myTwos = myHand.Count(c => c.GameState == 1 && c.GameNum == 16);
int remainingTwos = RemainingCount(16, myHand);
if (remainingTwos == 0 && myTwos > 0)
{
score += myTwos * 0.06; // 每张独占 2 +6%
}
// 3. 持有的炸弹数
var byRank = myHand.Where(c => c.GameState == 1)
.GroupBy(c => c.GameNum)
.Where(g => g.Count() >= 4);
int myBombs = byRank.Count();
score += myBombs * 0.08; // 每个炸弹 +8%
// 4. 手牌效率(通过 HandOptimizer 间接)
int minPlays = HandOptimizer.MinPlays(myHand);
int handSize = myHand.Count(c => c.GameState == 1);
double efficiency = (double)(handSize - minPlays)
/ Math.Max(handSize, 1);
score += efficiency * 0.15; // 效率贡献最多 15%
// 5. 已知牌越多 → 确定性权重越高
double seenRatio = _seenCardIds.Count / (double)Math.Max(48 - handSize, 1);
score += seenRatio * 0.10; // 已知牌占比 +10%
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)
{
int bombs = 0;
var myCounts = new Dictionary<int, int>();
foreach (var c in myHand.Where(c => c.GameState == 1))
myCounts[c.GameNum] = myCounts.GetValueOrDefault(c.GameNum, 0) + 1;
foreach (int rank in new[] { 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 16 })
{
int myCount = myCounts.GetValueOrDefault(rank, 0);
int seenCount = CountSeenByRank(rank);
int outside = 4 - myCount - seenCount;
if (outside == 4) bombs++;
}
return bombs;
}
/// <summary>外面有炸弹的 rank 列表</summary>
public List<int> BombRanks(TCardInfoPdkF[] myHand)
{
var result = new List<int>();
var myCounts = new Dictionary<int, int>();
foreach (var c in myHand.Where(c => c.GameState == 1))
myCounts[c.GameNum] = myCounts.GetValueOrDefault(c.GameNum, 0) + 1;
foreach (int rank in new[] { 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 16 })
{
int myCount = myCounts.GetValueOrDefault(rank, 0);
int seenCount = CountSeenByRank(rank);
if (4 - myCount - seenCount == 4) result.Add(rank);
}
return result;
}
/// <summary>根据已知牌判断:这个出牌有没有可能被压制</summary>
public bool CanAnyoneBeat(TCardInfoPdkF[] myHand, PlayOutCardPdkF play)
{
if (play.Ids == null || play.Ids.Length == 0) return false;
int rank = play.GameNum;
int count = play.Ids.Length;
switch (play.Type)
{
case CardType1.DanZhang:
// 外面有没有更大的单牌?
return HasHigherUnseen(rank, 1, myHand);
case CardType1.DuiZi:
return HasHigherUnseen(rank, 2, myHand);
case CardType1.ZhaDan:
// 外面有没有更大的炸弹?
return HasHigherUnseen(rank, 4, myHand);
case CardType1.ShunZi:
// 外面有没有更大的顺子?
return HasHigherStraight(rank, count, myHand);
case CardType1.LianDui:
return HasHigherLianDui(rank, count, myHand);
case CardType1.FeiJi:
case CardType1.FeijiDai2:
return HasHigherPlane(rank, count, myHand);
default:
return HasHigherUnseen(rank, count, myHand);
}
}
/// <summary>生成约束采样的对手手牌(用于 ISMCTS 模拟)</summary>
public List<TCardInfoPdkF>[] SampleOpponentHands(
TCardInfoPdkF[] myHand, PdkBotView view, Random rng)
{
int myPos = view.MyPos;
int opp1 = myPos % 3; // 下家 (0-indexed)
int opp2 = (myPos + 1) % 3; // 上家
// 未知牌池
var knownIds = new HashSet<byte>(_seenCardIds);
foreach (var c in myHand) knownIds.Add(c.ID);
var pool = new List<TCardInfoPdkF>();
for (byte suit = 1; suit <= 4; suit++)
{
for (byte n = 1; n <= 13; n++)
{
byte id = (byte)((suit - 1) * 13 + n);
if (!knownIds.Contains(id))
{
int g = n == 1 ? 14 : n == 2 ? 16 : n;
pool.Add(new TCardInfoPdkF { ID = id, Flower = suit, GameNum = (byte)g, GameState = 1 });
}
}
}
Shuffle(pool, rng);
// 约束检查:对手不能持有与 pass 约束冲突的牌
int opp1Need = view.ShenYuCard[opp1];
int opp2Need = view.ShenYuCard[opp2];
var h1 = new List<TCardInfoPdkF>();
var h2 = new List<TCardInfoPdkF>();
int idx = 0;
foreach (var card in pool)
{
bool okFor1 = SatisfiesConstraints(card, opp1);
bool okFor2 = SatisfiesConstraints(card, opp2);
if (h1.Count < opp1Need && okFor1 && (h2.Count >= opp2Need || !okFor2 || rng.Next(2) == 0))
h1.Add(card);
else if (h2.Count < opp2Need && okFor2)
h2.Add(card);
else if (okFor1 && h1.Count < opp1Need)
h1.Add(card);
else if (okFor2 && h2.Count < opp2Need)
h2.Add(card);
idx++;
if (h1.Count >= opp1Need && h2.Count >= opp2Need) break;
}
return new[] { h1, h2 };
}
// ---- 私有方法 ----
private void RecordPassConstraint(int pos, PlayOutCardPdkF play)
{
int idx = pos - 1;
_opponentConstraints[idx].Add(new PassConstraint
{
Type = play.Type,
GameNum = play.GameNum,
Count = play.Ids?.Length ?? 1
});
// 只保留最近 10 条
if (_opponentConstraints[idx].Count > 10)
_opponentConstraints[idx].RemoveAt(0);
}
private bool SatisfiesConstraints(TCardInfoPdkF card, int oppIdx)
{
foreach (var c in _opponentConstraints[oppIdx])
{
switch (c.Type)
{
case CardType1.DanZhang:
if (card.GameNum > c.GameNum) return false;
break;
case CardType1.DuiZi:
// 简化:不能持有比 pass 对子更高的对子
if (card.GameNum > c.GameNum) return false;
break;
}
}
return true;
}
private int CountSeenByRank(int gameNum)
{
// 近似ID 与 GameNum 的映射。ID=(suit-1)*13 + n, GameNum: 1→14, 2→16, else→n
int n = gameNum switch { 14 => 1, 16 => 2, _ => gameNum };
int count = 0;
for (int suit = 1; suit <= 4; suit++)
{
byte id = (byte)((suit - 1) * 13 + n);
if (_seenCardIds.Contains(id)) count++;
}
return count;
}
private bool HasHigherUnseen(int gameNum, int needPerRank, TCardInfoPdkF[] myHand)
{
var myCounts = new Dictionary<int, int>();
foreach (var c in myHand.Where(c => c.GameState == 1 && c.GameNum > gameNum))
myCounts[c.GameNum] = myCounts.GetValueOrDefault(c.GameNum, 0) + 1;
foreach (int rank in new[] { 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 16 })
{
if (rank <= gameNum) continue;
int myCount = myCounts.GetValueOrDefault(rank, 0);
int seenCount = CountSeenByRank(rank);
int available = 4 - myCount - seenCount;
if (available >= needPerRank) return true;
}
return false;
}
private bool HasHigherStraight(int startRank, int length, TCardInfoPdkF[] myHand)
{
// 检查是否存在比当前顺子更大的同长顺子
// 最大顺子10-J-Q-K-A (startRank=10, length=5),再高没有了
int maxStart = 14 - length + 1; // A(14) 是最高
for (int s = startRank + 1; s <= maxStart; s++)
{
bool possible = true;
for (int r = s; r < s + length; r++)
{
int myCount = myHand.Count(c => c.GameState == 1 && c.GameNum == r);
int seenCount = CountSeenByRank(r);
if (4 - myCount - seenCount < 1) { possible = false; break; }
}
if (possible) return true;
}
return false;
}
private bool HasHigherLianDui(int startRank, int pairs, TCardInfoPdkF[] myHand)
{
// 连对≥3 对连续,检查是否有更大起点的连对
int maxStart = 14 - pairs; // A(14) 最高
for (int s = startRank + 1; s <= maxStart; s++)
{
bool possible = true;
for (int r = s; r < s + pairs; r++)
{
int myCount = myHand.Count(c => c.GameState == 1 && c.GameNum == r);
int seenCount = CountSeenByRank(r);
if (4 - myCount - seenCount < 2) { possible = false; break; }
}
if (possible) return true;
}
return false;
}
private bool HasHigherPlane(int startRank, int triples, TCardInfoPdkF[] myHand)
{
int maxStart = 14 - triples;
for (int s = startRank + 1; s <= maxStart; s++)
{
bool possible = true;
for (int r = s; r < s + triples; r++)
{
int myCount = myHand.Count(c => c.GameState == 1 && c.GameNum == r);
int seenCount = CountSeenByRank(r);
if (4 - myCount - seenCount < 3) { possible = false; break; }
}
if (possible) return true;
}
return false;
}
private static void Shuffle<T>(List<T> list, Random rng)
{
for (int i = list.Count - 1; i > 0; i--)
{
int j = rng.Next(i + 1);
(list[i], list[j]) = (list[j], list[i]);
}
}
private struct PassConstraint
{
public CardType1 Type;
public byte GameNum;
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);
}
}