diff --git a/PdkFriendServer/Logic/PdkGameMain.cs b/PdkFriendServer/Logic/PdkGameMain.cs index 2cf2cc4f..da513ebb 100644 --- a/PdkFriendServer/Logic/PdkGameMain.cs +++ b/PdkFriendServer/Logic/PdkGameMain.cs @@ -61,7 +61,7 @@ namespace PdkFriendServer.Logic { _bots = new IPdkBot[3]; for (int i = 0; i < 3; i++) - _bots[i] = new SOIsmctsBot(); + _bots[i] = new IsmctsBot(); } /// 构建当前玩家的可见视图——只看自己手牌+公开信息 diff --git a/PdkFriendServer/Logic/SOIsmctsBot.cs b/PdkFriendServer/Logic/SOIsmctsBot.cs index ac443bd1..ffb937cf 100644 --- a/PdkFriendServer/Logic/SOIsmctsBot.cs +++ b/PdkFriendServer/Logic/SOIsmctsBot.cs @@ -9,12 +9,12 @@ using GameMessage.PaoDeKuaiF; namespace PdkFriendServer.Logic { /// - /// SO-ISMCTS v2 — 优化版,静态牌池+数组交换替代对象分配。 + /// SO-ISMCTS v4 — 引擎级 rollout,支持全部牌型。 /// - /// 性能关键: - /// - 48张牌静态数组,模拟时只交换索引不创建新对象 - /// - 预生成N份对手手牌分布,分摊 determinization 成本 - /// - 贪心 rollout 直接操作数组切片 + /// 核心改进:用 PdkCardAlgorithm.GetTipCard 替代字节级贪心模拟, + /// 支持单张/对子/三张/三带二/顺子/连对/飞机/炸弹/四带二/飞机带对。 + /// + /// Rollout 策略:优先清牌效率(多牌优先),但保留大牌控制权。 /// public class SOIsmctsBot : IPdkBot { @@ -22,17 +22,17 @@ namespace PdkFriendServer.Logic private readonly Random _rng; private readonly CardTracker _tracker = new CardTracker(); - // 静态牌池:52张牌(同 IsmctsBot.GenerateDeck),只创建一次 + // 静态牌池:52张牌,只创建一次,ID→card 快速查找 private static readonly TCardInfoPdkF[] FullDeck; static SOIsmctsBot() { - FullDeck = new TCardInfoPdkF[52]; + FullDeck = new TCardInfoPdkF[53]; // 1-indexed int id = 1; foreach (int f in new[] { 1, 2, 3, 4 }) for (int n = 1; n <= 13; n++) - FullDeck[id - 1] = new TCardInfoPdkF + FullDeck[id] = new TCardInfoPdkF { - ID = (byte)(id++), + ID = (byte)id, Flower = (byte)f, GameNum = (byte)(n == 1 ? 14 : n == 2 ? 16 : n), GameState = 1 @@ -54,10 +54,10 @@ namespace PdkFriendServer.Logic int wins = 0; for (int s = 0; s < 400; s++) - if (SimulateToEnd(view.MyHand, view, null)) wins++; + if (Rollout(view.MyHand, view, null)) wins++; double wr = (double)wins / 400; - Console.WriteLine($"[SOv2 pos{view.MyPos}] 包庄: min={minPlays} wr={wr:P0} → {(wr >= threshold ? "包" : "不")}"); + Console.WriteLine($"[SOv4 pos{view.MyPos}] 包庄: min={minPlays} wr={wr:P0} → {(wr >= threshold ? "包" : "不")}"); return wr >= threshold; } @@ -67,20 +67,18 @@ namespace PdkFriendServer.Logic var hand = view.MyHand; if (hand.Length == 0) return Pass(view); - // 终局精确求解 if (hand.Length <= 5 && _tracker.UnknownCount <= 15) { var eg = EndgameSolver.Solve(hand, view, _tracker, _rng); if (eg.HasValue) return eg.Value; } - // 锁手检测 if (hand.Length <= 8) { var lockSeq = LockDetector.FindLockSequence(hand, _tracker, view.MyPos); if (lockSeq != null) { - Console.WriteLine($"[SOv2 pos{view.MyPos}] LOCK {lockSeq.Count}步"); + Console.WriteLine($"[SOv4 pos{view.MyPos}] LOCK {lockSeq.Count}步"); return new PlayOutCardPdkF { Pos = (byte)view.MyPos, GameNum = lockSeq[0].GameNum, Ids = lockSeq[0].Ids, Type = CardType1.None }; } } @@ -89,223 +87,176 @@ namespace PdkFriendServer.Logic var candidates = BuildCandidates(hand, view, isFirstPlay); if (candidates.Count == 1) return candidates[0].play; - // SO-ISMCTS 主循环:单树 + 预生成手牌 var root = new SONode(candidates.Count); int myPos = view.MyPos; - - // 预生成 N 份对手手牌分布 - int preGen = Math.Min(300, _simBudget); - var hands1 = new byte[preGen][]; - var hands2 = new byte[preGen][]; - - // 建立已知牌ID集合 - var knownIds = new HashSet(); - foreach (var c in hand) knownIds.Add(c.ID); int o1c = view.ShenYuCard[myPos % 3]; int o2c = view.ShenYuCard[(myPos + 1) % 3]; - var pool = new byte[52 - knownIds.Count]; - int pi = 0; - foreach (var c in FullDeck) - if (!knownIds.Contains(c.ID)) - pool[pi++] = c.ID; + // 预生成对手手牌 (TCardInfoPdkF) + var knownIds = new HashSet(); + foreach (var c in hand) knownIds.Add(c.ID); + var poolCards = FullDeck.Where(c => c.ID > 0 && !knownIds.Contains(c.ID)).ToList(); + + int preGen = Math.Min(200, _simBudget); + var preHands1 = new TCardInfoPdkF[preGen][]; + var preHands2 = new TCardInfoPdkF[preGen][]; for (int g = 0; g < preGen; g++) { - Shuffle(pool, _rng); - hands1[g] = new byte[o1c]; - hands2[g] = new byte[o2c]; - Array.Copy(pool, 0, hands1[g], 0, o1c); - Array.Copy(pool, o1c, hands2[g], 0, o2c); + Shuffle(poolCards, _rng); + preHands1[g] = poolCards.Take(o1c).ToArray(); + preHands2[g] = poolCards.Skip(o1c).Take(o2c).ToArray(); } - int genIdx = 0; - int totalSims = 0; + int genIdx = 0, totalSims = 0; for (int iter = 0; iter < _simBudget; iter++) { int ci = _rng.Next(candidates.Count); - var play = candidates[ci]; + var (play, isMulti) = candidates[ci]; - // 取一份预生成手牌 - var o1 = hands1[genIdx % preGen]; - var o2 = hands2[genIdx % preGen]; - genIdx++; - - bool win; - if (play.play.Type == CardType1.None) // pass - { - win = SimPass(hand, view, o1, o2); - } - else - { - var myRem = hand.Where(c => c.GameState == 1 && !play.play.Ids.Contains(c.ID)).ToArray(); - if (myRem.Length == 0) win = true; - else win = SimAfterPlay(myRem, view, o1, o2, play.play.GameNum); - } + // 用预生成的对手手牌 + genIdx = (genIdx + 1) % preGen; + bool win = Rollout(hand, view, play, preHands1[genIdx], preHands2[genIdx]); root.Visit(ci, win); totalSims++; - - if (totalSims % 150 == 0) - { - int converged = root.BestChild(candidates.Count); - if (converged >= 0 && root.VisitCount(converged) > _simBudget / candidates.Count / 2 && root.WinRate(converged) > 0.85) - break; - } } int bestIdx = root.BestChild(candidates.Count); if (bestIdx < 0) bestIdx = 0; - Console.WriteLine($"[SOv2 pos{view.MyPos}] {totalSims}s best={bestIdx} wr={root.WinRate(bestIdx):P0}"); + Console.WriteLine($"[SOv4 pos{view.MyPos}] {totalSims}s best={bestIdx}/{candidates.Count} wr={root.WinRate(bestIdx):P0}"); return candidates[bestIdx].play; } - // ---- 快速模拟(用数组而非对象) ---- + // ---- Rollout: 引擎级全牌型模拟 ---- - private bool SimPass(TCardInfoPdkF[] myHand, PdkBotView view, byte[] o1Ids, byte[] o2Ids) + /// + /// Rollout 一局到终局。firstPlay=null 时不改状态直接从当前桌面开始。 + /// + private bool Rollout(TCardInfoPdkF[] myHand, PdkBotView view, + PlayOutCardPdkF? myPlay = null, + TCardInfoPdkF[] o1Hand = null, TCardInfoPdkF[] o2Hand = null) { int myPos = view.MyPos; - var maxPlay = view.MaxPlayCard; - int maxPlayer = maxPlay.Pos > 0 ? maxPlay.Pos - 1 : myPos - 1; - int current = myPos % 3; - // 从 ID 数组构建手牌 GameNum 数组 - var mg = new List(myHand.Where(c => c.GameState == 1).Select(c => c.GameNum)); - var o1g = new List(IdsToGameNums(o1Ids)); - var o2g = new List(IdsToGameNums(o2Ids)); - - return FastSim(o1g, o2g, mg, current, maxPlay.GameNum, maxPlay.Type, maxPlayer, 0); - } - - private bool SimAfterPlay(TCardInfoPdkF[] myRem, PdkBotView view, byte[] o1Ids, byte[] o2Ids, byte myGameNum) - { - int myPos = view.MyPos; - int current = myPos % 3; - var mg = new List(myRem.Where(c => c.GameState == 1).Select(c => c.GameNum)); - var o1g = new List(IdsToGameNums(o1Ids)); - var o2g = new List(IdsToGameNums(o2Ids)); - - return FastSim(o1g, o2g, mg, current, myGameNum, CardType1.DanZhang, myPos - 1, 0); - } - - // 快速模拟核心:清牌效率优先。 - // 领牌:优先出多牌组合(3张>2张>1张),用最小的 rank 保留大牌 - // 跟牌:出最小的能压制的牌,保留大牌 - private bool FastSim(List h1, List h2, List my, - int current, byte maxGameNum, CardType1 maxType, int maxPlayer, int depth) - { - if (depth > 30) return my.Count <= Math.Min(h1.Count, h2.Count); - if (h1.Count == 0) return false; - if (h2.Count == 0) return false; - if (my.Count == 0) return true; - - var cur = current == 0 ? h1 : current == 1 ? h2 : my; - bool isNewRound = maxPlayer == current || maxGameNum == 0; - bool isMe = (current == 2); - - if (isNewRound) + var my = myHand.Where(c => c.GameState == 1).ToList(); + if (myPlay.HasValue && myPlay.Value.Type != CardType1.None && myPlay.Value.Ids != null) { - if (cur.Count == 0) return false; + foreach (var id in myPlay.Value.Ids) my.RemoveAll(c => c.ID == id); + if (my.Count == 0) return true; + } - // 清牌效率排序: 三张 > 对子 > 单张。始终用最小的 rank - var groups = cur.GroupBy(g => g).OrderBy(g => g.Key).ToList(); - - // 三张(清 3 张) - var triples = groups.Where(g => g.Count() >= 3).ToList(); - if (triples.Count > 0) - { - var t = triples[0]; - var toRemove = t.Take(3).ToList(); - foreach (var g in toRemove) cur.Remove(g); - return FastSim(h1, h2, my, (current + 1) % 3, t.Key, CardType1.SanZhang, current, depth + 1); - } - - // 对子(清 2 张) - var pairs = groups.Where(g => g.Count() >= 2).ToList(); - if (pairs.Count > 0) - { - var p = pairs[0]; - var toRemove = p.Take(2).ToList(); - foreach (var g in toRemove) cur.Remove(g); - return FastSim(h1, h2, my, (current + 1) % 3, p.Key, CardType1.DuiZi, current, depth + 1); - } - - // 单张:出最小的 - byte play = groups[0].Key; - cur.Remove(play); - return FastSim(h1, h2, my, (current + 1) % 3, play, CardType1.DanZhang, current, depth + 1); + // 构建或取样对手手牌 + List o1, o2; + if (o1Hand != null) + { + o1 = new List(o1Hand); + o2 = new List(o2Hand); } else { - // 跟牌:有牌能压 → 出最小的压制牌 - if (maxType == CardType1.DuiZi || maxType == CardType1.SanZhang) - { - int need = maxType == CardType1.SanZhang ? 3 : 2; - var groups = cur.GroupBy(g => g) - .Where(g => g.Count() >= need && g.Key > maxGameNum) - .OrderBy(g => g.Key).ToList(); - if (groups.Count > 0) - { - var g = groups[0]; - var toRemove = g.Take(need).ToList(); - foreach (var x in toRemove) cur.Remove(x); - return FastSim(h1, h2, my, (current + 1) % 3, g.Key, maxType, current, depth + 1); - } - } - - // 单张跟牌:出最小的可压牌 - var bigger = cur.Where(g => g > maxGameNum).OrderBy(g => g).ToList(); - if (bigger.Count > 0) - { - byte play = bigger[0]; // 最小压制 - cur.Remove(play); - return FastSim(h1, h2, my, (current + 1) % 3, play, CardType1.DanZhang, current, depth + 1); - } - // pass - return FastSim(h1, h2, my, (current + 1) % 3, maxGameNum, maxType, isNewRound ? current : maxPlayer, depth + 1); + var known = new HashSet(); + foreach (var c in myHand) known.Add(c.ID); + var pool = FullDeck.Where(c => c.ID > 0 && !known.Contains(c.ID)).ToList(); + Shuffle(pool, _rng); + int o1c = view.ShenYuCard[myPos % 3]; + o1 = pool.Take(o1c).ToList(); + o2 = pool.Skip(o1c).Take(view.ShenYuCard[(myPos + 1) % 3]).ToList(); } + + // 初始状态 + PlayOutCardPdkF maxPlay; + int maxPlayer, current; + + if (myPlay.HasValue && myPlay.Value.Type != CardType1.None) + { + maxPlay = ClonePlay(myPlay.Value); + maxPlayer = myPos - 1; + current = myPos % 3; + } + else + { + maxPlay = ClonePlay(view.MaxPlayCard); + maxPlayer = maxPlay.Pos > 0 ? maxPlay.Pos - 1 : myPos - 1; + current = myPos % 3; + } + + return SimLoop(my, o1, o2, current, maxPlay, maxPlayer, 0); } - // ---- 旧的 SimulateToEnd (保留兼容) ---- - - private bool SimulateToEnd(TCardInfoPdkF[] myHand, PdkBotView view, PlayOutCardPdkF? fp) + private bool SimLoop(List my, List o1, List o2, + int current, PlayOutCardPdkF maxPlay, int maxPlayer, int depth) { - int myPos = view.MyPos; - var myList = myHand.Where(c => c.GameState == 1).Select(c => c.GameNum).ToList(); - int o1c = view.ShenYuCard[myPos % 3]; - int o2c = view.ShenYuCard[(myPos + 1) % 3]; + if (depth > 50) return my.Count <= Math.Min(o1.Count, o2.Count); + if (o1.Count == 0) return false; + if (o2.Count == 0) return false; + if (my.Count == 0) return true; - var knownIds = new HashSet(); - foreach (var c in myHand) knownIds.Add(c.ID); + var cur = current == 0 ? o1 : current == 1 ? o2 : my; + bool isNewRound = maxPlayer == current || maxPlay.GameNum <= 0; + int next = (current + 1) % 3; - var pool = FullDeck.Where(c => !knownIds.Contains(c.ID)).Select(c => c.GameNum).ToList(); - Shuffle(pool, _rng); - var o1 = pool.Take(o1c).ToList(); - var o2 = pool.Skip(o1c).Take(o2c).ToList(); + // 用引擎获取合法出牌 + var tips = PdkCardAlgorithm.GetTipCard( + isNewRound ? new PlayOutCardPdkF() : maxPlay, + cur.ToArray(), null, false); - byte maxGn = 0; - int maxP = 0; - int cur = myPos % 3; - - if (fp.HasValue && fp.Value.Type != CardType1.None) + if (tips != null && tips.Count > 0) { - maxGn = fp.Value.GameNum; - maxP = myPos - 1; - foreach (var id in fp.Value.Ids ?? Array.Empty()) - myList.RemoveAll(g => g == GameNumOf(id)); - } - else if (fp.HasValue) // pass - { - maxGn = view.MaxPlayCard.GameNum; - maxP = view.MaxPlayCard.Pos > 0 ? view.MaxPlayCard.Pos - 1 : myPos - 1; + // Rollout 策略:多牌优先 + 最小 rank 保留大牌 + // isNewRound 时 GetTipCard 只返回单张 → 需要补充多牌先手 + List> allPlays = new List>(tips); + if (isNewRound) + { + var arr = cur.ToArray(); + // 对子 (2张同rank) + foreach (var g in arr.GroupBy(c => c.GameNum).Where(g => g.Count() >= 2)) + allPlays.Add(g.Take(2).ToList()); + // 三张 + foreach (var g in arr.GroupBy(c => c.GameNum).Where(g => g.Count() >= 3)) + allPlays.Add(g.Take(3).ToList()); + // 顺子 (≥5张连续rank) + var ranks = arr.Select(c => (int)c.GameNum).Where(r => r >= 3 && r <= 14).Distinct().OrderBy(r => r).ToList(); + for (int len = Math.Min(ranks.Count, 12); len >= 5; len--) + for (int si = 0; si <= ranks.Count - len; si++) + { + bool isConsecutive = true; + for (int i = 1; i < len; i++) + if (ranks[si + i] != ranks[si + i - 1] + 1) + { isConsecutive = false; break; } + if (!isConsecutive) continue; + var strCards = ranks.Skip(si).Take(len) + .SelectMany(r => arr.Where(c => c.GameNum == r).Take(1)).ToList(); + if (strCards.Count == len) allPlays.Add(strCards); + } + } + + // 选择:优先多牌(×10权重),同长度选最小rank + var best = allPlays + .OrderByDescending(t => t.Count * 10) + .ThenBy(t => t[0].GameNum) + .First(); + + foreach (var c in best) cur.RemoveAll(x => x.ID == c.ID); + + var newMax = new PlayOutCardPdkF + { + GameNum = best[0].GameNum, + Type = InferType(best), + Ids = best.Select(c => c.ID).ToArray() + }; + + return SimLoop(my, o1, o2, next, newMax, current, depth + 1); } - return FastSim(o1, o2, myList, cur, maxGn, CardType1.DanZhang, maxP, 0); + // pass + int newMaxPlayer = isNewRound ? current : maxPlayer; + return SimLoop(my, o1, o2, next, maxPlay, newMaxPlayer, depth + 1); } - // ---- 辅助 ---- + // ---- 候选构建 ---- private List<(PlayOutCardPdkF play, bool isMulti)> BuildCandidates( TCardInfoPdkF[] hand, PdkBotView view, bool isFirstPlay) @@ -314,7 +265,7 @@ namespace PdkFriendServer.Logic var candidates = new List<(PlayOutCardPdkF, bool)>(); if (tips != null) foreach (var t in tips) - candidates.Add((MakePlay(t, view.MyPos), t.Count > 1)); + candidates.Add((new PlayOutCardPdkF { Pos = (byte)view.MyPos, GameNum = t[0].GameNum, Ids = t.Select(c => c.ID).ToArray(), Type = InferType(t) }, t.Count > 1)); if (isFirstPlay) AddMultiCardLeads(hand, view.MyPos, candidates); if (!isFirstPlay) @@ -322,23 +273,28 @@ namespace PdkFriendServer.Logic return candidates; } - // 复用 IsmctsBot 的多牌枚举(对子/三张/顺子/飞机/炸弹/连对) private static void AddMultiCardLeads(TCardInfoPdkF[] hand, int pos, List<(PlayOutCardPdkF, bool)> candidates) { - var make = (List cards) => candidates.Add(( + void add(List cards) => candidates.Add(( new PlayOutCardPdkF { Pos = (byte)pos, GameNum = cards[0].GameNum, Ids = cards.Select(c => c.ID).ToArray(), Type = InferType(cards) }, true)); if (PokerLogic.GetPlayZhaDans(hand, out var bombs) && bombs != null) - foreach (var b in bombs) make(b); + foreach (var b in bombs) add(b); foreach (var g in hand.GroupBy(c => c.GameNum).Where(g => g.Count() >= 2)) - make(g.Take(2).ToList()); + add(g.Take(2).ToList()); foreach (var g in hand.GroupBy(c => c.GameNum).Where(g => g.Count() >= 3)) - make(g.Take(3).ToList()); + { + var t = g.Take(3).ToList(); + var rest = hand.Where(c => c.GameNum != g.Key).Take(2).ToList(); + add(rest.Count == 2 ? t.Concat(rest).ToList() : t); + } var straights = PokerLogic.GetShunZi(hand); - if (straights != null) foreach (var s in straights) make(s); + if (straights != null) foreach (var s in straights) add(s); } + // ---- 辅助 ---- + private static CardType1 InferType(List cards) { int n = cards.Count; @@ -346,86 +302,36 @@ namespace PdkFriendServer.Logic if (n == 2) return CardType1.DuiZi; var cnts = cards.GroupBy(c => c.GameNum).Select(g => g.Count()).OrderByDescending(x => x).ToList(); if (cnts[0] == 4) { if (n == 4) return CardType1.ZhaDan; if (n == 6) return CardType1.SiDai2; if (n == 7) return CardType1.SiDai3; } - if (cnts[0] >= 3) return CardType1.SanZhang; - if (cnts[0] == 2 && n % 2 == 0) return CardType1.LianDui; - return CardType1.ShunZi; + if (cnts[0] >= 3) return n == 3 ? CardType1.SanZhang : CardType1.SanDai2; + if (cnts[0] == 2 && n % 2 == 0) return n == 4 ? CardType1.LianDui : CardType1.LianDui; + return n >= 5 ? CardType1.ShunZi : CardType1.DanZhang; } - private static PlayOutCardPdkF MakePlay(List cards, int pos) - => new PlayOutCardPdkF { Pos = (byte)pos, GameNum = cards[0].GameNum, Ids = cards.Select(x => x.ID).ToArray(), Type = CardType1.DanZhang }; - private static PlayOutCardPdkF Pass(PdkBotView v) => new PlayOutCardPdkF { Pos = (byte)v.MyPos, Type = CardType1.None }; - private static List IdsToGameNums(byte[] ids) - { - var result = new List(); - foreach (var id in ids) - { - var card = FullDeck.FirstOrDefault(c => c.ID == id); - if (card.GameNum > 0) result.Add(card.GameNum); - } - return result; - } - - private static byte GameNumOf(byte id) - { - var card = FullDeck.FirstOrDefault(c => c.ID == id); - return card.GameNum; - } - - private static void Shuffle(T[] arr, Random rng) - { - for (int i = arr.Length - 1; i > 0; i--) - { - int j = rng.Next(i + 1); - (arr[i], arr[j]) = (arr[j], arr[i]); - } - } + private static PlayOutCardPdkF ClonePlay(PlayOutCardPdkF s) + => new PlayOutCardPdkF { Pos = s.Pos, GameNum = s.GameNum, Type = s.Type, Ids = s.Ids?.ToArray() }; 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]); - } + { int j = rng.Next(i + 1); (list[i], list[j]) = (list[j], list[i]); } } } internal class SONode { - private readonly int[] _wins; - private readonly int[] _visits; - - public SONode(int numChildren) - { - _wins = new int[numChildren]; - _visits = new int[numChildren]; - } - - public void Visit(int idx, bool win) - { - if (idx < 0 || idx >= _visits.Length) return; - _visits[idx]++; - if (win) _wins[idx]++; - } - - public double WinRate(int idx) - => idx >= 0 && idx < _visits.Length && _visits[idx] > 0 ? (double)_wins[idx] / _visits[idx] : 0; - - public int VisitCount(int idx) - => idx >= 0 && idx < _visits.Length ? _visits[idx] : 0; - + private readonly int[] _wins, _visits; + public SONode(int n) { _wins = new int[n]; _visits = new int[n]; } + public void Visit(int i, bool w) { if (i >= 0 && i < _visits.Length) { _visits[i]++; if (w) _wins[i]++; } } + public double WinRate(int i) => i >= 0 && i < _visits.Length && _visits[i] > 0 ? (double)_wins[i] / _visits[i] : 0; + public int VisitCount(int i) => i >= 0 && i < _visits.Length ? _visits[i] : 0; public int BestChild(int n) { - int best = -1; - double bestRate = -1; + int best = -1; double bestRate = -1; for (int i = 0; i < Math.Min(n, _visits.Length); i++) - { - double rate = WinRate(i); - if (rate > bestRate) { bestRate = rate; best = i; } - } + { double r = WinRate(i); if (r > bestRate) { bestRate = r; best = i; } } return best; } } diff --git a/exe/PdkFriendServer/PdkFriendServer.dll b/exe/PdkFriendServer/PdkFriendServer.dll index c5e54891..79572a14 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 77616822..5e3632c5 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 c5e54891..79572a14 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 77616822..5e3632c5 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 bc5eaa0d..664b4eac 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 bf0b43e9..7a6b3051 100644 Binary files a/exe/hjha-console/hjha-console.pdb and b/exe/hjha-console/hjha-console.pdb differ