This commit is contained in:
2026-04-02 00:12:02 +02:00
parent f8454d8950
commit 39e943b25e
3 changed files with 588 additions and 8 deletions

View File

@@ -36,6 +36,8 @@ public partial class MainWindow : Window
private readonly DigitalDojoApiClient _client;
private readonly ObservableCollection<string> _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<string, int> _lastRadar = new(StringComparer.OrdinalIgnoreCase);
private readonly Dictionary<Direction, PeekEntitiesAsyncResponse> _lastPeek = [];
private readonly Dictionary<BotAction, DateTimeOffset> _lastUse = [];
@@ -65,9 +67,13 @@ public partial class MainWindow : Window
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()
{
@@ -234,6 +240,11 @@ public partial class MainWindow : Window
_survivalTicks--;
}
if (_escapeLockTicks > 0)
{
_escapeLockTicks--;
}
DecayBlockedDirections();
await PollTelemetryAsync(ct);
DecayHeat();
@@ -297,6 +308,7 @@ public partial class MainWindow : Window
_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)
{
@@ -312,11 +324,15 @@ public partial class MainWindow : Window
_recentDeaths.Dequeue();
}
_survivalTicks = Math.Max(_survivalTicks, 180 + _recentDeaths.Count * 30);
_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}");
}
@@ -330,7 +346,7 @@ public partial class MainWindow : Window
_levelName,
_x,
_y,
$"survival_ticks={_survivalTicks};damage_ticks={_recentDamageTicks}");
$"survival_ticks={_survivalTicks};damage_ticks={_recentDamageTicks};escape_lock={_escapeLockTicks};pressure_streak={_pressureStreak}");
}
catch (Exception ex)
{
@@ -420,6 +436,7 @@ public partial class MainWindow : Window
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)
@@ -432,10 +449,22 @@ public partial class MainWindow : Window
closeThreats++;
}
var underPressure = hpRatio < 0.58 || _recentDamageTicks > 0 || closeThreats >= 2 || _survivalTicks > 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;
if (Ready(BotAction.Special) && closeThreats >= 2)
var specialThreatThreshold = SpecialThreatThreshold(profile);
var specialHpFloor = SpecialHpFloor(profile);
if (Ready(BotAction.Special) && closeThreats >= specialThreatThreshold && (hpRatio >= specialHpFloor || closeThreats >= specialThreatThreshold + 1))
{
return new BotDecision(
"specialattack",
@@ -448,7 +477,7 @@ public partial class MainWindow : Window
});
}
if (immediateDir.HasValue && !underPressure && Ready(BotAction.Hit))
if (immediateDir.HasValue && Ready(BotAction.Hit) && ShouldUseImmediateHit(profile, hpRatio, closeThreats, underPressure))
{
var dir = immediateDir.Value;
return new BotDecision(
@@ -462,11 +491,11 @@ public partial class MainWindow : Window
});
}
if (immediateDir.HasValue && Ready(BotAction.Move))
if (immediateDir.HasValue && Ready(BotAction.Move) && ShouldRetreatOnImmediate(profile, hpRatio, closeThreats))
{
var retreatDirection = Opposite(immediateDir.Value);
if (Ready(BotAction.Dash) && (hpRatio < 0.40 || _survivalTicks > 0))
if (Ready(BotAction.Dash) && (hpRatio < DashHpThreshold(profile) || _pressureStreak >= 3 || _survivalTicks > 0))
{
return new BotDecision(
"dash-retreat",
@@ -478,6 +507,8 @@ public partial class MainWindow : Window
if (response.Executed)
{
ApplyMovement(retreatDirection, Math.Max(response.BlocksDashed, 1));
_escapeDirection = retreatDirection;
_escapeLockTicks = Math.Max(_escapeLockTicks, EscapeLockTicks(profile));
}
return response.Executed;
@@ -496,6 +527,8 @@ public partial class MainWindow : Window
if (moved)
{
ApplyMovement(retreatDirection, 1);
_escapeDirection = retreatDirection;
_escapeLockTicks = Math.Max(_escapeLockTicks, EscapeLockTicks(profile));
}
return response.Executed;
@@ -558,6 +591,11 @@ public partial class MainWindow : Window
if (moved)
{
ApplyMovement(moveDirection, 1);
if (underPressure)
{
_escapeDirection = moveDirection;
_escapeLockTicks = Math.Max(_escapeLockTicks, EscapeLockTicks(profile));
}
}
return response.Executed;
@@ -569,6 +607,14 @@ public partial class MainWindow : Window
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, double>
{
[Direction.North] = RadarScore(Direction.North),
@@ -615,6 +661,20 @@ public partial class MainWindow : Window
}
}
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));
@@ -624,6 +684,18 @@ public partial class MainWindow : Window
}
}
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;
@@ -690,6 +762,8 @@ public partial class MainWindow : Window
{
if (_enemyHeat.Count == 0)
{
DecaySpatialHeat(_visitHeat, 0.94, 0.15);
DecaySpatialHeat(_deathHeat, 0.97, 0.10);
return;
}
@@ -702,6 +776,9 @@ public partial class MainWindow : Window
_enemyHeat.Remove(key);
}
}
DecaySpatialHeat(_visitHeat, 0.94, 0.15);
DecaySpatialHeat(_deathHeat, 0.97, 0.10);
}
private void ApplyMovement(Direction direction, int steps)
@@ -710,6 +787,7 @@ public partial class MainWindow : Window
_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)
@@ -764,6 +842,151 @@ public partial class MainWindow : Window
};
}
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))
@@ -794,10 +1017,15 @@ public partial class MainWindow : Window
_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();
@@ -819,7 +1047,7 @@ public partial class MainWindow : Window
$"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}) | SurvivalMode: {(_survivalTicks > 0 ? "ON" : "OFF")}";
$"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
@@ -955,6 +1183,15 @@ public partial class MainWindow : Window
string Reason,
Func<CancellationToken, Task<bool>> Execute);
private enum LevelProfile
{
Unknown,
Default,
BiggerMap,
MoreBots,
NewBots,
}
private enum Direction
{
North = 0,