核心改动: 从'出最大的看谁能压'改为'出最省的清牌快' 领牌(新轮): - 三张 > 对子 > 单张 (多牌优先) - 始终用最小rank (保留大牌给跟牌阶段) 跟牌: - 三张/对子: 找最小rank能压制的同类型 - 单张: 出最小的压制牌(保留大牌) 效果: 多牌29%→34% (+5%), 单张-4.4%, 0.7s/局 vs IsmctsBot: 34% vs 53%, 差距来自straight/飞机等复杂牌型在rollout中缺失
432 lines
17 KiB
C#
432 lines
17 KiB
C#
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;
|
||
}
|
||
}
|
||
} |