Files
hjha-server/PdkFriendServer/Logic/CardTracker.cs
xiaoou 1b1404f38e feat: 确定性牌计数推理v2 — AbsoluteHandStrength + 包庄否决 + 混合策略修正
CardTracker:
- 新增 AbsoluteHandStrength: 综合评估手牌实力(rank控制/绝对最高牌/炸弹/效率/已知率)
- 用于包庄决策的安全检查

IsmctsBot:
- EstimateWinProbability: 混合策略回退为纯ISMCTS + 确定性否决
  (混合加成导致虚假信心, v3验证0%胜率)
- 策略: detStrength<0.15 → 直接否决不包, 否则纯ISMCTS
- 阈值保持 v2 水平: minPlays≤3→0.50,≤4→0.60,≤5→0.65

验证: 100局0错误 0.8s/局 ENDGAME=983次 LOCK=3次
2026-07-13 00:59:48 +08:00

560 lines
22 KiB
C#
Raw 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 计)</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;
}
}
}