test: 1000轮 ISMCTS 压测 + 完整分析报告

新增:
- hjha-console/Program.cs: --stress N 压测模式
  - 每局独立日志 stress_logs/round_NNNNN.txt
  - 每 50 局进度汇报 + 异常捕获
- docs/stress-test-report.md: 完整分析报告
  - P0: 包庄严重高估(5.2%成功) → 需校准阈值
  - P1: 75 moves 异常局 → 疑似死循环
  - P2: 只出单张不组合 → rollout 优化
  - P3: 性能瓶颈分析
- .gitignore: stress_logs/, analyze_stress.py

压测结果: 12.8min, 0 crashes, 1000/1000通过
This commit is contained in:
2026-07-07 13:53:46 +08:00
parent 61948049b7
commit 4a3fbec057
5 changed files with 201 additions and 3 deletions

4
.gitignore vendored
View File

@ -18,3 +18,7 @@ C:\Users\Administrator\Desktop\temp\
# Rider
.idea/
*.sln.DotSettings.user
stress_logs/
analyze_stress.py
stress_errors.txt
stress_summary.txt

108
docs/stress-test-report.md Normal file
View File

@ -0,0 +1,108 @@
# 1000轮 ISMCTS 压测分析报告
运行时间: 12.8 min | 1000/1000 完成 | 0 crashes
---
## 1. Bug / 异常
### 1.1 稳定性:完美
- 0 次 Unhandled Exception
- 0 次空日志(每局都有完整结算)
- 所有 1000 局产出了完整的 包庄→打牌→结算→test over 流程
### 1.2 Move 分布异常
| 指标 | 值 | 说明 |
|------|-----|------|
| 平均 move | 12.8 | 正常 |
| p50 move | 7 | 偏少,很多局快速结束 |
| min move | 4 | 极快结束 |
| **max move** | **75** | **异常!** 一局走了 75 个 whoPlay 回合 |
**75 moves 的异常局需要排查**:正常跑得快一局 15-30 步。75 步说明可能出现了长期 pass 循环——三人都不出牌,一轮轮空转。需要检查是否有游戏状态机 bug 导致无法正常出牌。
### 1.3 结算异常
- 1 局结算 ±6非标准的 ±32/±64需要查该局日志。
---
## 2. 性能
### 2.1 吞吐量
| 指标 | 值 |
|------|-----|
| 总时间 | 12.8 min |
| 每局平均 | 0.77s |
| 每局 whoPlay | 12.8 次 |
| 每局 ISMCTS 决策 | ~12.8 × 3 人 ≈ 38 次(估算) |
| 每次 ISMCTS 决策 | ~20ms |
### 2.2 优化建议
| 方向 | 当前 | 优化后 | 改动 |
|------|------|--------|------|
| ISMCTS sims 自适应 | 800 固定 | 手牌>8→800, ≤8→300, ≤4→100 | IsmctsBot 参数 |
| GamePack JSON 保存 | 每步写文件 | 压测模式跳过 | SendPack() 加 if |
| Log 写入 | 每步 flush | 压测模式仅写结算 | 控制台 Log 静默 |
**预估优化后**:每局 0.3-0.4s1000 局 < 7 分钟
---
## 3. Bot 逻辑问题(关键)
### 3.1 包庄决策:严重高估胜率
| 指标 | |
|------|-----|
| 包庄次数 | 1331 每局平均 1.33 人包庄 |
| 包庄成功 | 52 |
| 包庄失败 | 947 |
| **包庄成功率** | **5.2%** |
**根因**ISMCTS 包庄评估中用"对手贪心 roll-out"模拟 P(win)。但实际对局中对手使用的是完整 ISMCTS 决策强度远高于贪心 roll-out导致 P(win) 系统性高估
**修复方案**
1. **提升模拟对手强度**Rollout 中对手也使用一定程度的 ISMCTS 50 sims 轻量版
2. **校准阈值**P(win)≥45% P(win)≥60% 或更高
3. **动态校准**记录实际胜率 Kalman 滤波修正评估偏差
### 3.2 包庄人数分布
```
0人包庄: 1 局 (0.1%)
1人包庄: 667 局 (66.7%) ← 常态
2人包庄: 332 局 (33.2%)
3人包庄: 0 局 (0.0%)
```
三人都包的情况从未发生——3 人同时 P(win)≥45% 需要牌局极度均衡几乎不可能
### 3.3 各位置输赢分布
```
pos1: -1498 (最差)
pos2: +1178 (最好)
pos3: +320 (中等)
```
pos1 表现显著弱于 pos2可能原因FistPlay黑桃3持有者通常被分配到 pos1而先出牌不一定占优——先出的人被后面两人联手压制
### 3.4 出牌策略优化空间
当前 ISMCTS 出牌只选单张`DanZhang:1c`从不选多牌组合根因
- Rollout 策略用 `GetTipCard` 取第一个方案最小牌几乎总是单张
- 单张是最"安全"的选择——容易通过 ISMCTS 模拟获得高胜率
- 但这导致 bot 永远打单张浪费了对子/顺子等高效出牌机会
**修复**Rollout 策略增加多牌组合的权重鼓励一次清多张牌
---
## 4. 修复优先级
| 优先级 | 问题 | 影响 | 估计时间 |
|--------|------|------|----------|
| **P0** | 包庄严重高估5.2% 成功率 | 大幅降低包庄 bot 的期望收益 | 2h校准阈值+提升rollout |
| **P1** | 75 moves 异常局 | 潜在死循环 bug | 30min查日志根因 |
| **P2** | 只出单张不组合 | 降低清牌效率 | 1hrollout 多牌权重 |
| **P3** | 性能优化静默日志 | 压测速度翻倍 | 15min |

