Files
hjha-server/PdkFriendServer/Logic/SOIsmctsBot.cs
xiaoou 5cc2ae8bd0 fix: 确定性牌计数推理 — UnknownCount修复 + 绝对大牌判断 + EndgameSolver修正
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求解正常触发
2026-07-13 00:40:27 +08:00

338 lines
14 KiB
C#
Raw Permalink 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(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;
}
}
}