using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Globalization; using System.Linq; using System.Net.Http; using System.Threading; using System.Threading.Tasks; using Avalonia.Controls; using Avalonia.Layout; using Avalonia.Media; using Avalonia.Threading; namespace DDApplication; public partial class MainWindow : Window { private const int MapRadius = 10; private static readonly TimeSpan TickDelay = TimeSpan.FromMilliseconds(70); private static readonly IReadOnlyDictionary Cooldowns = new Dictionary { [BotAction.Move] = TimeSpan.FromMilliseconds(250), [BotAction.Hit] = TimeSpan.FromMilliseconds(250), [BotAction.Radar] = TimeSpan.FromMilliseconds(250), [BotAction.Shoot] = TimeSpan.FromMilliseconds(1000), [BotAction.Peek] = TimeSpan.FromMilliseconds(1000), [BotAction.Scan] = TimeSpan.FromMilliseconds(2000), [BotAction.Dash] = TimeSpan.FromMilliseconds(5000), [BotAction.Special] = TimeSpan.FromMilliseconds(5000), [BotAction.Teleport] = TimeSpan.FromMilliseconds(20000), }; private readonly string _configuredApiKey = Program.Configuration["APIKEY"] ?? string.Empty; private readonly HttpClient _httpClient = new() { Timeout = TimeSpan.FromSeconds(8) }; private readonly DigitalDojoApiClient _client; private readonly ObservableCollection _logEntries = []; private readonly Dictionary<(int X, int Y), double> _enemyHeat = []; private readonly Dictionary<(int X, int Y), double> _visitHeat = []; private readonly Dictionary<(int X, int Y), double> _deathHeat = []; private readonly Dictionary _lastRadar = new(StringComparer.OrdinalIgnoreCase); private readonly Dictionary _lastPeek = []; private readonly Dictionary _lastUse = []; private readonly Dictionary _blockedDirectionTicks = []; private readonly Queue _recentDeaths = new(); private readonly TextBlock[,] _mapCells = new TextBlock[MapRadius * 2 + 1, MapRadius * 2 + 1]; private readonly Random _random = new(); private CancellationTokenSource? _runCts; private Task? _runTask; private BotSessionLogger? _sessionLogger; private Guid _playerToken; private Guid? _gameToken; private bool _botRunning; private int _x; private int _y; private int _peekDirectionIndex; private int _kills; private int _deaths; private double _currentHealth; private double _maxHealth; private double _progress; private double _remainingTime; private string _levelName = "-"; private DateTimeOffset _lastStatsAt = DateTimeOffset.MinValue; private DateTimeOffset _lastStatusAt = DateTimeOffset.MinValue; private (int X, int Y)? _nearestEnemy; private Direction _lastMoveDirection = Direction.North; private Direction _escapeDirection = Direction.North; private int _recentDamageTicks; private int _survivalTicks; private int _escapeLockTicks; private bool _lastUnderPressure; private int _pressureStreak; private LevelProfile _currentLevelProfile = LevelProfile.Unknown; public MainWindow() { _client = new DigitalDojoApiClient("https://game-dd.countit.at", _httpClient); InitializeComponent(); BuildKnowledgeGrid(); ApiKeyBox.Text = _configuredApiKey; LogList.ItemsSource = _logEntries; StartButton.Click += async (_, _) => await StartBotAsync(); StopButton.Click += async (_, _) => await StopBotAsync(closeGame: true); RenderTelemetry("Waiting to start."); } protected override async void OnClosed(EventArgs e) { await StopBotAsync(closeGame: false); _httpClient.Dispose(); base.OnClosed(e); } private async Task StartBotAsync() { if (_botRunning) { return; } if (!Guid.TryParse(ApiKeyBox.Text, out var parsedToken)) { SetStatus("Invalid API key GUID."); return; } _playerToken = parsedToken; _runCts = new CancellationTokenSource(); _botRunning = true; _sessionLogger = new BotSessionLogger(); _sessionLogger.LogEvent("session-start", "Bot session created"); StartButton.IsEnabled = false; StopButton.IsEnabled = true; ApiKeyBox.IsEnabled = false; ResetKnowledgeState(); var started = await EnsureGameStartedAsync(_runCts.Token); if (!started) { await StopBotAsync(closeGame: false); return; } AppendLog($"Logging to {_sessionLogger.EventsPath}"); SetStatus("Bot running."); _runTask = Task.Run(() => BotLoopAsync(_runCts.Token)); } private async Task StopBotAsync(bool closeGame) { if (_runCts is not null) { _runCts.Cancel(); } if (_runTask is not null) { try { await _runTask; } catch (OperationCanceledException) { // Normal shutdown path. } } _runTask = null; _runCts?.Dispose(); _runCts = null; if (closeGame && _playerToken != Guid.Empty) { try { await _client.CloseAsync(_playerToken); AppendLog("Game closed."); } catch (Exception ex) { AppendLog($"Close failed: {ex.Message}"); } } if (_sessionLogger is not null) { _sessionLogger.LogEvent("session-stop", "Bot session stopping"); _sessionLogger.WriteSummaryAndDispose(_currentHealth, _maxHealth, _kills, _deaths); AppendLog($"Summary CSV written: {_sessionLogger.SummaryCsvPath}"); _sessionLogger = null; } _botRunning = false; StartButton.IsEnabled = true; StopButton.IsEnabled = false; ApiKeyBox.IsEnabled = true; SetStatus("Stopped."); } private async Task EnsureGameStartedAsync(CancellationToken ct) { try { var status = await _client.StatusAsync(_playerToken, ct); if (status.Running) { _gameToken = Guid.TryParse(status.Gameid, out var existingGameId) ? existingGameId : null; _levelName = status.Level ?? "-"; AppendLog("Reusing running game."); return true; } } catch (ApiException) { // No game running yet. } catch (Exception ex) { AppendLog($"Status check failed: {ex.Message}"); return false; } try { var created = await _client.CreateAsync(_playerToken, ct); _gameToken = Guid.TryParse(created.Gameid, out var gameId) ? gameId : null; _levelName = created.Level ?? "-"; AppendLog($"Game created ({_gameToken?.ToString() ?? "unknown"})."); return true; } catch (Exception ex) { AppendLog($"Create failed: {ex.Message}"); return false; } } private async Task BotLoopAsync(CancellationToken ct) { while (!ct.IsCancellationRequested) { try { if (_recentDamageTicks > 0) { _recentDamageTicks--; } if (_survivalTicks > 0) { _survivalTicks--; } if (_escapeLockTicks > 0) { _escapeLockTicks--; } DecayBlockedDirections(); await PollTelemetryAsync(ct); DecayHeat(); await GatherSensorsAsync(ct); var decision = BuildDecision(); if (decision is not null) { var executed = await decision.Value.Execute(ct); _sessionLogger?.LogDecision( decision.Value.Label, executed, _currentHealth, _maxHealth, _kills, _deaths, _x, _y, _lastUnderPressure, decision.Value.Reason); if (executed) { AppendLog($"{decision.Value.Label}: {decision.Value.Reason}"); } } RenderTelemetry(decision?.Reason ?? "Scanning..."); RenderKnowledgeGrid(); } catch (OperationCanceledException) { break; } catch (Exception ex) { AppendLog($"Loop error: {ex.Message}"); await Task.Delay(300, ct); } await Task.Delay(TickDelay, ct); } } private async Task PollTelemetryAsync(CancellationToken ct) { var now = DateTimeOffset.UtcNow; if (now - _lastStatsAt >= TimeSpan.FromMilliseconds(650)) { _lastStatsAt = now; try { var stats = await _client.StatsAsync(_playerToken, ct); var previousHealth = _currentHealth; var previousDeaths = _deaths; _kills = stats.Stats?.Kills ?? _kills; _deaths = stats.Stats?.Deaths ?? _deaths; _currentHealth = stats.Health?.Currenthealth ?? _currentHealth; _maxHealth = stats.Health?.Maxhealth ?? _maxHealth; _progress = stats.Level?.Progress ?? _progress; _remainingTime = stats.Level?.Remainingtime ?? _remainingTime; _levelName = stats.Level?.Name ?? _levelName; UpdateLevelProfile(_levelName); if (previousHealth > 0.1 && _currentHealth + 0.05 < previousHealth) { _recentDamageTicks = 20; } if (_deaths > previousDeaths) { var nowDeath = DateTimeOffset.UtcNow; _recentDeaths.Enqueue(nowDeath); while (_recentDeaths.Count > 0 && nowDeath - _recentDeaths.Peek() > TimeSpan.FromSeconds(90)) { _recentDeaths.Dequeue(); } _survivalTicks = Math.Max(_survivalTicks, 24 + _recentDeaths.Count * 4); _escapeLockTicks = Math.Max(_escapeLockTicks, 8); _escapeDirection = Opposite(_lastMoveDirection); _pressureStreak = 0; _x = 0; _y = 0; _enemyHeat.Clear(); _nearestEnemy = null; MarkDeathZone(_x, _y); _sessionLogger?.LogEvent("death", $"death_count={_deaths};death_window_90s={_recentDeaths.Count}"); } _sessionLogger?.LogTelemetry( _currentHealth, _maxHealth, _kills, _deaths, _progress, _remainingTime, _levelName, _x, _y, $"survival_ticks={_survivalTicks};damage_ticks={_recentDamageTicks};escape_lock={_escapeLockTicks};pressure_streak={_pressureStreak}"); } catch (Exception ex) { AppendLog($"Stats failed: {ex.Message}"); } } if (now - _lastStatusAt >= TimeSpan.FromSeconds(2)) { _lastStatusAt = now; try { var status = await _client.StatusAsync(_playerToken, ct); _levelName = status.Level ?? _levelName; if (_gameToken is null && Guid.TryParse(status.Gameid, out var gameId)) { _gameToken = gameId; } } catch { // Status occasionally fails while game is rotating; ignore. } } } private async Task GatherSensorsAsync(CancellationToken ct) { if (Ready(BotAction.Radar)) { try { var radar = await _client.RadarAsync(_playerToken, ct); MarkUsed(BotAction.Radar); _lastRadar.Clear(); foreach (var kvp in radar.RadarResults ?? new Dictionary()) { _lastRadar[kvp.Key] = kvp.Value; } ApplyRadarHeat(); } catch (Exception ex) { AppendLog($"Radar failed: {ex.Message}"); } } if (Ready(BotAction.Scan)) { try { var scan = await _client.ScanAsync(_playerToken, ct); MarkUsed(BotAction.Scan); var sx = scan.DifferenceToNearestPlayer?.X; var sy = scan.DifferenceToNearestPlayer?.Y; _nearestEnemy = sx.HasValue && sy.HasValue ? (sx.Value, sy.Value) : null; if (_nearestEnemy.HasValue) { AddHeat(_x + _nearestEnemy.Value.X, _y + _nearestEnemy.Value.Y, 5.5); } } catch (Exception ex) { AppendLog($"Scan failed: {ex.Message}"); } } if (Ready(BotAction.Peek)) { var dir = (Direction)(_peekDirectionIndex % 4); _peekDirectionIndex++; try { var peek = await _client.PeekAsync(_playerToken, (int)dir, ct); MarkUsed(BotAction.Peek); _lastPeek[dir] = peek; ApplyPeekHeat(dir, peek); } catch (Exception ex) { AppendLog($"Peek {dir} failed: {ex.Message}"); } } } private BotDecision? BuildDecision() { var hpRatio = _maxHealth > 0.1 ? _currentHealth / _maxHealth : 1.0; var profile = _currentLevelProfile; var immediateDir = _lastPeek .Where(x => x.Value.Executed && x.Value.PlayersInSight > 0 && x.Value.SightedPlayerDistance == 1) .Select(x => (Direction?)x.Key) .FirstOrDefault(); var closeThreats = _lastPeek.Values.Count(x => x.Executed && x.PlayersInSight > 0 && (x.SightedPlayerDistance ?? 99) <= 2); if (_nearestEnemy.HasValue && Math.Abs(_nearestEnemy.Value.X) + Math.Abs(_nearestEnemy.Value.Y) <= 2) { closeThreats++; } if (hpRatio < 0.75 || _recentDamageTicks > 0 || closeThreats >= 2) { _pressureStreak = Math.Min(_pressureStreak + 1, 12); } else { _pressureStreak = Math.Max(_pressureStreak - 1, 0); } var underPressure = hpRatio < PressureHpThreshold(profile) || _recentDamageTicks > 0 || closeThreats >= PressureThreatThreshold(profile) || _survivalTicks > 0 || _pressureStreak >= 2; _lastUnderPressure = underPressure; var specialThreatThreshold = SpecialThreatThreshold(profile); var specialHpFloor = SpecialHpFloor(profile); if (Ready(BotAction.Special) && closeThreats >= specialThreatThreshold && (hpRatio >= specialHpFloor || closeThreats >= specialThreatThreshold + 1)) { return new BotDecision( "specialattack", "Multiple enemies close; AoE yields best kill rate.", async ct => { var response = await _client.SpecialattackAsync(_playerToken, ct); MarkUsed(BotAction.Special); return response.Executed; }); } if (immediateDir.HasValue && Ready(BotAction.Hit) && ShouldUseImmediateHit(profile, hpRatio, closeThreats, underPressure)) { var dir = immediateDir.Value; return new BotDecision( "hit", $"Adjacent enemy at {dir}; highest DPS action.", async ct => { var response = await _client.HitAsync(_playerToken, (int)dir, ct); MarkUsed(BotAction.Hit); return response.Executed; }); } if (immediateDir.HasValue && Ready(BotAction.Move) && ShouldRetreatOnImmediate(profile, hpRatio, closeThreats)) { var retreatDirection = Opposite(immediateDir.Value); if (Ready(BotAction.Dash) && (hpRatio < DashHpThreshold(profile) || _pressureStreak >= 3 || _survivalTicks > 0)) { return new BotDecision( "dash-retreat", $"High pressure escape via dash -> {retreatDirection}.", async ct => { var response = await _client.DashAsync(_playerToken, (int)retreatDirection, ct); MarkUsed(BotAction.Dash); if (response.Executed) { ApplyMovement(retreatDirection, Math.Max(response.BlocksDashed, 1)); _escapeDirection = retreatDirection; _escapeLockTicks = Math.Max(_escapeLockTicks, EscapeLockTicks(profile)); } return response.Executed; }); } return new BotDecision( "move-retreat", $"Adjacent enemy and pressure high; kite to {retreatDirection}.", async ct => { var response = await _client.MoveAsync(_playerToken, (int)retreatDirection, ct); MarkUsed(BotAction.Move); var moved = response.Executed && response.Move == true; RegisterMoveResult(retreatDirection, moved); if (moved) { ApplyMovement(retreatDirection, 1); _escapeDirection = retreatDirection; _escapeLockTicks = Math.Max(_escapeLockTicks, EscapeLockTicks(profile)); } return response.Executed; }); } if (immediateDir.HasValue && Ready(BotAction.Hit)) { var dir = immediateDir.Value; return new BotDecision( "hit-last-stand", $"Adjacent enemy at {dir}; strike while move cooldown is active.", async ct => { var response = await _client.HitAsync(_playerToken, (int)dir, ct); MarkUsed(BotAction.Hit); return response.Executed; }); } var bestSight = _lastPeek .Where(x => x.Value.Executed && x.Value.PlayersInSight > 0) .OrderBy(x => x.Value.SightedPlayerDistance ?? int.MaxValue) .FirstOrDefault(); if (bestSight.Value is not null && Ready(BotAction.Shoot)) { var dir = bestSight.Key; var dist = bestSight.Value.SightedPlayerDistance ?? 99; if (!underPressure || dist >= 2) { return new BotDecision( "shoot", $"Enemy in line of sight to {dir}; ranged hit before moving.", async ct => { var response = await _client.ShootAsync(_playerToken, (int)dir, ct); MarkUsed(BotAction.Shoot); return response.Executed; }); } } var moveDirection = GetBestMoveDirection(chase: !underPressure); if (Ready(BotAction.Move)) { var reason = underPressure ? $"Pressure high; reposition toward safer lane -> {moveDirection}." : $"Controlled hunt path -> {moveDirection}."; return new BotDecision( "move", reason, async ct => { var response = await _client.MoveAsync(_playerToken, (int)moveDirection, ct); MarkUsed(BotAction.Move); var moved = response.Executed && response.Move == true; RegisterMoveResult(moveDirection, moved); if (moved) { ApplyMovement(moveDirection, 1); if (underPressure) { _escapeDirection = moveDirection; _escapeLockTicks = Math.Max(_escapeLockTicks, EscapeLockTicks(profile)); } } return response.Executed; }); } return null; } private Direction GetBestMoveDirection(bool chase) { if (!chase && _escapeLockTicks > 0) { if (!_blockedDirectionTicks.TryGetValue(_escapeDirection, out var blocked) || blocked <= 0) { return _escapeDirection; } } var scored = new Dictionary { [Direction.North] = RadarScore(Direction.North), [Direction.East] = RadarScore(Direction.East), [Direction.South] = RadarScore(Direction.South), [Direction.West] = RadarScore(Direction.West), }; foreach (var dir in scored.Keys.ToArray()) { var score = 0.0; score += chase ? scored[dir] * 1.35 : -scored[dir] * 1.0; if (_lastPeek.TryGetValue(dir, out var peek) && peek.Executed) { if (peek.PlayersInSight > 0) { var dist = Math.Max(1, peek.SightedPlayerDistance ?? 6); score += chase ? 7.0 / dist + peek.PlayersInSight * 0.8 : -10.0 / dist - peek.PlayersInSight * 0.7; } else if (!chase) { score += 0.9; } } if (_nearestEnemy.HasValue) { var targetDir = DirectionFromVector(_nearestEnemy.Value.X, _nearestEnemy.Value.Y); var awayDir = Opposite(targetDir); if (chase && dir == targetDir) { score += 4.0; } else if (!chase && dir == awayDir) { score += 4.5; } else if (!chase && dir == targetDir) { score -= 3.5; } } var visitPenalty = VisitHeatFor(dir); var deathPenalty = DeathHeatFor(dir); if (_currentLevelProfile == LevelProfile.BiggerMap) { score -= visitPenalty * 1.4; } else { score -= visitPenalty * 0.8; } score -= deathPenalty * 1.6; if (_survivalTicks > 0 && _nearestEnemy.HasValue) { var safest = Opposite(DirectionFromVector(_nearestEnemy.Value.X, _nearestEnemy.Value.Y)); if (dir == safest) { score += 2.8; } } if (_escapeLockTicks > 0) { if (dir == _escapeDirection) { score += 4.5; } else if (dir == Opposite(_escapeDirection)) { score -= 4.0; } } if (_blockedDirectionTicks.TryGetValue(dir, out var blockedTicks) && blockedTicks > 0) { score -= 5.0 + blockedTicks * 0.4; } if (dir == Opposite(_lastMoveDirection)) { score -= 0.6; } score += _survivalTicks > 0 ? _random.NextDouble() * 0.2 : _random.NextDouble() * 0.7; scored[dir] = score; } return scored.OrderByDescending(x => x.Value).First().Key; } private void ApplyRadarHeat() { var north = RadarScore(Direction.North); var east = RadarScore(Direction.East); var south = RadarScore(Direction.South); var west = RadarScore(Direction.West); ApplyDirectionalHeat(Direction.North, north, 0.35); ApplyDirectionalHeat(Direction.East, east, 0.35); ApplyDirectionalHeat(Direction.South, south, 0.35); ApplyDirectionalHeat(Direction.West, west, 0.35); } private void ApplyPeekHeat(Direction dir, PeekEntitiesAsyncResponse peek) { if (!peek.Executed || peek.PlayersInSight <= 0) { return; } var (dx, dy) = Delta(dir); var sightDistance = peek.SightedPlayerDistance ?? 8; var maxTrace = Math.Clamp(sightDistance, 1, 10); for (var i = 1; i <= maxTrace; i++) { var certainty = i == sightDistance ? 5.0 : 0.6; AddHeat(_x + dx * i, _y + dy * i, certainty + peek.PlayersInSight * 0.3); } } private void ApplyDirectionalHeat(Direction dir, int count, double factor) { if (count <= 0) { return; } var (dx, dy) = Delta(dir); for (var i = 1; i <= 8; i++) { AddHeat(_x + dx * i, _y + dy * i, count * factor * (9 - i)); } } private void DecayHeat() { if (_enemyHeat.Count == 0) { DecaySpatialHeat(_visitHeat, 0.94, 0.15); DecaySpatialHeat(_deathHeat, 0.97, 0.10); return; } var keys = _enemyHeat.Keys.ToArray(); foreach (var key in keys) { _enemyHeat[key] *= 0.92; if (_enemyHeat[key] < 0.2) { _enemyHeat.Remove(key); } } DecaySpatialHeat(_visitHeat, 0.94, 0.15); DecaySpatialHeat(_deathHeat, 0.97, 0.10); } private void ApplyMovement(Direction direction, int steps) { var (dx, dy) = Delta(direction); _x += dx * steps; _y += dy * steps; AddHeat(_x, _y, -2.5); MarkVisit(_x, _y, 1.0); } private void AddHeat(int worldX, int worldY, double value) { var key = (worldX, worldY); _enemyHeat.TryGetValue(key, out var current); _enemyHeat[key] = Math.Max(0.0, current + value); } private int RadarScore(Direction direction) { var keys = direction switch { Direction.North => new[] { "north", "n", "up", "0" }, Direction.East => new[] { "east", "e", "right", "1" }, Direction.South => new[] { "south", "s", "down", "2" }, Direction.West => new[] { "west", "w", "left", "3" }, _ => Array.Empty(), }; var score = 0; foreach (var key in keys) { if (_lastRadar.TryGetValue(key, out var value)) { score = Math.Max(score, value); } } return score; } private static Direction DirectionFromVector(int x, int y) { if (Math.Abs(x) >= Math.Abs(y)) { return x >= 0 ? Direction.East : Direction.West; } return y >= 0 ? Direction.South : Direction.North; } private static (int dx, int dy) Delta(Direction direction) { return direction switch { Direction.North => (0, -1), Direction.East => (1, 0), Direction.South => (0, 1), Direction.West => (-1, 0), _ => (0, 0), }; } private void UpdateLevelProfile(string? levelName) { var newProfile = ResolveLevelProfile(levelName); if (newProfile == _currentLevelProfile) { return; } _currentLevelProfile = newProfile; _sessionLogger?.LogEvent("level-profile", $"level={levelName ?? "unknown"};profile={newProfile}"); } private static LevelProfile ResolveLevelProfile(string? levelName) { var value = levelName?.Trim().ToLowerInvariant() ?? string.Empty; if (value.Contains("bigger")) return LevelProfile.BiggerMap; if (value.Contains("more bots")) return LevelProfile.MoreBots; if (value.Contains("new bots")) return LevelProfile.NewBots; if (value.Contains("default")) return LevelProfile.Default; return LevelProfile.Unknown; } private double PressureHpThreshold(LevelProfile profile) => profile switch { LevelProfile.BiggerMap => 0.66, LevelProfile.MoreBots => 0.62, LevelProfile.NewBots => 0.68, LevelProfile.Default => 0.58, _ => 0.60, }; private int PressureThreatThreshold(LevelProfile profile) => profile switch { LevelProfile.BiggerMap => 2, LevelProfile.MoreBots => 2, LevelProfile.NewBots => 2, LevelProfile.Default => 2, _ => 2, }; private int SpecialThreatThreshold(LevelProfile profile) => profile switch { LevelProfile.MoreBots => 4, LevelProfile.NewBots => 3, LevelProfile.BiggerMap => 2, LevelProfile.Default => 2, _ => 2, }; private double SpecialHpFloor(LevelProfile profile) => profile switch { LevelProfile.MoreBots => 0.78, LevelProfile.NewBots => 0.55, LevelProfile.BiggerMap => 0.42, LevelProfile.Default => 0.35, _ => 0.40, }; private double DashHpThreshold(LevelProfile profile) => profile switch { LevelProfile.BiggerMap => 0.50, LevelProfile.NewBots => 0.58, LevelProfile.MoreBots => 0.40, LevelProfile.Default => 0.30, _ => 0.45, }; private int EscapeLockTicks(LevelProfile profile) => profile switch { LevelProfile.BiggerMap => 8, LevelProfile.NewBots => 9, LevelProfile.MoreBots => 6, LevelProfile.Default => 5, _ => 6, }; private bool ShouldUseImmediateHit(LevelProfile profile, double hpRatio, int closeThreats, bool underPressure) => profile switch { LevelProfile.Default => hpRatio >= 0.22 && closeThreats <= 2, LevelProfile.BiggerMap => hpRatio >= 0.25 && closeThreats <= 2, LevelProfile.MoreBots => hpRatio >= 0.35 && closeThreats <= 3, LevelProfile.NewBots => !underPressure && hpRatio >= 0.50, _ => hpRatio >= 0.30 && closeThreats <= 2, }; private bool ShouldRetreatOnImmediate(LevelProfile profile, double hpRatio, int closeThreats) => profile switch { LevelProfile.Default => hpRatio < 0.22 || closeThreats >= 3, LevelProfile.BiggerMap => hpRatio < 0.30 || closeThreats >= 3, LevelProfile.MoreBots => hpRatio < 0.32 || closeThreats >= 3, LevelProfile.NewBots => hpRatio < 0.68 || closeThreats >= 2, _ => hpRatio < 0.30 || closeThreats >= 3, }; private void MarkVisit(int x, int y, double amount) { var key = (x, y); _visitHeat.TryGetValue(key, out var current); _visitHeat[key] = Math.Min(current + amount, 20); } private void MarkDeathZone(int x, int y) { for (var dx = -1; dx <= 1; dx++) { for (var dy = -1; dy <= 1; dy++) { var key = (x + dx, y + dy); _deathHeat.TryGetValue(key, out var current); _deathHeat[key] = Math.Min(current + (dx == 0 && dy == 0 ? 4.0 : 1.5), 12); } } } private static void DecaySpatialHeat(Dictionary<(int X, int Y), double> heatMap, double decay, double minimum) { if (heatMap.Count == 0) { return; } foreach (var key in heatMap.Keys.ToArray()) { heatMap[key] *= decay; if (heatMap[key] < minimum) { heatMap.Remove(key); } } } private double VisitHeatFor(Direction direction) { var (dx, dy) = Delta(direction); var key = (_x + dx, _y + dy); return _visitHeat.TryGetValue(key, out var heat) ? heat : 0; } private double DeathHeatFor(Direction direction) { var (dx, dy) = Delta(direction); var key = (_x + dx, _y + dy); return _deathHeat.TryGetValue(key, out var heat) ? heat : 0; } private bool Ready(BotAction action) { if (!_lastUse.TryGetValue(action, out var time)) { return true; } return DateTimeOffset.UtcNow - time >= Cooldowns[action]; } private void MarkUsed(BotAction action) { _lastUse[action] = DateTimeOffset.UtcNow; } private void ResetKnowledgeState() { _x = 0; _y = 0; _kills = 0; _deaths = 0; _currentHealth = 0; _maxHealth = 10; _progress = 0; _remainingTime = 0; _peekDirectionIndex = 0; _gameToken = null; _nearestEnemy = null; _recentDamageTicks = 0; _survivalTicks = 0; _escapeLockTicks = 0; _lastUnderPressure = false; _lastMoveDirection = Direction.North; _escapeDirection = Direction.North; _pressureStreak = 0; _enemyHeat.Clear(); _visitHeat.Clear(); _deathHeat.Clear(); _lastRadar.Clear(); _lastPeek.Clear(); _lastUse.Clear(); _blockedDirectionTicks.Clear(); _recentDeaths.Clear(); _logEntries.Clear(); RenderKnowledgeGrid(); } private void RenderTelemetry(string decisionReason) { Dispatcher.UIThread.Post(() => { var hpPercent = _maxHealth <= 0.01 ? 0 : (_currentHealth / _maxHealth) * 100.0; var kdr = (_deaths == 0 ? _kills : (double)_kills / _deaths).ToString("0.00", CultureInfo.InvariantCulture); StatsText.Text = $"Level: {_levelName} | Kills: {_kills} | Deaths: {_deaths} | K/D: {kdr} | " + $"HP: {_currentHealth.ToString("0.0", CultureInfo.InvariantCulture)}/{_maxHealth.ToString("0.0", CultureInfo.InvariantCulture)} ({hpPercent.ToString("0", CultureInfo.InvariantCulture)}%) | " + $"Progress: {_progress.ToString("0.0", CultureInfo.InvariantCulture)}% | Remaining: {_remainingTime.ToString("0.00", CultureInfo.InvariantCulture)} min | " + $"Pos (estimated): ({_x}, {_y}) | Profile: {_currentLevelProfile} | SurvivalMode: {(_survivalTicks > 0 ? "ON" : "OFF")}"; var nearest = _nearestEnemy.HasValue ? $"({_nearestEnemy.Value.X}, {_nearestEnemy.Value.Y})" : "none"; var radar = _lastRadar.Count == 0 ? "none" : string.Join(", ", _lastRadar.OrderBy(x => x.Key).Select(x => $"{x.Key}:{x.Value}")); DecisionText.Text = $"Reasoning: {decisionReason}\n" + $"Nearest from scan (relative): {nearest}\n" + $"Radar: {radar}"; CooldownText.Text = string.Join( Environment.NewLine, Cooldowns .OrderBy(x => x.Key.ToString()) .Select(x => $"{x.Key,-8} {RemainingText(x.Key)}")); }); } private string RemainingText(BotAction action) { if (!_lastUse.TryGetValue(action, out var last)) { return "ready"; } var remaining = Cooldowns[action] - (DateTimeOffset.UtcNow - last); if (remaining <= TimeSpan.Zero) { return "ready"; } return $"{remaining.TotalMilliseconds.ToString("0", CultureInfo.InvariantCulture)} ms"; } private void SetStatus(string text) { Dispatcher.UIThread.Post(() => StatusText.Text = text); } private void AppendLog(string message) { Dispatcher.UIThread.Post(() => { var stamp = DateTime.Now.ToString("HH:mm:ss", CultureInfo.InvariantCulture); _logEntries.Insert(0, $"[{stamp}] {message}"); while (_logEntries.Count > 250) { _logEntries.RemoveAt(_logEntries.Count - 1); } }); } private void BuildKnowledgeGrid() { for (var row = 0; row < _mapCells.GetLength(0); row++) { for (var col = 0; col < _mapCells.GetLength(1); col++) { var block = new TextBlock { Text = " ", Width = 18, Height = 18, TextAlignment = TextAlignment.Center, VerticalAlignment = VerticalAlignment.Center, HorizontalAlignment = HorizontalAlignment.Center, FontSize = 11, FontWeight = FontWeight.SemiBold }; _mapCells[row, col] = block; KnowledgeGrid.Children.Add(block); } } } private void RenderKnowledgeGrid() { Dispatcher.UIThread.Post(() => { for (var row = 0; row < _mapCells.GetLength(0); row++) { for (var col = 0; col < _mapCells.GetLength(1); col++) { var worldX = _x + (col - MapRadius); var worldY = _y + (row - MapRadius); var cell = _mapCells[row, col]; var symbol = " "; var foreground = Brushes.Gray; if (worldX == _x && worldY == _y) { symbol = "P"; foreground = Brushes.DodgerBlue; } else if (_nearestEnemy.HasValue && worldX == _x + _nearestEnemy.Value.X && worldY == _y + _nearestEnemy.Value.Y) { symbol = "N"; foreground = Brushes.OrangeRed; } else if (_enemyHeat.TryGetValue((worldX, worldY), out var heat)) { if (heat >= 5) { symbol = "*"; foreground = Brushes.OrangeRed; } else if (heat >= 2) { symbol = "+"; foreground = Brushes.Gold; } else { symbol = "."; foreground = Brushes.LightGray; } } cell.Text = symbol; cell.Foreground = foreground; } } }); } private readonly record struct BotDecision( string Label, string Reason, Func> Execute); private enum LevelProfile { Unknown, Default, BiggerMap, MoreBots, NewBots, } private enum Direction { North = 0, East = 1, South = 2, West = 3, } private enum BotAction { Move, Hit, Radar, Shoot, Peek, Scan, Dash, Special, Teleport, } private static Direction Opposite(Direction direction) { return direction switch { Direction.North => Direction.South, Direction.East => Direction.West, Direction.South => Direction.North, Direction.West => Direction.East, _ => Direction.North, }; } private void RegisterMoveResult(Direction direction, bool moved) { _lastMoveDirection = direction; if (moved) { _blockedDirectionTicks.Remove(direction); return; } _blockedDirectionTicks.TryGetValue(direction, out var current); _blockedDirectionTicks[direction] = Math.Min(current + 4, 20); } private void DecayBlockedDirections() { foreach (var key in _blockedDirectionTicks.Keys.ToArray()) { _blockedDirectionTicks[key]--; if (_blockedDirectionTicks[key] <= 0) { _blockedDirectionTicks.Remove(key); } } } }