feat: PdkBot — 外挂式自动出牌机器人

架构:
- IPdkBot 接口:外挂接入点,与核心逻辑解耦
- PdkBotView:只传自己手牌+公开信息(MaxPlayCard/ShenYuCard/Rule)
  不暴露其他玩家手牌——模拟真人视角
- PdkBot:score-based 最优策略
  评分维度:直接获胜>>对手威胁>>牌效率>>炸弹保留>>先手权>>黑桃3

对接:
- PdkGameMain.BotMode=true 启用
- WaitWanJiaShuRu中BuildBotView→bot.DecidePlay/DecideBaoZhuang
- GameBaoZhuang解除WhoPlay=GetNextPos注释以支持逐人轮询
- SetFalseOperation加DeskGameDo null守卫

文件:
- PdkFriendServer/Logic/PdkBot.cs(核心机器人)
- PdkFriendServer/Logic/PdkGameMain.cs(BotMode+对接)
- hjha-console/Program.cs(main.BotMode=true启动)
This commit is contained in:
2026-07-07 13:11:13 +08:00
parent 418b5db27f
commit ec87d921a3
11 changed files with 621 additions and 118 deletions

View File

@ -0,0 +1,194 @@
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; init; }
/// <summary>桌上最后一手牌谁打的什么GameNum==0表示桌上无牌/新一轮</summary>
public PlayOutCardPdkF MaxPlayCard { get; init; }
/// <summary>当前轮到谁出牌1-indexed</summary>
public int WhoPlay { get; init; }
/// <summary>我在第几个位置1-indexed</summary>
public int MyPos { get; init; }
/// <summary>各玩家剩余牌数 [pos0,pos1,pos2]</summary>
public byte[] ShenYuCard { get; init; }
/// <summary>各玩家已出炸弹数</summary>
public int[] ZhaDans { get; init; }
/// <summary>游戏规则</summary>
public PdkRule Rule { get; init; }
/// <summary>当前游戏阶段 3=包庄 4=打牌</summary>
public int GameZT { get; init; }
}
/// <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)
{
// 简单策略手牌好的话包庄手牌≤10张说明大牌多
return view.MyHand.Length <= 10;
}
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];
}
}
}

View File

