using System; using System.Collections.Generic; using System.Linq; using GameMessage; using GameMessage.PaoDeKuaiF; namespace PdkFriendServer.Logic { /// /// 出牌追踪器 — 累积已知牌信息,推断对手手牌约束。 /// /// 原理:48 张牌去掉我的 16 张和已出现的牌 = 未知牌池。 /// 对手 pass 某种牌型 → 该对手手上没有能压制此牌型的牌。 /// 随着牌局进行,未知牌池不断缩小,推断精度不断提高。 /// /// 用法:IsmctsBot 持有一个 CardTracker 实例,每次 DecidePlay 时调用 Update()。 /// public class CardTracker { // 48 位 bitset:已确认出现的牌(ID 级别,1-52 但 跑得快只用到其中 48 张) private readonly HashSet _seenCardIds = new HashSet(); // 上次看到的 MaxPlayCard(用于检测新出牌) private PlayOutCardPdkF _lastMaxPlay; // 上次我的手牌 ID 集合 private HashSet _lastMyHandIds; // 对手约束:[pos-2] = 该对手 pass 过的牌型列表 private readonly List[] _opponentConstraints; public CardTracker() { _opponentConstraints = new List[3]; for (int i = 0; i < 3; i++) _opponentConstraints[i] = new List(); } /// /// 每次 bot 决策前调用,更新已知信息。 /// myPos 从 1 开始。 /// public void Update(PdkBotView view) { int myPos = view.MyPos; var handIds = new HashSet(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() }; } /// /// 重置追踪器(每局开始时调用) /// public void Reset() { _seenCardIds.Clear(); _lastMyHandIds = null; _lastMaxPlay = new PlayOutCardPdkF(); foreach (var c in _opponentConstraints) c.Clear(); } // ---- 查询接口 ---- /// /// 返回外面还有多少未知卡(48 张发牌去掉我的手牌和已见牌) /// public int UnknownCount(TCardInfoPdkF[] myHand) { int myCount = myHand.Count(c => c.GameState == 1); return 48 - myCount - _seenCardIds.Count; } /// 返回指定 rank 剩余未知张数 (0-4) 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; } /// 返回未知牌池(排除我的手牌和已见牌后的所有未出现牌) public List GetUnknownPool(TCardInfoPdkF[] myHand) { var knownIds = new HashSet(_seenCardIds); foreach (var c in myHand) knownIds.Add(c.ID); var pool = new List(); 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; } /// /// 检查指定牌型的出牌是否绝对无法被压制。 /// 基于确定性信息:我的手牌 + 已见牌 = 对手可能持有的牌池完全已知。 /// 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; } /// 外面是否存在能压制该 rank 的炸弹 private bool HasBombOutside(TCardInfoPdkF[] myHand) { return BombsOutside(myHand) > 0; } /// 检查未知牌池中是否存在更大 rank 的 count 张同 rank 牌 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; } /// 检查是否存在更大起点的同长顺子(count=张数) 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; } /// 检查是否存在更大起点的连对(pairs=对数) 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; } /// 检查是否存在更大起点的飞机(triples=三连数) 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; } /// /// 确定性手牌强度:基于绝对牌计数评估手牌实力,0.0-1.0。 /// 核心思想:我的手牌 + 已见牌 = 对手的牌池完全确定, /// 可以精确计算哪些 rank 已被我控制、哪些出牌无敌。 /// 用于修正 ISMCTS 模拟噪声。 /// 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); } /// /// 预测对手手牌组成。 /// 基于:未知牌池中各 rank 剩余张数 + 对手手牌张数 → 期望持有量。 /// 用于修正 ISMCTS 随机采样,使模拟更接近真实分布。 /// 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; } /// 在外面可能存在的炸弹数(按 rank 计) public int BombsOutside(TCardInfoPdkF[] myHand) { int bombs = 0; var myCounts = new Dictionary(); 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; } /// 外面有炸弹的 rank 列表 public List BombRanks(TCardInfoPdkF[] myHand) { var result = new List(); var myCounts = new Dictionary(); 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; } /// 根据已知牌判断:这个出牌有没有可能被压制 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); } } /// 生成约束采样的对手手牌(用于 ISMCTS 模拟) public List[] 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(_seenCardIds); foreach (var c in myHand) knownIds.Add(c.ID); var pool = new List(); 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(); var h2 = new List(); 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(); 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(List 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; } } /// /// 对手手牌预测结果 — 基于剩余牌计数 + pass 约束的确定性推断 /// public class OpponentPrediction { /// 每个 rank 对手至少持有一张的概率 (0.0-1.0) public Dictionary RankProbabilities = new Dictionary(); /// 对手最可能的最高牌 rank (概率>30%) public int MaxRank; /// 对手可能有炸弹的概率 public double BombProbability; /// 对手很可能持有对子的 rank 列表 public List LikelyPairs = new List(); /// 对手很可能持有三张的 rank 列表 public List LikelyTriples = new List(); /// 对手可能持有的最大顺子长度 public int MaxStraightLength; /// 对手是否很可能没有高于指定 rank 的单牌 public bool LikelyNoHigherThan(int rank) => MaxRank > 0 && MaxRank <= rank; /// 对手是否很可能没有高于指定 rank 的对子 public bool LikelyNoPairHigherThan(int rank) => !LikelyPairs.Any(r => r > rank); } }