Improved
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user