@ -40,12 +40,47 @@ namespace PdkFriendServer.Logic
/// </summary>
public PdkGameDo DeskGameDo = null;
/// <summary>
/// 机器人玩家 [pos0,pos1,pos2]null=人工
/// </summary>
private IPdkBot[] _bots = null;
/// <summary>
/// 启用 Bot 自动对局
/// </summary>
public bool BotMode { get; set; }
public PdkGameMain(PdkGameDo _gamedo)
{
this.DeskGameDo = _gamedo;
this.GamePack = new PdkGamePack();
}
/// <summary>懒初始化3个botBotMode==true时首次WaitWanJiaShuRu调用</summary>
private void InitBots()
{
_bots = new IPdkBot[3];
for (int i = 0; i < 3; i++)
_bots[i] = new PdkBot();
}
/// <summary>构建当前玩家的可见视图——只看自己手牌+公开信息</summary>
private PdkBotView BuildBotView(int pos)
{
return new PdkBotView
{
MyHand = GamePack.CardPack.Cards[pos - 1].CardInfos
.Where(x => x.GameState == 1).ToArray(),
MaxPlayCard = GamePack.Info.MaxPlayCard,
WhoPlay = pos,
MyPos = pos,
ShenYuCard = GamePack.Info.ShenYuCard,
ZhaDans = GamePack.Info.ZhaDans,
Rule = this.Rule,
GameZT = GamePack.Info.GameZT
};
}
/// <summary>
/// 游戏开始
/// </summary>
@ -241,7 +276,7 @@ namespace PdkFriendServer.Logic
{
if (GamePack.Info.BaoZhuang[i] == 0)
{
// GamePack.Info.WhoPlay = GetNextPos(GamePack.Info.WhoPlay);
GamePack.Info.WhoPlay = (byte)(i + 1);
SendPack();
return; //还有玩家没有操作,不处理流程
}
@ -505,6 +540,7 @@ namespace PdkFriendServer.Logic
void SetFalseOperation()
{
if (DeskGameDo == null) return;
for (int i = 0; i < GamePack.Info.PlayNum; i++)
{
this.SetWaitOperation(i, false);
@ -611,75 +647,79 @@ namespace PdkFriendServer.Logic
void WaitWanJiaShuRu()
{
if (DeskGameDo != null) return;
//正式连客户端以后要屏蔽这些代码。return
string str = "";
byte num = 0;
int pos = GamePack.Info.WhoPlay;
switch (GamePack.Info.GameZT)
{
case 3://包庄流程只能输入1和2
Log("包庄操作 pos:" + GamePack.Info.WhoPlay);
case 3://包庄流程
{
// Bot 接管:懒初始化
if (BotMode && _bots == null)
InitBots();
if (_bots != null && _bots[pos - 1] != null)
{
var view = BuildBotView(pos);
var bao = _bots[pos - 1].DecideBaoZhuang(view);
GamePack.Info.BaoZhuang[pos - 1] = bao ? (byte)1 : (byte)2;
Log($"bot-包庄: pos{pos}={(bao ? "" : "")}");
return;
}
// 人工输入
Log("包庄操作 pos:" + pos);
while (true)
{
str = Console.ReadLine();
string str = Console.ReadLine();
if (string.IsNullOrWhiteSpace(str)) continue;
Log(str);
var ss = str.Split(',');
if (ss.Length < 2) continue;
byte.TryParse(ss[0], out var pos);
byte.TryParse(ss[1], out num);
GamePack.Info.WhoPlay = pos;
GamePack.Info.BaoZhuang[pos - 1] = num;
if (CheckPack(GamePack.Info))
{
break; //如果包输入正确就跳走
}
else
{
Log("检测不合格,输入非法值");
}
byte.TryParse(ss[0], out var ipos);
byte.TryParse(ss[1], out byte num);
GamePack.Info.WhoPlay = ipos;
GamePack.Info.BaoZhuang[ipos - 1] = num;
if (CheckPack(GamePack.Info)) break;
Log("检测不合格,输入非法值");
}
break;
}
case 4: //打牌流程
Log("打牌操作 pos:" + GamePack.Info.WhoPlay);
{
// Bot 接管:懒初始化
if (BotMode && _bots == null)
InitBots();
if (_bots != null && _bots[pos - 1] != null)
{
var view = BuildBotView(pos);
var botPlay = _bots[pos - 1].DecidePlay(view);
GamePack.Info.ActivePlayCard = botPlay;
return;
}
// 人工输入
Log("打牌操作 pos:" + pos);
while (true)
{
str = Console.ReadLine(); //逗号分隔
string str = Console.ReadLine();
if (string.IsNullOrWhiteSpace(str)) continue;
var ss = str.Split(',');
if (ss.Length <= 0) continue;
Log(str);
var selectCards = GamePack.CardPack.Cards[GamePack.Info.WhoPlay - 1].CardInfos.Where(x => ss.Any(s => s == x.ID.ToString())).ToArray();
var selectCards = GamePack.CardPack.Cards[pos - 1].CardInfos
.Where(x => ss.Any(s => s == x.ID.ToString())).ToArray();
PlayOutCardPdkF payCard;
if (selectCards == null || selectCards.Length <= 0) //不要
if (selectCards == null || selectCards.Length <= 0)
{
payCard = new PlayOutCardPdkF();
payCard.Pos = GamePack.Info.WhoPlay;
payCard.Type = CardType1.None;
payCard = new PlayOutCardPdkF { Pos = (byte)pos, Type = CardType1.None };
GamePack.Info.ActivePlayCard = payCard;
if (CheckPack(GamePack.Info))
{
break; //如果包输入正确就跳走
}
else
{
Log("检测不合格,输入非法值");
}
if (CheckPack(GamePack.Info)) break;
Log("检测不合格,输入非法值");
}
else if (PdkCardAlgorithm.GetOutCard(selectCards,
GamePack.CardPack.Cards[GamePack.Info.WhoPlay - 1].CardInfos.Where(x => x.GameState == 1).ToArray(),
GamePack.CardPack.Cards[pos - 1].CardInfos.Where(x => x.GameState == 1).ToArray(),
this.Rule, out payCard))
{
payCard.Pos = GamePack.Info.WhoPlay;
payCard.Pos = (byte)pos;
GamePack.Info.ActivePlayCard = payCard;
if (CheckPack(GamePack.Info))
{
break; //如果包输入正确就跳走
}
else
{
Log("检测不合格,输入非法值");
}
if (CheckPack(GamePack.Info)) break;
Log("检测不合格,输入非法值");
}
else
{
@ -687,10 +727,7 @@ namespace PdkFriendServer.Logic
}
}
break;
case 6:
//GameOver();
break;
}
}
}