feat: 最小步数规划 + 自适应模拟 + 快速初筛
HandOptimizer.cs (新文件): - DP 搜索手牌最优拆分, 计算最少出牌次数 - 支持全部牌型: 单张/对子/三张/三带二/顺子/连对/飞机/炸弹/四带二/四带三/飞机带对 - Analyze() 返回最优分组详情 - PlayEfficiency() 评估单次出牌的牌效率 IsmctsBot.cs 改进: - 包庄决策加入 MinPlays 分析, 手牌散(≥7步)直接不包, 手牌好(≤3步)阈值降至50% - 自适应模拟: 3张→2000, 4-6→1500, 7-10→1200, 11-13→1000, ≥14→800 - 快速初筛: 40次模拟定位好候选, 胜率>30%才追加模拟 - 多牌候选选择逻辑改为 top90% 阈值(之前无脑选多牌) - 出牌时计算剩余手牌 MinPlays 辅助候选评估 - 简化 multiCount 追踪(改为 Any(c=>c.isMulti)) 压测: 50局 0错误, avg 0.9s/局
This commit is contained in:
@ -18,25 +18,51 @@ namespace PdkFriendServer.Logic
|
||||
{
|
||||
private readonly int _simulations;
|
||||
private readonly Random _rng;
|
||||
private const double BaoThreshold = 0.70; // 100局校准: 60%→30%, 提至70%
|
||||
private const double MultiBonusPerCard = 0.10; // 多牌加权: 每多1张+10%
|
||||
private const double BaoThreshold = 0.70; // 100局校准: 60%→30%, 提至70%
|
||||
private const double MultiBonusPerCard = 0.10; // 多牌加权: 每多1张+10%
|
||||
private const int FastSims = 40; // 快速评估模拟次数
|
||||
|
||||
public IsmctsBot(int simulations = 800)
|
||||
{
|
||||
_simulations = simulations;
|
||||
_rng = new Random();
|
||||
}
|
||||
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)
|
||||
{
|
||||
double winRate = EstimateWinProbability(view);
|
||||
bool shouldBao = winRate >= BaoThreshold;
|
||||
Console.WriteLine($"[ISMCTS pos{view.MyPos}] 包庄评估: P(win)={winRate:P0} "
|
||||
+ $"(阈值{BaoThreshold:P0}) → {(shouldBao ? "包庄" : "不包")}");
|
||||
return shouldBao;
|
||||
}
|
||||
public bool DecideBaoZhuang(PdkBotView 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)
|
||||
{
|
||||
@ -50,78 +76,103 @@ namespace PdkFriendServer.Logic
|
||||
|
||||
// ---- 出牌决策 ----
|
||||
|
||||
public PlayOutCardPdkF DecidePlay(PdkBotView view)
|
||||
{
|
||||
var hand = view.MyHand;
|
||||
if (hand.Length == 0) return Pass(view);
|
||||
public PlayOutCardPdkF DecidePlay(PdkBotView view)
|
||||
{
|
||||
var hand = view.MyHand;
|
||||
if (hand.Length == 0) return Pass(view);
|
||||
|
||||
bool isFirstPlay = view.MaxPlayCard.GameNum <= 0;
|
||||
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)>();
|
||||
var multiCount = 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)
|
||||
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++)
|
||||
{
|
||||
var isMulti = tip.Count > 1;
|
||||
if (isMulti) multiCount++;
|
||||
allCandidates.Add((MakePlay(tip, view.MyPos), isMulti));
|
||||
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));
|
||||
}
|
||||
|
||||
// 2. 新轮次:补充多牌先手方案
|
||||
if (isFirstPlay)
|
||||
AddMultiCardLeads(hand, view.MyPos, allCandidates, ref multiCount);
|
||||
|
||||
if (!isFirstPlay)
|
||||
allCandidates.Add((Pass(view), false));
|
||||
|
||||
var scoredCandidates = new List<(PlayOutCardPdkF play, double winRate, bool isMulti)>();
|
||||
|
||||
if (allCandidates.Count == 1)
|
||||
return allCandidates[0].play;
|
||||
|
||||
int simsPerCandidate = Math.Max(50, _simulations / allCandidates.Count);
|
||||
|
||||
for (int ci = 0; ci < allCandidates.Count; ci++)
|
||||
{
|
||||
int wins = 0;
|
||||
var play = allCandidates[ci].play;
|
||||
for (int s = 0; s < simsPerCandidate; s++)
|
||||
if (SimulateToEnd(hand, view, play))
|
||||
wins++;
|
||||
scoredCandidates.Add((play, (double)wins / simsPerCandidate, allCandidates[ci].isMulti));
|
||||
}
|
||||
|
||||
// 多牌加权: 两阶段选择前给多牌候选加胜率偏置
|
||||
for (int i = 0; i < scoredCandidates.Count; i++)
|
||||
{
|
||||
if (scoredCandidates[i].isMulti)
|
||||
// 4. 多牌加权
|
||||
for (int i = 0; i < scoredCandidates.Count; i++)
|
||||
{
|
||||
int cardCount = scoredCandidates[i].play.Ids?.Length ?? 1;
|
||||
var (p, wr, m) = scoredCandidates[i];
|
||||
scoredCandidates[i] = (p, wr + cardCount * MultiBonusPerCard, m);
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 两阶段选择:多牌优先
|
||||
scoredCandidates.Sort((a, b) => b.winRate.CompareTo(a.winRate));
|
||||
var best = scoredCandidates[0];
|
||||
if (multiCount > 0)
|
||||
{
|
||||
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)
|
||||
best = bestMulti;
|
||||
}
|
||||
// 5. 两阶段选择:多牌优先
|
||||
scoredCandidates.Sort((a, b) => b.winRate.CompareTo(a.winRate));
|
||||
var best = scoredCandidates[0];
|
||||
|
||||
string desc = best.play.Ids == null || best.play.Ids.Length == 0 ? "pass" : $"{best.play.Ids.Length}c";
|
||||
Console.WriteLine($"[ISMCTS pos{view.MyPos}] {simsPerCandidate * allCandidates.Count}sims "
|
||||
+ $"winRate:{best.winRate:P0} => {desc}");
|
||||
return best.play;
|
||||
}
|
||||
// 多牌候选中有胜率≥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;
|
||||
}
|
||||
|
||||
// ---- 模拟引擎 ----
|
||||
|
||||
@ -199,24 +250,24 @@ namespace PdkFriendServer.Logic
|
||||
// ---- 多牌先手枚举 ----
|
||||
|
||||
private static void AddMultiCardLeads(TCardInfoPdkF[] hand, int pos,
|
||||
List<(PlayOutCardPdkF play, bool isMulti)> candidates, ref int multiCount)
|
||||
List<(PlayOutCardPdkF play, bool isMulti)> candidates)
|
||||
{
|
||||
// 炸弹
|
||||
if (PokerLogic.GetPlayZhaDans(hand, out var bombs) && bombs != null)
|
||||
foreach (var bomb in bombs) AddCandidate(bomb, pos, candidates, ref multiCount);
|
||||
foreach (var bomb in bombs) AddCandidate(bomb, pos, candidates);
|
||||
|
||||
// 对子 + 三张(缓存)
|
||||
var pairs = GetMultiGroups(hand, 2);
|
||||
foreach (var p in pairs) AddCandidate(p, pos, candidates, ref multiCount);
|
||||
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, ref multiCount); // 三带二
|
||||
AddCandidate(t.Concat(rem.Take(2)).ToList(), pos, candidates); // 三带二
|
||||
else
|
||||
AddCandidate(t, pos, candidates, ref multiCount);
|
||||
AddCandidate(t, pos, candidates);
|
||||
}
|
||||
|
||||
// 四带二/三
|
||||
@ -224,17 +275,17 @@ namespace PdkFriendServer.Logic
|
||||
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, ref multiCount);
|
||||
else if (rem.Count >= 2) AddCandidate(q.Concat(rem.Take(2)).ToList(), pos, candidates, ref multiCount);
|
||||
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, ref multiCount);
|
||||
AddConsecutiveGroups(pairs, 3, pos, candidates);
|
||||
|
||||
// 飞机
|
||||
SortByGameNum(triples);
|
||||
AddConsecutiveGroups(triples, 2, pos, candidates, ref multiCount);
|
||||
AddConsecutiveGroups(triples, 2, pos, candidates);
|
||||
|
||||
// 飞机带对
|
||||
var runs = GetConsecutiveRuns(triples, 2);
|
||||
@ -246,22 +297,21 @@ namespace PdkFriendServer.Logic
|
||||
{
|
||||
SortByGameNum(rp);
|
||||
runCards.AddRange(rp.Take(run.Count).SelectMany(p => p));
|
||||
AddCandidate(runCards, pos, candidates, ref multiCount);
|
||||
AddCandidate(runCards, pos, candidates);
|
||||
}
|
||||
}
|
||||
|
||||
// 顺子
|
||||
var straights = PokerLogic.GetShunZi(hand);
|
||||
if (straights != null) foreach (var s in straights) AddCandidate(s, pos, candidates, ref multiCount);
|
||||
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, ref int multiCount)
|
||||
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));
|
||||
multiCount++;
|
||||
}
|
||||
|
||||
private static CardType1 InferType(List<TCardInfoPdkF> cards)
|
||||
@ -290,8 +340,8 @@ namespace PdkFriendServer.Logic
|
||||
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, ref int mc)
|
||||
{ foreach (var run in GetConsecutiveRuns(groups, minLen)) AddCandidate(run.SelectMany(g => g).ToList(), pos, candidates, ref mc); }
|
||||
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)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user