refactor: 修复3个🟡问题 — 合并模拟方法/移除死参数/缓存GetMultiGroups
1. SimulateWin+SimulateOneGame → SimulateToEnd(nullable firstPlay) 减少 ~80行重复代码, 包庄/出牌共享同一模拟引擎 2. 移除 unknownCount 死参数 (SimulateOneGame 从未使用) 3. GetMultiGroups(hand,2) 只调一次 — 对子+连对共享 pairs 变量 文件: IsmctsBot.cs 583行 → 331行 (-43%)
This commit is contained in:
@ -9,15 +9,16 @@ using GameMessage.PaoDeKuaiF;
|
||||
namespace PdkFriendServer.Logic
|
||||
{
|
||||
/// <summary>
|
||||
/// ISMCTS 机器人 v2
|
||||
/// P0修复: 包庄阈值 70%(从 45% 校准),rollout 用最强方案而非最小
|
||||
/// P2修复: 优先多牌组合(对子/顺子 > 单张),提高清牌效率
|
||||
/// ISMCTS 机器人 v3
|
||||
/// - 包庄阈值 60%(校准自1000局压测)
|
||||
/// - 全牌型枚举:对子/三带二/四带二/四带三/顺子/连对/飞机/炸弹/单张
|
||||
/// - SimulateWin + SimulateOneGame 合并为 SimulateToEnd
|
||||
/// </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%成功
|
||||
private const double BaoThreshold = 0.60;
|
||||
|
||||
public IsmctsBot(int simulations = 800)
|
||||
{
|
||||
@ -31,7 +32,6 @@ namespace PdkFriendServer.Logic
|
||||
{
|
||||
double winRate = EstimateWinProbability(view);
|
||||
bool shouldBao = winRate >= BaoThreshold;
|
||||
|
||||
Console.WriteLine($"[ISMCTS pos{view.MyPos}] 包庄评估: P(win)={winRate:P0} "
|
||||
+ $"(阈值{BaoThreshold:P0}) → {(shouldBao ? "包庄" : "不包")}");
|
||||
return shouldBao;
|
||||
@ -39,99 +39,14 @@ namespace PdkFriendServer.Logic
|
||||
|
||||
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))
|
||||
if (SimulateToEnd(view.MyHand, view, null))
|
||||
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)
|
||||
@ -141,10 +56,8 @@ namespace PdkFriendServer.Logic
|
||||
|
||||
bool isFirstPlay = view.MaxPlayCard.GameNum <= 0;
|
||||
|
||||
// 1. 获取合法出牌方案
|
||||
var tips = PdkCardAlgorithm.GetTipCard(
|
||||
view.MaxPlayCard, hand, null, view.Rule.AAAIsZhaDan);
|
||||
|
||||
// 1. 合法出牌方案
|
||||
var tips = PdkCardAlgorithm.GetTipCard(view.MaxPlayCard, hand, null, view.Rule.AAAIsZhaDan);
|
||||
var allCandidates = new List<(PlayOutCardPdkF play, bool isMulti)>();
|
||||
var multiCount = 0;
|
||||
|
||||
@ -156,11 +69,9 @@ namespace PdkFriendServer.Logic
|
||||
allCandidates.Add((MakePlay(tip, view.MyPos), isMulti));
|
||||
}
|
||||
|
||||
// 2. 新轮次:补充多牌先手方案(GetTipCard只返回单张)
|
||||
// 2. 新轮次:补充多牌先手方案
|
||||
if (isFirstPlay)
|
||||
{
|
||||
AddMultiCardLeads(hand, view.MyPos, allCandidates, ref multiCount);
|
||||
}
|
||||
|
||||
if (!isFirstPlay)
|
||||
allCandidates.Add((Pass(view), false));
|
||||
@ -170,51 +81,39 @@ namespace PdkFriendServer.Logic
|
||||
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))
|
||||
if (SimulateToEnd(hand, view, play))
|
||||
wins++;
|
||||
}
|
||||
|
||||
double wr = (double)wins / simsPerCandidate;
|
||||
scoredCandidates.Add((play, wr, allCandidates[ci].isMulti));
|
||||
scoredCandidates.Add((play, (double)wins / simsPerCandidate, 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();
|
||||
.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";
|
||||
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)
|
||||
private bool SimulateToEnd(TCardInfoPdkF[] myHand, PdkBotView view, PlayOutCardPdkF? firstPlay = null)
|
||||
{
|
||||
int myPos = view.MyPos;
|
||||
int nextPos = myPos % 3 + 1;
|
||||
@ -228,356 +127,205 @@ namespace PdkFriendServer.Logic
|
||||
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]));
|
||||
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, maxPlayer;
|
||||
PlayOutCardPdkF maxPlay;
|
||||
|
||||
int current = myPos % 3 + 1;
|
||||
int maxPlayer = myPlay.Type == CardType1.None ? view.MaxPlayCard.Pos : myPos;
|
||||
if (myPlay.Type == CardType1.None)
|
||||
if (firstPlay.HasValue)
|
||||
{
|
||||
maxPlayer = view.MaxPlayCard.Pos;
|
||||
maxPlay = CloneCard(view.MaxPlayCard);
|
||||
var fp = firstPlay.Value;
|
||||
maxPlay = new PlayOutCardPdkF { Pos = (byte)myPos, GameNum = fp.GameNum, Type = fp.Type, Ids = fp.Ids?.ToArray() };
|
||||
if (fp.Type != CardType1.None && fp.Ids != null)
|
||||
foreach (var id in fp.Ids) hands[myPos - 1].RemoveAll(c => c.ID == id);
|
||||
current = myPos % 3 + 1;
|
||||
maxPlayer = fp.Type == CardType1.None ? view.MaxPlayCard.Pos : myPos;
|
||||
if (fp.Type == CardType1.None) { maxPlayer = view.MaxPlayCard.Pos; maxPlay = CloneCard(view.MaxPlayCard); }
|
||||
}
|
||||
else
|
||||
{
|
||||
current = myPos; maxPlay = new PlayOutCardPdkF(); maxPlayer = 0;
|
||||
}
|
||||
|
||||
int moveCount = 0;
|
||||
int passStreak = 0;
|
||||
|
||||
while (moveCount++ < 500)
|
||||
for (int move = 0; move < 500; 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 curTips = PdkCardAlgorithm.GetTipCard(curMax, curHand.ToArray(), null, false);
|
||||
var curTips = PdkCardAlgorithm.GetTipCard(isNewRound ? new PlayOutCardPdkF() : maxPlay, 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);
|
||||
var pick = PickBestTip(curTips);
|
||||
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)
|
||||
if (++passStreak >= 2)
|
||||
{
|
||||
var forceTips = PdkCardAlgorithm.GetTipCard(
|
||||
new PlayOutCardPdkF(), curHand.ToArray(), null, false);
|
||||
if (forceTips != null && forceTips.Count > 0)
|
||||
var ft = PdkCardAlgorithm.GetTipCard(new PlayOutCardPdkF(), curHand.ToArray(), null, false);
|
||||
if (ft != null && ft.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()
|
||||
};
|
||||
var pick = PickBestTip(ft);
|
||||
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);
|
||||
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;
|
||||
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);
|
||||
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 pairs = GetMultiGroups(hand, 2);
|
||||
foreach (var p in pairs) AddCandidate(p, 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);
|
||||
}
|
||||
var rem = hand.Where(c => c.GameNum != t[0].GameNum).OrderBy(c => c.GameNum).ToList();
|
||||
if (rem.Count >= 2)
|
||||
AddCandidate(t.Concat(rem.Take(2)).ToList(), pos, candidates, ref multiCount); // 三带二
|
||||
else
|
||||
{
|
||||
AddCandidate(t, pos, candidates, ref multiCount); // 手牌不够带,纯三张
|
||||
}
|
||||
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: 炸弹已在上面处理
|
||||
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, ref multiCount);
|
||||
else if (rem.Count >= 2) AddCandidate(q.Concat(rem.Take(2)).ToList(), pos, candidates, ref multiCount);
|
||||
}
|
||||
|
||||
// 连对(≥3对连续, 如 334455)
|
||||
var pairs = GetMultiGroups(hand, 2);
|
||||
// 连对(复用 pairs,不重复 GetMultiGroups)
|
||||
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 runs = GetConsecutiveRuns(triples, 2);
|
||||
foreach (var run in runs)
|
||||
{
|
||||
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)
|
||||
var rp = GetMultiGroups(hand.Where(c => !runCards.Any(r => r.ID == c.ID)).ToArray(), 2);
|
||||
if (rp.Count >= run.Count)
|
||||
{
|
||||
SortByGameNum(remainingPairs);
|
||||
var attached = remainingPairs.Take(pairCount).SelectMany(p => p).ToList();
|
||||
runCards.AddRange(attached);
|
||||
SortByGameNum(rp);
|
||||
runCards.AddRange(rp.Take(run.Count).SelectMany(p => p));
|
||||
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);
|
||||
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));
|
||||
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 CardType1.SanDai2;
|
||||
}
|
||||
if (counts[0] == 2 && n % 2 == 0)
|
||||
{
|
||||
// 连对 vs 普通多张:检查是否连续对子
|
||||
return CardType1.LianDui;
|
||||
}
|
||||
return CardType1.ShunZi; // 顺子或其他 ≥3 张组合
|
||||
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 CardType1.SanDai2;
|
||||
if (counts[0] == 2 && n % 2 == 0) return CardType1.LianDui;
|
||||
return CardType1.ShunZi;
|
||||
}
|
||||
|
||||
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)
|
||||
};
|
||||
}
|
||||
=> 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;
|
||||
var r = new List<List<TCardInfoPdkF>>();
|
||||
foreach (var g in hand.GroupBy(c => c.GameNum).Where(g => g.Count() >= count))
|
||||
r.Add(g.Take(count).ToList());
|
||||
return r;
|
||||
}
|
||||
|
||||
/// <summary>按 GameNum 排序多牌组列表</summary>
|
||||
private static void SortByGameNum(List<List<TCardInfoPdkF>> groups)
|
||||
{
|
||||
groups.Sort((a, b) => a[0].GameNum.CompareTo(b[0].GameNum));
|
||||
}
|
||||
private static void SortByGameNum(List<List<TCardInfoPdkF>> g) => g.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);
|
||||
}
|
||||
}
|
||||
private static void AddConsecutiveGroups(List<List<TCardInfoPdkF>> groups, int minLen, int pos,
|
||||
List<(PlayOutCardPdkF, bool)> candidates, ref int mc)
|
||||
{ foreach (var run in GetConsecutiveRuns(groups, minLen)) AddCandidate(run.SelectMany(g => g).ToList(), pos, candidates, ref mc); }
|
||||
|
||||
/// <summary>从已排序的组中提取所有连续段(≥minLen 组)</summary>
|
||||
private static List<List<List<TCardInfoPdkF>>> GetConsecutiveRuns(
|
||||
List<List<TCardInfoPdkF>> groups, int minLen)
|
||||
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;
|
||||
|
||||
var cur = new List<List<TCardInfoPdkF>> { groups[0] };
|
||||
int prev = 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;
|
||||
int n = groups[i][0].GameNum;
|
||||
if (n == prev + 1) cur.Add(groups[i]);
|
||||
else { if (cur.Count >= minLen) runs.Add(new List<List<TCardInfoPdkF>>(cur)); cur.Clear(); cur.Add(groups[i]); }
|
||||
prev = n;
|
||||
}
|
||||
if (current.Count >= minLen)
|
||||
runs.Add(current);
|
||||
|
||||
if (cur.Count >= minLen) runs.Add(cur);
|
||||
return runs;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从合法方案中选最优:优先出多张牌的组合,其次出最大的
|
||||
/// P2修复: 之前只取 tips[0](最小单张),改为优先多牌组合
|
||||
/// P0修复: 选最大(最后一项),模拟更强对手
|
||||
/// </summary>
|
||||
private static List<TCardInfoPdkF> PickBestTip(List<List<TCardInfoPdkF>> tips, bool isNewRound)
|
||||
private static List<TCardInfoPdkF> PickBestTip(List<List<TCardInfoPdkF>> tips)
|
||||
{
|
||||
if (tips == null || tips.Count == 0) return null;
|
||||
|
||||
// 优先多牌组合(2张>1张,3张>2张等),同数量选最大的
|
||||
var best = tips[tips.Count - 1]; // 默认最大
|
||||
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];
|
||||
}
|
||||
int s = tips[i].Count * 10 + tips[i][0].GameNum;
|
||||
if (s > bestScore) { bestScore = s; 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;
|
||||
var d = new List<TCardInfoPdkF>(); int id = 1;
|
||||
foreach (int f in new[] { 1, 2, 3, 4 })
|
||||
for (int n = 1; n <= 13; n++)
|
||||
d.Add(new TCardInfoPdkF { ID = (byte)id++, Flower = (byte)f, GameNum = (byte)(n == 1 ? 14 : n == 2 ? 16 : n), GameState = 1 });
|
||||
return d;
|
||||
}
|
||||
|
||||
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 void Shuffle<T>(List<T> l) { 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 CloneCard(PlayOutCardPdkF src)
|
||||
{
|
||||
return new PlayOutCardPdkF
|
||||
{
|
||||
Pos = src.Pos, GameNum = src.GameNum,
|
||||
Type = src.Type, Ids = src.Ids?.ToArray()
|
||||
};
|
||||
}
|
||||
private static PlayOutCardPdkF CloneCard(PlayOutCardPdkF s)
|
||||
=> new PlayOutCardPdkF { Pos = s.Pos, GameNum = s.GameNum, Type = s.Type, Ids = s.Ids?.ToArray() };
|
||||
|
||||
private static PlayOutCardPdkF Pass(PdkBotView view)
|
||||
{
|
||||
return new PlayOutCardPdkF { Pos = (byte)view.MyPos, Type = CardType1.None };
|
||||
}
|
||||
private static PlayOutCardPdkF Pass(PdkBotView v)
|
||||
=> new PlayOutCardPdkF { Pos = (byte)v.MyPos, Type = CardType1.None };
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user