feat: ISMCTS 4项优化 — 模拟终止+候选剪枝+SanDai2带对子

A: 模拟截断 MinPlays评估
  旧: 张数最少=赢(不知道手牌结构) → 4张散牌 vs 3张对子, 4张赢(错)
  新: MinPlays最少=赢 → 散牌minPlays=4 vs 对子minPlays=2, 对子赢(对)

C: 候选剪枝 fastRate<0.15
  低胜率候选直接丢弃(非pass), 节省模拟+排序时间
  全剪枝时自动回退不剪枝

E: SanDai2/SiDai 优先带对子
  旧: 带最小两张散牌
  新: 优先找对子带, 找不到才带散牌(对子本身是一手牌)

注: B(预测引导采样)回退 — PredictOpponent在每局800次模拟中重复调
  用导致1.6s/局, 在线性能不可接受

验证: 100局0错误 0.8s/局
This commit is contained in:
2026-07-13 01:45:45 +08:00
parent c23b53adf9
commit 68e8c038f4
7 changed files with 61 additions and 10 deletions

View File

@ -164,16 +164,19 @@ namespace PdkFriendServer.Logic
if (SimulateToEnd(hand, view, play))
wins++;
// 有前途的候选多模拟
double fastRate = (double)wins / fastSims;
// 胜率太低 → 剪枝,不参与后续评估
if (fastRate < 0.15 && allCandidates.Count > 3)
continue;
// 有前途的候选多模拟
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());
@ -181,6 +184,22 @@ namespace PdkFriendServer.Logic
scoredCandidates.Add((play, totalRate, afterMin, allCandidates[ci].isMulti));
}
// 如果所有候选都被剪枝,回退到不做剪枝,全部重新评估
if (scoredCandidates.Count == 0)
{
for (int ci = 0; ci < allCandidates.Count; ci++)
{
int wins = 0;
var play = allCandidates[ci].play;
for (int s = 0; s < fastSims; s++)
if (SimulateToEnd(hand, view, play)) wins++;
double rate = (double)wins / fastSims;
int am = HandOptimizer.MinPlays(
hand.Where(c => play.Ids == null || !play.Ids.Contains(c.ID)).ToArray());
scoredCandidates.Add((play, rate, am, allCandidates[ci].isMulti));
}
}
// 4. 多牌加权
for (int i = 0; i < scoredCandidates.Count; i++)
{
@ -460,7 +479,11 @@ namespace PdkFriendServer.Logic
}
current = current % 3 + 1;
}
return hands[myPos - 1].Count <= hands[nextPos - 1].Count && hands[myPos - 1].Count <= hands[oppPos - 1].Count;
// 截断:用 MinPlays 评估而非比较张数
int myMin = HandOptimizer.MinPlays(hands[myPos - 1].ToArray());
int n1Min = HandOptimizer.MinPlays(hands[nextPos - 1].ToArray());
int n2Min = HandOptimizer.MinPlays(hands[oppPos - 1].ToArray());
return myMin <= n1Min && myMin <= n2Min;
}
// ---- 多牌先手枚举 ----
@ -479,20 +502,48 @@ namespace PdkFriendServer.Logic
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();
var rem = hand.Where(c => c.GameState == 1 && c.GameNum != t[0].GameNum).ToList();
if (rem.Count >= 2)
AddCandidate(t.Concat(rem.Take(2)).ToList(), pos, candidates); // 三带二
{
// 三带二:优先带对子(对子本身是一手牌),找不到对子才带两张最小散牌
var pair = rem.GroupBy(c => c.GameNum)
.Where(g => g.Count() >= 2)
.OrderBy(g => g.Key)
.FirstOrDefault();
if (pair != null)
AddCandidate(t.Concat(pair.Take(2)).ToList(), pos, candidates);
else
AddCandidate(t.Concat(rem.OrderBy(c => c.GameNum).Take(2)).ToList(), pos, candidates);
}
else
AddCandidate(t, pos, candidates);
}
// 四带二/三
// 四带二/三:优先带对子
var quads = GetMultiGroups(hand, 4);
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);
else if (rem.Count >= 2) AddCandidate(q.Concat(rem.Take(2)).ToList(), pos, candidates);
var rem = hand.Where(c => c.GameState == 1 && c.GameNum != q[0].GameNum).ToList();
var pair = rem.GroupBy(c => c.GameNum)
.Where(g => g.Count() >= 2)
.OrderBy(g => g.Key)
.FirstOrDefault();
if (rem.Count >= 3)
{
if (pair != null)
AddCandidate(q.Concat(pair.Take(2)).Concat(rem
.Where(c => c.GameNum != pair.Key).OrderBy(c => c.GameNum).Take(1))
.ToList(), pos, candidates);
else
AddCandidate(q.Concat(rem.OrderBy(c => c.GameNum).Take(3)).ToList(), pos, candidates);
}
else if (rem.Count >= 2)
{
if (pair != null)
AddCandidate(q.Concat(pair.Take(2)).ToList(), pos, candidates);
else
AddCandidate(q.Concat(rem.OrderBy(c => c.GameNum).Take(2)).ToList(), pos, candidates);
}
}
// 连对(复用 pairs不重复 GetMultiGroups

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.