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:
427
PdkFriendServer/Logic/HandOptimizer.cs
Normal file
427
PdkFriendServer/Logic/HandOptimizer.cs
Normal file
@ -0,0 +1,427 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using GameMessage;
|
||||
using GameMessage.PaoDeKuaiF;
|
||||
|
||||
namespace PdkFriendServer.Logic
|
||||
{
|
||||
/// <summary>
|
||||
/// 手牌最优拆分引擎 — DP 搜索最小出牌次数
|
||||
///
|
||||
/// 给定 16 张手牌,枚举所有合法牌型分组,找出最少几轮能出完。
|
||||
/// 用于 ISMCTS 的包庄决策和出牌候选排序。
|
||||
///
|
||||
/// 牌型支持:单张/对子/三张/三带二/顺子/连对/飞机/炸弹/四带二/四带三/飞机带对
|
||||
/// GameNum 映射:3-14=A, 16=2
|
||||
/// </summary>
|
||||
public static class HandOptimizer
|
||||
{
|
||||
// 顺子允许的 rank 范围:3-14 (A),不包括 2(16)
|
||||
private static readonly int[] StraightRanks = { 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14 };
|
||||
|
||||
/// <summary>
|
||||
/// 计算手牌的最小出牌次数
|
||||
/// </summary>
|
||||
public static int MinPlays(TCardInfoPdkF[] hand)
|
||||
{
|
||||
if (hand == null || hand.Length == 0) return 0;
|
||||
return MinPlaysImpl(ToCounts(hand), new Dictionary<string, int>());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 计算手牌的最小出牌次数 + 返回最优拆分详情
|
||||
/// </summary>
|
||||
public static HandAnalysis Analyze(TCardInfoPdkF[] hand)
|
||||
{
|
||||
var counts = ToCounts(hand);
|
||||
int min = MinPlaysImpl(counts, new Dictionary<string, int>());
|
||||
|
||||
// 尝试找出具体的最优拆分方案
|
||||
var groups = FindOptimalGroups(counts, min);
|
||||
|
||||
return new HandAnalysis
|
||||
{
|
||||
MinPlays = min,
|
||||
OptimalGroups = groups,
|
||||
TotalCards = hand.Length
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 评估出牌后的牌效率(用于候选排序)
|
||||
/// 返回 1 - (剩余minPlays/当前minPlays),值越大效率越好
|
||||
/// </summary>
|
||||
public static double PlayEfficiency(TCardInfoPdkF[] hand, PlayOutCardPdkF play)
|
||||
{
|
||||
if (play.Ids == null || play.Ids.Length == 0) return 0;
|
||||
int before = MinPlays(hand);
|
||||
var remaining = RemovePlayed(hand, play);
|
||||
if (remaining.Length == 0) return 1.0; // 直接赢
|
||||
int after = MinPlays(remaining);
|
||||
if (before == 0) return 0.5;
|
||||
return 1.0 - (double)after / before;
|
||||
}
|
||||
|
||||
// ---- 核心 DP ----
|
||||
|
||||
private static int MinPlaysImpl(Dictionary<int, int> counts, Dictionary<string, int> memo)
|
||||
{
|
||||
string key = CountsToKey(counts);
|
||||
if (memo.TryGetValue(key, out int cached)) return cached;
|
||||
|
||||
int totalCards = counts.Values.Sum();
|
||||
if (totalCards == 0) { memo[key] = 0; return 0; }
|
||||
if (totalCards == 1) { memo[key] = 1; return 1; }
|
||||
|
||||
int best = totalCards; // 最坏:全出单张
|
||||
|
||||
// 炸弹 (4 same) + 四带二/四带三
|
||||
foreach (var kv in counts.Where(c => c.Value >= 4).ToList())
|
||||
{
|
||||
int rank = kv.Key;
|
||||
// 纯炸弹
|
||||
ModifyCount(counts, rank, -4);
|
||||
best = Math.Min(best, 1 + MinPlaysImpl(counts, memo));
|
||||
|
||||
// 四带二
|
||||
var kicker2 = FindKickers(counts, 2);
|
||||
if (kicker2 != null)
|
||||
{
|
||||
foreach (var k in kicker2) ModifyCount(counts, k, -1);
|
||||
best = Math.Min(best, 1 + MinPlaysImpl(counts, memo));
|
||||
foreach (var k in kicker2) ModifyCount(counts, k, +1);
|
||||
}
|
||||
|
||||
// 四带三
|
||||
var kicker3 = FindKickers(counts, 3);
|
||||
if (kicker3 != null)
|
||||
{
|
||||
foreach (var k in kicker3) ModifyCount(counts, k, -1);
|
||||
best = Math.Min(best, 1 + MinPlaysImpl(counts, memo));
|
||||
foreach (var k in kicker3) ModifyCount(counts, k, +1);
|
||||
}
|
||||
|
||||
ModifyCount(counts, rank, +4);
|
||||
}
|
||||
|
||||
// 飞机 (≥2 组连续三张) + 飞机带对
|
||||
var triples = counts.Where(c => c.Value >= 3).Select(c => c.Key).OrderBy(r => r).ToList();
|
||||
for (int len = 2; len <= triples.Count; len++)
|
||||
{
|
||||
for (int start = 0; start <= triples.Count - len; start++)
|
||||
{
|
||||
if (!IsConsecutive(triples, start, len)) continue;
|
||||
var ranks = triples.Skip(start).Take(len).ToList();
|
||||
|
||||
// 纯飞机
|
||||
foreach (var r in ranks) ModifyCount(counts, r, -3);
|
||||
best = Math.Min(best, 1 + MinPlaysImpl(counts, memo));
|
||||
|
||||
// 飞机带对 (每三张带一对)
|
||||
var pairKickers = FindKickers(counts, len * 2);
|
||||
if (pairKickers != null)
|
||||
{
|
||||
foreach (var k in pairKickers) ModifyCount(counts, k, -1);
|
||||
best = Math.Min(best, 1 + MinPlaysImpl(counts, memo));
|
||||
foreach (var k in pairKickers) ModifyCount(counts, k, +1);
|
||||
}
|
||||
|
||||
foreach (var r in ranks) ModifyCount(counts, r, +3);
|
||||
}
|
||||
}
|
||||
|
||||
// 三张 (bare triple) + 三带二
|
||||
foreach (var kv in counts.Where(c => c.Value >= 3).ToList())
|
||||
{
|
||||
int rank = kv.Key;
|
||||
// 纯三张
|
||||
ModifyCount(counts, rank, -3);
|
||||
best = Math.Min(best, 1 + MinPlaysImpl(counts, memo));
|
||||
|
||||
// 三带二
|
||||
var kickers = FindKickers(counts, 2);
|
||||
if (kickers != null)
|
||||
{
|
||||
foreach (var k in kickers) ModifyCount(counts, k, -1);
|
||||
best = Math.Min(best, 1 + MinPlaysImpl(counts, memo));
|
||||
foreach (var k in kickers) ModifyCount(counts, k, +1);
|
||||
}
|
||||
|
||||
ModifyCount(counts, rank, +3);
|
||||
}
|
||||
|
||||
// 顺子 (≥5 张连续)
|
||||
foreach (int startRank in StraightRanks)
|
||||
{
|
||||
for (int len = StraightRanks.Length; len >= 5; len--)
|
||||
{
|
||||
int maxIdx = Array.IndexOf(StraightRanks, startRank) + len - 1;
|
||||
if (maxIdx >= StraightRanks.Length) continue;
|
||||
|
||||
var ranks = StraightRanks.Skip(Array.IndexOf(StraightRanks, startRank)).Take(len).ToList();
|
||||
if (ranks.All(r => counts.GetValueOrDefault(r, 0) >= 1))
|
||||
{
|
||||
foreach (var r in ranks) ModifyCount(counts, r, -1);
|
||||
best = Math.Min(best, 1 + MinPlaysImpl(counts, memo));
|
||||
foreach (var r in ranks) ModifyCount(counts, r, +1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 连对 (≥3 对连续)
|
||||
var pairRanks = counts.Where(c => c.Value >= 2).Select(c => c.Key).OrderBy(r => r).ToList();
|
||||
for (int len = 3; len <= pairRanks.Count; len++)
|
||||
{
|
||||
for (int start = 0; start <= pairRanks.Count - len; start++)
|
||||
{
|
||||
if (!IsConsecutive(pairRanks, start, len)) continue;
|
||||
var ranks = pairRanks.Skip(start).Take(len).ToList();
|
||||
foreach (var r in ranks) ModifyCount(counts, r, -2);
|
||||
best = Math.Min(best, 1 + MinPlaysImpl(counts, memo));
|
||||
foreach (var r in ranks) ModifyCount(counts, r, +2);
|
||||
}
|
||||
}
|
||||
|
||||
// 对子 (2 same)
|
||||
foreach (var kv in counts.Where(c => c.Value >= 2).ToList())
|
||||
{
|
||||
ModifyCount(counts, kv.Key, -2);
|
||||
best = Math.Min(best, 1 + MinPlaysImpl(counts, memo));
|
||||
ModifyCount(counts, kv.Key, +2);
|
||||
}
|
||||
|
||||
// 单张
|
||||
var singleRank = counts.First(c => c.Value >= 1).Key;
|
||||
ModifyCount(counts, singleRank, -1);
|
||||
best = Math.Min(best, 1 + MinPlaysImpl(counts, memo));
|
||||
ModifyCount(counts, singleRank, +1);
|
||||
|
||||
memo[key] = best;
|
||||
return best;
|
||||
}
|
||||
|
||||
// ---- 贪心找最优分组 ----
|
||||
|
||||
private static List<string> FindOptimalGroups(Dictionary<int, int> counts, int minPlays)
|
||||
{
|
||||
// 贪心逼近:按牌型优先级从大到小选
|
||||
var groups = new List<string>();
|
||||
var remaining = new Dictionary<int, int>(counts);
|
||||
|
||||
while (remaining.Values.Sum() > 0)
|
||||
{
|
||||
string bestGroup = null;
|
||||
int bestSize = 0;
|
||||
|
||||
// 优先大牌型
|
||||
foreach (var g in EnumerateGroups(remaining))
|
||||
{
|
||||
int size = CardCount(g);
|
||||
if (size > bestSize)
|
||||
{
|
||||
bestSize = size;
|
||||
bestGroup = g;
|
||||
}
|
||||
}
|
||||
|
||||
if (bestGroup == null) break;
|
||||
groups.Add(bestGroup);
|
||||
ApplyGroup(remaining, bestGroup);
|
||||
}
|
||||
|
||||
// 收尾:剩余单张
|
||||
foreach (var kv in remaining.Where(c => c.Value > 0).ToList())
|
||||
{
|
||||
for (int i = 0; i < kv.Value; i++)
|
||||
groups.Add($"单张{GameNumToString(kv.Key)}");
|
||||
}
|
||||
|
||||
return groups;
|
||||
}
|
||||
|
||||
private static IEnumerable<string> EnumerateGroups(Dictionary<int, int> counts)
|
||||
{
|
||||
// 炸弹
|
||||
foreach (var kv in counts.Where(c => c.Value >= 4).ToList())
|
||||
yield return $"炸弹{GameNumToString(kv.Key)}";
|
||||
|
||||
// 飞机
|
||||
var triples = counts.Where(c => c.Value >= 3).Select(c => c.Key).OrderBy(r => r).ToList();
|
||||
for (int len = Math.Min(triples.Count, 4); len >= 2; len--)
|
||||
for (int s = 0; s <= triples.Count - len; s++)
|
||||
if (IsConsecutive(triples, s, len))
|
||||
yield return $"飞机{len}组";
|
||||
|
||||
// 三张
|
||||
foreach (var kv in counts.Where(c => c.Value >= 3).ToList())
|
||||
yield return $"三张{GameNumToString(kv.Key)}";
|
||||
|
||||
// 顺子
|
||||
if (counts.Values.Sum() >= 5)
|
||||
{
|
||||
for (int len = 12; len >= 5; len--)
|
||||
{
|
||||
for (int si = 0; si <= StraightRanks.Length - len; si++)
|
||||
{
|
||||
var ranks = StraightRanks.Skip(si).Take(len).ToList();
|
||||
if (ranks.All(r => counts.GetValueOrDefault(r, 0) >= 1))
|
||||
yield return $"顺子{len}张";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 连对
|
||||
var pairs = counts.Where(c => c.Value >= 2).Select(c => c.Key).OrderBy(r => r).ToList();
|
||||
for (int len = Math.Min(pairs.Count, 6); len >= 3; len--)
|
||||
for (int s = 0; s <= pairs.Count - len; s++)
|
||||
if (IsConsecutive(pairs, s, len))
|
||||
yield return $"连对{len}对";
|
||||
|
||||
// 对子
|
||||
foreach (var kv in counts.Where(c => c.Value >= 2).ToList())
|
||||
yield return $"对子{GameNumToString(kv.Key)}";
|
||||
}
|
||||
|
||||
// ---- 辅助方法 ----
|
||||
|
||||
private static Dictionary<int, int> ToCounts(TCardInfoPdkF[] hand)
|
||||
{
|
||||
var counts = new Dictionary<int, int>();
|
||||
foreach (var c in hand.Where(c => c.GameState == 1))
|
||||
{
|
||||
int g = c.GameNum;
|
||||
counts[g] = counts.GetValueOrDefault(g, 0) + 1;
|
||||
}
|
||||
return counts;
|
||||
}
|
||||
|
||||
private static string CountsToKey(Dictionary<int, int> counts)
|
||||
{
|
||||
var parts = counts.Where(c => c.Value > 0)
|
||||
.OrderBy(c => c.Key)
|
||||
.Select(c => $"{c.Key}:{c.Value}");
|
||||
return string.Join(",", parts);
|
||||
}
|
||||
|
||||
private static void ModifyCount(Dictionary<int, int> counts, int rank, int delta)
|
||||
{
|
||||
if (!counts.ContainsKey(rank)) counts[rank] = 0;
|
||||
counts[rank] += delta;
|
||||
if (counts[rank] <= 0) counts.Remove(rank);
|
||||
}
|
||||
|
||||
private static List<int> FindKickers(Dictionary<int, int> counts, int needed)
|
||||
{
|
||||
// 找最低的 available ranks 做带牌
|
||||
var result = new List<int>();
|
||||
var available = counts.Where(c => c.Key != 16 && c.Value >= 1)
|
||||
.Select(c => c.Key).OrderBy(r => r).ToList();
|
||||
foreach (var r in available)
|
||||
{
|
||||
for (int i = 0; i < Math.Min(counts[r], needed - result.Count); i++)
|
||||
result.Add(r);
|
||||
if (result.Count >= needed) break;
|
||||
}
|
||||
return result.Count >= needed ? result : null;
|
||||
}
|
||||
|
||||
private static bool IsConsecutive(List<int> ranks, int start, int len)
|
||||
{
|
||||
for (int i = 1; i < len; i++)
|
||||
if (ranks[start + i] != ranks[start + i - 1] + 1)
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static TCardInfoPdkF[] RemovePlayed(TCardInfoPdkF[] hand, PlayOutCardPdkF play)
|
||||
{
|
||||
if (play.Ids == null) return hand;
|
||||
var ids = new HashSet<byte>(play.Ids);
|
||||
return hand.Where(c => c.GameState == 1 && !ids.Contains(c.ID)).ToArray();
|
||||
}
|
||||
|
||||
private static int CardCount(string group)
|
||||
{
|
||||
if (group.StartsWith("炸弹")) return 4;
|
||||
if (group.StartsWith("飞机") && group.Contains("组"))
|
||||
return int.Parse(group.Substring(2, 1)) * 3;
|
||||
if (group.StartsWith("三张")) return 3;
|
||||
if (group.StartsWith("顺子"))
|
||||
return int.Parse(group.Substring(2, group.IndexOf("张") - 2));
|
||||
if (group.StartsWith("连对"))
|
||||
return int.Parse(group.Substring(2, group.IndexOf("对") - 2)) * 2;
|
||||
if (group.StartsWith("对子")) return 2;
|
||||
return 1;
|
||||
}
|
||||
|
||||
private static void ApplyGroup(Dictionary<int, int> counts, string group)
|
||||
{
|
||||
// 贪心版本:简单移除
|
||||
if (group.StartsWith("炸弹") && group.Length > 2)
|
||||
{
|
||||
int rank = ParseGameNum(group.Substring(2));
|
||||
if (counts.ContainsKey(rank)) counts[rank] -= Math.Min(4, counts[rank]);
|
||||
}
|
||||
else if (group.StartsWith("对子") && group.Length > 2)
|
||||
{
|
||||
int rank = ParseGameNum(group.Substring(2));
|
||||
if (counts.ContainsKey(rank)) counts[rank] -= Math.Min(2, counts[rank]);
|
||||
}
|
||||
else if (group.StartsWith("三张") && group.Length > 2)
|
||||
{
|
||||
int rank = ParseGameNum(group.Substring(2));
|
||||
if (counts.ContainsKey(rank)) counts[rank] -= Math.Min(3, counts[rank]);
|
||||
}
|
||||
CleanupCounts(counts);
|
||||
}
|
||||
|
||||
private static void CleanupCounts(Dictionary<int, int> counts)
|
||||
{
|
||||
foreach (var k in counts.Where(c => c.Value <= 0).Select(c => c.Key).ToList())
|
||||
counts.Remove(k);
|
||||
}
|
||||
|
||||
private static int ParseGameNum(string s)
|
||||
{
|
||||
return s switch
|
||||
{
|
||||
"A" or "a" => 14,
|
||||
"2" => 16,
|
||||
"J" or "j" => 11,
|
||||
"Q" or "q" => 12,
|
||||
"K" or "k" => 13,
|
||||
_ => int.TryParse(s, out int n) ? n : 0
|
||||
};
|
||||
}
|
||||
|
||||
private static string GameNumToString(int gn)
|
||||
{
|
||||
return gn switch { 14 => "A", 16 => "2", 11 => "J", 12 => "Q", 13 => "K", _ => gn.ToString() };
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 手牌分析结果
|
||||
/// </summary>
|
||||
public class HandAnalysis
|
||||
{
|
||||
/// <summary>最少几轮出完</summary>
|
||||
public int MinPlays { get; set; }
|
||||
|
||||
/// <summary>最优分组方案(贪心近似)</summary>
|
||||
public List<string> OptimalGroups { get; set; }
|
||||
|
||||
/// <summary>手牌总数</summary>
|
||||
public int TotalCards { get; set; }
|
||||
|
||||
/// <summary>牌效率评分:手牌是否容易出干净</summary>
|
||||
public double EfficiencyScore => TotalCards > 0 ? (double)(TotalCards - MinPlays) / TotalCards : 0;
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
var gs = OptimalGroups != null ? string.Join(" → ", OptimalGroups) : "N/A";
|
||||
return $"最少{MinPlays}轮出完 (效率{EfficiencyScore:P0}) [{gs}]";
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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