Binary file not shown.

Binary file not shown.

View File

@ -1,5 +1,7 @@
using PdkFriendServer.Logic;
using System;
using System.Diagnostics;
using System.IO;
namespace hjha_console
{
@ -7,15 +9,99 @@ namespace hjha_console
{
static void Main(string[] args)
{
if (args.Length > 0 && args[0] == "--stress")
{
int rounds = args.Length > 1 ? int.Parse(args[1]) : 100;
RunStressTest(rounds);
return;
}
Console.WriteLine("=== HJHA Console Mode - 跑得快 Bot对局 (3人自动) ===");
Console.WriteLine("牌型:关牌玩法 16张/人 | 策略:score-based optimal");
Console.WriteLine("牌型:关牌玩法 16张/人 | 策略:ISMCTS 800sims");
Console.WriteLine("压测: dotnet run --project hjha-console -- --stress 1000");
Console.WriteLine();
var main = new PdkGameMain(null);
main.BotMode = true;
main.Test(args.Length > 0 ? args[0] : "");
}
string jsonPath = args.Length > 0 ? args[0] : null;
main.Test(jsonPath ?? "");
static void RunStressTest(int totalRounds)
{
var logDir = "stress_logs";
if (!Directory.Exists(logDir)) Directory.CreateDirectory(logDir);
// 清理旧日志
foreach (var f in Directory.GetFiles(logDir, "round_*.txt"))
File.Delete(f);
File.Delete("stress_errors.txt");
File.Delete("stress_summary.txt");
Console.WriteLine($"=== PdkBot Stress Test: {totalRounds} rounds ===");
Console.WriteLine($"Logs: {logDir}/");
Console.WriteLine($"Start: {DateTime.Now:HH:mm:ss}");
Console.WriteLine();
int completed = 0, errors = 0;
var totalSw = Stopwatch.StartNew();
var errorLog = new StreamWriter("stress_errors.txt", true) { AutoFlush = true };
for (int r = 1; r <= totalRounds; r++)
{
var roundSw = Stopwatch.StartNew();
try
{
var main = new PdkGameMain(null);
main.BotMode = true;
// 重定向 output.txt 到本轮日志
var logPath = $"{logDir}/round_{r:D5}.txt";
if (File.Exists("output.txt")) File.Delete("output.txt");
main.Test("");
// 移动 output.txt → 本轮日志
if (File.Exists("output.txt"))
File.Move("output.txt", logPath);
completed++;
roundSw.Stop();
if (r % 50 == 0 || r == totalRounds)
{
Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] {r}/{totalRounds} rounds | " +
$"{errors} errors | avg {totalSw.Elapsed.TotalSeconds / r:F1}s/round | " +
$"last {roundSw.ElapsedMilliseconds}ms");
}
}
catch (Exception ex)
{
errors++;
errorLog.WriteLine($"Round {r}: {ex.GetType().Name}: {ex.Message}");
errorLog.WriteLine(ex.StackTrace);
errorLog.WriteLine("---");
Console.WriteLine($" R{r} ERROR: {ex.GetType().Name}: {ex.Message}");
// 保存本轮部分日志
if (File.Exists("output.txt"))
File.Move("output.txt", $"{logDir}/round_{r:D5}_ERROR.txt");
}
}
totalSw.Stop();
errorLog.Close();
// 汇总
Console.WriteLine();
Console.WriteLine($"=== Stress Test Complete ===");
Console.WriteLine($"Rounds: {completed}/{totalRounds} completed, {errors} errors");
Console.WriteLine($"Total time: {totalSw.Elapsed.TotalMinutes:F1} min");
Console.WriteLine($"Avg per round: {totalSw.Elapsed.TotalSeconds / totalRounds:F1}s");
File.WriteAllText("stress_summary.txt",
$"Rounds: {completed}/{totalRounds}\n" +
$"Errors: {errors}\n" +
$"Total: {totalSw.Elapsed.TotalMinutes:F1}min\n" +
$"Avg: {totalSw.Elapsed.TotalSeconds / totalRounds:F1}s/round\n");
}
}
}