Files
xiaoou ce2a4c98ef review: code review + 3 critical fixes + 文档更新
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 差异+集成步骤)
  + 附牌策略说明(引擎允许任意牌做挂件)
2026-07-07 14:39:50 +08:00

196 lines
6.8 KiB
C#
Raw Permalink 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; 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];
}
}
}