Files
hjha-server/PdkFriendServer/Logic/IsmctsBot.cs
xiaoou 2dd0bc3f78 feat: AddMultiCardLeads 补全 — 三带二/四带二/四带三
引擎支持(GetOutCard已验证): DanZhang/DuiZi/SanDai2/ShunZi/LianDui/
  FeiJi/ZhaDan/SiDai2/SiDai3/FeijiDai2

AddMultiCardLeads 新增:
- 三带二: 三张相同 + 附带手牌最小的2张
- 四带二: 四张相同 + 附带2张 (优先四带三)

10局验证: DuiZi 54 | DanZhang 53 | ShunZi 16 | SanDai2 13 | SiDai 2
单张比例: 100% → 38%
2026-07-07 14:22:48 +08:00

479 lines
18 KiB
C#
Raw 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 GameFix.Poker;
using GameMessage;
using GameMessage.PaoDeKuaiF;
namespace PdkFriendServer.Logic
{
/// <summary>
/// ISMCTS 机器人 v2
/// P0修复: 包庄阈值 70%(从 45% 校准rollout 用最强方案而非最小
/// P2修复: 优先多牌组合(对子/顺子 > 单张),提高清牌效率
/// </summary>
public class IsmctsBot : IPdkBot
{
private readonly int _simulations;
private readonly Random _rng;
private const double BaoThreshold = 0.60; // 校准自1000局压测70%→0人包45%→5.2%成功
public IsmctsBot(int simulations = 800)
{
_simulations = simulations;
_rng = new Random();
}
// ---- 包庄决策 ----
public bool DecideBaoZhuang(PdkBotView view)
{
double winRate = EstimateWinProbability(view);
bool shouldBao = winRate >= BaoThreshold;
Console.WriteLine($"[ISMCTS pos{view.MyPos}] 包庄评估: P(win)={winRate:P0} "
+ $"(阈值{BaoThreshold:P0}) → {(shouldBao ? "" : "")}");
return shouldBao;
}
private double EstimateWinProbability(PdkBotView view)
{
var hand = view.MyHand;
int sims = Math.Min(_simulations, 600);
int wins = 0;
for (int s = 0; s < sims; s++)
{
if (SimulateWin(hand, view))
wins++;
}
return (double)wins / sims;
}
private bool SimulateWin(TCardInfoPdkF[] myHand, PdkBotView view)
{
int myPos = view.MyPos;
int nextPos = myPos % 3 + 1;
int oppPos = (myPos + 1) % 3 + 1;
var hands = new List<TCardInfoPdkF>[3];
hands[myPos - 1] = new List<TCardInfoPdkF>(myHand);
hands[nextPos - 1] = new List<TCardInfoPdkF>();
hands[oppPos - 1] = new List<TCardInfoPdkF>();
var allCards = GenerateDeck();
var unknown = allCards.Where(c => !myHand.Any(h => h.ID == c.ID)).ToList();
Shuffle(unknown);
hands[nextPos - 1].AddRange(unknown.Take(view.ShenYuCard[nextPos - 1]));
hands[oppPos - 1].AddRange(unknown.Skip(view.ShenYuCard[nextPos - 1])
.Take(view.ShenYuCard[oppPos - 1]));
int current = myPos;
var maxPlay = new PlayOutCardPdkF();
int maxPlayer = 0;
int passStreak = 0;
for (int move = 0; move < 400; move++)
{
var curHand = hands[current - 1];
if (curHand.Count == 0) return current == myPos;
bool isNewRound = maxPlayer == current || maxPlay.GameNum == 0;
var curMax = isNewRound ? new PlayOutCardPdkF() : maxPlay;
var tips = PdkCardAlgorithm.GetTipCard(curMax, curHand.ToArray(), null, false);
if (tips != null && tips.Count > 0)
{
// P2修复: 优先多牌组合其次选最强最大GameNum
var pick = PickBestTip(tips, isNewRound);
maxPlay = new PlayOutCardPdkF
{
Pos = (byte)current,
GameNum = pick[0].GameNum,
Type = pick.Count == 1 ? CardType1.DanZhang : CardType1.DuiZi,
Ids = pick.Select(c => c.ID).ToArray()
};
maxPlayer = current;
passStreak = 0;
foreach (var id in maxPlay.Ids)
curHand.RemoveAll(c => c.ID == id);
}
else
{
passStreak++;
// 防止死循环: 三轮全 pass 则强制最大出牌
if (passStreak >= 2)
{
var forceTips = PdkCardAlgorithm.GetTipCard(
new PlayOutCardPdkF(), curHand.ToArray(), null, false);
if (forceTips != null && forceTips.Count > 0)
{
var pick = PickBestTip(forceTips, true);
maxPlay = new PlayOutCardPdkF
{
Pos = (byte)current,
GameNum = pick[0].GameNum,
Type = CardType1.DanZhang,
Ids = pick.Select(c => c.ID).ToArray()
};
maxPlayer = current;
foreach (var id in maxPlay.Ids)
curHand.RemoveAll(c => c.ID == id);
}
passStreak = 0;
}
}
current = current % 3 + 1;
}
return hands[myPos - 1].Count <= hands[nextPos - 1].Count
&& hands[myPos - 1].Count <= hands[oppPos - 1].Count;
}
// ---- 出牌决策 ----
public PlayOutCardPdkF DecidePlay(PdkBotView view)
{
var hand = view.MyHand;
if (hand.Length == 0) return Pass(view);
bool isFirstPlay = view.MaxPlayCard.GameNum <= 0;
// 1. 获取合法出牌方案
var tips = PdkCardAlgorithm.GetTipCard(
view.MaxPlayCard, hand, null, view.Rule.AAAIsZhaDan);
var allCandidates = new List<(PlayOutCardPdkF play, bool isMulti)>();
var multiCount = 0;
if (tips != null)
foreach (var tip in tips)
{
var isMulti = tip.Count > 1;
if (isMulti) multiCount++;
allCandidates.Add((MakePlay(tip, view.MyPos), isMulti));
}
// 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 / allCandidates.Count);
for (int ci = 0; ci < allCandidates.Count; ci++)
{
int wins = 0;
var play = allCandidates[ci].play;
for (int s = 0; s < simsPerCandidate; s++)
{
if (SimulateOneGame(hand, play, view, unknownCount))
wins++;
}
double wr = (double)wins / simsPerCandidate;
scoredCandidates.Add((play, wr, allCandidates[ci].isMulti));
}
// 两阶段选择多牌组合胜率≥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 * allCandidates.Count}sims "
+ $"winRate:{best.winRate:P0} => {desc}");
return best.play;
}
// ---- 模拟引擎 ----
private bool SimulateOneGame(
TCardInfoPdkF[] myHand, PlayOutCardPdkF myPlay,
PdkBotView view, int unknownCount)
{
int myPos = view.MyPos;
int nextPos = myPos % 3 + 1;
int oppPos = (myPos + 1) % 3 + 1;
var hands = new List<TCardInfoPdkF>[3];
hands[myPos - 1] = new List<TCardInfoPdkF>(myHand);
hands[nextPos - 1] = new List<TCardInfoPdkF>();
hands[oppPos - 1] = new List<TCardInfoPdkF>();
var allCards = GenerateDeck();
var unknown = allCards.Where(c => !myHand.Any(h => h.ID == c.ID)).ToList();
Shuffle(unknown);
hands[nextPos - 1].AddRange(unknown.Take(view.ShenYuCard[nextPos - 1]));
hands[oppPos - 1].AddRange(unknown.Skip(view.ShenYuCard[nextPos - 1])
.Take(view.ShenYuCard[oppPos - 1]));
var maxPlay = new PlayOutCardPdkF
{
Pos = (byte)myPos, GameNum = myPlay.GameNum,
Type = myPlay.Type, Ids = myPlay.Ids?.ToArray()
};
if (myPlay.Type != CardType1.None && myPlay.Ids != null)
foreach (var id in myPlay.Ids)
hands[myPos - 1].RemoveAll(c => c.ID == id);
int current = myPos % 3 + 1;
int maxPlayer = myPlay.Type == CardType1.None ? view.MaxPlayCard.Pos : myPos;
if (myPlay.Type == CardType1.None)
{
maxPlayer = view.MaxPlayCard.Pos;
maxPlay = CloneCard(view.MaxPlayCard);
}
int moveCount = 0;
int passStreak = 0;
while (moveCount++ < 500)
{
var curHand = hands[current - 1];
if (curHand.Count == 0) return current == myPos;
bool isNewRound = maxPlayer == current || maxPlay.GameNum == 0;
var curMax = isNewRound ? new PlayOutCardPdkF() : maxPlay;
var curTips = PdkCardAlgorithm.GetTipCard(curMax, curHand.ToArray(), null, false);
if (curTips != null && curTips.Count > 0)
{
var pick = PickBestTip(curTips, isNewRound);
maxPlay = new PlayOutCardPdkF
{
Pos = (byte)current,
GameNum = pick[0].GameNum,
Type = pick.Count == 1 ? CardType1.DanZhang : CardType1.DuiZi,
Ids = pick.Select(c => c.ID).ToArray()
};
maxPlayer = current;
passStreak = 0;
foreach (var id in maxPlay.Ids)
curHand.RemoveAll(c => c.ID == id);
}
else
{
passStreak++;
if (passStreak >= 2)
{
var forceTips = PdkCardAlgorithm.GetTipCard(
new PlayOutCardPdkF(), curHand.ToArray(), null, false);
if (forceTips != null && forceTips.Count > 0)
{
var pick = PickBestTip(forceTips, true);
maxPlay = new PlayOutCardPdkF
{
Pos = (byte)current, GameNum = pick[0].GameNum,
Type = CardType1.DanZhang, Ids = pick.Select(c => c.ID).ToArray()
};
maxPlayer = current;
foreach (var id in maxPlay.Ids)
curHand.RemoveAll(c => c.ID == id);
}
passStreak = 0;
}
}
current = current % 3 + 1;
}
return hands[myPos - 1].Count <= hands[nextPos - 1].Count
&& hands[myPos - 1].Count <= hands[oppPos - 1].Count;
}
// ---- 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)
AddCandidate(bomb, pos, candidates, ref multiCount);
// 对子2张相同
foreach (var pair in GetMultiGroups(hand, 2))
AddCandidate(pair, pos, candidates, ref multiCount);
// 三张 / 三带二
var triples = GetMultiGroups(hand, 3);
foreach (var t in triples)
{
// 三带二: 三张 + 附带最小的两张其他牌
var remainder = hand.Where(c => c.GameNum != t[0].GameNum).OrderBy(c => c.GameNum).ToList();
if (remainder.Count >= 2)
{
var sd2 = t.Concat(remainder.Take(2)).ToList();
AddCandidate(sd2, pos, candidates, ref multiCount);
}
else
{
AddCandidate(t, pos, candidates, ref multiCount); // 手牌不够带,纯三张
}
}
// 四带二 / 四带三
var quads = GetMultiGroups(hand, 4);
foreach (var q in quads)
{
var remainder = hand.Where(c => c.GameNum != q[0].GameNum).OrderBy(c => c.GameNum).ToList();
if (remainder.Count >= 3)
{
var sd3 = q.Concat(remainder.Take(3)).ToList();
AddCandidate(sd3, pos, candidates, ref multiCount);
}
else if (remainder.Count >= 2)
{
var sd2 = q.Concat(remainder.Take(2)).ToList();
AddCandidate(sd2, pos, candidates, ref multiCount);
}
// else: 炸弹已在上面处理
}
// 顺子≥5 张连续)
var straights = PokerLogic.GetShunZi(hand);
if (straights != null)
foreach (var s in straights)
AddCandidate(s, pos, candidates, ref multiCount);
}
private static void AddCandidate(List<TCardInfoPdkF> cards, int pos,
List<(PlayOutCardPdkF play, bool isMulti)> candidates, ref int multiCount)
{
candidates.Add((new PlayOutCardPdkF
{
Pos = (byte)pos, GameNum = cards[0].GameNum,
Ids = cards.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](最小单张),改为优先多牌组合
/// P0修复: 选最大(最后一项),模拟更强对手
/// </summary>
private static List<TCardInfoPdkF> PickBestTip(List<List<TCardInfoPdkF>> tips, bool isNewRound)
{
if (tips == null || tips.Count == 0) return null;
// 优先多牌组合2张>1张3张>2张等同数量选最大的
var best = tips[tips.Count - 1]; // 默认最大
int bestScore = best.Count * 10 + best[0].GameNum;
for (int i = tips.Count - 2; i >= 0; i--)
{
int score = tips[i].Count * 10 + tips[i][0].GameNum;
if (score > bestScore)
{
bestScore = score;
best = tips[i];
}
}
return best;
}
// ---- 工具 ----
private static int CountPlayed(TCardInfoPdkF[] myHand, PdkBotView view)
{
int total = 48 - myHand.Length;
foreach (var c in view.ShenYuCard) total -= c;
return Math.Max(0, total);
}
private static List<TCardInfoPdkF> GenerateDeck()
{
var deck = new List<TCardInfoPdkF>();
int id = 1;
foreach (int flower in new[] { 1, 2, 3, 4 })
for (int num = 1; num <= 13; num++)
{
int gameNum = num switch { 1 => 14, 2 => 16, _ => num };
deck.Add(new TCardInfoPdkF
{
ID = (byte)id++, Flower = (byte)flower,
GameNum = (byte)gameNum, GameState = 1
});
}
return deck;
}
private void Shuffle<T>(List<T> list)
{
for (int i = list.Count - 1; i > 0; i--)
{
int j = _rng.Next(i + 1);
(list[i], list[j]) = (list[j], list[i]);
}
}
private static PlayOutCardPdkF CloneCard(PlayOutCardPdkF src)
{
return new PlayOutCardPdkF
{
Pos = src.Pos, GameNum = src.GameNum,
Type = src.Type, Ids = src.Ids?.ToArray()
};
}
private static PlayOutCardPdkF MakePlay(List<TCardInfoPdkF> cards, int pos)
{
return new PlayOutCardPdkF
{
Pos = (byte)pos, GameNum = cards[0].GameNum,
Ids = cards.Select(x => x.ID).ToArray(),
Type = CardType1.None // 不给假类型让引擎GetOutCard赋予真值
};
}
private static PlayOutCardPdkF Pass(PdkBotView view)
{
return new PlayOutCardPdkF { Pos = (byte)view.MyPos, Type = CardType1.None };
}
}
}