Files
hjha-server/PdkFriendServer/Logic/IsmctsBot.cs
xiaoou 180be25ffa fix: code review — 修复4个问题
1. CardTracker.UnknownCount: 48→52 (硬编码修正)
2. IsmctsBot.GenerateDeck → 静态FullDeck池复用 (减少对象分配)
3. CardTracker: 删除3个死字段(_passStreak/_isNewRound/_bombCounts)
4. CardTracker.Update: 清理pass约束记录逻辑

验证: 100局0错误, 0.9s/局
2026-07-13 00:21:15 +08:00

532 lines
24 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>
/// ISMCTS 机器人 v3
/// - 包庄阈值 60%校准自1000局压测
/// - 全牌型枚举:对子/三带二/四带二/四带三/顺子/连对/飞机/炸弹/单张
/// - SimulateWin + SimulateOneGame 合并为 SimulateToEnd
/// </summary>
public class IsmctsBot : IPdkBot
{
private readonly int _simulations;
private readonly Random _rng;
private readonly CardTracker _tracker = new CardTracker();
private const double BaoThreshold = 0.70;
private const double MultiBonusPerCard = 0.10;
private const int FastSims = 40;
public IsmctsBot(int simulations = 800)
{
_simulations = simulations;
_rng = new Random();
}
// ---- 自适应模拟次数 ----
private int AdaptiveSims(int handSize)
{
if (handSize <= 3) return 2000;
if (handSize <= 6) return 1500;
if (handSize <= 10) return 1200;
if (handSize <= 13) return 1000;
return 800;
}
// ---- 包庄决策 ----
public bool DecideBaoZhuang(PdkBotView view)
{
_tracker.Update(view);
int minPlays = HandOptimizer.MinPlays(view.MyHand);
double handEfficiency = view.MyHand.Length > 0
? (double)(view.MyHand.Length - minPlays) / view.MyHand.Length : 0;
// 手牌散 → 不包
if (minPlays >= 7 || handEfficiency < 0.3) return false;
// 手牌极好 → 阈值降低
double adjustedThreshold = BaoThreshold;
if (minPlays <= 3) adjustedThreshold = 0.50; // 手牌极好
else if (minPlays <= 4) adjustedThreshold = 0.60; // 手牌好
else if (minPlays <= 5) adjustedThreshold = 0.65; // 手牌还行
double winRate = EstimateWinProbability(view);
bool shouldBao = winRate >= adjustedThreshold;
Console.WriteLine($"[ISMCTS pos{view.MyPos}] 包庄评估: minPlays={minPlays} eff={handEfficiency:P0} "
+ $"P(win)={winRate:P0} (阈值{adjustedThreshold:P0}) → {(shouldBao ? "" : "")}");
return shouldBao;
}
private double EstimateWinProbability(PdkBotView view)
{
int sims = Math.Min(_simulations, 600);
int wins = 0;
for (int s = 0; s < sims; s++)
if (SimulateToEnd(view.MyHand, view, null))
wins++;
return (double)wins / sims;
}
// ---- 出牌决策 ----
public PlayOutCardPdkF DecidePlay(PdkBotView view)
{
_tracker.Update(view);
var hand = view.MyHand;
if (hand.Length == 0) return Pass(view);
// 终局精确求解手牌≤5 + 未知牌少 → 替代 ISMCTS
if (hand.Length <= 5 && _tracker.UnknownCount <= 15)
{
var endgameResult = EndgameSolver.Solve(hand, view, _tracker, _rng);
if (endgameResult.HasValue)
{
var ep = endgameResult.Value;
Console.WriteLine($"[ISMCTS pos{view.MyPos}] ENDGAME hand={hand.Length} unknown={_tracker.UnknownCount} → 精确求解");
return new PlayOutCardPdkF
{
Pos = (byte)view.MyPos,
GameNum = ep.GameNum,
Ids = ep.Ids,
Type = CardType1.None
};
}
}
// 锁手检测:残局时跳过 ISMCTS走必胜序列
if (hand.Length <= 8)
{
var lockSeq = LockDetector.FindLockSequence(hand, _tracker, view.MyPos);
if (lockSeq != null)
{
var firstPlay = lockSeq[0];
Console.WriteLine($"[ISMCTS pos{view.MyPos}] LOCK {lockSeq.Count}步必胜 → 跳过模拟");
// 通过引擎的出牌验证
return new PlayOutCardPdkF
{
Pos = (byte)view.MyPos,
GameNum = firstPlay.GameNum,
Ids = firstPlay.Ids,
Type = CardType1.None
};
}
}
bool isFirstPlay = view.MaxPlayCard.GameNum <= 0;
// 1. 合法出牌方案
var tips = PdkCardAlgorithm.GetTipCard(view.MaxPlayCard, hand, null, view.Rule.AAAIsZhaDan);
var allCandidates = new List<(PlayOutCardPdkF play, bool isMulti)>();
if (tips != null)
foreach (var tip in tips)
{
allCandidates.Add((MakePlay(tip, view.MyPos), tip.Count > 1));
}
// 2. 新轮次:补充多牌先手方案
if (isFirstPlay)
AddMultiCardLeads(hand, view.MyPos, allCandidates);
if (!isFirstPlay)
allCandidates.Add((Pass(view), false));
if (allCandidates.Count == 1)
return allCandidates[0].play;
// 3. 自适应模拟次数 + 快速初筛
int simsPerCandidate = Math.Max(40, AdaptiveSims(hand.Length) / allCandidates.Count);
int fastSims = Math.Min(FastSims, simsPerCandidate / 2);
var scoredCandidates = new List<(PlayOutCardPdkF play, double winRate, int afterMinPlays, bool isMulti)>();
for (int ci = 0; ci < allCandidates.Count; ci++)
{
int wins = 0;
var play = allCandidates[ci].play;
// 快速评估(少量模拟定位好候选)
for (int s = 0; s < fastSims; s++)
if (SimulateToEnd(hand, view, play))
wins++;
// 有前途的候选多模拟
double fastRate = (double)wins / fastSims;
int extraSims = fastRate > 0.3 ? simsPerCandidate - fastSims : 0;
for (int s = 0; s < extraSims; s++)
if (SimulateToEnd(hand, view, play))
wins++;
double totalRate = (double)wins / (fastSims + extraSims);
// 牌效率:出牌后剩余手牌的最少步数
int afterMin = HandOptimizer.MinPlays(
hand.Where(c =>
play.Ids == null || !play.Ids.Contains(c.ID)).ToArray());
scoredCandidates.Add((play, totalRate, afterMin, allCandidates[ci].isMulti));
}
// 4. 多牌加权
for (int i = 0; i < scoredCandidates.Count; i++)
{
if (scoredCandidates[i].isMulti)
{
int cardCount = scoredCandidates[i].play.Ids?.Length ?? 1;
var (p, wr, am, m) = scoredCandidates[i];
scoredCandidates[i] = (p, wr + cardCount * MultiBonusPerCard, am, m);
}
}
// 4.5. afterMin 牌效率加权(强化版)
int beforeMin = HandOptimizer.MinPlays(hand);
for (int i = 0; i < scoredCandidates.Count; i++)
{
var (p, wr, am, m) = scoredCandidates[i];
if (am < beforeMin)
{
// 手牌结构变好 → 大幅加分
double improvement = (double)(beforeMin - am) / beforeMin;
wr += improvement * 0.25; // max +25%
}
else if (am > beforeMin)
{
// 手牌被打散 → 惩罚
double degradation = (double)(am - beforeMin) / beforeMin;
if (m)
wr -= degradation * 0.30; // 多牌打散 → 严重(-30%)
else
wr -= degradation * 0.10; // 单张打散 → 中(-10%)
}
scoredCandidates[i] = (p, wr, am, m);
}
// 5. 炸弹感知加权
int bombsOutside = _tracker.BombsOutside(hand);
for (int i = 0; i < scoredCandidates.Count; i++)
{
var (p, wr, am, m) = scoredCandidates[i];
bool isBomb = p.Ids != null && p.Ids.Length == 4
&& hand.Count(c => c.GameNum == p.GameNum && c.GameState == 1) == 4;
if (isBomb)
{
// 我方炸弹
if (bombsOutside == 0) wr += 0.25; // 外面没炸弹 → 我方炸弹无敌
else if (am <= 3) wr += 0.15; // 快赢了,果断炸
else wr -= 0.05; // 先留着
}
else if (bombsOutside > 0 && am <= 3)
{
// 外面有炸弹但我快赢了 → 出牌保守,少加多牌分
if (m) wr -= 0.05;
}
scoredCandidates[i] = (p, wr, am, m);
}
// 5.5. 领牌保留策略:有绝对最高牌(2)时,领牌优先出最小的
isFirstPlay = view.MaxPlayCard.GameNum <= 0;
bool hasHighestCard = hand.Any(c => c.GameState == 1 && c.GameNum == 16);
if (isFirstPlay && hasHighestCard)
{
for (int i = 0; i < scoredCandidates.Count; i++)
{
var (p, wr, am, m) = scoredCandidates[i];
if (!m && p.Ids != null && p.Ids.Length == 1)
{
int rank = p.GameNum;
if (rank <= 6) wr += 0.15;
else if (rank <= 10) wr += 0.05;
else if (rank <= 13) wr -= 0.05;
else wr -= 0.10;
}
scoredCandidates[i] = (p, wr, am, m);
}
}
// 6. 两阶段选择:多牌优先
scoredCandidates.Sort((a, b) => b.winRate.CompareTo(a.winRate));
var best = scoredCandidates[0];
// 多牌候选中有胜率≥top 90%的 → 优先多牌
if (allCandidates.Any(c => c.isMulti))
{
var bestMulti = scoredCandidates.Where(x => x.isMulti)
.OrderByDescending(x => x.play.Ids?.Length ?? 0)
.ThenByDescending(x => x.winRate)
.FirstOrDefault();
if (bestMulti.play.Ids != null && bestMulti.play.Ids.Length > 0)
{
double topRate = scoredCandidates[0].winRate;
if (bestMulti.winRate >= topRate * 0.85)
best = bestMulti;
}
}
string desc = best.play.Ids == null || best.play.Ids.Length == 0
? "pass" : $"{best.play.Ids.Length}c winRate:{best.winRate:P0}";
int totalSims = allCandidates.Count * fastSims
+ scoredCandidates.Count(c => c.winRate > 0.3) * (simsPerCandidate - fastSims);
Console.WriteLine($"[ISMCTS pos{view.MyPos}] {totalSims}sims => {desc}");
return best.play;
}
// ---- 模拟引擎 ----
private bool SimulateToEnd(TCardInfoPdkF[] myHand, PdkBotView view, PlayOutCardPdkF? firstPlay = null)
{
int myPos = view.MyPos;
int nextPos = myPos % 3 + 1;
int oppPos = (myPos + 1) % 3 + 1;
var hands = new List<TCardInfoPdkF>[3];
hands[myPos - 1] = new List<TCardInfoPdkF>(myHand);
hands[nextPos - 1] = new List<TCardInfoPdkF>();
hands[oppPos - 1] = new List<TCardInfoPdkF>();
var allCards = GetDeckCopy();
var unknown = allCards.Where(c => !myHand.Any(h => h.ID == c.ID)).ToList();
// 使用 CardTracker 进行约束采样
var sampled = _tracker.SampleOpponentHands(myHand, view, _rng);
hands[nextPos - 1].AddRange(sampled[0]);
hands[oppPos - 1].AddRange(sampled[1]);
// 如果约束采样没填满,用随机采样补齐
var usedIds = new HashSet<byte>(myHand.Select(c => c.ID));
foreach (var c in hands[nextPos - 1]) usedIds.Add(c.ID);
foreach (var c in hands[oppPos - 1]) usedIds.Add(c.ID);
var remaining = unknown.Where(c => !usedIds.Contains(c.ID)).ToList();
Shuffle(remaining);
int n1Need = view.ShenYuCard[nextPos - 1] - hands[nextPos - 1].Count;
int n2Need = view.ShenYuCard[oppPos - 1] - hands[oppPos - 1].Count;
if (n1Need > 0) hands[nextPos - 1].AddRange(remaining.Take(n1Need));
if (n2Need > 0) hands[oppPos - 1].AddRange(remaining.Skip(n1Need).Take(n2Need));
int current, maxPlayer;
PlayOutCardPdkF maxPlay;
if (firstPlay.HasValue)
{
var fp = firstPlay.Value;
maxPlay = new PlayOutCardPdkF { Pos = (byte)myPos, GameNum = fp.GameNum, Type = fp.Type, Ids = fp.Ids?.ToArray() };
if (fp.Type != CardType1.None && fp.Ids != null)
foreach (var id in fp.Ids) hands[myPos - 1].RemoveAll(c => c.ID == id);
current = myPos % 3 + 1;
maxPlayer = fp.Type == CardType1.None ? view.MaxPlayCard.Pos : myPos;
if (fp.Type == CardType1.None) { maxPlayer = view.MaxPlayCard.Pos; maxPlay = CloneCard(view.MaxPlayCard); }
}
else
{
current = myPos; maxPlay = new PlayOutCardPdkF(); maxPlayer = 0;
}
int passStreak = 0;
for (int move = 0; move < 500; move++)
{
var curHand = hands[current - 1];
if (curHand.Count == 0) return current == myPos;
bool isNewRound = maxPlayer == current || maxPlay.GameNum == 0;
var curTips = PdkCardAlgorithm.GetTipCard(isNewRound ? new PlayOutCardPdkF() : maxPlay, curHand.ToArray(), null, false);
if (curTips != null && curTips.Count > 0)
{
var pick = PickBestTip(curTips);
maxPlay = new PlayOutCardPdkF { Pos = (byte)current, GameNum = pick[0].GameNum, Type = pick.Count == 1 ? CardType1.DanZhang : CardType1.DuiZi, Ids = pick.Select(c => c.ID).ToArray() };
maxPlayer = current; passStreak = 0;
foreach (var id in maxPlay.Ids) curHand.RemoveAll(c => c.ID == id);
}
else
{
if (++passStreak >= 2)
{
var ft = PdkCardAlgorithm.GetTipCard(new PlayOutCardPdkF(), curHand.ToArray(), null, false);
if (ft != null && ft.Count > 0)
{
var pick = PickBestTip(ft);
maxPlay = new PlayOutCardPdkF { Pos = (byte)current, GameNum = pick[0].GameNum, Type = CardType1.DanZhang, Ids = pick.Select(c => c.ID).ToArray() };
maxPlayer = current;
foreach (var id in maxPlay.Ids) curHand.RemoveAll(c => c.ID == id);
}
passStreak = 0;
}
}
current = current % 3 + 1;
}
return hands[myPos - 1].Count <= hands[nextPos - 1].Count && hands[myPos - 1].Count <= hands[oppPos - 1].Count;
}
// ---- 多牌先手枚举 ----
private static void AddMultiCardLeads(TCardInfoPdkF[] hand, int pos,
List<(PlayOutCardPdkF play, bool isMulti)> candidates)
{
// 炸弹
if (PokerLogic.GetPlayZhaDans(hand, out var bombs) && bombs != null)
foreach (var bomb in bombs) AddCandidate(bomb, pos, candidates);
// 对子 + 三张(缓存)
var pairs = GetMultiGroups(hand, 2);
foreach (var p in pairs) AddCandidate(p, pos, candidates);
var triples = GetMultiGroups(hand, 3);
foreach (var t in triples)
{
var rem = hand.Where(c => c.GameNum != t[0].GameNum).OrderBy(c => c.GameNum).ToList();
if (rem.Count >= 2)
AddCandidate(t.Concat(rem.Take(2)).ToList(), pos, candidates); // 三带二
else
AddCandidate(t, pos, candidates);
}
// 四带二/三
var quads = GetMultiGroups(hand, 4);
foreach (var q in quads)
{
var rem = hand.Where(c => c.GameNum != q[0].GameNum).OrderBy(c => c.GameNum).ToList();
if (rem.Count >= 3) AddCandidate(q.Concat(rem.Take(3)).ToList(), pos, candidates);
else if (rem.Count >= 2) AddCandidate(q.Concat(rem.Take(2)).ToList(), pos, candidates);
}
// 连对(复用 pairs不重复 GetMultiGroups
SortByGameNum(pairs);
AddConsecutiveGroups(pairs, 3, pos, candidates);
// 飞机
SortByGameNum(triples);
AddConsecutiveGroups(triples, 2, pos, candidates);
// 飞机带对
var runs = GetConsecutiveRuns(triples, 2);
foreach (var run in runs)
{
var runCards = run.SelectMany(t => t).ToList();
var rp = GetMultiGroups(hand.Where(c => !runCards.Any(r => r.ID == c.ID)).ToArray(), 2);
if (rp.Count >= run.Count)
{
SortByGameNum(rp);
runCards.AddRange(rp.Take(run.Count).SelectMany(p => p));
AddCandidate(runCards, pos, candidates);
}
}
// 顺子
var straights = PokerLogic.GetShunZi(hand);
if (straights != null) foreach (var s in straights) AddCandidate(s, pos, candidates);
}
// ---- 辅助方法 ----
private static void AddCandidate(List<TCardInfoPdkF> cards, int pos,
List<(PlayOutCardPdkF play, bool isMulti)> candidates)
{
candidates.Add((new PlayOutCardPdkF { Pos = (byte)pos, GameNum = cards[0].GameNum, Ids = cards.Select(c => c.ID).ToArray(), Type = InferType(cards) }, true));
}
private static CardType1 InferType(List<TCardInfoPdkF> cards)
{
int n = cards.Count;
if (n == 1) return CardType1.DanZhang;
if (n == 2) return CardType1.DuiZi;
var counts = cards.GroupBy(c => c.GameNum).Select(g => g.Count()).OrderByDescending(x => x).ToList();
if (counts[0] == 4) { if (n == 4) return CardType1.ZhaDan; if (n == 6) return CardType1.SiDai2; if (n == 7) return CardType1.SiDai3; }
if (counts[0] >= 3) return CardType1.SanDai2;
if (counts[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 = InferType(cards) };
private static List<List<TCardInfoPdkF>> GetMultiGroups(TCardInfoPdkF[] hand, int count)
{
var r = new List<List<TCardInfoPdkF>>();
foreach (var g in hand.GroupBy(c => c.GameNum).Where(g => g.Count() >= count))
r.Add(g.Take(count).ToList());
return r;
}
private static void SortByGameNum(List<List<TCardInfoPdkF>> g) => g.Sort((a, b) => a[0].GameNum.CompareTo(b[0].GameNum));
private static void AddConsecutiveGroups(List<List<TCardInfoPdkF>> groups, int minLen, int pos,
List<(PlayOutCardPdkF, bool)> candidates)
{ foreach (var run in GetConsecutiveRuns(groups, minLen)) AddCandidate(run.SelectMany(g => g).ToList(), pos, candidates); }
private static List<List<List<TCardInfoPdkF>>> GetConsecutiveRuns(List<List<TCardInfoPdkF>> groups, int minLen)
{
var runs = new List<List<List<TCardInfoPdkF>>>();
if (groups.Count < minLen) return runs;
var cur = new List<List<TCardInfoPdkF>> { groups[0] };
int prev = groups[0][0].GameNum;
for (int i = 1; i < groups.Count; i++)
{
int n = groups[i][0].GameNum;
if (n == prev + 1) cur.Add(groups[i]);
else { if (cur.Count >= minLen) runs.Add(new List<List<TCardInfoPdkF>>(cur)); cur.Clear(); cur.Add(groups[i]); }
prev = n;
}
if (cur.Count >= minLen) runs.Add(cur);
return runs;
}
private static List<TCardInfoPdkF> PickBestTip(List<List<TCardInfoPdkF>> tips)
{
if (tips == null || tips.Count == 0) return null;
var best = tips[tips.Count - 1];
int bestScore = best.Count * 10 + best[0].GameNum;
for (int i = tips.Count - 2; i >= 0; i--)
{
int s = tips[i].Count * 10 + tips[i][0].GameNum;
if (s > bestScore) { bestScore = s; best = tips[i]; }
}
return best;
}
// 静态牌池52张牌只创建一次
private static readonly TCardInfoPdkF[] FullDeck;
static IsmctsBot()
{
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
};
}
private static List<TCardInfoPdkF> GetDeckCopy()
{
return FullDeck.Where(c => c.ID > 0).Select(c => new TCardInfoPdkF
{
ID = c.ID, Flower = c.Flower, GameNum = c.GameNum, GameState = 1
}).ToList();
}
private void Shuffle<T>(List<T> l) { for (int i = l.Count - 1; i > 0; i--) { int j = _rng.Next(i + 1); (l[i], l[j]) = (l[j], l[i]); } }
private static PlayOutCardPdkF CloneCard(PlayOutCardPdkF s)
=> new PlayOutCardPdkF { Pos = s.Pos, GameNum = s.GameNum, Type = s.Type, Ids = s.Ids?.ToArray() };
private static PlayOutCardPdkF Pass(PdkBotView v)
=> new PlayOutCardPdkF { Pos = (byte)v.MyPos, Type = CardType1.None };
}
}