结论: 切回IsmctsBot — SO-ISMCTS实验总结

SO-ISMCTS v4 (引擎级rollout+全牌型):
- GetTipCard在isNewRound时只返单张 → rollout对手只出单张
- 补多牌枚举后仍无法达到IsmctsBot的53%多牌率
- 根因: MO-ISMCTS的独立世界树更适合不完全信息卡牌游戏
  SO的优势(信息共享)在对手手牌随机化的场景下被稀释

IsmctsBot最终指标: 多牌53%, 1.0s/局, 全牌型
SOIsmctsBot保留为备选方案
This commit is contained in:
2026-07-12 23:51:24 +08:00
parent 6a267738df
commit b5b02db2e6
8 changed files with 161 additions and 255 deletions

View File

@ -61,7 +61,7 @@ namespace PdkFriendServer.Logic
{ {
_bots = new IPdkBot[3]; _bots = new IPdkBot[3];
for (int i = 0; i < 3; i++) for (int i = 0; i < 3; i++)
_bots[i] = new SOIsmctsBot(); _bots[i] = new IsmctsBot();
} }
/// <summary>构建当前玩家的可见视图——只看自己手牌+公开信息</summary> /// <summary>构建当前玩家的可见视图——只看自己手牌+公开信息</summary>

View File

