From 7566fe99a1e5d99ef46d4d8c1a2f06197b9dc3d8 Mon Sep 17 00:00:00 2001 From: xiaoou Date: Mon, 20 Jul 2026 15:40:59 +0800 Subject: [PATCH] =?UTF-8?q?feat(PX):=20=E8=87=AA=E8=AE=AD=E7=BB=83v1.5=20P?= =?UTF-8?q?0=E5=87=86=E5=A4=87=20=E2=80=94=20NHN=E6=A8=A1=E5=BC=8F=20+=20?= =?UTF-8?q?=E6=80=BB=E5=88=86=E6=B3=95=E8=83=9C=E7=8E=87=E8=A7=A3=E6=9E=90?= =?UTF-8?q?=20+=20=E8=AF=84=E4=BC=B0=E7=AE=A1=E7=BA=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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→总分法解析, 闭环可验证 --- PdkFriendServer/Logic/PdkGameMain.cs | 3 +- PdkFriendServer/Logic/TrainingCollector.cs | 45 ++++++++++++++++++++++ hjha-console/Program.cs | 30 +++++++++++---- scripts/training_orchestrator.py | 22 +++++++---- 4 files changed, 85 insertions(+), 15 deletions(-) diff --git a/PdkFriendServer/Logic/PdkGameMain.cs b/PdkFriendServer/Logic/PdkGameMain.cs index 43aa47c9..0de144e4 100644 --- a/PdkFriendServer/Logic/PdkGameMain.cs +++ b/PdkFriendServer/Logic/PdkGameMain.cs @@ -492,7 +492,6 @@ namespace PdkFriendServer.Logic #endif // 训练数据收集:输出本局所有 ISMCTS 决策 - if (TrainingCollector.Enabled) { float[] rewards = new float[GamePack.Info.PlayNum]; for (int i = 0; i < GamePack.Info.PlayNum; i++) @@ -501,6 +500,8 @@ namespace PdkFriendServer.Logic float s = GamePack.Info.Score[i]; rewards[i] = s > 0 ? 1f : s < 0 ? -1f : 0f; } + var rawScores = GamePack.Info.Score.Select(s => (float)s).ToArray(); + TrainingCollector.AccumulateScore(BotTypes, rawScores); TrainingCollector.FlushGame(_flushGameCounter++, rewards, GamePack.Info.OverPos, BotTypes); } diff --git a/PdkFriendServer/Logic/TrainingCollector.cs b/PdkFriendServer/Logic/TrainingCollector.cs index 003845cb..01897342 100644 --- a/PdkFriendServer/Logic/TrainingCollector.cs +++ b/PdkFriendServer/Logic/TrainingCollector.cs @@ -16,6 +16,15 @@ namespace PdkFriendServer.Logic private static readonly List _records = new List(); private static readonly string _sessionPrefix = DateTime.Now.ToString("yyyyMMdd_HHmm"); + /// 总分累加器:botType → 累积 Score(用于评估统计) + public static readonly Dictionary ScoreAccumulator = new Dictionary(); + + /// 每 bot 的胜局计数:botType → wins + public static readonly Dictionary BotWins = new Dictionary(); + + /// 每 bot 的总局数:botType → total games + public static readonly Dictionary BotGames = new Dictionary(); + private static readonly int[] Ranks = { 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 16 }; private const int NumRanks = 13; private const int NumSuits = 4; @@ -58,6 +67,42 @@ namespace PdkFriendServer.Logic _records.Clear(); } + /// + /// 累加 bot 得分:botType → Score 总和 + win/game 计数(总分法评估用) + /// + public static void AccumulateScore(string[] botTypes, float[] rawScores) + { + if (botTypes == null || rawScores == null) return; + for (int i = 0; i < botTypes.Length && i < rawScores.Length; i++) + { + string t = botTypes[i]; + if (!ScoreAccumulator.ContainsKey(t)) + ScoreAccumulator[t] = 0; + ScoreAccumulator[t] += rawScores[i]; + + if (!BotGames.ContainsKey(t)) + BotGames[t] = 0; + BotGames[t]++; + + if (rawScores[i] > 0) + { + if (!BotWins.ContainsKey(t)) + BotWins[t] = 0; + BotWins[t]++; + } + } + } + + /// + /// 重置总分累加器 + /// + public static void ResetAccumulator() + { + ScoreAccumulator.Clear(); + BotWins.Clear(); + BotGames.Clear(); + } + /// /// 状态编码:与 Python encode_game_state 完全一致。 /// diff --git a/hjha-console/Program.cs b/hjha-console/Program.cs index de879368..c958e3d8 100644 --- a/hjha-console/Program.cs +++ b/hjha-console/Program.cs @@ -8,10 +8,11 @@ namespace hjha_console class Program { public static int RotateInterval = 0; // 0=disabled, N=rotate every N rounds - private static readonly string[][] Rotations = new[] { // 3种排列 + private static readonly string[][] Rotations = new[] { // 4种排列,覆盖所有 NHN/NNH/HNN 组合 new[] { "ISMCTS", "ISMCTS", "NEURAL" }, new[] { "NEURAL", "ISMCTS", "ISMCTS" }, - new[] { "ISMCTS", "NEURAL", "ISMCTS" } + new[] { "ISMCTS", "NEURAL", "ISMCTS" }, + new[] { "NEURAL", "ISMCTS", "NEURAL" } // NHN: N插花坐 }; static void Main(string[] args) @@ -22,6 +23,7 @@ namespace hjha_console 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; @@ -50,6 +52,9 @@ namespace hjha_console 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}"); @@ -136,11 +141,22 @@ namespace hjha_console 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"); + 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()); } } } diff --git a/scripts/training_orchestrator.py b/scripts/training_orchestrator.py index f8d831c8..3d2f4e08 100644 --- a/scripts/training_orchestrator.py +++ b/scripts/training_orchestrator.py @@ -209,13 +209,21 @@ def train_v(prev_winrate=None): with open(sf) as f: summary = f.read() log(f" {summary.strip()}") - # parse Neural win rate from summary - m = re.search(r'NEURAL.*?(\d+)\s*wins?.*?(\d+)\s*games?', summary, re.IGNORECASE) - if not m: - m = re.search(r'position.*?(\d+).*?wins?.*?(\d+)', summary, re.IGNORECASE) - if m: - wins, games = int(m.group(1)), int(m.group(2)) - winrate = wins / games * 100 if games > 0 else 0.0 + # parse Neural win rate from #BOT_SCORES (总分法: 每局 Score>0 算赢) + in_bot_scores = False + for line in summary.splitlines(): + line = line.strip() + if line == "#BOT_SCORES": + in_bot_scores = True + continue + if in_bot_scores and line.startswith("NEURAL="): + # Format: NEURAL=wins/games,wr=XX.X%,score=YYY + m = re.search(r'wr=(\d+\.?\d*)%', line) + if m: + winrate = float(m.group(1)) + break + if winrate == 0.0: + log(" ⚠️ BOT_SCORES not found, fallback to legacy parse") log(f" Neural winrate: {winrate:.1f}%") except Exception as e: log(f" eval failed: {e}")