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/局
427 lines
16 KiB
C#
427 lines
16 KiB
C#
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}]";
|
||
}
|
||
}
|
||
} |