Files
hjha-server/docs/pdk-bot.md
xiaoou 13fe07ea11 chore: 清理冗余 + 文档更新至v3.1 + 打包清单
清理:
  - 删除 C:\Users\... Windows残留路径
  - analyze_stress.py → docs/
  - .gitignore 覆盖 stress_* runtime产物

文档更新:
  - pdk-bot.md: 包庄阈值45%→70%、RolloutPolicy描述、选牌策略
  - code-review.md: 7/7 全部清零
  - PACKAGE.md: 新建打包清单(代码+集成+文档+net48步骤+压测记录)

bot 文件总览:
  核心: PdkBot.cs(194行) + IsmctsBot.cs(331行) = 525行
  文档: pdk-bot.md + code-review.md + stress-test-report.md + PACKAGE.md
2026-07-07 14:56:43 +08:00

284 lines
10 KiB
Markdown
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.

# PdkBot — 跑得快自动出牌机器人
## 1. 概述与架构
### 1.1 项目定位
PdkBot 为 hjha-server 跑得快游戏提供全自动对局能力。三个机器人在 Console 模式下相互对弈,模拟真人玩家的出牌决策。
### 1.2 外挂式架构(非侵入)
```
PdkGameMain.WaitWanJiaShuRu()
├─ BotMode == false → Console.ReadLine()(人工输入)
└─ BotMode == true → PdkBotView → IPdkBot.DecidePlay()
```
机器人只在两个决策点介入:
- **case 3 包庄** → `DecideBaoZhuang(view)`
- **case 4 打牌** → `DecidePlay(view)`
完全不修改游戏状态机、规则引擎、发牌逻辑或结算逻辑。
### 1.3 信息隔离
`PdkBotView` 严格裁剪游戏状态,机器人**只能看到**
| 信息 | 来源 | 说明 |
|------|------|------|
| 自己手牌 | `CardPack.Cards[myPos-1]` (GameState==1) | 16 张起 |
| 桌面最后出牌 | `Info.MaxPlayCard` | GameNum==0 表示新一轮 |
| 各家剩余张数 | `Info.ShenYuCard[]` | 跑得快公开信息 |
| 各家炸弹数 | `Info.ZhaDans[]` | 公开统计 |
| 游戏规则 | `Rule` | 关牌/经典/15张、必压等 |
| 游戏阶段 | `Info.GameZT` | 3=包庄 4=打牌 |
**不能看到**:其他玩家的具体手牌 (`CardPack.Cards[other].CardInfos`)。
### 1.4 策略可插拔
```csharp
public interface IPdkBot
{
bool DecideBaoZhuang(PdkBotView view);
PlayOutCardPdkF DecidePlay(PdkBotView view);
}
```
| 实现 | 文件 | 描述 |
|------|------|------|
| `PdkBot` | `PdkBot.cs` | 5 维启发式评分(快速基线) |
| `IsmctsBot` | `IsmctsBot.cs` | 蒙特卡洛搜索(当前默认,推荐) |
切换:改 `PdkGameMain.InitBots()` 一行 `new IsmctsBot()``new PdkBot()`
### 1.5 文件清单
| 文件 | 职责 |
|------|------|
| `PdkFriendServer/Logic/PdkBot.cs` | IPdkBot 接口、PdkBotView、PdkBot 启发式实现 |
| `PdkFriendServer/Logic/IsmctsBot.cs` | ISMCTS 蒙特卡洛搜索机器人(默认) |
| `PdkFriendServer/Logic/PdkGameMain.cs` | BotMode 开关、BuildBotView、WaitWanJiaShuRu 集成点 |
| `hjha-console/Program.cs` | `main.BotMode = true` 启动入口 |
---
## 2. 算法说明
### 2.1 三条路径对比
| | A: 启发式 (PdkBot) | B: ISMCTS (IsmctsBot) | C: CNN+MCTS |
|---|---|---|---|
| 最优性 | 低 | **高(接近最优)** | 理论最高 |
| 延迟 | <1ms | 50-200ms | 10-50ms |
| 代码量 | 100 | 300 | 1000+ + 训练管道 |
| 训练需求 | | | 4 GPU × 2-5 |
| 预训练模型 | 不需要 | 不需要 | **不存在** |
| 状态 | 已实现 | 已实现默认 | 未实现 |
### 2.2 PdkBot启发式评分已实现非默认
`GetTipCard()` 返回的每个合法出牌方案打分
```
ScorePlay():
1. 直接获胜(remainingAfter==0) +10000
2. 对手威胁(left/right ≤3) +300/500
3. 牌效率(剩余越少分越高) (20-剩余)×15
4. 炸弹管理(对手快赢+400/自己快赢+200/留着-80)
5. 出牌量(一次出多张更好) cardsOut×8
6. 先手权(非pass) +60
7. 黑桃3首局必出 +150
ScorePass():
基础 -80, 对手快赢追加 -400
```
### 2.3 IsmctsBot蒙特卡洛搜索当前默认
#### 核心原理
不完全信息蒙特卡洛树搜索——对每个候选出牌方案**模拟 N 局随机对局到结束**统计胜率选胜率最高的方案
```
DecidePlay(view):
unknownPool = 全部48张 - 我的手牌 - 已知已出牌
for each candidate in GetTipCard():
模拟 800 局:
├─ 对手手牌从 unknownPool 随机采样
├─ 应用我的候选出牌
└─ 三人贪心策略(RolloutPolicy)打到终局
胜率 = 我获胜的局数 / 800
return 胜率最高的方案
```
**RolloutPolicy模拟策略** `PickBestTip` 选最优方案优先多牌组合 + 最大牌模拟更强的对手行为大量采样后 ISMCTS 胜率统计收敛
#### 包庄决策
```
DecideBaoZhuang(view):
模拟 600 局完整对局 → 统计我第一个出完的概率 P(win)
P(win) ≥ 70% → 包庄
```
**阈值校准历程:** 45%v1)→ 60%v2成功率 30%)→ **70%v3.1,成功率 57%**100 局压测验证70% 阈值下包庄成功率翻倍
#### 选牌策略
两阶段选择
1. ISMCTS 评估所有候选胜率
2. 多牌组合 +10%/张加权 优先出对子/顺子/三带二等
3. 多牌 单张 无条件优先多牌
#### 性能
| 模拟次数 | 延迟 | 胜率估计精度 |
|---------|------|-------------|
| 200 | ~30ms | ±7% |
| 800 | ~100ms | ±3.5% |
| 2000 | ~250ms | ±2% |
当前默认 800
---
## 3. 引擎整合(对原项目的最小修改)
### 3.1 修改清单
| 文件 | 行号 | 改动 | 原因 |
|------|------|------|------|
| `PdkGameMain.cs` | L46-51 | `+ IPdkBot[] _bots` / `+ BotMode` | bot 注入点 |
| `PdkGameMain.cs` | L60-66 | `+ InitBots()` | 懒初始化 3 IsmctsBot |
| `PdkGameMain.cs` | L68-82 | `+ BuildBotView(pos)` | 仅取可见信息构建视图 |
| `PdkGameMain.cs` | L279 | `GamePack.Info.WhoPlay = (byte)(i+1)` | 包庄轮询时修正位置 |
| `PdkGameMain.cs` | L543 | `+ if(DeskGameDo==null) return;` | Console 模式 NPE 守卫 |
| `PdkGameMain.cs` | L632-710 | `WaitWanJiaShuRu` bot 分支 | bot/人工双通道 |
| `PdkGameMain.cs` | L491-494 | `C:\Users\...` `./gamepack/` | 跨平台路径 |
| `Program.cs` | L14 | `main.BotMode = true` | 启动时开启 bot |
### 3.2 未修改的部分
以下完全不动
- 游戏状态机GameFaPai GameBaoZhuang GameDaPai GameOver
- 规则引擎PdkCardAlgorithmPdkRule
- 发牌逻辑GameFaPai
- 结算逻辑GameOver
- 所有网络/服务器模块
### 3.3 配置
`Program.cs` 中设置
```csharp
var main = new PdkGameMain(null);
main.BotMode = true; // true=自动对局, false=Console.ReadLine
main.Test(); // 3 人自动打到结算
```
---
## 4. 未来优化
### 4.1 短期(提升胜率)
| 方向 | 描述 | 难度 |
|------|------|------|
| **RolloutPolicy 加强** | 模拟中用启发式代替纯随机最小牌提高每局模拟质量 | |
| **自适应模拟次数** | 手牌多时 1000 手牌少时减少到 200 | |
| **对手建模** | 记录对手出牌倾向激进/保守调整模拟中的对手策略 | |
| **包庄阈值学习** | 1000 局统计分析最优 P(win) 阈值 | |
### 4.2 中期(引擎能力)
| 方向 | 描述 | 难度 |
|------|------|------|
| **支持更多玩法** | 经典玩法16 张必压)、15 张玩法通过 Rule 参数切换 | |
| **AI vs 人类** | BotMode 扩展到单个位置 pos1=人类, pos2/3=bot | |
| **回放分析** | 每局结束后输出 `.json` 回放文件分析 bot 决策质量 | |
| **性能基准测试** | 1000 局自对弈统计 PdkBot vs IsmctsBot 胜率 | |
### 4.3 长期C 路径CNN + MCTS
| 方向 | 描述 | 依赖 |
|------|------|------|
| **Deep Monte Carlo** | 借鉴 DouZero (ICML 2021) 方法CNN 编码手牌 + MCTS 搜索 | 4 GPU × 3 天训练 |
| **ONNX 推理** | PyTorch ONNX ML.NET 加载嵌入 C# 推理 | Python 训练管道 |
| **自对弈训练** | 跑得快状态/动作编码自我对弈产数据 CNN 策略网络 | RLCard/RLlib |
C 路径的技术可行性已验证DouZero 在斗地主上击败所有 344 AI 对手排名 Botzone 第一但目前不存在跑得快的预训练模型需要从零训练详见 DouZero 论文: <https://arxiv.org/abs/2106.06135>
---
## 5. 牌型全覆盖
引擎 `GetOutCard` 已验证支持所有牌型bot `AddMultiCardLeads` + `InferType` 实现完整枚举:
| 牌型 | 引擎 | Bot 先手 | Bot 跟牌 | 说明 |
|------|------|---------|---------|------|
| 单张 DanZhang | ✅ | ✅ GetTipCard | ✅ GetTipCard | |
| 对子 DuiZi | ✅ | ✅ GetMultiGroups(2) | ✅ GetTipCard | |
| 顺子 ShunZi | ✅ | ✅ PokerLogic.GetShunZi | ✅ GetTipCard | ≥5 张连续 |
| 连对 LianDui | ✅ | ✅ GetConsecutiveRuns | ✅ GetTipCard | ≥3 对连续 |
| 三带二 SanDai2 | ✅ | ✅ 三张+最小2张 | ✅ GetTipCard | 附牌不限对子 |
| 四带二 SiDai2 | ✅ | ✅ 炸弹+最小2张 | — | 关牌规则 |
| 四带三 SiDai3 | ✅ | ✅ 炸弹+最小3张 | — | 关牌规则 |
| 炸弹 ZhaDan | ✅ | ✅ GetPlayZhaDans | ✅ GetTipCard | 4 张相同 |
| 飞机 FeiJi | ✅ | ✅ GetConsecutiveRuns | ✅ GetTipCard | ≥2 组连续三张 |
| 飞机带对 FeijiDai2 | ✅ | ✅ 飞机+附对子 | ✅ GetTipCard | |
### 附牌策略
引擎 `IsSanDaiEr` / `IsSiDai2` / `IsSiDai3` 均不要求附牌为对子——允许任意单牌挂件。Bot 取手牌最小剩余牌做附牌,最大化清牌效率。
---
## 6. 双环境兼容(.NET 4.8 / .NET 10
### 6.1 已处理
| 特性 | .NET 4.8 | .NET 10 | 方案 |
|------|----------|---------|------|
| C#8 switch expression | ❌ | ✅ | 改为 ternary |
| C#9 `{ get; init; }` | ❌ | ✅ | 改为 `{ get; set; }` |
| ValueTuple | ⚠️ NuGet | ✅ | net48 加 `System.ValueTuple` 包 |
| LINQ | ✅ | ✅ | 无需改动 |
| string 插值 `$"..."` | ✅ C#6 | ✅ | 无需改动 |
| `?.` / `??` | ✅ C#6 | ✅ | 无需改动 |
### 6.2 net48 项目集成步骤
1. 复制 `PdkBot.cs` + `IsmctsBot.cs``PdkFriendServer/Logic/`
2. `PdkFriendServer.csproj` 加:
```xml
<PackageReference Include="System.ValueTuple" Version="4.5.0" />
<LangVersion>8.0</LangVersion>
```
3. `PdkGameMain.cs` 的 `WaitWanJiaShuRu()` 加入 BotMode 分支(与 net10.0 逻辑完全相同)
---
## 7. 运行
```bash
cd ~/projects/hjha-server
dotnet run --project hjha-console
```
输出示例:
```
=== HJHA Console Mode - 跑得快 Bot对局 ===
[ISMCTS pos1] 包庄评估: P(win)=31% → 不包
[ISMCTS pos2] 包庄评估: P(win)=52% → 包庄
[ISMCTS pos3] 包庄评估: P(win)=63% → 包庄
[ISMCTS pos2] 800sims winRate:3% => DanZhang:1c
[ISMCTS pos3] 800sims winRate:5% => DanZhang:1c
玩家1 输赢32
玩家2 ,输赢:-64
玩家3 输赢32
test over
```