C#: TrainingCollector新增q_value列, IsmctsBot传入winRate CSV: reward→q_value, ISMCTS状态带真实评估值, Neural填0 Python: 优先取ISMCTS状态+评估值训练, 无q_value的Neural状态跳过 旧CSV格式(reward)自动fallback到Neural-only过滤 信号从'这局谁赢'改为'ISMCTS评估这步有多好'
704 lines
33 KiB
C#
704 lines
33 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>
|
||
/// 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 };
|
||
}
|
||
}
|