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.Controls.Primitives; 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 bool _multiplayerModeDefault = bool.TryParse(Program.Configuration["MULTIPLAYER"], out var multiplayer) && multiplayer; 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 (int X, int Y)? _lastObservedEnemyRelative; private (int X, int Y)? _enemyVelocityEstimate; 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; private bool _isMultiplayerMode; private AggressivenessProfile _selectedAggressiveness = AggressivenessProfile.Aggressive; private ModeOptimizationProfile _selectedModeOptimization = ModeOptimizationProfile.Auto; public MainWindow() { _client = new DigitalDojoApiClient("https://game-dd.countit.at", _httpClient); InitializeComponent(); BuildKnowledgeGrid(); ApiKeyBox.Text = _configuredApiKey; MultiplayerCheckBox.IsChecked = _multiplayerModeDefault; AggressivenessProfileBox.SelectedIndex = 2; ModeOptimizationProfileBox.SelectedIndex = 0; SyncProfileSelectionState(); MultiplayerCheckBox.PropertyChanged += (_, e) => { if (e.Property == ToggleButton.IsCheckedProperty) { SyncProfileSelectionState(); } }; AggressivenessProfileBox.SelectionChanged += (_, _) => SyncProfileSelectionState(); ModeOptimizationProfileBox.SelectionChanged += (_, _) => SyncProfileSelectionState(); 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; MultiplayerCheckBox.IsEnabled = false; AggressivenessProfileBox.IsEnabled = false; ModeOptimizationProfileBox.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; MultiplayerCheckBox.IsEnabled = true; AggressivenessProfileBox.IsEnabled = true; ModeOptimizationProfileBox.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; } if (IsMultiplayerMode) { // In multiplayer, the lobby controls game creation/start. AppendLog("Multiplayer mode enabled; waiting for lobby game start (no create call)."); return true; } 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(); } // Minimal survival mode - deaths don't matter, only kills _survivalTicks = Math.Max(_survivalTicks, 4); _escapeLockTicks = Math.Max(_escapeLockTicks, 2); _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 previousEnemy = _nearestEnemy; var sx = scan.DifferenceToNearestPlayer?.X; var sy = scan.DifferenceToNearestPlayer?.Y; _nearestEnemy = sx.HasValue && sy.HasValue ? (sx.Value, sy.Value) : null; if (_nearestEnemy.HasValue) { if (previousEnemy.HasValue) { _enemyVelocityEstimate = ( _nearestEnemy.Value.X - previousEnemy.Value.X, _nearestEnemy.Value.Y - previousEnemy.Value.Y); } _lastObservedEnemyRelative = _nearestEnemy; 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 < PressureHpThresholdAdjusted(profile) || _recentDamageTicks > 0 || closeThreats >= PressureThreatThresholdAdjusted(profile) || _survivalTicks > 0 || _pressureStreak >= 2; _lastUnderPressure = underPressure; var predictedEnemy = PredictEnemyRelative(); var bestSight = _lastPeek .Where(x => x.Value.Executed && x.Value.PlayersInSight > 0) .OrderBy(x => x.Value.SightedPlayerDistance ?? int.MaxValue) .FirstOrDefault(); if (Ready(BotAction.Hit) && immediateDir.HasValue && closeThreats >= 1) { var dir = immediateDir.Value; return new BotDecision( "hit-lock", $"Enemy committed in close range at {dir}; preemptive melee pressure.", async ct => { var response = await _client.HitAsync(_playerToken, (int)dir, ct); MarkUsed(BotAction.Hit); return response.Executed; }); } // Teleport if we can and enemies are nearby - aggressive repositioning for kills if (Ready(BotAction.Teleport) && closeThreats >= 1 && _nearestEnemy.HasValue) { var targetX = _nearestEnemy.Value.X; var targetY = _nearestEnemy.Value.Y; // Teleport closer to enemy for aggressive positioning var moveTowardX = targetX > 0 ? targetX - 1 : targetX + 1; var moveTowardY = targetY > 0 ? targetY - 1 : targetY + 1; var teleportDirection = DirectionFromVector(moveTowardX, moveTowardY); if (IsDirectionUnsafeForHighSpeedMove(teleportDirection)) { moveTowardX = targetX; moveTowardY = targetY; } return new BotDecision( "teleport-aggro", "Close enemy; teleport for aggressive positioning.", async ct => { var response = await _client.TeleportAsync(_playerToken, moveTowardX, moveTowardY, ct); MarkUsed(BotAction.Teleport); if (response.Executed && !response.LandedInWall) { _x = moveTowardX; _y = moveTowardY; } if (response.LandedInWall) { var dir = DirectionFromVector(moveTowardX, moveTowardY); _blockedDirectionTicks.TryGetValue(dir, out var blocked); _blockedDirectionTicks[dir] = Math.Max(blocked, 14); _survivalTicks = Math.Max(_survivalTicks, 10); _sessionLogger?.LogEvent("teleport-wall", $"x={moveTowardX};y={moveTowardY}"); } return response.Executed; }); } if (Ready(BotAction.Dash) && TryGetAggressiveDashDirection(predictedEnemy, bestSight.Key, bestSight.Value, out var dashDirection, out var dashReason)) { return new BotDecision( "dash-aggro", dashReason, async ct => { var response = await _client.DashAsync(_playerToken, (int)dashDirection, ct); MarkUsed(BotAction.Dash); if (response.Executed) { var movedBlocks = Math.Max(response.BlocksDashed, 0); if (movedBlocks > 0) { ApplyMovement(dashDirection, movedBlocks); } if (response.DamageTaken > 0) { _blockedDirectionTicks.TryGetValue(dashDirection, out var blocked); _blockedDirectionTicks[dashDirection] = Math.Max(blocked, 12); _survivalTicks = Math.Max(_survivalTicks, 8); _sessionLogger?.LogEvent("dash-self-damage", $"dir={dashDirection};damage={response.DamageTaken};blocks={response.BlocksDashed}"); } } return response.Executed; }); } var specialThreatThreshold = SpecialThreatThresholdAdjusted(profile); var specialHpFloor = SpecialHpFloorAdjusted(profile); if (Ready(BotAction.Special) && closeThreats >= specialThreatThreshold && (hpRatio >= specialHpFloor || closeThreats > specialThreatThreshold)) { return new BotDecision( "specialattack", $"Multiple enemies ({closeThreats}) close; AoE for maximum kills.", 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 < DashHpThresholdAdjusted(profile) || _pressureStreak >= 3 || _survivalTicks > 0)) { return new BotDecision( "dash-retreat", $"Critical HP escape via dash -> {retreatDirection}.", async ct => { var response = await _client.DashAsync(_playerToken, (int)retreatDirection, ct); MarkUsed(BotAction.Dash); if (response.Executed) { var movedBlocks = Math.Max(response.BlocksDashed, 0); if (movedBlocks > 0) { ApplyMovement(retreatDirection, movedBlocks); } if (response.DamageTaken > 0) { _blockedDirectionTicks.TryGetValue(retreatDirection, out var blocked); _blockedDirectionTicks[retreatDirection] = Math.Max(blocked, 12); _survivalTicks = Math.Max(_survivalTicks, 10); _sessionLogger?.LogEvent("dash-self-damage", $"dir={retreatDirection};damage={response.DamageTaken};blocks={response.BlocksDashed}"); } _escapeDirection = retreatDirection; _escapeLockTicks = Math.Max(_escapeLockTicks, EscapeLockTicks(profile)); } return response.Executed; }); } return new BotDecision( "move-retreat", $"Retreating from {immediateDir.Value} -> {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}; aggressive strike.", async ct => { var response = await _client.HitAsync(_playerToken, (int)dir, ct); MarkUsed(BotAction.Hit); return response.Executed; }); } if (Ready(BotAction.Shoot) && TryGetPredictiveShotDirection(bestSight.Key, bestSight.Value, predictedEnemy, out var shotDirection, out var shotReason)) { return new BotDecision( "shoot", shotReason, async ct => { var response = await _client.ShootAsync(_playerToken, (int)shotDirection, ct); MarkUsed(BotAction.Shoot); return response.Executed; }); } var moveDirection = GetBestMoveDirection(chase: true); if (Ready(BotAction.Move)) { return new BotDecision( "move", $"Aggressive hunt -> {moveDirection}.", 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); } 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 += scored[dir] * 0.5; if (_lastPeek.TryGetValue(dir, out var peek) && peek.Executed) { if (peek.PlayersInSight > 0) { var dist = Math.Max(1, peek.SightedPlayerDistance ?? 6); // Aggressive scoring: high bonus for chasing visible enemies score += 15.0 / dist + peek.PlayersInSight * 3.0; } } if (_nearestEnemy.HasValue) { var targetVector = PredictEnemyRelative() ?? _nearestEnemy.Value; var targetDir = DirectionFromVector(targetVector.X, targetVector.Y); if (dir == targetDir) { score += 12.0; } else if (dir == Opposite(targetDir)) { score -= 8.0; } } // Reduce penalties for visiting/death zones - we're aggressive var visitPenalty = VisitHeatFor(dir); var deathPenalty = DeathHeatFor(dir); score -= visitPenalty * 0.2; score -= deathPenalty * 0.3; if (_escapeLockTicks > 0) { if (dir == _escapeDirection) { score += 2.0; } else if (dir == Opposite(_escapeDirection)) { score -= 1.5; } } if (_blockedDirectionTicks.TryGetValue(dir, out var blockedTicks) && blockedTicks > 0) { score -= 3.0 + blockedTicks * 0.2; } if (dir == Opposite(_lastMoveDirection)) { score -= 0.2; } score += _random.NextDouble() * 0.5; 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.15, LevelProfile.MoreBots => 0.12, LevelProfile.NewBots => 0.18, LevelProfile.Default => 0.10, _ => 0.15, }; private int PressureThreatThreshold(LevelProfile profile) => profile switch { LevelProfile.BiggerMap => 5, LevelProfile.MoreBots => 6, LevelProfile.NewBots => 5, LevelProfile.Default => 6, _ => 5, }; private int SpecialThreatThreshold(LevelProfile profile) => profile switch { LevelProfile.MoreBots => 2, LevelProfile.NewBots => 1, LevelProfile.BiggerMap => 1, LevelProfile.Default => 1, _ => 1, }; private double SpecialHpFloor(LevelProfile profile) => profile switch { LevelProfile.MoreBots => 0.20, LevelProfile.NewBots => 0.15, LevelProfile.BiggerMap => 0.10, LevelProfile.Default => 0.05, _ => 0.10, }; private double DashHpThreshold(LevelProfile profile) => profile switch { LevelProfile.BiggerMap => 0.08, LevelProfile.NewBots => 0.12, LevelProfile.MoreBots => 0.10, LevelProfile.Default => 0.05, _ => 0.10, }; private int EscapeLockTicks(LevelProfile profile) => profile switch { LevelProfile.BiggerMap => 2, LevelProfile.NewBots => 2, LevelProfile.MoreBots => 2, LevelProfile.Default => 2, _ => 2, }; private bool ShouldUseImmediateHit(LevelProfile profile, double hpRatio, int closeThreats, bool underPressure) { var hpFloor = SelectedAggressiveness switch { AggressivenessProfile.Conservative => 0.30, AggressivenessProfile.Balanced => 0.16, AggressivenessProfile.Aggressive => 0.05, AggressivenessProfile.Extreme => 0.01, _ => 0.05, }; if (IsMultiplayerOptimized) { hpFloor = Math.Max(hpFloor, 0.18); } return hpRatio >= hpFloor || (!underPressure && closeThreats <= 1); } private bool ShouldRetreatOnImmediate(LevelProfile profile, double hpRatio, int closeThreats) { var hpRetreat = SelectedAggressiveness switch { AggressivenessProfile.Conservative => 0.30, AggressivenessProfile.Balanced => 0.18, AggressivenessProfile.Aggressive => 0.02, AggressivenessProfile.Extreme => 0.01, _ => 0.02, }; if (IsMultiplayerOptimized) { hpRetreat = Math.Max(hpRetreat, 0.20); } return hpRatio < hpRetreat || (IsMultiplayerOptimized && closeThreats >= 3); } private double PressureHpThresholdAdjusted(LevelProfile profile) { var value = PressureHpThreshold(profile); value += SelectedAggressiveness switch { AggressivenessProfile.Conservative => 0.20, AggressivenessProfile.Balanced => 0.08, AggressivenessProfile.Aggressive => 0.00, AggressivenessProfile.Extreme => -0.05, _ => 0.00, }; if (IsMultiplayerOptimized) { value += 0.08; } return Math.Clamp(value, 0.01, 0.95); } private int PressureThreatThresholdAdjusted(LevelProfile profile) { var value = PressureThreatThreshold(profile); value += SelectedAggressiveness switch { AggressivenessProfile.Conservative => -2, AggressivenessProfile.Balanced => -1, AggressivenessProfile.Aggressive => 0, AggressivenessProfile.Extreme => 1, _ => 0, }; if (IsMultiplayerOptimized) { value -= 1; } return Math.Clamp(value, 1, 8); } private int SpecialThreatThresholdAdjusted(LevelProfile profile) { var value = SpecialThreatThreshold(profile); value += SelectedAggressiveness switch { AggressivenessProfile.Conservative => 1, AggressivenessProfile.Balanced => 0, AggressivenessProfile.Aggressive => 0, AggressivenessProfile.Extreme => -1, _ => 0, }; if (IsMultiplayerOptimized) { value += 1; } return Math.Clamp(value, 1, 6); } private double SpecialHpFloorAdjusted(LevelProfile profile) { var value = SpecialHpFloor(profile); value += SelectedAggressiveness switch { AggressivenessProfile.Conservative => 0.20, AggressivenessProfile.Balanced => 0.10, AggressivenessProfile.Aggressive => 0.00, AggressivenessProfile.Extreme => -0.05, _ => 0.00, }; if (IsMultiplayerOptimized) { value += 0.10; } return Math.Clamp(value, 0.01, 0.95); } private double DashHpThresholdAdjusted(LevelProfile profile) { var value = DashHpThreshold(profile); value += SelectedAggressiveness switch { AggressivenessProfile.Conservative => 0.18, AggressivenessProfile.Balanced => 0.10, AggressivenessProfile.Aggressive => 0.00, AggressivenessProfile.Extreme => -0.03, _ => 0.00, }; if (IsMultiplayerOptimized) { value += 0.10; } return Math.Clamp(value, 0.01, 0.95); } private (int X, int Y)? PredictEnemyRelative() { if (!_nearestEnemy.HasValue) { return _lastObservedEnemyRelative; } if (_enemyVelocityEstimate is null) { return _nearestEnemy; } return ( _nearestEnemy.Value.X + _enemyVelocityEstimate.Value.X, _nearestEnemy.Value.Y + _enemyVelocityEstimate.Value.Y); } private bool TryGetPredictiveShotDirection( Direction? sightDirection, PeekEntitiesAsyncResponse? sightResponse, (int X, int Y)? predictedEnemy, out Direction direction, out string reason) { if (sightDirection is null || sightResponse is null) { if (predictedEnemy.HasValue) { direction = DirectionFromVector(predictedEnemy.Value.X, predictedEnemy.Value.Y); reason = $"Predicted enemy lane -> {direction}; taking lead shot."; return true; } direction = Direction.North; reason = string.Empty; return false; } var currentDir = sightDirection.Value; var dist = sightResponse.SightedPlayerDistance ?? 99; if (predictedEnemy.HasValue && dist >= 2) { var predictedDir = DirectionFromVector(predictedEnemy.Value.X, predictedEnemy.Value.Y); if (predictedDir != currentDir) { direction = predictedDir; reason = $"Enemy moving toward {predictedDir}; lead shot instead of static line {currentDir}."; return true; } } direction = currentDir; reason = $"Enemy at {currentDir} ({dist} dist); ranged kill."; return true; } private bool TryGetAggressiveDashDirection( (int X, int Y)? predictedEnemy, Direction? sightDirection, PeekEntitiesAsyncResponse? sightResponse, out Direction direction, out string reason) { if (_nearestEnemy.HasValue) { var target = predictedEnemy ?? _nearestEnemy.Value; var dist = Math.Abs(target.X) + Math.Abs(target.Y); if (dist >= 2 && dist <= 4) { var candidate = DirectionFromVector(target.X, target.Y); if (!IsDirectionUnsafeForHighSpeedMove(candidate)) { direction = candidate; reason = $"Aggressive gap-close toward enemy at {direction} (predicted distance {dist})."; return true; } } } if (sightDirection.HasValue && sightResponse is not null && sightResponse.Executed && sightResponse.PlayersInSight > 0) { var dist = sightResponse.SightedPlayerDistance ?? 99; if (dist >= 2 && dist <= 4 && !IsDirectionUnsafeForHighSpeedMove(sightDirection.Value)) { direction = sightDirection.Value; reason = $"Enemy visible at {direction} ({dist} dist); dash to force close-range combat."; return true; } } direction = Direction.North; reason = string.Empty; return false; } private bool IsDirectionUnsafeForHighSpeedMove(Direction direction) { if (_blockedDirectionTicks.TryGetValue(direction, out var blocked) && blocked > 0) { return true; } var deathHeatLimit = SelectedAggressiveness switch { AggressivenessProfile.Conservative => 2.5, AggressivenessProfile.Balanced => 3.2, AggressivenessProfile.Aggressive => 4.0, AggressivenessProfile.Extreme => 5.0, _ => 4.0, }; if (IsMultiplayerOptimized) { deathHeatLimit -= 0.6; } return DeathHeatFor(direction) >= deathHeatLimit; } 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} | Aggro: {SelectedAggressiveness} | Mode: {SelectedModeOptimization} | 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 bool IsMultiplayerMode => _isMultiplayerMode; private AggressivenessProfile SelectedAggressiveness => _selectedAggressiveness; private ModeOptimizationProfile SelectedModeOptimization => _selectedModeOptimization; private bool IsMultiplayerOptimized => SelectedModeOptimization switch { ModeOptimizationProfile.SingleplayerOptimized => false, ModeOptimizationProfile.MultiplayerOptimized => true, _ => _isMultiplayerMode, }; private void SyncProfileSelectionState() { _isMultiplayerMode = MultiplayerCheckBox.IsChecked == true; _selectedAggressiveness = AggressivenessProfileBox.SelectedIndex switch { 0 => AggressivenessProfile.Conservative, 1 => AggressivenessProfile.Balanced, 2 => AggressivenessProfile.Aggressive, 3 => AggressivenessProfile.Extreme, _ => AggressivenessProfile.Aggressive, }; _selectedModeOptimization = ModeOptimizationProfileBox.SelectedIndex switch { 1 => ModeOptimizationProfile.SingleplayerOptimized, 2 => ModeOptimizationProfile.MultiplayerOptimized, _ => ModeOptimizationProfile.Auto, }; } 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 AggressivenessProfile { Conservative, Balanced, Aggressive, Extreme, } private enum ModeOptimizationProfile { Auto, SingleplayerOptimized, MultiplayerOptimized, } 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); } } } }