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:
2026-07-12 22:26:29 +08:00
parent 8de64cc6ae
commit 2f3b62e835
2 changed files with 569 additions and 92 deletions

View 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}]";
}
}
}

View File

@ -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)
{