added First bot with logging

This commit is contained in:
2026-04-01 23:02:17 +02:00
parent e0ee552012
commit 284a45ece1
6 changed files with 1948 additions and 13 deletions

View File

@@ -0,0 +1,221 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
namespace DDApplication;
internal sealed class BotSessionLogger : IDisposable
{
private readonly object _gate = new();
private readonly DateTimeOffset _startedAt = DateTimeOffset.UtcNow;
private readonly Dictionary<string, int> _actionAttempts = new(StringComparer.OrdinalIgnoreCase);
private readonly Dictionary<string, int> _actionSuccess = new(StringComparer.OrdinalIgnoreCase);
private StreamWriter? _eventsWriter;
private bool _disposed;
public string SessionId { get; } = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
public string DirectoryPath { get; }
public string EventsPath { get; }
public string SummaryPath { get; }
public string LatestSymlinkPath { get; }
public BotSessionLogger()
{
var baseDir = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
DirectoryPath = Path.Combine(baseDir, "DDApplication", "logs");
Directory.CreateDirectory(DirectoryPath);
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");
_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();
}
public void LogDecision(
string label,
bool executed,
double hp,
double maxHp,
int kills,
int deaths,
int x,
int y,
bool pressure,
string details)
{
lock (_gate)
{
if (_disposed || _eventsWriter is null)
{
return;
}
Increment(_actionAttempts, label);
if (executed)
{
Increment(_actionSuccess, label);
}
_eventsWriter.WriteLine(string.Join(",",
Csv(DateTimeOffset.UtcNow.ToString("O", CultureInfo.InvariantCulture)),
Csv("decision"),
Csv(label),
Csv(executed ? "1" : "0"),
Csv(hp.ToString("0.00", CultureInfo.InvariantCulture)),
Csv(maxHp.ToString("0.00", CultureInfo.InvariantCulture)),
Csv(kills.ToString(CultureInfo.InvariantCulture)),
Csv(deaths.ToString(CultureInfo.InvariantCulture)),
Csv(x.ToString(CultureInfo.InvariantCulture)),
Csv(y.ToString(CultureInfo.InvariantCulture)),
Csv(pressure ? "1" : "0"),
Csv(details)));
}
}
public void LogTelemetry(
double hp,
double maxHp,
int kills,
int deaths,
double progress,
double remaining,
string level,
int x,
int y,
string details)
{
lock (_gate)
{
if (_disposed || _eventsWriter is null)
{
return;
}
_eventsWriter.WriteLine(string.Join(",",
Csv(DateTimeOffset.UtcNow.ToString("O", CultureInfo.InvariantCulture)),
Csv("telemetry"),
Csv(level),
Csv(""),
Csv(hp.ToString("0.00", CultureInfo.InvariantCulture)),
Csv(maxHp.ToString("0.00", CultureInfo.InvariantCulture)),
Csv(kills.ToString(CultureInfo.InvariantCulture)),
Csv(deaths.ToString(CultureInfo.InvariantCulture)),
Csv(x.ToString(CultureInfo.InvariantCulture)),
Csv(y.ToString(CultureInfo.InvariantCulture)),
Csv(""),
Csv($"progress={progress:0.00};remaining={remaining:0.00};{details}")));
}
}
public void LogEvent(string label, string details)
{
lock (_gate)
{
if (_disposed || _eventsWriter is null)
{
return;
}
_eventsWriter.WriteLine(string.Join(",",
Csv(DateTimeOffset.UtcNow.ToString("O", CultureInfo.InvariantCulture)),
Csv("event"),
Csv(label),
Csv(""),
Csv(""),
Csv(""),
Csv(""),
Csv(""),
Csv(""),
Csv(""),
Csv(""),
Csv(details)));
}
}
public void WriteSummaryAndDispose(double finalHp, double finalMaxHp, int finalKills, int finalDeaths)
{
lock (_gate)
{
if (_disposed)
{
return;
}
var elapsed = DateTimeOffset.UtcNow - _startedAt;
var minutes = Math.Max(elapsed.TotalMinutes, 0.01);
var kd = finalDeaths == 0 ? finalKills : (double)finalKills / finalDeaths;
var lines = new List<string>
{
$"session={SessionId}",
$"started_utc={_startedAt:O}",
$"ended_utc={DateTimeOffset.UtcNow:O}",
$"duration_minutes={minutes:0.00}",
$"kills={finalKills}",
$"deaths={finalDeaths}",
$"kd={kd:0.000}",
$"kills_per_min={(finalKills / minutes):0.000}",
$"deaths_per_min={(finalDeaths / minutes):0.000}",
$"final_hp={finalHp:0.00}/{finalMaxHp:0.00}",
"actions="
};
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;
lines.Add($" {attempt.Key}: attempts={attempt.Value}, success={success}, rate={rate:0.000}");
}
File.WriteAllLines(SummaryPath, lines, Encoding.UTF8);
_eventsWriter?.Dispose();
_eventsWriter = null;
_disposed = true;
}
}
public void Dispose()
{
WriteSummaryAndDispose(0, 0, 0, 0);
}
private static void Increment(Dictionary<string, int> dict, string key)
{
dict.TryGetValue(key, out var current);
dict[key] = current + 1;
}
private static string Csv(string value)
{
var safe = value.Replace("\"", "\"\"");
return $"\"{safe}\"";
}
private void UpdateLatestSymlink()
{
try
{
if (File.Exists(LatestSymlinkPath) || Directory.Exists(LatestSymlinkPath))
{
File.Delete(LatestSymlinkPath);
}
File.CreateSymbolicLink(LatestSymlinkPath, EventsPath);
}
catch
{
// Best-effort only; logging should continue even if symlink creation fails.
}
}
}