Code Review 发现 (docs/code-review.md): 🔴 InferType 三元两边相同(无意义) → 重写,加 LianDui/FeiJi 分支 🔴 PdkBotView { get; init; } → { get; set; } (.NET 4.8 兼容) 🔴 PdkBot.DecideBaoZhuang ≤10永远false → bigCards≥6 文档更新 (docs/pdk-bot.md): + 5. 牌型全覆盖表(引擎+Bot枚举覆盖10种牌型) + 6. 双环境兼容指南(net48/n10 差异+集成步骤) + 附牌策略说明(引擎允许任意牌做挂件)
196 lines
6.8 KiB
C#
196 lines
6.8 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using System.Linq;
|
||
using GameFix.PaoDeKuaiF;
|
||
using GameFix.Poker;
|
||
using GameMessage;
|
||
using GameMessage.PaoDeKuaiF;
|
||
|
||
namespace PdkFriendServer.Logic
|
||
{
|
||
/// <summary>
|
||
/// 机器人可见的牌局信息(模拟真人视角——只看自己手牌+桌面公开信息)
|
||
/// </summary>
|
||
public class PdkBotView
|
||
{
|
||
/// <summary>我的手牌(仅本轮可出的牌,GameState==1)</summary>
|
||
public TCardInfoPdkF[] MyHand { get; set; }
|
||
|
||
/// <summary>桌上最后一手牌(谁打的什么),GameNum==0表示桌上无牌/新一轮</summary>
|
||
public PlayOutCardPdkF MaxPlayCard { get; set; }
|
||
|
||
/// <summary>当前轮到谁出牌(1-indexed)</summary>
|
||
public int WhoPlay { get; set; }
|
||
|
||
/// <summary>我在第几个位置(1-indexed)</summary>
|
||
public int MyPos { get; set; }
|
||
|
||
/// <summary>各玩家剩余牌数 [pos0,pos1,pos2]</summary>
|
||
public byte[] ShenYuCard { get; set; }
|
||
|
||
/// <summary>各玩家已出炸弹数</summary>
|
||
public int[] ZhaDans { get; set; }
|
||
|
||
/// <summary>游戏规则</summary>
|
||
public PdkRule Rule { get; set; }
|
||
|
||
/// <summary>当前游戏阶段 3=包庄 4=打牌</summary>
|
||
public int GameZT { get; set; }
|
||
}
|
||
|
||
/// <summary>
|
||
/// 机器人决策接口——外挂接入点
|
||
/// </summary>
|
||
public interface IPdkBot
|
||
{
|
||
/// <summary>包庄决策</summary>
|
||
bool DecideBaoZhuang(PdkBotView view);
|
||
|
||
/// <summary>打牌决策:返回要出的牌(空=不要)</summary>
|
||
PlayOutCardPdkF DecidePlay(PdkBotView view);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 跑得快最优出牌机器人 — 有限信息策略
|
||
/// 只能看到自己手牌+桌面公开信息(最后出牌、各家剩余张数)
|
||
/// </summary>
|
||
public class PdkBot : IPdkBot
|
||
{
|
||
public bool DecideBaoZhuang(PdkBotView view)
|
||
{
|
||
// 启发式:手牌中 ≥J 的大牌多则包庄(速算,不如 ISMCTS 精确)
|
||
int bigCards = view.MyHand.Count(c => c.GameNum >= 11);
|
||
return bigCards >= 6;
|
||
}
|
||
|
||
public PlayOutCardPdkF DecidePlay(PdkBotView view)
|
||
{
|
||
var hand = view.MyHand;
|
||
bool isFirstPlay = view.MaxPlayCard.GameNum <= 0;
|
||
|
||
// 获取合法出牌方案
|
||
var maxHand = isFirstPlay ? null
|
||
: GetMaxPlayerHand(view);
|
||
var tips = PdkCardAlgorithm.GetTipCard(
|
||
view.MaxPlayCard,
|
||
hand,
|
||
maxHand, // null = 不限上家手牌(因为我们不知道)
|
||
view.Rule.AAAIsZhaDan);
|
||
|
||
var candidates = new List<(PlayOutCardPdkF play, int score)>();
|
||
|
||
// 不要(不是先出时可选)
|
||
if (!isFirstPlay)
|
||
candidates.Add((Pass(view), ScorePass(view)));
|
||
|
||
if (tips != null)
|
||
{
|
||
foreach (var tip in tips)
|
||
{
|
||
var play = MakePlay(tip, view.MyPos);
|
||
candidates.Add((play, ScorePlay(play, hand.Length - tip.Count, view)));
|
||
}
|
||
}
|
||
|
||
if (candidates.Count == 0)
|
||
return Pass(view);
|
||
|
||
candidates.Sort((a, b) => b.score.CompareTo(a.score));
|
||
var best = candidates[0];
|
||
var desc = best.play.Type == CardType1.None ? "pass" : $"{best.play.Type}:{best.play.Ids?.Length ?? 0}cards";
|
||
System.Console.WriteLine($"[Bot pos{view.MyPos}] hand:{hand.Length} L:{GetLeftCount(view)} R:{GetRightCount(view)} => {desc}");
|
||
return best.play;
|
||
}
|
||
|
||
// ---- 辅助方法 ----
|
||
|
||
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 = cards.Count == 1 ? CardType1.DanZhang
|
||
: cards.Count == 2 ? CardType1.DuiZi
|
||
: cards.Count == 4 && cards.All(c => c.GameNum == cards[0].GameNum) ? CardType1.ZhaDan
|
||
: CardType1.DanZhang // 精确牌型由CheckPack/GetOutCard修正
|
||
};
|
||
}
|
||
|
||
private static PlayOutCardPdkF Pass(PdkBotView view)
|
||
{
|
||
return new PlayOutCardPdkF { Pos = (byte)view.MyPos, Type = CardType1.None };
|
||
}
|
||
|
||
private static TCardInfoPdkF[] GetMaxPlayerHand(PdkBotView view)
|
||
{
|
||
// 我们不知道上家具体手牌,返回null让GetTipCard忽略maxHand限制
|
||
return null;
|
||
}
|
||
|
||
// ---- 评分引擎 ----
|
||
|
||
private int ScorePlay(PlayOutCardPdkF play, int remainingAfter, PdkBotView view)
|
||
{
|
||
int score = 0;
|
||
int leftCnt = GetLeftCount(view);
|
||
int rightCnt = GetRightCount(view);
|
||
|
||
// 1. 直接赢
|
||
if (remainingAfter == 0) return 10000;
|
||
|
||
// 2. 对手威胁
|
||
if (leftCnt <= 3 || rightCnt <= 3) score += 300;
|
||
if (leftCnt <= 2 || rightCnt <= 2) score += 500;
|
||
|
||
// 3. 牌效率:剩余越少越好
|
||
score += (20 - remainingAfter) * 15;
|
||
|
||
// 4. 炸弹:留到关键时刻
|
||
bool isBomb = play.Type == CardType1.ZhaDan ||
|
||
(play.Ids != null && play.Ids.Length == 4 &&
|
||
view.MyHand.Count(x => x.GameNum == play.GameNum && x.GameState == 1) == 4);
|
||
if (isBomb)
|
||
{
|
||
if (leftCnt <= 5 || rightCnt <= 5) score += 400; // 对手快赢了
|
||
else if (remainingAfter <= 4) score += 200; // 自己快赢了
|
||
else score -= 80; // 先留着
|
||
}
|
||
|
||
// 5. 出更多牌更好
|
||
int cardsOut = view.MyHand.Length - remainingAfter;
|
||
score += cardsOut * 8;
|
||
|
||
// 6. 保持先手权
|
||
if (play.Type != CardType1.None) score += 60;
|
||
|
||
// 7. 黑桃3首局必出
|
||
if (view.Rule.HeiTao3BiChu && view.MaxPlayCard.GameNum <= 0)
|
||
if (play.Ids != null && play.Ids.Any(x => x == 3)) score += 150;
|
||
|
||
return score;
|
||
}
|
||
|
||
private int ScorePass(PdkBotView view)
|
||
{
|
||
int score = -80;
|
||
// 对手快赢时不要很危险
|
||
if (GetLeftCount(view) <= 3 || GetRightCount(view) <= 3) score -= 400;
|
||
return score;
|
||
}
|
||
|
||
private int GetLeftCount(PdkBotView view)
|
||
{
|
||
int next = view.MyPos % view.ShenYuCard.Length;
|
||
return view.ShenYuCard[next];
|
||
}
|
||
|
||
private int GetRightCount(PdkBotView view)
|
||
{
|
||
int prev = (view.MyPos - 2 + view.ShenYuCard.Length) % view.ShenYuCard.Length;
|
||
return view.ShenYuCard[prev];
|
||
}
|
||
}
|
||
}
|