CardTracker: - UnknownCount: 52→48 - handSize - seenCount (修正底牌+手牌漏算) - 新增 RemainingCount/GetUnknownPool: 精确剩余牌计数 - 新增 IsAbsolutelyHighest: 全牌型确定性无敌判断 (单张/对子/三张/炸弹/顺子/连对/飞机) EndgameSolver: - GeneratePool → 调用 tracker.GetUnknownPool (修复已见牌未排除bug) IsmctsBot: - 确定性无敌出牌加成: 无敌出牌 +15-25% win rate bonus SOIsmctsBot: - 同步 UnknownCount(hand) 调用 验证: 100局0错误, 0.8s/局, ENDGAME求解正常触发
155 lines
5.8 KiB
C#
155 lines
5.8 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using System.Linq;
|
||
using GameFix.PaoDeKuaiF;
|
||
using GameMessage;
|
||
using GameMessage.PaoDeKuaiF;
|
||
|
||
namespace PdkFriendServer.Logic
|
||
{
|
||
/// <summary>
|
||
/// 终局精确求解器 — 手牌≤5张时,枚举所有对手手牌分布,每分布模拟到底。
|
||
/// 替代 ISMCTS 采样:残局决策更精确。
|
||
/// </summary>
|
||
public static class EndgameSolver
|
||
{
|
||
private const int MaxDepth = 30;
|
||
|
||
public static PlayOutCardPdkF? Solve(
|
||
TCardInfoPdkF[] hand,
|
||
PdkBotView view,
|
||
CardTracker tracker,
|
||
Random rng)
|
||
{
|
||
if (hand.Length > 5) return null;
|
||
if (tracker.UnknownCount(hand) > 15) return null;
|
||
|
||
int myPos = view.MyPos;
|
||
int opp1 = myPos % 3;
|
||
int opp2 = (myPos + 1) % 3;
|
||
int o1c = view.ShenYuCard[opp1];
|
||
int o2c = view.ShenYuCard[opp2];
|
||
|
||
var pool = GeneratePool(hand, tracker);
|
||
if (pool.Count < o1c + o2c) return null;
|
||
|
||
bool isFirstPlay = view.MaxPlayCard.GameNum <= 0;
|
||
var tips = PdkCardAlgorithm.GetTipCard(view.MaxPlayCard, hand, null, view.Rule.AAAIsZhaDan);
|
||
|
||
var candidates = new List<List<TCardInfoPdkF>>();
|
||
if (tips != null) foreach (var t in tips) candidates.Add(t);
|
||
if (!isFirstPlay) candidates.Add(new List<TCardInfoPdkF>());
|
||
|
||
if (candidates.Count == 0) return null;
|
||
|
||
int samples = Math.Min(100, ComputeCombinations(pool.Count, o1c + o2c));
|
||
var bestPlay = candidates[0]; // safe default
|
||
int bestWins = -1;
|
||
|
||
for (int ci = 0; ci < candidates.Count; ci++)
|
||
{
|
||
var play = candidates[ci];
|
||
int wins = 0;
|
||
|
||
var myRem = hand.Where(c => c.GameState == 1).ToList();
|
||
if (play.Count > 0)
|
||
foreach (var p in play) myRem.RemoveAll(h => h.ID == p.ID);
|
||
if (myRem.Count == 0) { bestPlay = play; break; }
|
||
|
||
for (int s = 0; s < samples; s++)
|
||
{
|
||
var shuf = new List<TCardInfoPdkF>(pool);
|
||
Shuffle(shuf, rng);
|
||
var o1 = shuf.Take(o1c).ToList();
|
||
var o2 = shuf.Skip(o1c).Take(o2c).ToList();
|
||
|
||
PlayOutCardPdkF maxPlay;
|
||
int maxPlayer;
|
||
int current;
|
||
|
||
if (play.Count == 0) // pass
|
||
{
|
||
maxPlay = Clone(view.MaxPlayCard);
|
||
maxPlayer = view.MaxPlayCard.Pos > 0 ? view.MaxPlayCard.Pos - 1 : myPos - 1;
|
||
current = myPos % 3;
|
||
}
|
||
else
|
||
{
|
||
maxPlay = new PlayOutCardPdkF { GameNum = play[0].GameNum, Type = CardType1.DanZhang };
|
||
maxPlayer = myPos - 1;
|
||
current = myPos % 3;
|
||
}
|
||
|
||
if (Simulate(o1, o2, myRem, current, maxPlay, maxPlayer, 0))
|
||
wins++;
|
||
}
|
||
|
||
if (wins > bestWins) { bestWins = wins; bestPlay = play; }
|
||
}
|
||
|
||
if (bestPlay.Count == 0)
|
||
return new PlayOutCardPdkF { Pos = (byte)myPos, Type = CardType1.None };
|
||
return new PlayOutCardPdkF
|
||
{
|
||
Pos = (byte)myPos,
|
||
GameNum = bestPlay[0].GameNum,
|
||
Ids = bestPlay.Select(c => c.ID).ToArray(),
|
||
Type = CardType1.None
|
||
};
|
||
}
|
||
|
||
// 简化模拟:给定对手手牌,模拟到终局
|
||
private static bool Simulate(
|
||
List<TCardInfoPdkF> h1, List<TCardInfoPdkF> h2, List<TCardInfoPdkF> myHand,
|
||
int current, // 0=h1, 1=h2, 2=me
|
||
PlayOutCardPdkF maxPlay, int maxPlayer, int depth)
|
||
{
|
||
if (depth > MaxDepth) return myHand.Count <= Math.Min(h1.Count, h2.Count);
|
||
if (h1.Count == 0) return false;
|
||
if (h2.Count == 0) return false;
|
||
if (myHand.Count == 0) return true;
|
||
|
||
var cur = current == 0 ? h1 : current == 1 ? h2 : myHand;
|
||
bool isNewRound = maxPlayer == current || maxPlay.GameNum <= 0;
|
||
int next = (current + 1) % 3;
|
||
|
||
var tips = PdkCardAlgorithm.GetTipCard(
|
||
isNewRound ? new PlayOutCardPdkF() : maxPlay,
|
||
cur.ToArray(), null, false);
|
||
|
||
if (tips != null && tips.Count > 0)
|
||
{
|
||
var pick = tips[tips.Count - 1]; // 贪心:出最大的
|
||
foreach (var c in pick) cur.RemoveAll(h => h.ID == c.ID);
|
||
|
||
var newMax = new PlayOutCardPdkF { GameNum = pick[0].GameNum, Type = CardType1.DanZhang };
|
||
return Simulate(h1, h2, myHand, next, newMax, current, depth + 1);
|
||
}
|
||
|
||
// pass
|
||
return Simulate(h1, h2, myHand, next, maxPlay, isNewRound ? current : maxPlayer, depth + 1);
|
||
}
|
||
|
||
private static List<TCardInfoPdkF> GeneratePool(TCardInfoPdkF[] hand, CardTracker tracker)
|
||
{
|
||
return tracker.GetUnknownPool(hand);
|
||
}
|
||
|
||
private static int ComputeCombinations(int n, int k)
|
||
{
|
||
if (k > n) return 0;
|
||
long result = 1;
|
||
for (int i = 1; i <= k; i++) result = result * (n - i + 1) / i;
|
||
return (int)Math.Min(result, 100);
|
||
}
|
||
|
||
private static void Shuffle<T>(List<T> l, Random rng)
|
||
{
|
||
for (int i = l.Count - 1; i > 0; i--)
|
||
{ int j = rng.Next(i + 1); (l[i], l[j]) = (l[j], l[i]); }
|
||
}
|
||
|
||
private static PlayOutCardPdkF Clone(PlayOutCardPdkF s)
|
||
=> new PlayOutCardPdkF { Pos = s.Pos, GameNum = s.GameNum, Type = s.Type, Ids = s.Ids?.ToArray() };
|
||
}
|
||
} |