Files
hjha-server/PdkFriendServer/Logic/IsmctsBot.cs
xiaoou f66f56d0f8 feat: ISMCTS q-value as training label
C#: TrainingCollector新增q_value列, IsmctsBot传入winRate
CSV: reward→q_value, ISMCTS状态带真实评估值, Neural填0
Python: 优先取ISMCTS状态+评估值训练, 无q_value的Neural状态跳过
旧CSV格式(reward)自动fallback到Neural-only过滤

信号从'这局谁赢'改为'ISMCTS评估这步有多好'
2026-07-13 11:38:42 +08:00

704 lines
33 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>
/// 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)
{
// 确定性安全检查:手牌极弱 → 直接否定包庄
double detStrength = _tracker.AbsoluteHandStrength(view.MyHand);
if (detStrength < 0.15) return 0.0;
// ISMCTS 模拟(确定性只做否决,不混入加成以避免虚假信心)
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(hand) <= 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(hand)} → 精确求解");
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;
// 胜率太低 → 剪枝,不参与后续评估
if (fastRate < 0.15 && allCandidates.Count > 3)
continue;
// 有前途的候选多模拟
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));
}
// 如果所有候选都被剪枝,回退到不做剪枝,全部重新评估
if (scoredCandidates.Count == 0)
{
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 rate = (double)wins / fastSims;
int am = HandOptimizer.MinPlays(
hand.Where(c => play.Ids == null || !play.Ids.Contains(c.ID)).ToArray());
scoredCandidates.Add((play, rate, am, 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);
}
}
// 5.6. 确定性无敌出牌:基于牌计数,如果某出牌绝对无人能压制 → 大幅加分
for (int i = 0; i < scoredCandidates.Count; i++)
{
var (p, wr, am, m) = scoredCandidates[i];
if (p.Type != CardType1.None && p.Ids != null && p.Ids.Length > 0)
{
if (_tracker.IsAbsolutelyHighest(hand, p.GameNum, p.Ids.Length, p.Type))
{
// 确定性无敌 → 大幅加分(相当于省掉 ISMCTS 模拟噪声)
double bonus = 0.15;
if (am <= 2) bonus = 0.25; // 马上就要赢
else if (m) bonus = 0.20; // 多牌无敌
wr += bonus;
scoredCandidates[i] = (p, wr, am, m);
}
}
}
// 5.7. 对手牌型预测加权:按牌型区分压制条件
var opp1Pred = _tracker.PredictOpponent(view.MyPos % 3, hand, view);
var opp2Pred = _tracker.PredictOpponent((view.MyPos + 1) % 3, hand, view);
for (int i = 0; i < scoredCandidates.Count; i++)
{
var (p, wr, am, m) = scoredCandidates[i];
if (p.Type == CardType1.None || p.Ids == null || p.Ids.Length == 0)
continue;
int myRank = p.GameNum;
// 按牌型判定:对手是否有能力压制
bool oppLikelyCanBeat = false;
bool oppLikelyCantBeat = false;
switch (p.Type)
{
case CardType1.DanZhang:
// 单张:对手有更高 rank 的单张就能压制
oppLikelyCanBeat = opp1Pred.MaxRank > myRank || opp2Pred.MaxRank > myRank;
oppLikelyCantBeat = opp1Pred.MaxRank > 0 && opp2Pred.MaxRank > 0
&& opp1Pred.MaxRank <= myRank && opp2Pred.MaxRank <= myRank;
break;
case CardType1.DuiZi:
// 对子对手需要有更高的对子LikelyPairs ≥ 2张同rank
oppLikelyCanBeat = opp1Pred.LikelyPairs.Any(r => r > myRank)
|| opp2Pred.LikelyPairs.Any(r => r > myRank);
oppLikelyCantBeat = opp1Pred.LikelyNoPairHigherThan(myRank)
&& opp2Pred.LikelyNoPairHigherThan(myRank)
&& opp1Pred.MaxRank > 0 && opp2Pred.MaxRank > 0;
break;
case CardType1.SanZhang:
case CardType1.SanDai2:
// 三张/三带二对手需要有更高的三张LikelyTriples ≥ 3张同rank
oppLikelyCanBeat = opp1Pred.LikelyTriples.Any(r => r > myRank)
|| opp2Pred.LikelyTriples.Any(r => r > myRank);
oppLikelyCantBeat = opp1Pred.LikelyTriples.Count == 0
&& opp2Pred.LikelyTriples.Count == 0
&& opp1Pred.MaxRank > 0 && opp2Pred.MaxRank > 0;
break;
case CardType1.ShunZi:
// 顺子:对手需要有更长的同长顺子
int myLen = p.Ids.Length;
oppLikelyCanBeat = (opp1Pred.MaxStraightLength >= myLen
&& opp1Pred.MaxRank > myRank)
|| (opp2Pred.MaxStraightLength >= myLen
&& opp2Pred.MaxRank > myRank);
break;
default:
// 连对/飞机/炸弹:用 rank 近似
oppLikelyCanBeat = opp1Pred.MaxRank > myRank || opp2Pred.MaxRank > myRank;
oppLikelyCantBeat = opp1Pred.MaxRank > 0 && opp2Pred.MaxRank > 0
&& opp1Pred.MaxRank <= myRank && opp2Pred.MaxRank <= myRank;
break;
}
if (oppLikelyCantBeat)
wr += 0.10; // 两个对手都可能压不了 → 近似无敌
else if (oppLikelyCanBeat)
wr -= 0.04; // 对手大概率能压制 → 轻微惩罚
// 对手可能有炸弹 → 大牌型谨慎
double maxBombProb = Math.Max(opp1Pred.BombProbability, opp2Pred.BombProbability);
if (maxBombProb > 0.3 && (p.Type == CardType1.ZhaDan || p.Type == CardType1.ShunZi
|| p.Type == CardType1.SanDai2 || p.Type == CardType1.LianDui))
wr -= 0.03;
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}");
TrainingCollector.Record(view, _tracker, TrainingCollector.ActionToIdx(best.play), view.MyPos-1, (float)best.winRate);
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, curHand, false);
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, curHand, true);
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;
}
// 截断:用 MinPlays 评估而非比较张数
int myMin = HandOptimizer.MinPlays(hands[myPos - 1].ToArray());
int n1Min = HandOptimizer.MinPlays(hands[nextPos - 1].ToArray());
int n2Min = HandOptimizer.MinPlays(hands[oppPos - 1].ToArray());
return myMin <= n1Min && myMin <= n2Min;
}
// ---- 多牌先手枚举 ----
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.GameState == 1 && c.GameNum != t[0].GameNum).ToList();
if (rem.Count >= 2)
{
// 三带二:优先带对子(对子本身是一手牌),找不到对子才带两张最小散牌
var pair = rem.GroupBy(c => c.GameNum)
.Where(g => g.Count() >= 2)
.OrderBy(g => g.Key)
.FirstOrDefault();
if (pair != null)
AddCandidate(t.Concat(pair.Take(2)).ToList(), pos, candidates);
else
AddCandidate(t.Concat(rem.OrderBy(c => c.GameNum).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.GameState == 1 && c.GameNum != q[0].GameNum).ToList();
var pair = rem.GroupBy(c => c.GameNum)
.Where(g => g.Count() >= 2)
.OrderBy(g => g.Key)
.FirstOrDefault();
if (rem.Count >= 3)
{
if (pair != null)
AddCandidate(q.Concat(pair.Take(2)).Concat(rem
.Where(c => c.GameNum != pair.Key).OrderBy(c => c.GameNum).Take(1))
.ToList(), pos, candidates);
else
AddCandidate(q.Concat(rem.OrderBy(c => c.GameNum).Take(3)).ToList(), pos, candidates);
}
else if (rem.Count >= 2)
{
if (pair != null)
AddCandidate(q.Concat(pair.Take(2)).ToList(), pos, candidates);
else
AddCandidate(q.Concat(rem.OrderBy(c => c.GameNum).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;
}
/// <summary>
/// 智能出牌选择:模拟中的虚拟对手应该合理出牌,而非纯贪心。
/// - 跟牌时:出刚好能压制的最小牌(保留大牌)
/// - 领牌时:优先多牌组合(提高清牌效率)
/// </summary>
private static List<TCardInfoPdkF> PickBestTip(List<List<TCardInfoPdkF>> tips,
List<TCardInfoPdkF> curHand, bool isNewRound)
{
if (tips == null || tips.Count == 0) return null;
if (!isNewRound)
{
// 跟牌:出刚好能压制的最小牌
// 策略:出最小 rank 的有效牌,保留大牌用于后续压制
var best = tips[0];
int bestScore = int.MaxValue;
foreach (var tip in tips)
{
// 同数量优先选最小 rank不同数量优先选少的
int score = tip.Count * 1000 + tip[0].GameNum;
if (score < bestScore) { bestScore = score; best = tip; }
}
return best;
}
else
{
// 领牌:优先高效清牌
var best = tips[0];
int bestScore = int.MinValue;
foreach (var tip in tips)
{
// 优先多牌;同数量出最小 rank保留大牌控制权
int score = tip.Count * 100 - tip[0].GameNum;
if (score > bestScore) { bestScore = score; best = tip; }
}
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 };
}
}