Files
hjha-server/PdkFriendServer/Logic/HandOptimizer.cs
xiaoou 2f3b62e835 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/局
2026-07-12 22:26:29 +08:00

427 lines
16 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 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}]";
}
}
}