Files
hjha-server/PdkFriendServer/Logic/SOIsmctsBot.cs
xiaoou 6a267738df fix: FastSim清牌效率优先 — 领牌出多牌+最小rank, 跟牌出最小压制
核心改动: 从'出最大的看谁能压'改为'出最省的清牌快'

领牌(新轮):
- 三张 > 对子 > 单张 (多牌优先)
- 始终用最小rank (保留大牌给跟牌阶段)

跟牌:
- 三张/对子: 找最小rank能压制的同类型
- 单张: 出最小的压制牌(保留大牌)

效果: 多牌29%→34% (+5%), 单张-4.4%, 0.7s/局
vs IsmctsBot: 34% vs 53%, 差距来自straight/飞机等复杂牌型在rollout中缺失
2026-07-12 23:41:50 +08:00

432 lines
17 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);
}
// 快速模拟核心:清牌效率优先。
// 领牌:优先出多牌组合(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;
// 清牌效率排序: 三张 > 对子 > 单张。始终用最小的 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);
}
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);
}
}
// ---- 旧的 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;
}
}
}