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→总分法解析, 闭环可验证
This commit is contained in:
@ -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);
|
||||
}
|
||||
|
||||
|
||||
@ -16,6 +16,15 @@ namespace PdkFriendServer.Logic
|
||||
private static readonly List<TrainingRecord> _records = new List<TrainingRecord>();
|
||||
private static readonly string _sessionPrefix = DateTime.Now.ToString("yyyyMMdd_HHmm");
|
||||
|
||||
/// <summary>总分累加器:botType → 累积 Score(用于评估统计)</summary>
|
||||
public static readonly Dictionary<string, float> ScoreAccumulator = new Dictionary<string, float>();
|
||||
|
||||
/// <summary>每 bot 的胜局计数:botType → wins</summary>
|
||||
public static readonly Dictionary<string, int> BotWins = new Dictionary<string, int>();
|
||||
|
||||
/// <summary>每 bot 的总局数:botType → total games</summary>
|
||||
public static readonly Dictionary<string, int> BotGames = new Dictionary<string, int>();
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 累加 bot 得分:botType → Score 总和 + win/game 计数(总分法评估用)
|
||||
/// </summary>
|
||||
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]++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 重置总分累加器
|
||||
/// </summary>
|
||||
public static void ResetAccumulator()
|
||||
{
|
||||
ScoreAccumulator.Clear();
|
||||
BotWins.Clear();
|
||||
BotGames.Clear();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 状态编码:与 Python encode_game_state 完全一致。
|
||||
/// </summary>
|
||||
|
||||
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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}")
|
||||
|
||||
Reference in New Issue
Block a user