CardTracker: - UnknownCount: 52→48 - handSize - seenCount (修正底牌+手牌漏算) - 新增 RemainingCount/GetUnknownPool: 精确剩余牌计数 - 新增 IsAbsolutelyHighest: 全牌型确定性无敌判断 (单张/对子/三张/炸弹/顺子/连对/飞机) EndgameSolver: - GeneratePool → 调用 tracker.GetUnknownPool (修复已见牌未排除bug) IsmctsBot: - 确定性无敌出牌加成: 无敌出牌 +15-25% win rate bonus SOIsmctsBot: - 同步 UnknownCount(hand) 调用 验证: 100局0错误, 0.8s/局, ENDGAME求解正常触发
338 lines
14 KiB
C#
338 lines
14 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 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(hand) <= 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;
|
||
}
|
||
}
|
||
} |