diff --git a/DDApp/DDApplication/BotSessionLogger.cs b/DDApp/DDApplication/BotSessionLogger.cs index 79538c1..603d850 100644 --- a/DDApp/DDApplication/BotSessionLogger.cs +++ b/DDApp/DDApplication/BotSessionLogger.cs @@ -20,8 +20,9 @@ internal sealed class BotSessionLogger : IDisposable public string SessionId { get; } = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture); public string DirectoryPath { get; } public string EventsPath { get; } - public string SummaryPath { get; } + public string SummaryCsvPath { get; } public string LatestSymlinkPath { get; } + public string LatestSummarySymlinkPath { get; } public BotSessionLogger() { @@ -31,12 +32,13 @@ internal sealed class BotSessionLogger : IDisposable var stamp = DateTimeOffset.UtcNow.ToString("yyyyMMdd_HHmmss", CultureInfo.InvariantCulture); EventsPath = Path.Combine(DirectoryPath, $"bot_{stamp}_{SessionId}.events.csv"); - SummaryPath = Path.Combine(DirectoryPath, $"bot_{stamp}_{SessionId}.summary.txt"); - LatestSymlinkPath = Path.Combine(DirectoryPath, "latest.log"); + SummaryCsvPath = Path.Combine(DirectoryPath, $"bot_{stamp}_{SessionId}.summary.csv"); + LatestSymlinkPath = Path.Combine(DirectoryPath, "latest.log.csv"); + LatestSummarySymlinkPath = Path.Combine(DirectoryPath, "latest-summary.log.csv"); _eventsWriter = new StreamWriter(EventsPath, append: false, Encoding.UTF8) { AutoFlush = true }; _eventsWriter.WriteLine("timestamp_utc,event,label,executed,hp,max_hp,kills,deaths,pos_x,pos_y,pressure,details"); - UpdateLatestSymlink(); + UpdateLatestSymlink(LatestSymlinkPath, EventsPath); } public void LogDecision( @@ -175,7 +177,31 @@ internal sealed class BotSessionLogger : IDisposable lines.Add($" {attempt.Key}: attempts={attempt.Value}, success={success}, rate={rate:0.000}"); } - File.WriteAllLines(SummaryPath, lines, Encoding.UTF8); + using (var csvWriter = new StreamWriter(SummaryCsvPath, append: false, Encoding.UTF8)) + { + csvWriter.WriteLine("type,key,value"); + csvWriter.WriteLine($"{Csv("metric")},{Csv("session")},{Csv(SessionId)}"); + csvWriter.WriteLine($"{Csv("metric")},{Csv("started_utc")},{Csv(_startedAt.ToString("O", CultureInfo.InvariantCulture))}"); + csvWriter.WriteLine($"{Csv("metric")},{Csv("ended_utc")},{Csv(DateTimeOffset.UtcNow.ToString("O", CultureInfo.InvariantCulture))}"); + csvWriter.WriteLine($"{Csv("metric")},{Csv("duration_minutes")},{Csv(minutes.ToString("0.00", CultureInfo.InvariantCulture))}"); + csvWriter.WriteLine($"{Csv("metric")},{Csv("kills")},{Csv(finalKills.ToString(CultureInfo.InvariantCulture))}"); + csvWriter.WriteLine($"{Csv("metric")},{Csv("deaths")},{Csv(finalDeaths.ToString(CultureInfo.InvariantCulture))}"); + csvWriter.WriteLine($"{Csv("metric")},{Csv("kd")},{Csv(kd.ToString("0.000", CultureInfo.InvariantCulture))}"); + csvWriter.WriteLine($"{Csv("metric")},{Csv("kills_per_min")},{Csv((finalKills / minutes).ToString("0.000", CultureInfo.InvariantCulture))}"); + csvWriter.WriteLine($"{Csv("metric")},{Csv("deaths_per_min")},{Csv((finalDeaths / minutes).ToString("0.000", CultureInfo.InvariantCulture))}"); + csvWriter.WriteLine($"{Csv("metric")},{Csv("final_hp")},{Csv($"{finalHp:0.00}/{finalMaxHp:0.00}")}"); + + foreach (var attempt in _actionAttempts.OrderBy(x => x.Key)) + { + _actionSuccess.TryGetValue(attempt.Key, out var success); + var rate = attempt.Value == 0 ? 0 : (double)success / attempt.Value; + csvWriter.WriteLine($"{Csv("action")},{Csv(attempt.Key + ".attempts")},{Csv(attempt.Value.ToString(CultureInfo.InvariantCulture))}"); + csvWriter.WriteLine($"{Csv("action")},{Csv(attempt.Key + ".success")},{Csv(success.ToString(CultureInfo.InvariantCulture))}"); + csvWriter.WriteLine($"{Csv("action")},{Csv(attempt.Key + ".rate")},{Csv(rate.ToString("0.000", CultureInfo.InvariantCulture))}"); + } + } + + UpdateLatestSymlink(LatestSummarySymlinkPath, SummaryCsvPath); _eventsWriter?.Dispose(); _eventsWriter = null; @@ -200,16 +226,16 @@ internal sealed class BotSessionLogger : IDisposable return $"\"{safe}\""; } - private void UpdateLatestSymlink() + private static void UpdateLatestSymlink(string symlinkPath, string targetPath) { try { - if (File.Exists(LatestSymlinkPath) || Directory.Exists(LatestSymlinkPath)) + if (File.Exists(symlinkPath) || Directory.Exists(symlinkPath)) { - File.Delete(LatestSymlinkPath); + File.Delete(symlinkPath); } - File.CreateSymbolicLink(LatestSymlinkPath, EventsPath); + File.CreateSymbolicLink(symlinkPath, targetPath); } catch { @@ -219,3 +245,6 @@ internal sealed class BotSessionLogger : IDisposable } + + + diff --git a/DDApp/DDApplication/MainWindow.axaml.cs b/DDApp/DDApplication/MainWindow.axaml.cs index 5eabd07..5dae0ee 100644 --- a/DDApp/DDApplication/MainWindow.axaml.cs +++ b/DDApp/DDApplication/MainWindow.axaml.cs @@ -169,7 +169,7 @@ public partial class MainWindow : Window { _sessionLogger.LogEvent("session-stop", "Bot session stopping"); _sessionLogger.WriteSummaryAndDispose(_currentHealth, _maxHealth, _kills, _deaths); - AppendLog($"Summary written: {_sessionLogger.SummaryPath}"); + AppendLog($"Summary CSV written: {_sessionLogger.SummaryCsvPath}"); _sessionLogger = null; } diff --git a/DDApp/DDApplication/README.md b/DDApp/DDApplication/README.md index a9a6e6b..f967392 100644 --- a/DDApp/DDApplication/README.md +++ b/DDApp/DDApplication/README.md @@ -35,7 +35,8 @@ dotnet run --project /home/tikaiz/RiderProjects/digitalDojo/DDApp/DDApplication/ - The strategy is heuristic and tunable; it is designed for high kill throughput while respecting API cooldowns. - Session logs are written to `~/.local/share/DDApplication/logs` on Linux: - `*.events.csv` contains timestamped telemetry, decisions, and events - - `*.summary.txt` contains aggregate K/D, kills/min, deaths/min, and action success rates + - `*.summary.csv` contains machine-readable summary metrics and action rates + - `latest-summary.log.csv` is a symlink that auto-points to the newest `*.summary.csv` - `latest.log` is a symlink that auto-points to the newest `*.events.csv` session log