Files
hjha-server/PdkFriendServer/Logic/SOIsmctsBot.cs
xiaoou 8b30caff1a perf: FastSim rollout 支持对子 (DuiZi) — 模拟更真实
FastSim改进:
- 新轮次: 优先找最大对子 → 再出最大单张
- 跟牌: 对子跟牌→找≥2张同rank且大于对手
- 单张跟牌: 找最小可压牌

效果: 多牌29%, ShunZi 8.8%, DuiZi 19.0%, 0.8s/局

已知局限: 贪婪rollout低估清牌效率, 后续需全局序列优化
2026-07-12 23:31:11 +08:00

411 lines
16 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 v2 — 优化版,静态牌池+数组交换替代对象分配。
///
/// 性能关键:
/// - 48张牌静态数组模拟时只交换索引不创建新对象
/// - 预生成N份对手手牌分布分摊 determinization 成本
/// - 贪心 rollout 直接操作数组切片
/// </summary>
public class SOIsmctsBot : IPdkBot
{
private readonly int _simBudget;
private readonly Random _rng;
private readonly CardTracker _tracker = new CardTracker();
// 静态牌池52张牌同 IsmctsBot.GenerateDeck只创建一次
private static readonly TCardInfoPdkF[] FullDeck;
static SOIsmctsBot()
{
FullDeck = new TCardInfoPdkF[52];
int id = 1;
foreach (int f in new[] { 1, 2, 3, 4 })
for (int n = 1; n <= 13; n++)
FullDeck[id - 1] = 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 (SimulateToEnd(view.MyHand, view, null)) wins++;
double wr = (double)wins / 400;
Console.WriteLine($"[SOv2 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($"[SOv2 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;
// 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<byte>();
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;
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);
}
int genIdx = 0;
int totalSims = 0;
for (int iter = 0; iter < _simBudget; iter++)
{
int ci = _rng.Next(candidates.Count);
var play = 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);
}
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}");
return candidates[bestIdx].play;
}
// ---- 快速模拟(用数组而非对象) ----
private bool SimPass(TCardInfoPdkF[] myHand, PdkBotView view, byte[] o1Ids, byte[] o2Ids)
{
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<byte>(myHand.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, 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);
}
// 快速模拟核心:全部用 byte[]。支持单张+对子。
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;
if (isNewRound)
{
if (cur.Count == 0) return false;
// 找对子(贪心:出最大的对子)
var groups = cur.GroupBy(g => g).Where(g => g.Count() >= 2).OrderByDescending(g => g.Key).ToList();
if (groups.Count > 0)
{
var pair = groups[0];
var toRemove = pair.Take(2).ToList();
foreach (var g in toRemove) cur.Remove(g);
return FastSim(h1, h2, my, (current + 1) % 3, pair.Key, CardType1.DuiZi, current, depth + 1);
}
// 单张
var sorted = cur.OrderByDescending(g => g).ToList();
byte play = sorted[0];
cur.Remove(play);
return FastSim(h1, h2, my, (current + 1) % 3, play, CardType1.DanZhang, current, depth + 1);
}
else
{
// 跟牌:找能压制的同类型牌
var bigger = cur.Where(g => g > maxGameNum).OrderBy(g => g).ToList();
if (maxType == CardType1.DuiZi)
{
// 对子:找 ≥2 张且大于 maxGameNum
var pairGroups = cur.GroupBy(g => g).Where(g => g.Count() >= 2 && g.Key > maxGameNum).OrderBy(g => g.Key).ToList();
if (pairGroups.Count > 0)
{
var pair = pairGroups[0];
var toRemove = pair.Take(2).ToList();
foreach (var g in toRemove) cur.Remove(g);
return FastSim(h1, h2, my, (current + 1) % 3, pair.Key, CardType1.DuiZi, current, depth + 1);
}
}
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);
}
}
// ---- 旧的 SimulateToEnd (保留兼容) ----
private bool SimulateToEnd(TCardInfoPdkF[] myHand, PdkBotView view, PlayOutCardPdkF? fp)
{
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];
var knownIds = new HashSet<byte>();
foreach (var c in myHand) knownIds.Add(c.ID);
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();
byte maxGn = 0;
int maxP = 0;
int cur = myPos % 3;
if (fp.HasValue && fp.Value.Type != CardType1.None)
{
maxGn = fp.Value.GameNum;
maxP = myPos - 1;
foreach (var id in fp.Value.Ids ?? Array.Empty<byte>())
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;
}
return FastSim(o1, o2, myList, cur, maxGn, CardType1.DanZhang, maxP, 0);
}
// ---- 辅助 ----
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((MakePlay(t, view.MyPos), t.Count > 1));
if (isFirstPlay)
AddMultiCardLeads(hand, view.MyPos, candidates);
if (!isFirstPlay)
candidates.Add((Pass(view), false));
return candidates;
}
// 复用 IsmctsBot 的多牌枚举(对子/三张/顺子/飞机/炸弹/连对)
private static void AddMultiCardLeads(TCardInfoPdkF[] hand, int pos,
List<(PlayOutCardPdkF, bool)> candidates)
{
var make = (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) make(b);
foreach (var g in hand.GroupBy(c => c.GameNum).Where(g => g.Count() >= 2))
make(g.Take(2).ToList());
foreach (var g in hand.GroupBy(c => c.GameNum).Where(g => g.Count() >= 3))
make(g.Take(3).ToList());
var straights = PokerLogic.GetShunZi(hand);
if (straights != null) foreach (var s in straights) make(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 CardType1.SanZhang;
if (cnts[0] == 2 && n % 2 == 0) return CardType1.LianDui;
return CardType1.ShunZi;
}
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)
=> new PlayOutCardPdkF { Pos = (byte)v.MyPos, Type = CardType1.None };
private static List<byte> IdsToGameNums(byte[] ids)
{
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)
{
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;
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;
public int BestChild(int n)
{
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; }
}
return best;
}
}
}