Files
hjha-server/PdkFriendServer/Logic/CardTracker.cs
xiaoou 4263146487 fix: code review — 清理CardTracker死代码 + IsmctsBot未使用变量
CardTracker.BombsOutside: 删除13行注释+空循环(seenByRank从未填充)
IsmctsBot: 删除未使用的bombRanks变量
2026-07-12 22:51:48 +08:00

385 lines
14 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;
// 当前轮次 pass 计数
private int _passStreak;
private bool _isNewRound;
// 每个位置的炸弹计数
private readonly int[] _bombCounts = new int[3];
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);
_passStreak = 0;
_isNewRound = false;
}
else if (_lastMaxPlay.Ids != null)
{
// 同一手牌还在桌上 → 有人 pass 了
_passStreak++;
// 记录 pass 约束(轮到谁 pass 了就是谁)
int passer = currentMax.Pos > 0
? (currentMax.Pos % 3 + 1) // 下一位
: 0;
if (passer > 0 && passer != myPos)
{
RecordPassConstraint(passer, currentMax);
}
}
}
else
{
_isNewRound = true;
_passStreak = 0;
}
_lastMaxPlay = new PlayOutCardPdkF
{
Pos = currentMax.Pos,
GameNum = currentMax.GameNum,
Type = currentMax.Type,
Ids = currentMax.Ids?.ToArray()
};
// 3. 追踪炸弹计数
for (int i = 0; i < 3; i++)
_bombCounts[i] = view.ZhaDans[i];
}
/// <summary>
/// 重置追踪器(每局开始时调用)
/// </summary>
public void Reset()
{
_seenCardIds.Clear();
_lastMyHandIds = null;
_lastMaxPlay = new PlayOutCardPdkF();
_passStreak = 0;
_isNewRound = true;
foreach (var c in _opponentConstraints) c.Clear();
Array.Clear(_bombCounts, 0, 3);
}
// ---- 查询接口 ----
/// <summary>返回外面还有多少未知卡</summary>
public int UnknownCount => 48 - _seenCardIds.Count;
/// <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;
}
}
}