@ -9,12 +9,12 @@ using GameMessage.PaoDeKuaiF;
namespace PdkFriendServer.Logic namespace PdkFriendServer.Logic
{ {
/// <summary> /// <summary>
/// SO-ISMCTS v2优化版,静态牌池+数组交换替代对象分配 /// SO-ISMCTS v4引擎级 rollout支持全部牌型
/// ///
/// 性能关键: /// 核心改进:用 PdkCardAlgorithm.GetTipCard 替代字节级贪心模拟,
/// - 48张牌静态数组模拟时只交换索引不创建新对象 /// 支持单张/对子/三张/三带二/顺子/连对/飞机/炸弹/四带二/飞机带对。
/// - 预生成N份对手手牌分布分摊 determinization 成本 ///
/// - 贪心 rollout 直接操作数组切片 /// Rollout 策略:优先清牌效率(多牌优先),但保留大牌控制权。
/// </summary> /// </summary>
public class SOIsmctsBot : IPdkBot public class SOIsmctsBot : IPdkBot
{ {
@ -22,17 +22,17 @@ namespace PdkFriendServer.Logic
private readonly Random _rng; private readonly Random _rng;
private readonly CardTracker _tracker = new CardTracker(); private readonly CardTracker _tracker = new CardTracker();
// 静态牌池52张牌(同 IsmctsBot.GenerateDeck,只创建一次 // 静态牌池52张牌只创建一次ID→card 快速查找
private static readonly TCardInfoPdkF[] FullDeck; private static readonly TCardInfoPdkF[] FullDeck;
static SOIsmctsBot() static SOIsmctsBot()
{ {
FullDeck = new TCardInfoPdkF[52]; FullDeck = new TCardInfoPdkF[53]; // 1-indexed
int id = 1; int id = 1;
foreach (int f in new[] { 1, 2, 3, 4 }) foreach (int f in new[] { 1, 2, 3, 4 })
for (int n = 1; n <= 13; n++) 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, Flower = (byte)f,
GameNum = (byte)(n == 1 ? 14 : n == 2 ? 16 : n), GameNum = (byte)(n == 1 ? 14 : n == 2 ? 16 : n),
GameState = 1 GameState = 1
@ -54,10 +54,10 @@ namespace PdkFriendServer.Logic
int wins = 0; int wins = 0;
for (int s = 0; s < 400; s++) 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; 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; return wr >= threshold;
} }
@ -67,20 +67,18 @@ namespace PdkFriendServer.Logic
var hand = view.MyHand; var hand = view.MyHand;
if (hand.Length == 0) return Pass(view); if (hand.Length == 0) return Pass(view);
// 终局精确求解
if (hand.Length <= 5 && _tracker.UnknownCount <= 15) if (hand.Length <= 5 && _tracker.UnknownCount <= 15)
{ {
var eg = EndgameSolver.Solve(hand, view, _tracker, _rng); var eg = EndgameSolver.Solve(hand, view, _tracker, _rng);
if (eg.HasValue) return eg.Value; if (eg.HasValue) return eg.Value;
} }
// 锁手检测
if (hand.Length <= 8) if (hand.Length <= 8)
{ {
var lockSeq = LockDetector.FindLockSequence(hand, _tracker, view.MyPos); var lockSeq = LockDetector.FindLockSequence(hand, _tracker, view.MyPos);
if (lockSeq != null) 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 }; 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); var candidates = BuildCandidates(hand, view, isFirstPlay);
if (candidates.Count == 1) return candidates[0].play; if (candidates.Count == 1) return candidates[0].play;
// SO-ISMCTS 主循环:单树 + 预生成手牌
var root = new SONode(candidates.Count); var root = new SONode(candidates.Count);
int myPos = view.MyPos; 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<byte>();
foreach (var c in hand) knownIds.Add(c.ID);
int o1c = view.ShenYuCard[myPos % 3]; int o1c = view.ShenYuCard[myPos % 3];
int o2c = view.ShenYuCard[(myPos + 1) % 3]; int o2c = view.ShenYuCard[(myPos + 1) % 3];
var pool = new byte[52 - knownIds.Count]; // 预生成对手手牌 (TCardInfoPdkF)
int pi = 0; var knownIds = new HashSet<byte>();
foreach (var c in FullDeck) foreach (var c in hand) knownIds.Add(c.ID);
if (!knownIds.Contains(c.ID)) var poolCards = FullDeck.Where(c => c.ID > 0 && !knownIds.Contains(c.ID)).ToList();
pool[pi++] = c.ID;
int preGen = Math.Min(200, _simBudget);
var preHands1 = new TCardInfoPdkF[preGen][];
var preHands2 = new TCardInfoPdkF[preGen][];
for (int g = 0; g < preGen; g++) for (int g = 0; g < preGen; g++)
{ {
Shuffle(pool, _rng); Shuffle(poolCards, _rng);
hands1[g] = new byte[o1c]; preHands1[g] = poolCards.Take(o1c).ToArray();
hands2[g] = new byte[o2c]; preHands2[g] = poolCards.Skip(o1c).Take(o2c).ToArray();
Array.Copy(pool, 0, hands1[g], 0, o1c);
Array.Copy(pool, o1c, hands2[g], 0, o2c);
} }
int genIdx = 0; int genIdx = 0, totalSims = 0;
int totalSims = 0;
for (int iter = 0; iter < _simBudget; iter++) for (int iter = 0; iter < _simBudget; iter++)
{ {
int ci = _rng.Next(candidates.Count); int ci = _rng.Next(candidates.Count);
var play = candidates[ci]; var (play, isMulti) = candidates[ci];
// 取一份预生成手牌 // 用预生成的对手手牌
var o1 = hands1[genIdx % preGen]; genIdx = (genIdx + 1) % preGen;
var o2 = hands2[genIdx % preGen]; bool win = Rollout(hand, view, play, preHands1[genIdx], preHands2[genIdx]);
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);
}
root.Visit(ci, win); root.Visit(ci, win);
totalSims++; 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); int bestIdx = root.BestChild(candidates.Count);
if (bestIdx < 0) bestIdx = 0; 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; return candidates[bestIdx].play;
} }
// ---- 快速模拟(用数组而非对象) ---- // ---- Rollout: 引擎级全牌型模拟 ----
private bool SimPass(TCardInfoPdkF[] myHand, PdkBotView view, byte[] o1Ids, byte[] o2Ids) /// <summary>
/// Rollout 一局到终局。firstPlay=null 时不改状态直接从当前桌面开始。
/// </summary>
private bool Rollout(TCardInfoPdkF[] myHand, PdkBotView view,
PlayOutCardPdkF? myPlay = null,
TCardInfoPdkF[] o1Hand = null, TCardInfoPdkF[] o2Hand = null)
{ {
int myPos = view.MyPos; 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 my = myHand.Where(c => c.GameState == 1).ToList();
var mg = new List<byte>(myHand.Where(c => c.GameState == 1).Select(c => c.GameNum)); if (myPlay.HasValue && myPlay.Value.Type != CardType1.None && myPlay.Value.Ids != null)
var o1g = new List<byte>(IdsToGameNums(o1Ids));
var o2g = new List<byte>(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<byte>(myRem.Where(c => c.GameState == 1).Select(c => c.GameNum));
var o1g = new List<byte>(IdsToGameNums(o1Ids));
var o2g = new List<byte>(IdsToGameNums(o2Ids));
return FastSim(o1g, o2g, mg, current, myGameNum, CardType1.DanZhang, myPos - 1, 0);
}
// 快速模拟核心:清牌效率优先。
// 领牌:优先出多牌组合(3张>2张>1张),用最小的 rank 保留大牌
// 跟牌:出最小的能压制的牌,保留大牌
private bool FastSim(List<byte> h1, List<byte> h2, List<byte> 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)
{ {
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(); List<TCardInfoPdkF> o1, o2;
if (o1Hand != null)
// 三张(清 3 张) {
var triples = groups.Where(g => g.Count() >= 3).ToList(); o1 = new List<TCardInfoPdkF>(o1Hand);
if (triples.Count > 0) o2 = new List<TCardInfoPdkF>(o2Hand);
{
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);
} }
else else
{ {
// 跟牌:有牌能压 → 出最小的压制牌 var known = new HashSet<byte>();
if (maxType == CardType1.DuiZi || maxType == CardType1.SanZhang) foreach (var c in myHand) known.Add(c.ID);
{ var pool = FullDeck.Where(c => c.ID > 0 && !known.Contains(c.ID)).ToList();
int need = maxType == CardType1.SanZhang ? 3 : 2; Shuffle(pool, _rng);
var groups = cur.GroupBy(g => g) int o1c = view.ShenYuCard[myPos % 3];
.Where(g => g.Count() >= need && g.Key > maxGameNum) o1 = pool.Take(o1c).ToList();
.OrderBy(g => g.Key).ToList(); o2 = pool.Skip(o1c).Take(view.ShenYuCard[(myPos + 1) % 3]).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);
} }
// 初始状态
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 SimLoop(List<TCardInfoPdkF> my, List<TCardInfoPdkF> o1, List<TCardInfoPdkF> o2,
int current, PlayOutCardPdkF maxPlay, int maxPlayer, int depth)
private bool SimulateToEnd(TCardInfoPdkF[] myHand, PdkBotView view, PlayOutCardPdkF? fp)
{ {
int myPos = view.MyPos; if (depth > 50) return my.Count <= Math.Min(o1.Count, o2.Count);
var myList = myHand.Where(c => c.GameState == 1).Select(c => c.GameNum).ToList(); if (o1.Count == 0) return false;
int o1c = view.ShenYuCard[myPos % 3]; if (o2.Count == 0) return false;
int o2c = view.ShenYuCard[(myPos + 1) % 3]; if (my.Count == 0) return true;
var knownIds = new HashSet<byte>(); var cur = current == 0 ? o1 : current == 1 ? o2 : my;
foreach (var c in myHand) knownIds.Add(c.ID); 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 tips = PdkCardAlgorithm.GetTipCard(
var o1 = pool.Take(o1c).ToList(); isNewRound ? new PlayOutCardPdkF() : maxPlay,
var o2 = pool.Skip(o1c).Take(o2c).ToList(); cur.ToArray(), null, false);
byte maxGn = 0; if (tips != null && tips.Count > 0)
int maxP = 0;
int cur = myPos % 3;
if (fp.HasValue && fp.Value.Type != CardType1.None)
{ {
maxGn = fp.Value.GameNum; // Rollout 策略:多牌优先 + 最小 rank 保留大牌
maxP = myPos - 1; // isNewRound 时 GetTipCard 只返回单张 → 需要补充多牌先手
foreach (var id in fp.Value.Ids ?? Array.Empty<byte>()) List<List<TCardInfoPdkF>> allPlays = new List<List<TCardInfoPdkF>>(tips);
myList.RemoveAll(g => g == GameNumOf(id)); if (isNewRound)
} {
else if (fp.HasValue) // pass var arr = cur.ToArray();
{ // 对子 (2张同rank)
maxGn = view.MaxPlayCard.GameNum; foreach (var g in arr.GroupBy(c => c.GameNum).Where(g => g.Count() >= 2))
maxP = view.MaxPlayCard.Pos > 0 ? view.MaxPlayCard.Pos - 1 : myPos - 1; 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( private List<(PlayOutCardPdkF play, bool isMulti)> BuildCandidates(
TCardInfoPdkF[] hand, PdkBotView view, bool isFirstPlay) TCardInfoPdkF[] hand, PdkBotView view, bool isFirstPlay)
@ -314,7 +265,7 @@ namespace PdkFriendServer.Logic
var candidates = new List<(PlayOutCardPdkF, bool)>(); var candidates = new List<(PlayOutCardPdkF, bool)>();
if (tips != null) if (tips != null)
foreach (var t in tips) 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) if (isFirstPlay)
AddMultiCardLeads(hand, view.MyPos, candidates); AddMultiCardLeads(hand, view.MyPos, candidates);
if (!isFirstPlay) if (!isFirstPlay)
@ -322,23 +273,28 @@ namespace PdkFriendServer.Logic
return candidates; return candidates;
} }
// 复用 IsmctsBot 的多牌枚举(对子/三张/顺子/飞机/炸弹/连对)
private static void AddMultiCardLeads(TCardInfoPdkF[] hand, int pos, private static void AddMultiCardLeads(TCardInfoPdkF[] hand, int pos,
List<(PlayOutCardPdkF, bool)> candidates) List<(PlayOutCardPdkF, bool)> candidates)
{ {
var make = (List<TCardInfoPdkF> cards) => candidates.Add(( void add(List<TCardInfoPdkF> cards) => candidates.Add((
new PlayOutCardPdkF { Pos = (byte)pos, GameNum = cards[0].GameNum, Ids = cards.Select(c => c.ID).ToArray(), Type = InferType(cards) }, true)); 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) 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)) 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)) 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); 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<TCardInfoPdkF> cards) private static CardType1 InferType(List<TCardInfoPdkF> cards)
{ {
int n = cards.Count; int n = cards.Count;
@ -346,86 +302,36 @@ namespace PdkFriendServer.Logic
if (n == 2) return CardType1.DuiZi; if (n == 2) return CardType1.DuiZi;
var cnts = cards.GroupBy(c => c.GameNum).Select(g => g.Count()).OrderByDescending(x => x).ToList(); 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] == 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] >= 3) return n == 3 ? CardType1.SanZhang : CardType1.SanDai2;
if (cnts[0] == 2 && n % 2 == 0) return CardType1.LianDui; if (cnts[0] == 2 && n % 2 == 0) return n == 4 ? CardType1.LianDui : CardType1.LianDui;
return CardType1.ShunZi; return n >= 5 ? CardType1.ShunZi : CardType1.DanZhang;
} }
private static PlayOutCardPdkF MakePlay(List<TCardInfoPdkF> 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) private static PlayOutCardPdkF Pass(PdkBotView v)
=> new PlayOutCardPdkF { Pos = (byte)v.MyPos, Type = CardType1.None }; => new PlayOutCardPdkF { Pos = (byte)v.MyPos, Type = CardType1.None };
private static List<byte> IdsToGameNums(byte[] ids) private static PlayOutCardPdkF ClonePlay(PlayOutCardPdkF s)
{ => new PlayOutCardPdkF { Pos = s.Pos, GameNum = s.GameNum, Type = s.Type, Ids = s.Ids?.ToArray() };
var result = new List<byte>();
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>(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 void Shuffle<T>(List<T> list, Random rng) private static void Shuffle<T>(List<T> list, Random rng)
{ {
for (int i = list.Count - 1; i > 0; i--) 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 internal class SONode
{ {
private readonly int[] _wins; private readonly int[] _wins, _visits;
private readonly int[] _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 SONode(int numChildren) 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;
_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;
public int BestChild(int n) public int BestChild(int n)
{ {
int best = -1; int best = -1; double bestRate = -1;
double bestRate = -1;
for (int i = 0; i < Math.Min(n, _visits.Length); i++) for (int i = 0; i < Math.Min(n, _visits.Length); i++)
{ { double r = WinRate(i); if (r > bestRate) { bestRate = r; best = i; } }
double rate = WinRate(i);
if (rate > bestRate) { bestRate = rate; best = i; }
}
return best; return best;
} }
} }

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.