Files
hjha-server/PdkFriendServer/Logic/SOIsmctsBot.cs
xiaoou b5b02db2e6 结论: 切回IsmctsBot — SO-ISMCTS实验总结
SO-ISMCTS v4 (引擎级rollout+全牌型):
- GetTipCard在isNewRound时只返单张 → rollout对手只出单张
- 补多牌枚举后仍无法达到IsmctsBot的53%多牌率
- 根因: MO-ISMCTS的独立世界树更适合不完全信息卡牌游戏
  SO的优势(信息共享)在对手手牌随机化的场景下被稀释

IsmctsBot最终指标: 多牌53%, 1.0s/局, 全牌型
SOIsmctsBot保留为备选方案
2026-07-12 23:51:24 +08:00

338 lines
14 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System;
using System.Collections.Generic;
using System.Linq;
using GameFix.PaoDeKuaiF;
using GameFix.Poker;
using GameMessage;
using GameMessage.PaoDeKuaiF;
namespace PdkFriendServer.Logic
{
/// <summary>
/// SO-ISMCTS v4 — 引擎级 rollout支持全部牌型。
///
/// 核心改进:用 PdkCardAlgorithm.GetTipCard 替代字节级贪心模拟,
/// 支持单张/对子/三张/三带二/顺子/连对/飞机/炸弹/四带二/飞机带对。
///
/// Rollout 策略:优先清牌效率(多牌优先),但保留大牌控制权。
/// </summary>
public class SOIsmctsBot : IPdkBot
{
private readonly int _simBudget;
private readonly Random _rng;
private readonly CardTracker _tracker = new CardTracker();
// 静态牌池52张牌只创建一次ID→card 快速查找
private static readonly TCardInfoPdkF[] FullDeck;
static SOIsmctsBot()
{
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] = new TCardInfoPdkF
{
ID = (byte)id,
Flower = (byte)f,
GameNum = (byte)(n == 1 ? 14 : n == 2 ? 16 : n),
GameState = 1
};
}
public SOIsmctsBot(int simBudget = 800)
{
_simBudget = simBudget;
_rng = new Random();
}
public bool DecideBaoZhuang(PdkBotView view)
{
_tracker.Update(view);
int minPlays = HandOptimizer.MinPlays(view.MyHand);
if (minPlays >= 7) return false;
double threshold = minPlays <= 3 ? 0.50 : minPlays <= 4 ? 0.60 : minPlays <= 5 ? 0.65 : 0.70;
int wins = 0;
for (int s = 0; s < 400; s++)
if (Rollout(view.MyHand, view, null)) wins++;
double wr = (double)wins / 400;
Console.WriteLine($"[SOv4 pos{view.MyPos}] 包庄: min={minPlays} wr={wr:P0} → {(wr >= threshold ? "" : "")}");
return wr >= threshold;
}
public PlayOutCardPdkF DecidePlay(PdkBotView view)
{
_tracker.Update(view);
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($"[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 };
}
}
bool isFirstPlay = view.MaxPlayCard.GameNum <= 0;
var candidates = BuildCandidates(hand, view, isFirstPlay);
if (candidates.Count == 1) return candidates[0].play;
var root = new SONode(candidates.Count);
int myPos = view.MyPos;
int o1c = view.ShenYuCard[myPos % 3];
int o2c = view.ShenYuCard[(myPos + 1) % 3];
// 预生成对手手牌 (TCardInfoPdkF)
var knownIds = new HashSet<byte>();
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(poolCards, _rng);
preHands1[g] = poolCards.Take(o1c).ToArray();
preHands2[g] = poolCards.Skip(o1c).Take(o2c).ToArray();
}
int genIdx = 0, totalSims = 0;
for (int iter = 0; iter < _simBudget; iter++)
{
int ci = _rng.Next(candidates.Count);
var (play, isMulti) = candidates[ci];
// 用预生成的对手手牌
genIdx = (genIdx + 1) % preGen;
bool win = Rollout(hand, view, play, preHands1[genIdx], preHands2[genIdx]);
root.Visit(ci, win);
totalSims++;
}
int bestIdx = root.BestChild(candidates.Count);
if (bestIdx < 0) bestIdx = 0;
Console.WriteLine($"[SOv4 pos{view.MyPos}] {totalSims}s best={bestIdx}/{candidates.Count} wr={root.WinRate(bestIdx):P0}");
return candidates[bestIdx].play;
}
// ---- Rollout: 引擎级全牌型模拟 ----
/// <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;
var my = myHand.Where(c => c.GameState == 1).ToList();
if (myPlay.HasValue && myPlay.Value.Type != CardType1.None && myPlay.Value.Ids != null)
{
foreach (var id in myPlay.Value.Ids) my.RemoveAll(c => c.ID == id);
if (my.Count == 0) return true;
}
// 构建或取样对手手牌
List<TCardInfoPdkF> o1, o2;
if (o1Hand != null)
{
o1 = new List<TCardInfoPdkF>(o1Hand);
o2 = new List<TCardInfoPdkF>(o2Hand);
}
else
{
var known = new HashSet<byte>();
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);
}
private bool SimLoop(List<TCardInfoPdkF> my, List<TCardInfoPdkF> o1, List<TCardInfoPdkF> o2,
int current, PlayOutCardPdkF maxPlay, int maxPlayer, int depth)
{
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 cur = current == 0 ? o1 : current == 1 ? o2 : my;
bool isNewRound = maxPlayer == current || maxPlay.GameNum <= 0;
int next = (current + 1) % 3;
// 用引擎获取合法出牌
var tips = PdkCardAlgorithm.GetTipCard(
isNewRound ? new PlayOutCardPdkF() : maxPlay,
cur.ToArray(), null, false);
if (tips != null && tips.Count > 0)
{
// Rollout 策略:多牌优先 + 最小 rank 保留大牌
// isNewRound 时 GetTipCard 只返回单张 → 需要补充多牌先手
List<List<TCardInfoPdkF>> allPlays = new List<List<TCardInfoPdkF>>(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);
}
// 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)
{
var tips = PdkCardAlgorithm.GetTipCard(view.MaxPlayCard, hand, null, view.Rule.AAAIsZhaDan);
var candidates = new List<(PlayOutCardPdkF, bool)>();
if (tips != null)
foreach (var t in tips)
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)
candidates.Add((Pass(view), false));
return candidates;
}
private static void AddMultiCardLeads(TCardInfoPdkF[] hand, int pos,
List<(PlayOutCardPdkF, bool)> candidates)
{
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));
if (PokerLogic.GetPlayZhaDans(hand, out var bombs) && bombs != null)
foreach (var b in bombs) add(b);
foreach (var g in hand.GroupBy(c => c.GameNum).Where(g => g.Count() >= 2))
add(g.Take(2).ToList());
foreach (var g in hand.GroupBy(c => c.GameNum).Where(g => g.Count() >= 3))
{
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) add(s);
}
// ---- 辅助 ----
private static CardType1 InferType(List<TCardInfoPdkF> cards)
{
int n = cards.Count;
if (n == 1) return CardType1.DanZhang;
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 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 Pass(PdkBotView v)
=> new PlayOutCardPdkF { Pos = (byte)v.MyPos, Type = CardType1.None };
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<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]); }
}
}
internal class SONode
{
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;
for (int i = 0; i < Math.Min(n, _visits.Length); i++)
{ double r = WinRate(i); if (r > bestRate) { bestRate = r; best = i; } }
return best;
}
}
}