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:
4
.gitignore
vendored
4
.gitignore
vendored
@ -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
108
docs/stress-test-report.md
Normal 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.4s,1000 局 < 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** | 只出单张不组合 | 降低清牌效率 | 1h(rollout 多牌权重) |
|
||||
| **P3** | 性能优化(静默日志) | 压测速度翻倍 | 15min |
|
||||
Binary file not shown.
Binary file not shown.
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user