Files
hjha-server/PdkFriendServer/Logic/PdkBot.cs
xiaoou ec87d921a3 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启动)
2026-07-07 13:11:13 +08:00

195 lines
6.8 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>
/// 机器人可见的牌局信息(模拟真人视角——只看自己手牌+桌面公开信息)
/// </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];
}
}
}