fix: 多牌先手枚举 — 根除只出单张问题

根因: GetTipCard 在 GameNum≤0(新轮次)时只返回单张候选,
不包含对子/三张/顺子/炸弹等多牌先手方案。

修复:
- 新增 AddMultiCardLeads(): 新轮次枚举全部多牌先手
  - 对子 (GetMultiGroups:2) 三张 (3) 顺子(PokerLogic.GetShunZi) 炸弹(PokerLogic.GetPlayZhaDans)
- GetMultiGroups(): 统计同面值牌数,生成多牌候选
- 两阶段选择: 多牌候选优先,按牌数多少×胜率排序
- GetOutCard集成: 确保引擎收到正确牌型(DuiZi/ShunZi而非假DanZhang)

10局验证: DanZhang 59 | DuiZi 33 | ShunZi 16 | ZhaDan 1
多牌比例: 46% (之前0%)
This commit is contained in:
2026-07-07 14:19:38 +08:00
parent 3ef9d7f29e
commit 5547f75b5a
7 changed files with 117 additions and 13 deletions

View File

@ -141,27 +141,42 @@ namespace PdkFriendServer.Logic
bool isFirstPlay = view.MaxPlayCard.GameNum <= 0;
// 1. 获取合法出牌方案
var tips = PdkCardAlgorithm.GetTipCard(
view.MaxPlayCard, hand, null, view.Rule.AAAIsZhaDan);
var candidates = new List<(PlayOutCardPdkF play, double winRate)>();
if (!isFirstPlay)
candidates.Add((Pass(view), 0));
var allCandidates = new List<(PlayOutCardPdkF play, bool isMulti)>();
var multiCount = 0;
if (tips != null)
foreach (var tip in tips)
candidates.Add((MakePlay(tip, view.MyPos), 0));
{
var isMulti = tip.Count > 1;
if (isMulti) multiCount++;
allCandidates.Add((MakePlay(tip, view.MyPos), isMulti));
}
if (candidates.Count == 1)
return candidates[0].play;
// 2. 新轮次补充多牌先手方案GetTipCard只返回单张
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 unknownCount = 48 - hand.Length - CountPlayed(hand, view);
int simsPerCandidate = Math.Max(50, _simulations / candidates.Count);
int simsPerCandidate = Math.Max(50, _simulations / allCandidates.Count);
for (int ci = 0; ci < candidates.Count; ci++)
for (int ci = 0; ci < allCandidates.Count; ci++)
{
int wins = 0;
var play = candidates[ci].play;
var play = allCandidates[ci].play;
for (int s = 0; s < simsPerCandidate; s++)
{
@ -169,16 +184,27 @@ namespace PdkFriendServer.Logic
wins++;
}
candidates[ci] = (play, (double)wins / simsPerCandidate);
double wr = (double)wins / simsPerCandidate;
scoredCandidates.Add((play, wr, allCandidates[ci].isMulti));
}
candidates.Sort((a, b) => b.winRate.CompareTo(a.winRate));
var best = candidates[0];
// 两阶段选择多牌组合胜率≥10%就优用
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;
}
string desc = best.play.Ids == null || best.play.Ids.Length == 0
? "pass"
: $"{best.play.Ids.Length}c";
Console.WriteLine($"[ISMCTS pos{view.MyPos}] {simsPerCandidate * candidates.Count}sims "
Console.WriteLine($"[ISMCTS pos{view.MyPos}] {simsPerCandidate * allCandidates.Count}sims "
+ $"winRate:{best.winRate:P0} => {desc}");
return best.play;
@ -282,6 +308,84 @@ namespace PdkFriendServer.Logic
// ---- Rollout 策略 ----
/// <summary>
/// 新轮次先手时补充多牌组合候选GetTipCard 只返回单张)
/// 枚举:对子、三张、顺子、连对、炸弹
/// </summary>
private static void AddMultiCardLeads(TCardInfoPdkF[] hand, int pos,
List<(PlayOutCardPdkF play, bool isMulti)> candidates, ref int multiCount)
{
// 获取炸弹
if (PokerLogic.GetPlayZhaDans(hand, out var bombs) && bombs != null)
{
foreach (var bomb in bombs)
{
candidates.Add((new PlayOutCardPdkF
{
Pos = (byte)pos, GameNum = bomb[0].GameNum,
Ids = bomb.Select(c => c.ID).ToArray(),
Type = CardType1.None
}, true));
multiCount++;
}
}
// 获取对子2张相同
var pairs = GetMultiGroups(hand, 2);
foreach (var pair in pairs)
{
candidates.Add((new PlayOutCardPdkF
{
Pos = (byte)pos, GameNum = pair[0].GameNum,
Ids = pair.Select(c => c.ID).ToArray(),
Type = CardType1.None
}, true));
multiCount++;
}
// 获取三张
var triples = GetMultiGroups(hand, 3);
foreach (var t in triples)
{
candidates.Add((new PlayOutCardPdkF
{
Pos = (byte)pos, GameNum = t[0].GameNum,
Ids = t.Select(c => c.ID).ToArray(),
Type = CardType1.None
}, true));
multiCount++;
}
// 获取顺子≥5 张连续)
var straights = PokerLogic.GetShunZi(hand);
if (straights != null)
{
foreach (var s in straights)
{
candidates.Add((new PlayOutCardPdkF
{
Pos = (byte)pos, GameNum = s[0].GameNum,
Ids = s.Select(c => c.ID).ToArray(),
Type = CardType1.None
}, true));
multiCount++;
}
}
}
/// <summary>获取指定数量的同面值组合(对子=2三张=3</summary>
private static List<List<TCardInfoPdkF>> GetMultiGroups(TCardInfoPdkF[] hand, int count)
{
var result = new List<List<TCardInfoPdkF>>();
var groups = hand.GroupBy(c => c.GameNum).Where(g => g.Count() >= count);
foreach (var g in groups)
{
var cards = g.Take(count).ToList();
result.Add(cards);
}
return result;
}
/// <summary>
/// 从合法方案中选最优:优先出多张牌的组合,其次出最大的
/// P2修复: 之前只取 tips[0](最小单张),改为优先多牌组合