Files
hjha-server/PdkFriendServer/Logic/IsmctsBot.cs
xiaoou 54d9f3add3 compat: switch expression → ternary for .NET 4.8 双兼容
IsmctsBot.cs:539: C#8 switch expression → C#7.3 ternary
  num switch {1=>14,2=>16,_=>num} → num==1?14:num==2?16:num

bot代码(.NET 4.8)兼容性清单:
   LINQ (GroupBy/OrderBy/SelectMany) — 原生
   string 插值 $"..." — C#6
   ?./?? — C#6
   ValueTuple — 需 NuGet System.ValueTuple (net48)
   已移除 switch expression — 唯一 C#8 特性

net48 项目集成只需3步:
  1. 复制 PdkBot.cs + IsmctsBot.cs → PdkFriendServer/Logic/
  2. csproj: <PackageReference Include="System.ValueTuple" Version="4.5.0"/>
  3. WaitWanJiaShuRu 加入 BotMode 分支(同 net10.0 逻辑)
2026-07-07 14:36:13 +08:00

573 lines
22 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: 炸弹已在上面处理
}
// 连对≥3对连续, 如 334455
var pairs = GetMultiGroups(hand, 2);
SortByGameNum(pairs);
AddConsecutiveGroups(pairs, 3, pos, candidates, ref multiCount);
// 飞机≥2组连续三张, 如 333444
SortByGameNum(triples);
AddConsecutiveGroups(triples, 2, pos, candidates, ref multiCount);
// 飞机带对: 连续三张 + 每组附带一个对子
var tripleConsecRuns = GetConsecutiveRuns(triples, 2);
foreach (var run in tripleConsecRuns)
{
var runCards = run.SelectMany(t => t).ToList();
int pairCount = run.Count; // 需要这么多对子
// 找不重叠的对子
var remainingPairs = GetMultiGroups(
hand.Where(c => !runCards.Any(r => r.ID == c.ID)).ToArray(), 2);
if (remainingPairs.Count >= pairCount)
{
SortByGameNum(remainingPairs);
var attached = remainingPairs.Take(pairCount).SelectMany(p => p).ToList();
runCards.AddRange(attached);
AddCandidate(runCards, pos, candidates, ref multiCount);
}
}
// 顺子≥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 = InferType(cards)
}, true));
multiCount++;
}
/// <summary>根据牌数和面值推断牌型——ISMCTS模拟需要正确Type</summary>
private static CardType1 InferType(List<TCardInfoPdkF> cards)
{
int n = cards.Count;
if (n == 1) return CardType1.DanZhang;
if (n == 2) return CardType1.DuiZi;
var counts = cards.GroupBy(c => c.GameNum).Select(g => g.Count()).OrderByDescending(x => x).ToList();
if (counts[0] == 4)
{
if (n == 4) return CardType1.ZhaDan;
if (n == 6) return CardType1.SiDai2;
if (n == 7) return CardType1.SiDai3;
}
if (counts[0] >= 3) return n == counts[0] + 2 ? CardType1.SanDai2 : CardType1.SanDai2;
return n >= 5 ? CardType1.ShunZi : CardType1.DuiZi;
}
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 = InferType(cards)
};
}
/// <summary>获取指定数量的同面值组合</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>按 GameNum 排序多牌组列表</summary>
private static void SortByGameNum(List<List<TCardInfoPdkF>> groups)
{
groups.Sort((a, b) => a[0].GameNum.CompareTo(b[0].GameNum));
}
/// <summary>找出所有连续段≥minLen每段生成一个候选</summary>
private static void AddConsecutiveGroups(List<List<TCardInfoPdkF>> groups, int minLen,
int pos, List<(PlayOutCardPdkF play, bool isMulti)> candidates, ref int multiCount)
{
var runs = GetConsecutiveRuns(groups, minLen);
foreach (var run in runs)
{
var cards = run.SelectMany(g => g).ToList();
AddCandidate(cards, pos, candidates, ref multiCount);
}
}
/// <summary>从已排序的组中提取所有连续段≥minLen 组)</summary>
private static List<List<List<TCardInfoPdkF>>> GetConsecutiveRuns(
List<List<TCardInfoPdkF>> groups, int minLen)
{
var runs = new List<List<List<TCardInfoPdkF>>>();
if (groups.Count < minLen) return runs;
var current = new List<List<TCardInfoPdkF>> { groups[0] };
int prevNum = groups[0][0].GameNum;
for (int i = 1; i < groups.Count; i++)
{
int curNum = groups[i][0].GameNum;
if (curNum == prevNum + 1)
{
current.Add(groups[i]);
}
else
{
if (current.Count >= minLen)
runs.Add(new List<List<TCardInfoPdkF>>(current));
current.Clear();
current.Add(groups[i]);
}
prevNum = curNum;
}
if (current.Count >= minLen)
runs.Add(current);
return runs;
}
/// <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 == 1 ? (byte)14 : num == 2 ? (byte)16 : (byte)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 Pass(PdkBotView view)
{
return new PlayOutCardPdkF { Pos = (byte)view.MyPos, Type = CardType1.None };
}
}
}