Files
hjha-server/hjha-console/Program.cs
xiaoou 7566fe99a1 feat(PX): 自训练v1.5 P0准备 — NHN模式 + 总分法胜率解析 + 评估管线
CB-01: Program.cs 加 --nhn 模式 [NEURAL,ISMCTS,NEURAL]
- 3人跑得快 N插花坐排列
- Rotations 数组补全为4种排列 (IIN/NII/INI/NHN)
- --nhn 参数: 不生成CSV, 直接评估

CB-02: 胜率解析统一总分法
- TrainingCollector 加 ScoreAccumulator/BotWins/BotGames 静态累加器
- GameOver 无条件调用 AccumulateScore (eval + 训练模式均记录)
- stress_summary.txt 输出 #BOT_SCORES: bot=wins/games,wr=XX%,score=YYY
- orchestrator eval 解析改用 #BOT_SCORES 格式, 正则回退逻辑移除

管线: orchestrator eval→stress_summary→总分法解析, 闭环可验证
2026-07-20 15:40:59 +08:00

163 lines
8.2 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 PdkFriendServer.Logic;
using System;
using System.Diagnostics;
using System.IO;
namespace hjha_console
{
class Program
{
public static int RotateInterval = 0; // 0=disabled, N=rotate every N rounds
private static readonly string[][] Rotations = new[] { // 4种排列覆盖所有 NHN/NNH/HNN 组合
new[] { "ISMCTS", "ISMCTS", "NEURAL" },
new[] { "NEURAL", "ISMCTS", "ISMCTS" },
new[] { "ISMCTS", "NEURAL", "ISMCTS" },
new[] { "NEURAL", "ISMCTS", "NEURAL" } // NHN: N插花坐
};
static void Main(string[] args)
{
if (args.Length > 0 && args[0] == "--eval") { PdkGameMain.BotTypes = new[] { "ISMCTS", "ISMCTS", "NEURAL" }; TrainingCollector.Enabled = false; args = new[] { "--stress", args.Length > 1 ? args[1] : "10" }; }
if (args.Length > 0 && args[0] == "--mix") { PdkGameMain.BotTypes = new[] { "ISMCTS", "ISMCTS", "NEURAL" }; TrainingCollector.Enabled = true; args = new[] { "--stress", args.Length > 1 ? args[1] : "10" }; }
if (args.Length > 0 && args[0] == "--mix-rotate") { PdkGameMain.BotTypes = new[] { "ISMCTS", "ISMCTS", "NEURAL" }; TrainingCollector.Enabled = true; Program.RotateInterval = 10; args = new[] { "--stress", args.Length > 1 ? args[1] : "10" }; }
if (args.Length > 0 && args[0] == "--mix-ismcts") { PdkGameMain.BotTypes = new[] { "ISMCTS", "ISMCTS", "ISMCTS" }; TrainingCollector.Enabled = true; args = new[] { "--stress", args.Length > 1 ? args[1] : "10" }; }
if (args.Length > 0 && args[0] == "--bots") { PdkGameMain.BotTypes = args[1].Split(','); TrainingCollector.Enabled = true; args = args.Length > 2 ? new[] { "--stress", args[2] } : new[] { "--stress", "10" }; }
if (args.Length > 0 && args[0] == "--n2h2v5") { PdkGameMain.BotTypes = new[] { "ISMCTS", "ISMCTS", "NEURAL_V5" }; TrainingCollector.Enabled = false; args = new[] { "--stress", args.Length > 1 ? args[1] : "10" }; }
if (args.Length > 0 && args[0] == "--nhn") { PdkGameMain.BotTypes = new[] { "NEURAL", "ISMCTS", "NEURAL" }; TrainingCollector.Enabled = false; args = new[] { "--stress", args.Length > 1 ? args[1] : "10" }; }
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张/人 | 策略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] : "");
}
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");
// 清空总分累加器
TrainingCollector.ResetAccumulator();
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++)
{
// 位置轮换: 每N局切换排列
if (RotateInterval > 0 && (r - 1) % RotateInterval == 0)
{
int ri = ((r - 1) / RotateInterval) % Rotations.Length;
PdkGameMain.BotTypes = Rotations[ri];
}
var roundSw = Stopwatch.StartNew();
try
{
var main = new PdkGameMain(null);
main.BotMode = true;
// 本轮日志路径
var logPath = $"{logDir}/round_{r:D5}.txt";
if (File.Exists("output.txt")) File.Delete("output.txt");
// 重定向 Console stdout → 本轮 ISMCTS 日志
var ismctsLog = $"{logDir}/round_{r:D5}_ismcts.txt";
var oldOut = Console.Out;
var ismctsWriter = new StreamWriter(ismctsLog) { AutoFlush = true };
Console.SetOut(ismctsWriter);
main.Test("");
Console.SetOut(oldOut); // 先恢复再Close (macOS兼容)
ismctsWriter.Close();
// 合并 output.txt + ISMCTS 日志 → 本轮日志
using (var combined = new StreamWriter(logPath, false))
{
if (File.Exists(ismctsLog))
{
combined.Write(File.ReadAllText(ismctsLog));
combined.WriteLine();
File.Delete(ismctsLog);
}
if (File.Exists("output.txt"))
{
combined.Write(File.ReadAllText("output.txt"));
}
}
if (File.Exists("output.txt")) File.Delete("output.txt");
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");
var sb = new System.Text.StringBuilder();
sb.AppendLine($"Rounds: {completed}/{totalRounds}");
sb.AppendLine($"Errors: {errors}");
sb.AppendLine($"Total: {totalSw.Elapsed.TotalMinutes:F1}min");
sb.AppendLine($"Avg: {totalSw.Elapsed.TotalSeconds / totalRounds:F1}s/round");
// Bot 得分统计(总分法:累积 Score 总和 + 胜局数)
sb.AppendLine("#BOT_SCORES");
foreach (var t in TrainingCollector.ScoreAccumulator.Keys)
{
int wins = TrainingCollector.BotWins.ContainsKey(t) ? TrainingCollector.BotWins[t] : 0;
int games = TrainingCollector.BotGames.ContainsKey(t) ? TrainingCollector.BotGames[t] : 0;
float score = TrainingCollector.ScoreAccumulator[t];
float wr = games > 0 ? (float)wins / games * 100f : 0f;
sb.AppendLine($"{t}={wins}/{games},wr={wr:F1}%,score={score:F0}");
}
File.WriteAllText("stress_summary.txt", sb.ToString());
}
}
}