diff --git a/DDApp/DDApplication/MainWindow.axaml.cs b/DDApp/DDApplication/MainWindow.axaml.cs index 2a46ef5..c3be011 100644 --- a/DDApp/DDApplication/MainWindow.axaml.cs +++ b/DDApp/DDApplication/MainWindow.axaml.cs @@ -315,26 +315,27 @@ public partial class MainWindow : Window _recentDamageTicks = 20; } - if (_deaths > previousDeaths) - { - var nowDeath = DateTimeOffset.UtcNow; - _recentDeaths.Enqueue(nowDeath); - while (_recentDeaths.Count > 0 && nowDeath - _recentDeaths.Peek() > TimeSpan.FromSeconds(90)) - { - _recentDeaths.Dequeue(); - } + 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}"); - } + // 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, @@ -433,285 +434,275 @@ public partial class MainWindow : Window } } - private BotDecision? BuildDecision() - { - var hpRatio = _maxHealth > 0.1 ? _currentHealth / _maxHealth : 1.0; - var profile = _currentLevelProfile; + 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 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++; - } + 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); - } + 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 underPressure = hpRatio < PressureHpThreshold(profile) || _recentDamageTicks > 0 || closeThreats >= PressureThreatThreshold(profile) || _survivalTicks > 0 || _pressureStreak >= 2; + _lastUnderPressure = underPressure; - var specialThreatThreshold = SpecialThreatThreshold(profile); - var specialHpFloor = SpecialHpFloor(profile); + // 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; - 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; - }); - } + 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) + { + _x = moveTowardX; + _y = moveTowardY; + } + 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; - }); - } + var specialThreatThreshold = SpecialThreatThreshold(profile); + var specialHpFloor = SpecialHpFloor(profile); - if (immediateDir.HasValue && Ready(BotAction.Move) && ShouldRetreatOnImmediate(profile, hpRatio, closeThreats)) - { - var retreatDirection = Opposite(immediateDir.Value); + if (Ready(BotAction.Special) && 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 (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)); - } + 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; + }); + } - return response.Executed; - }); - } + if (immediateDir.HasValue && Ready(BotAction.Move) && ShouldRetreatOnImmediate(profile, hpRatio, closeThreats)) + { + var retreatDirection = Opposite(immediateDir.Value); - 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)); - } + if (Ready(BotAction.Dash) && (hpRatio < DashHpThreshold(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) + { + ApplyMovement(retreatDirection, Math.Max(response.BlocksDashed, 1)); + _escapeDirection = retreatDirection; + _escapeLockTicks = Math.Max(_escapeLockTicks, EscapeLockTicks(profile)); + } - return response.Executed; - }); - } + 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; - }); - } + 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)); + } - var bestSight = _lastPeek - .Where(x => x.Value.Executed && x.Value.PlayersInSight > 0) - .OrderBy(x => x.Value.SightedPlayerDistance ?? int.MaxValue) - .FirstOrDefault(); + return response.Executed; + }); + } - 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; - }); - } - } + 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; + }); + } - var moveDirection = GetBestMoveDirection(chase: !underPressure); - if (Ready(BotAction.Move)) - { - var reason = underPressure - ? $"Pressure high; reposition toward safer lane -> {moveDirection}." - : $"Controlled hunt path -> {moveDirection}."; + var bestSight = _lastPeek + .Where(x => x.Value.Executed && x.Value.PlayersInSight > 0) + .OrderBy(x => x.Value.SightedPlayerDistance ?? int.MaxValue) + .FirstOrDefault(); - 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)); - } - } + // More aggressive shooting + if (bestSight.Value is not null && Ready(BotAction.Shoot)) + { + var dir = bestSight.Key; + var dist = bestSight.Value.SightedPlayerDistance ?? 99; + return new BotDecision( + "shoot", + $"Enemy at {dir} ({dist} dist); ranged kill.", + async ct => + { + var response = await _client.ShootAsync(_playerToken, (int)dir, ct); + MarkUsed(BotAction.Shoot); + return response.Executed; + }); + } - 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 null; - } + return response.Executed; + }); + } - private Direction GetBestMoveDirection(bool chase) - { - if (!chase && _escapeLockTicks > 0) - { - if (!_blockedDirectionTicks.TryGetValue(_escapeDirection, out var blocked) || blocked <= 0) - { - return _escapeDirection; - } - } + return null; + } - var scored = new Dictionary - { - [Direction.North] = RadarScore(Direction.North), - [Direction.East] = RadarScore(Direction.East), - [Direction.South] = RadarScore(Direction.South), - [Direction.West] = RadarScore(Direction.West), - }; + private Direction GetBestMoveDirection(bool chase) + { + if (!chase && _escapeLockTicks > 0) + { + if (!_blockedDirectionTicks.TryGetValue(_escapeDirection, out var blocked) || blocked <= 0) + { + return _escapeDirection; + } + } - foreach (var dir in scored.Keys.ToArray()) - { - var score = 0.0; - score += chase ? scored[dir] * 1.35 : -scored[dir] * 1.0; + var scored = new Dictionary + { + [Direction.North] = RadarScore(Direction.North), + [Direction.East] = RadarScore(Direction.East), + [Direction.South] = RadarScore(Direction.South), + [Direction.West] = RadarScore(Direction.West), + }; - 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; - } - } + foreach (var dir in scored.Keys.ToArray()) + { + var score = 0.0; + // Always chase aggressively - ignore radar safety + score += scored[dir] * 0.5; - 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; - } - } + 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; + } + } - var visitPenalty = VisitHeatFor(dir); - var deathPenalty = DeathHeatFor(dir); + if (_nearestEnemy.HasValue) + { + var targetDir = DirectionFromVector(_nearestEnemy.Value.X, _nearestEnemy.Value.Y); + // Always move toward nearest enemy + if (dir == targetDir) + { + score += 12.0; + } + else if (dir == Opposite(targetDir)) + { + score -= 8.0; + } + } - if (_currentLevelProfile == LevelProfile.BiggerMap) - { - score -= visitPenalty * 1.4; - } - else - { - score -= visitPenalty * 0.8; - } + // Reduce penalties for visiting/death zones - we're aggressive + var visitPenalty = VisitHeatFor(dir); + var deathPenalty = DeathHeatFor(dir); - score -= deathPenalty * 1.6; + score -= visitPenalty * 0.2; + score -= deathPenalty * 0.3; - 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 += 2.0; + } + else if (dir == Opposite(_escapeDirection)) + { + score -= 1.5; + } + } - 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 -= 3.0 + blockedTicks * 0.2; + } - if (_blockedDirectionTicks.TryGetValue(dir, out var blockedTicks) && blockedTicks > 0) - { - score -= 5.0 + blockedTicks * 0.4; - } + if (dir == Opposite(_lastMoveDirection)) + { + score -= 0.2; + } - if (dir == Opposite(_lastMoveDirection)) - { - score -= 0.6; - } + score += _random.NextDouble() * 0.5; + scored[dir] = score; + } - score += _survivalTicks > 0 ? _random.NextDouble() * 0.2 : _random.NextDouble() * 0.7; - scored[dir] = score; - } - - return scored.OrderByDescending(x => x.Value).First().Key; - } + return scored.OrderByDescending(x => x.Value).First().Key; + } private void ApplyRadarHeat() { @@ -864,77 +855,77 @@ public partial class MainWindow : Window 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 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 => 2, - LevelProfile.MoreBots => 2, - LevelProfile.NewBots => 2, - LevelProfile.Default => 2, - _ => 2, - }; + 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 => 4, - LevelProfile.NewBots => 3, - LevelProfile.BiggerMap => 2, - LevelProfile.Default => 2, - _ => 2, - }; + 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.78, - LevelProfile.NewBots => 0.55, - LevelProfile.BiggerMap => 0.42, - LevelProfile.Default => 0.35, - _ => 0.40, - }; + 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.50, - LevelProfile.NewBots => 0.58, - LevelProfile.MoreBots => 0.40, - LevelProfile.Default => 0.30, - _ => 0.45, - }; + 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 => 8, - LevelProfile.NewBots => 9, - LevelProfile.MoreBots => 6, - LevelProfile.Default => 5, - _ => 6, - }; + 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) => 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 ShouldUseImmediateHit(LevelProfile profile, double hpRatio, int closeThreats, bool underPressure) => profile switch + { + LevelProfile.Default => hpRatio >= 0.05, + LevelProfile.BiggerMap => hpRatio >= 0.05, + LevelProfile.MoreBots => hpRatio >= 0.05, + LevelProfile.NewBots => hpRatio >= 0.05, + _ => hpRatio >= 0.05, + }; - 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 bool ShouldRetreatOnImmediate(LevelProfile profile, double hpRatio, int closeThreats) => profile switch + { + LevelProfile.Default => hpRatio < 0.02, + LevelProfile.BiggerMap => hpRatio < 0.02, + LevelProfile.MoreBots => hpRatio < 0.02, + LevelProfile.NewBots => hpRatio < 0.02, + _ => hpRatio < 0.02, + }; private void MarkVisit(int x, int y, double amount) {