Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 8 additions & 6 deletions docs/playerbots.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,12 +65,14 @@ teleports, and unlocked doors. Cardinal movement costs 10; diagonal movement
costs 30. Failed steps are excluded for 10 seconds. Repeated A-B oscillation
suppresses the implicated transition for two minutes.

Rookgaard hunt regions come from loaded spawns on floors 6 through 15 within
180 tiles on each axis of temple `(32097, 32219, 7)`. The planner groups
overlapping eight-tile spawn kernels, then scores loaded health, attacks, armor,
experience, spawn chance, and interval against the bot's health, equipment,
weapon, defense, and skill. The navigator validates the highest-scoring
candidates and supplies spawn-adjacent patrol destinations.
Hunt regions come from all loaded hostile spawns on floors 6 through 15; there
is no player-distance or fixture-radius cap. The shared cache groups overlapping
eight-tile spawn kernels. Each controller scores candidates in bounded batches
using its health, equipment, weapon, defense, skill, cooldowns, and observed
performance. It then validates every suitable candidate incrementally through
the navigator and selects the highest route-adjusted score. Navigation remains
behind the destination/reachability interface so a later navigator can replace
tile planning without changing hunt selection.

Predicted threat rejects a region above `0.35` expected fight damage per maximum
health. It models up to three overlapping hostile spawns. Taking one maximum
Expand Down
64 changes: 63 additions & 1 deletion scripts/test-playerbot-gameplay.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ param(
[switch]$GoalArbitration,
[switch]$OracleDeparture,
[switch]$StaminaProjection,
[switch]$HuntRegionPlanning,
[switch]$Focused,
[switch]$SkipBuild,
[switch]$KeepStack
Expand Down Expand Up @@ -816,6 +817,55 @@ function Assert-StaminaProjectionEvents {
}
}

function Assert-HuntRegionPlanningEvents {
param([string]$Logs)

$events = @(ConvertFrom-PlayerbotLogs -Logs $Logs)
$scored = @($events | Where-Object {
$_.event -eq "hunt_region_scan" -and $_.phase -in @("scoring_started", "scored")
})
$build = @($scored | Where-Object { $_.cache -eq "build" })
$hit = @($scored | Where-Object { $_.cache -eq "hit" })
$cancelled = @($events | Where-Object { $_.event -eq "hunt_region_scan" -and $_.phase -eq "cancelled" })
$staleRevision = @($events | Where-Object { $_.event -eq "hunt_region_scan" -and $_.phase -eq "stale_revision" })
$scoringYields = @($events | Where-Object { $_.event -eq "hunt_region_scan" -and $_.phase -eq "scoring_yield" })
$yields = @($events | Where-Object {
$_.event -eq "hunt_region_scan" -and $_.phase -eq "reachability_yield" -and
$_.pathfinding_calls -ge 1 -and $_.batch_pathfinding_calls -le 1 -and $_.yields -ge 1
})
$selections = @($events | Where-Object { $_.event -eq "hunt_region_selection" -and $_.result -eq "selected" })
$selection = if ($selections.Count -gt 0) { $selections[$selections.Count - 1] } else { $null }
$candidates = @($events | Where-Object { $_.event -eq "hunt_region_candidate" })
$unreachableBeforeSelection = @($candidates | Where-Object {
$_.rejection_reason -eq "unreachable" -and $selection -and $_.region_id -lt $selection.region_id
})
$outsideLocalFixture = @($candidates | Where-Object {
[Math]::Max([Math]::Abs($_.center.x - 32105), [Math]::Abs($_.center.y - 32195)) -gt 32
})
$completed = @($events | Where-Object {
$_.event -eq "hunt_region_scan" -and $_.phase -eq "selected" -and $_.decision_latency_us -gt 0 -and
$_.expanded_nodes -ge 0
})
$selectedCandidate = if ($selection) { @($candidates | Where-Object { $_.region_id -eq $selection.region_id }) } else { @() }
$reachableCandidates = @($candidates | Where-Object { $_.suitable -and $_.reachable })
$bestScore = if ($reachableCandidates.Count -gt 0) { ($reachableCandidates | Measure-Object -Property score -Maximum).Maximum } else { $null }
$nodeBudget = @($candidates | Where-Object { $_.rejection_reason -eq "navigation_node_budget" })
$nodeBudgetMisclassified = @($nodeBudget | Where-Object {
$regionId = $_.region_id
@($candidates | Where-Object { $_.region_id -eq $regionId -and $_.rejection_reason -eq "unreachable" }).Count -gt 0
})
$tooManyPathCalls = @($events | Where-Object {
$_.event -eq "hunt_region_scan" -and $_.phase -eq "reachability_yield" -and $_.batch_pathfinding_calls -gt 1
})
if ($build.Count -lt 2 -or $hit.Count -lt 1 -or $cancelled.Count -ne 1 -or $staleRevision.Count -ne 1 -or $scoringYields.Count -lt 1 -or
$yields.Count -lt 1 -or $tooManyPathCalls.Count -ne 0 -or -not $selection -or $selectedCandidate.Count -ne 1 -or
$bestScore -eq $null -or [Math]::Abs($selectedCandidate[0].score - $bestScore) -gt 0.01 -or
$nodeBudget.Count -lt 1 -or $nodeBudgetMisclassified.Count -ne 0 -or
$unreachableBeforeSelection.Count -lt 1 -or $outsideLocalFixture.Count -lt 1 -or $completed.Count -lt 1) {
throw "Hunt planning telemetry was incomplete. build=$($build.Count), hit=$($hit.Count), cancelled=$($cancelled.Count), stale=$($staleRevision.Count), scoring_yields=$($scoringYields.Count), yields=$($yields.Count), over_budget=$($tooManyPathCalls.Count), selection=$($selection.Count), fallback=$($unreachableBeforeSelection.Count), outside=$($outsideLocalFixture.Count), completed=$($completed.Count)."
}
}

