Files
hjha-server/PdkFriendServer/Logic/EndgameSolver.cs
xiaoou 5cc2ae8bd0 fix: 确定性牌计数推理 — UnknownCount修复 + 绝对大牌判断 + EndgameSolver修正
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求解正常触发
2026-07-13 00:40:27 +08:00

155 lines
5.8 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 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() };
}
}