diff --git a/PdkFriendServer/Logic/PdkGameMain.cs b/PdkFriendServer/Logic/PdkGameMain.cs index 0841e8d7..6ddc1a2c 100644 --- a/PdkFriendServer/Logic/PdkGameMain.cs +++ b/PdkFriendServer/Logic/PdkGameMain.cs @@ -499,7 +499,7 @@ namespace PdkFriendServer.Logic float s = GamePack.Info.Score[i]; rewards[i] = s > 0 ? 1f : s < 0 ? -1f : 0f; } - TrainingCollector.FlushGame(_flushGameCounter++, rewards); + TrainingCollector.FlushGame(_flushGameCounter++, rewards, GamePack.Info.OverPos, BotTypes); } if (DeskGameDo != null) diff --git a/PdkFriendServer/Logic/TrainingCollector.cs b/PdkFriendServer/Logic/TrainingCollector.cs index 156c153a..4356c556 100644 --- a/PdkFriendServer/Logic/TrainingCollector.cs +++ b/PdkFriendServer/Logic/TrainingCollector.cs @@ -39,19 +39,21 @@ namespace PdkFriendServer.Logic /// 游戏结束,输出本轮训练数据到文件。 /// rewards: 每个玩家的奖励 (1=赢, 0=第二, -1=第三) /// - public static void FlushGame(int gameId, float[] rewards) + public static void FlushGame(int gameId, float[] rewards, int winnerPos, string[] botTypes) { if (!Enabled || _records.Count == 0) return; var path = $"training_data/game_{gameId:D5}.csv"; System.IO.Directory.CreateDirectory("training_data"); using var w = new System.IO.StreamWriter(path); - w.WriteLine("state,action,reward"); + w.WriteLine("state,action,reward,player"); foreach (var r in _records) { float reward = r.Player >= 0 && r.Player < rewards.Length ? rewards[r.Player] : 0; var stateStr = string.Join(",", r.State.Select(f => f.ToString("F6"))); - w.WriteLine($"\"{stateStr}\",{r.Action},{reward:F3}"); + w.WriteLine($"\"{stateStr}\",{r.Action},{reward:F3},{r.Player}"); } + // 游戏结果摘要 — 方便不中断训练即可评估模型进化 + w.WriteLine($"#RESULT,winner={winnerPos},bots={string.Join(",", botTypes ?? new[]{"ISMCTS","ISMCTS","ISMCTS"})}"); _records.Clear(); } diff --git a/scripts/scan_wins.py b/scripts/scan_wins.py new file mode 100644 index 00000000..fb18f3db --- /dev/null +++ b/scripts/scan_wins.py @@ -0,0 +1,67 @@ +#!/usr/bin/env python3 +"""扫描 training_data 目录下所有 CSV,按 bot 类型统计胜率""" +import sys, os, glob, re +from collections import Counter, defaultdict + +def scan(csv_dir='training_data'): + csvs = sorted(glob.glob(os.path.join(csv_dir, 'game_*.csv'))) + if not csvs: + print("No CSV files found") + return + + total = 0 + per_bot = defaultdict(lambda: {'wins': 0, 'games': 0}) + per_game = [] # (game_id, winner_pos, bots_str) + + for fp in csvs: + with open(fp) as f: + for line in f: + if line.startswith('#RESULT'): + parts = line.strip().split(',') + winner = int(parts[1].split('=')[1]) + bots = parts[2].split('=')[1].split(',') + game_id = os.path.splitext(os.path.basename(fp))[0] + per_game.append((game_id, winner, line.strip())) + total += 1 + + # winner_pos is 1-indexed, bots[winner-1] + for i, bt in enumerate(bots): + per_bot[bt]['games'] += 1 + winner_bt = bots[winner - 1] if 0 < winner <= len(bots) else '?' + per_bot[winner_bt]['wins'] += 1 + break + + if total == 0: + print("No #RESULT lines found. Run with updated hjha-console.") + return + + print(f"{total} games scanned") + print(f"{'Bot':<12} {'Wins':>6} {'Rate':>8} {'Games':>6}") + print("-" * 35) + for bt in sorted(per_bot.keys()): + d = per_bot[bt] + rate = d['wins'] / d['games'] * 100 if d['games'] > 0 else 0 + print(f"{bt:<12} {d['wins']:>6} {rate:>7.1f}% {d['games']:>6}") + + # 也统计按 bot 组合 + combo_stats = defaultdict(lambda: {'total': 0, 'winners': Counter()}) + for gid, winner, line in per_game: + m = re.search(r'bots=([^,]+(?:,[^,]+)*)', line) + if m: + key = m.group(1) + combo_stats[key]['total'] += 1 + combo_stats[key]['winners'][winner] += 1 + + if len(combo_stats) > 0: + print(f"\nBy bot composition:") + for combo in sorted(combo_stats.keys()): + d = combo_stats[combo] + bots = combo.split(',') + print(f" [{combo}] — {d['total']} games:") + for pos, count in sorted(d['winners'].items()): + bt = bots[pos-1] if 0 < pos <= len(bots) else '?' + print(f" pos{pos}({bt}): {count}/{d['total']} ({count/d['total']*100:.0f}%)") + +if __name__ == '__main__': + d = sys.argv[1] if len(sys.argv) > 1 else 'training_data' + scan(d) \ No newline at end of file