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/局
654 lines
25 KiB
C#
654 lines
25 KiB
C#
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);
|
||
}
|
||
} |