fix: 确定性牌计数推理 — UnknownCount修复 + 绝对大牌判断 + EndgameSolver修正
CardTracker: - UnknownCount: 52→48 - handSize - seenCount (修正底牌+手牌漏算) - 新增 RemainingCount/GetUnknownPool: 精确剩余牌计数 - 新增 IsAbsolutelyHighest: 全牌型确定性无敌判断 (单张/对子/三张/炸弹/顺子/连对/飞机) EndgameSolver: - GeneratePool → 调用 tracker.GetUnknownPool (修复已见牌未排除bug) IsmctsBot: - 确定性无敌出牌加成: 无敌出牌 +15-25% win rate bonus SOIsmctsBot: - 同步 UnknownCount(hand) 调用 验证: 100局0错误, 0.8s/局, ENDGAME求解正常触发
This commit is contained in:
@ -96,8 +96,151 @@ namespace PdkFriendServer.Logic
|
||||
|
||||
// ---- 查询接口 ----
|
||||
|
||||
/// <summary>返回外面还有多少未知卡</summary>
|
||||
public int UnknownCount => 52 - _seenCardIds.Count;
|
||||
/// <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>在外面可能存在的炸弹数(按 rank 计)</summary>
|
||||
public int BombsOutside(TCardInfoPdkF[] myHand)
|
||||
|
||||
@ -22,7 +22,7 @@ namespace PdkFriendServer.Logic
|
||||
Random rng)
|
||||
{
|
||||
if (hand.Length > 5) return null;
|
||||
if (tracker.UnknownCount > 15) return null;
|
||||
if (tracker.UnknownCount(hand) > 15) return null;
|
||||
|
||||
int myPos = view.MyPos;
|
||||
int opp1 = myPos % 3;
|
||||
@ -132,20 +132,7 @@ namespace PdkFriendServer.Logic
|
||||
|
||||
private static List<TCardInfoPdkF> GeneratePool(TCardInfoPdkF[] hand, CardTracker tracker)
|
||||
{
|
||||
var used = new HashSet<byte>();
|
||||
foreach (var c in hand) used.Add(c.ID);
|
||||
// CardTracker has constraint sampling — we just need the full pool here
|
||||
|
||||
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 (used.Contains(id)) continue;
|
||||
int g = n == 1 ? 14 : n == 2 ? 16 : n;
|
||||
pool.Add(new TCardInfoPdkF { ID = id, Flower = suit, GameNum = (byte)g, GameState = 1 });
|
||||
}
|
||||
return pool;
|
||||
return tracker.GetUnknownPool(hand);
|
||||
}
|
||||
|
||||
private static int ComputeCombinations(int n, int k)
|
||||
|
||||
@ -85,13 +85,13 @@ namespace PdkFriendServer.Logic
|
||||
if (hand.Length == 0) return Pass(view);
|
||||
|
||||
// 终局精确求解:手牌≤5 + 未知牌少 → 替代 ISMCTS
|
||||
if (hand.Length <= 5 && _tracker.UnknownCount <= 15)
|
||||
if (hand.Length <= 5 && _tracker.UnknownCount(hand) <= 15)
|
||||
{
|
||||
var endgameResult = EndgameSolver.Solve(hand, view, _tracker, _rng);
|
||||
if (endgameResult.HasValue)
|
||||
{
|
||||
var ep = endgameResult.Value;
|
||||
Console.WriteLine($"[ISMCTS pos{view.MyPos}] ENDGAME hand={hand.Length} unknown={_tracker.UnknownCount} → 精确求解");
|
||||
Console.WriteLine($"[ISMCTS pos{view.MyPos}] ENDGAME hand={hand.Length} unknown={_tracker.UnknownCount(hand)} → 精确求解");
|
||||
return new PlayOutCardPdkF
|
||||
{
|
||||
Pos = (byte)view.MyPos,
|
||||
@ -255,6 +255,24 @@ namespace PdkFriendServer.Logic
|
||||
}
|
||||
}
|
||||
|
||||
// 5.6. 确定性无敌出牌:基于牌计数,如果某出牌绝对无人能压制 → 大幅加分
|
||||
for (int i = 0; i < scoredCandidates.Count; i++)
|
||||
{
|
||||
var (p, wr, am, m) = scoredCandidates[i];
|
||||
if (p.Type != CardType1.None && p.Ids != null && p.Ids.Length > 0)
|
||||
{
|
||||
if (_tracker.IsAbsolutelyHighest(hand, p.GameNum, p.Ids.Length, p.Type))
|
||||
{
|
||||
// 确定性无敌 → 大幅加分(相当于省掉 ISMCTS 模拟噪声)
|
||||
double bonus = 0.15;
|
||||
if (am <= 2) bonus = 0.25; // 马上就要赢
|
||||
else if (m) bonus = 0.20; // 多牌无敌
|
||||
wr += bonus;
|
||||
scoredCandidates[i] = (p, wr, am, m);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 6. 两阶段选择:多牌优先
|
||||
scoredCandidates.Sort((a, b) => b.winRate.CompareTo(a.winRate));
|
||||
var best = scoredCandidates[0];
|
||||
|
||||
@ -67,7 +67,7 @@ namespace PdkFriendServer.Logic
|
||||
var hand = view.MyHand;
|
||||
if (hand.Length == 0) return Pass(view);
|
||||
|
||||
if (hand.Length <= 5 && _tracker.UnknownCount <= 15)
|
||||
if (hand.Length <= 5 && _tracker.UnknownCount(hand) <= 15)
|
||||
{
|
||||
var eg = EndgameSolver.Solve(hand, view, _tracker, _rng);
|
||||
if (eg.HasValue) return eg.Value;
|
||||
|
||||
Reference in New Issue
Block a user