diff --git a/PdkFriendServer/Logic/CardTracker.cs b/PdkFriendServer/Logic/CardTracker.cs index 3c4c6b77..dbca7c6e 100644 --- a/PdkFriendServer/Logic/CardTracker.cs +++ b/PdkFriendServer/Logic/CardTracker.cs @@ -96,8 +96,151 @@ namespace PdkFriendServer.Logic // ---- 查询接口 ---- - /// 返回外面还有多少未知卡 - public int UnknownCount => 52 - _seenCardIds.Count; + /// + /// 返回外面还有多少未知卡(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; + } /// 在外面可能存在的炸弹数(按 rank 计) public int BombsOutside(TCardInfoPdkF[] myHand) diff --git a/PdkFriendServer/Logic/EndgameSolver.cs b/PdkFriendServer/Logic/EndgameSolver.cs index 160b4dd5..d807b0ef 100644 --- a/PdkFriendServer/Logic/EndgameSolver.cs +++ b/PdkFriendServer/Logic/EndgameSolver.cs @@ -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 GeneratePool(TCardInfoPdkF[] hand, CardTracker tracker) { - var used = new HashSet(); - foreach (var c in hand) used.Add(c.ID); - // CardTracker has constraint sampling — we just need the full pool here - - 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 (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) diff --git a/PdkFriendServer/Logic/IsmctsBot.cs b/PdkFriendServer/Logic/IsmctsBot.cs index 22198b6c..cfb294bd 100644 --- a/PdkFriendServer/Logic/IsmctsBot.cs +++ b/PdkFriendServer/Logic/IsmctsBot.cs @@ -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]; diff --git a/PdkFriendServer/Logic/SOIsmctsBot.cs b/PdkFriendServer/Logic/SOIsmctsBot.cs index ffb937cf..b57b98d7 100644 --- a/PdkFriendServer/Logic/SOIsmctsBot.cs +++ b/PdkFriendServer/Logic/SOIsmctsBot.cs @@ -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; diff --git a/exe/PdkFriendServer/PdkFriendServer.dll b/exe/PdkFriendServer/PdkFriendServer.dll index 790c3a55..cc3e6ec3 100644 Binary files a/exe/PdkFriendServer/PdkFriendServer.dll and b/exe/PdkFriendServer/PdkFriendServer.dll differ diff --git a/exe/PdkFriendServer/PdkFriendServer.pdb b/exe/PdkFriendServer/PdkFriendServer.pdb index 68e1e801..1daf6035 100644 Binary files a/exe/PdkFriendServer/PdkFriendServer.pdb and b/exe/PdkFriendServer/PdkFriendServer.pdb differ diff --git a/exe/hjha-console/PdkFriendServer.dll b/exe/hjha-console/PdkFriendServer.dll index 790c3a55..cc3e6ec3 100644 Binary files a/exe/hjha-console/PdkFriendServer.dll and b/exe/hjha-console/PdkFriendServer.dll differ diff --git a/exe/hjha-console/PdkFriendServer.pdb b/exe/hjha-console/PdkFriendServer.pdb index 68e1e801..1daf6035 100644 Binary files a/exe/hjha-console/PdkFriendServer.pdb and b/exe/hjha-console/PdkFriendServer.pdb differ diff --git a/exe/hjha-console/hjha-console.dll b/exe/hjha-console/hjha-console.dll index 1df05785..8b9b480c 100644 Binary files a/exe/hjha-console/hjha-console.dll and b/exe/hjha-console/hjha-console.dll differ diff --git a/exe/hjha-console/hjha-console.pdb b/exe/hjha-console/hjha-console.pdb index 8e6067d3..ce88349c 100644 Binary files a/exe/hjha-console/hjha-console.pdb and b/exe/hjha-console/hjha-console.pdb differ