function Assert-GoalArbitrationInterruptEvents {
param([string]$Logs)

Expand Down Expand Up @@ -928,7 +978,7 @@ if (-not (Get-Command docker -ErrorAction SilentlyContinue)) {
}

$focusedScenarioRequested = $FullNavigation -or $CorpseLoot -or $DeathTelemetry -or $Healing -or $ValueLoot -or
$PickupProgression -or $GoalArbitration -or $OracleDeparture -or $StaminaProjection
$PickupProgression -or $GoalArbitration -or $OracleDeparture -or $StaminaProjection -or $HuntRegionPlanning
if ($Focused -and -not $focusedScenarioRequested) {
throw "-Focused requires at least one focused scenario switch."
}
Expand Down Expand Up @@ -1076,6 +1126,18 @@ try {
}
}

if ($HuntRegionPlanning) {
Invoke-Scenario -Name "hunt_region_planning" -DefaultTimeoutSeconds 180 -Body {
Invoke-Compose down --volumes --remove-orphans
$env:PLAYERBOT_GAMEPLAY_MODE = "hunt_planning"
$env:PLAYERBOT_HUNT_DURATION_SECONDS = "900"
Invoke-Compose up --detach
Wait-ForLog -Pattern 'PLAYERBOT_GAMEPLAY_TEST HUNT_PLANNING_START' | Out-Null
$planningLogs = Wait-ForLog -Pattern '"event":"hunt_region_scan".*"phase":"selected"'
Assert-HuntRegionPlanningEvents -Logs $planningLogs
}
}

if ($OracleDeparture) {
Invoke-Scenario -Name "oracle_departure" -DefaultTimeoutSeconds 180 -Body {
Invoke-Compose down --volumes --remove-orphans
Expand Down
11 changes: 10 additions & 1 deletion server/src/game.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
#include "items.h"
#include "monster.h"
#include "movement.h"
#include "playerbothuntregions.h"
#include "scheduler.h"
#include "server.h"
#include "spells.h"
Expand Down Expand Up @@ -5026,7 +5027,13 @@ bool Game::reload(ReloadTypes_t reloadType)
case RELOAD_TYPE_EVENTS: return g_events->load();
case RELOAD_TYPE_GLOBALEVENTS: return g_globalEvents->reload();
case RELOAD_TYPE_ITEMS: return Item::items.reload();
case RELOAD_TYPE_MONSTERS: return g_monsters.reload();
case RELOAD_TYPE_MONSTERS: {
const bool reloaded = g_monsters.reload();
if (reloaded) {
PlayerBotHuntRegionPlanner::invalidateCache();
}
return reloaded;
}
case RELOAD_TYPE_MOVEMENTS: return g_moveEvents->reload();
case RELOAD_TYPE_NPCS: {
Npcs::reload();
Expand All @@ -5044,6 +5051,7 @@ bool Game::reload(ReloadTypes_t reloadType)
std::cout << "[Error - Game::reload] Failed to reload monsters." << std::endl;
std::terminate();
}
PlayerBotHuntRegionPlanner::invalidateCache();
return true;
}

Expand Down Expand Up @@ -5087,6 +5095,7 @@ bool Game::reload(ReloadTypes_t reloadType)
std::cout << "[Error - Game::reload] Failed to reload monsters." << std::endl;
std::terminate();
}
PlayerBotHuntRegionPlanner::invalidateCache();

g_actions->reload();
g_config.reload();
Expand Down
Loading