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
8 changes: 7 additions & 1 deletion docs/playerbots.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,17 @@ Focused scenarios often use fixed destinations, controlled monsters, teleports,

Navigation is hierarchical: [`PlayerBotTopology`](../server/src/playerbottopology.cpp) indexes the loaded map into connected local regions and transition edges. The [controller](../server/src/playerbotcontroller.cpp) selects a global route, refines its next segment into tile-level actions, and considers eligible NPC travel connections. Whole-map routing describes its scope, not a guarantee that every destination is reachable.

Raid definitions and activation are innate bot knowledge. The [raid overlay](../server/src/playerbotraidhazards.h) adds configured species and spawn footprints to the cached static danger model without tracking hidden live creatures. Footprints describe potential danger, not monster leashes. Warnings survive event completion and raid reload; a future wave cannot be cleared merely because its tiles are currently empty. Script-only raid spawns have no inferred footprint, but visible threats still apply.

[Travel safety](../server/src/playerbottravelsafety.cpp) uses bounded private sightings and exact visible-tile clearance across activities. Fresh threats override clearance; losing sight does not establish death, and expired clearance restores the warning rather than declaring safety. Memory survives death recovery within the server run, but not server restart. Planning freezes evidence into a private spatial overlay without rebuilding the launch-time heatmap or mixing observations into shared static caches. Innate raid changes start a new planning epoch. Ordinary sightings do not discard whole search frontiers or return coverage: completed and retained executable legs are checked against fresh exposure before admission or reuse, and movement still checks current threats. Paid itineraries replan later legs after arrival; their retained first-leg checks do not establish fresh clearance of every future leg.

Unsafe observed encounters interrupt hunt, depot, service, trainer, progression, and loot work. The bot cancels conflicting movement/combat, heals normally, and attempts a visible ordinary step toward lower exposure or a reachable adjacent protection zone. Detailed routes exclude excessive transient exposure; the final movement gate also checks reused routes, transitions, and sidesteps. If no safe route exists, travel is deferred, not counted as repeated pathfinding failure. A remote warning permits only a bounded approach over visible zero-exposure ground to gain fresh evidence. Withdrawal is local, not a pursuit simulation: trapped bots or faster pursuers can still kill them. There is no automatic raid-hunting goal, global all-clear signal, or guarantee of survival.

Patrol approach selection and detailed navigation share the peak-danger ceiling. An exact or any-of goal whose usable endpoints all exceed that ceiling reports `risk_rejected` before route expansion; genuine search exhaustion remains unknown. Spawn danger adds direct attacks, but merges modeled damage conditions by the engine’s type/ID/sub-ID key. Built-in XML and Lua delayed damage conditions use their loaded tick rules with conservative sustained uptime; scripted spells and field damage are not inferred. These are forecasts, not measured encounter damage.

Hunt selection uses a [resumable route planner](../server/src/playerbothunttravel.cpp). Walking and NPC approach connections retain a coarse itinerary through connected pieces of the map's sectors. Ordinary segments use destination-guided tile search; stairs and teleports remain explicit crossings, checked against the live world. An unsafe or failed itinerary first tries an alternative coarse route, then searches progressively wider corridors of connected map pieces before falling back to the whole live graph. An itinerary whose coarse danger estimate and walked prefix both exceed the risk budget skips those fallbacks and stays unknown. All attempts share the original connection allowance; a restricted failure cannot prove unreachability. When the source and every goal are graph nodes, a graph "no path" is final for the loaded map; off-graph endpoints retain live validation. The graph includes every possible rope and ladder landing for connectivity, but itineraries use only the landing the climb script chooses. It also includes traversable doors, known shovel passages, and tiles blocked only by movable items, but not parcel-only climbs or machete/pick passages. Live tile changes still invalidate affected detailed searches. Scripted or admin map edits that add routes require `/reload scripts` after temporary blockers clear; the rebuild uses the live map. Corridor and whole-map searches use a shortcut-aware lower bound that includes directed teleports and floor redirects. Its relaxed geometry ignores obstacles and collapses floors, so guidance remains optimistic rather than excluding potentially better routes. Live shortcut changes restart affected search priorities; ordinary map changes do not discard the guidance. All valid adjacent NPC approach tiles form one destination set rather than separate searches; teleport tiles are never approach goals. Coarse travel estimates cannot establish an accepted route or reject one for danger: detailed connections supply that evidence. Each bot owns a risk profile (currently the same defaults). Hunt and service route decisions use detailed risk evidence; a required hunt return can use the least-risk executable depot approach when no safe one is available. The selector retains alternative fare and risk labels and uses safe route-cost bounds rather than a fixed shortlist. Paid offers must leave the minimum recovery reserve; free offers remain eligible.

Depot liquidation keeps seller candidates and unfinished routes across turns. Before detailed planning, it rules out candidates with no affordable travel chain or possible profit, using lower bounds on fare and travel time. It checks the remaining candidates in order of potential profit, then revalidates routes before withdrawing stock. A failed source route rules out that depot's other sellers for the scan. A failed seller route tries other conversation tiles before rejecting the seller. Service approaches track a wandering NPC, wait briefly if it leaves speech range, and replan around blocked steps. Withdrawals respect backpack space and carrying capacity; excess stock waits for another trip. Paid travel retains the recovery fare reserve. Newly available travel offers may wait until the next scan. Both legs use the shared sliced route engine, including walking-only seller routes. Walking-only requests exclude NPC offers and active blocked tiles. Interrupted or exhausted legs revalidate through that engine; execution checks live risk, funds, quoted fare and steps. If two paid legs do not fit the budget together, a safe walking source replaces the paid one. Candidate snapshots still run synchronously.
Depot liquidation keeps seller candidates and unfinished routes across turns. Before detailed planning, it rules out candidates with no affordable travel chain or possible profit, using lower bounds on fare and travel time. It checks the remaining candidates in order of potential profit, then revalidates routes before withdrawing stock. A failed source route rules out that depot's other sellers for the scan. A failed seller route tries other conversation tiles before rejecting the seller. Service approaches track a wandering NPC, wait briefly if it leaves speech range, and replan around blocked steps. Temporary threat rejection and active movement exclusions preserve the pending provider rather than blacklist it. Incomplete searches receive bounded retries; exhaustion reports `service_route_planning_incomplete`, not a missing shop offer. Withdrawals respect backpack space and carrying capacity; excess stock waits for another trip. Paid travel retains the recovery fare reserve. Newly available travel offers may wait until the next scan. Both legs use the shared sliced route engine, including walking-only seller routes. Walking-only requests exclude NPC offers and active blocked tiles. Interrupted or exhausted legs revalidate through that engine; execution checks live risk, funds, quoted fare and steps. If two paid legs do not fit the budget together, a safe walking source replaces the paid one. Candidate snapshots still run synchronously.

Coarse connectivity, successful ordinary walking segments and combat-profile-specific danger samples use bounded, shared caches. Live movement checks and watched map/line-of-sight dependencies protect reuse across callers; capability, risk-model and topology inputs remain part of cache identity. Unrestricted fallbacks reuse a source search tree across goals within the same actor's planning pass. Retargeting changes frontier priorities while retaining final settled costs. A settled goal supplies a route once the frontier lower bound rules out a cheaper target; otherwise the existing search continues. Unsettled goals remain unknown at the limit, never proof of unreachability or an unreachable-region cooldown. A graph-connected path that exhausts the detailed-search allowance remains unknown. Relevant world and provider changes invalidate dependent work. Harmless corpse decay that changes only movable/pickupable flags preserves route evidence; mobility changes on blockers, height items, and passages still invalidate it. The [shared planning budget](../server/src/playerbotplanningbudget.h) charges actual elapsed work across controllers and queues continuations fairly. It does not create parallel game threads or preempt an expensive operation. Topology primitives and candidate output remain synchronous, so neither the budget nor focused fixtures establish a hard latency bound or measured ten-bot capacity. Movement and NPC execution still replan against the live world.

Expand Down
8 changes: 8 additions & 0 deletions docs/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ Ranged positioning (#235) has `ranged_position_control` (the Paladin fights like

Each changed positioning decision or retreat request emits `ranged_position` with `action` (`hold`, `retreat`, `close`, `fight_in_place`), `reason`, `distance`, `range`, raw speeds, current cardinal step durations (`self_step_ms`, `target_step_ms`), and the requested retreat `tile`. `ranged_position_step` records observed `completed` or `failed` movement; a request alone is not displacement evidence.

Raid travel safety (#267) has `raid_travel` and `raid_recovery`, selected with `-Focused -RaidTravel` or exact `-Scenario` names. Both use real raid XML to spawn one Lua-defined **Raid Safety Wolf** on an isolated runway. The unique species avoids later XML loading overwriting its fixed 40-HP life drain every two seconds and speed 160. Position samples must prove withdrawal, a five-second live-threat guard, and arrival after controlled removal; safety telemetry alone cannot pass. The guard rejects unsafe forward retries, not distance lost to a closing pursuer while the bot waits for an ordinary step. Recovery injects one combat death, then requires normal recovery and protection against the same surviving threat. These controlled scenarios do not establish natural raid survival or sustained progression. Non-Docker checks are `lua server/tests/playerbot_raid_fixture_contracts.lua` and `pwsh -File server/tests/playerbot_raid_fixture_assertions.ps1`.

`-Scenario deep_hunt_return` seeds a level-15 Knight at `(33101,31745,9)` and checks arrival at the planner-selected depot locker approach. It does not prove unattended hunt progression.

`-SkipBuild` requires a known-current `angelion-server:latest` image and does not prove it matches the worktree. `-KeepStack` preserves the final stack for debugging. `-TimeoutSeconds` accepts 30–3600 seconds. Most focused scenarios use controlled state or destinations; map-derived planning modes improve integration evidence but still do not prove long-running progression.
Expand Down Expand Up @@ -110,6 +112,12 @@ sh server/tests/playerbotroutecorridor_contracts.sh
sh server/tests/playerbotguidedtree_contracts.sh
sh server/tests/playerbotroutecache_contracts.sh
sh server/tests/playerbotdangersamplekey_contracts.sh
sh server/tests/playerbotraidhazards_contracts.sh
sh server/tests/playerbottravelsafety_contracts.sh
sh server/tests/playerbottravelchurn_contracts.sh
sh server/tests/playerbotservicerouteretry_contracts.sh
# Uses server development headers; set CPPFLAGS as described below.
sh server/tests/playerbottransientnavigation_contracts.sh
sh server/tests/playerbotplanningbudget_contracts.sh
sh server/tests/playerbotdepottelemetry_contracts.sh
sh server/tests/playerbot_loot_contracts.sh
Expand Down
92 changes: 92 additions & 0 deletions scripts/playerbot-gameplay/assertions-raid.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
function Assert-RaidTravelEvents {
param([string]$Logs, [ValidateSet('raid_travel', 'raid_recovery')][string]$Mode)

if ($Logs -match 'RAID_FIXTURE_FAILED' -or
$Logs -notmatch ('PLAYERBOT_GAMEPLAY_TEST ' + $Mode.ToUpperInvariant() + '_PASS')) {
throw 'Raid travel lacks a successful independent position verifier.'
}
$events = @(ConvertFrom-PlayerbotLogs -Logs $Logs)
$phases = @{}
$phaseOrder = @('start', 'activated', 'spawned', 'withdrew')
if ($Mode -eq 'raid_recovery') { $phaseOrder += @('kill_injected', 'death', 'recovered') }
$phaseOrder += @('guarded', 'removed', 'arrived')
$nextPhase = 0
$withdrawal = $false
$recoverySafety = $false
$resume = $false
$dead = 0
$recovered = $false
for ($i = 0; $i -lt $events.Count; ++$i) {
$e = $events[$i]
if ($e.bot -and $e.bot -ne 'Bot One') { throw 'Raid fixture must seed only Bot One.' }
if ($e.event -eq 'raid_fixture') {
if ($e.scenario -ne $Mode -or $e.source -ne 'lua_position_verifier' -or
$nextPhase -ge $phaseOrder.Count -or $e.phase -ne $phaseOrder[$nextPhase]) {
throw 'Raid position evidence is missing, duplicated, or out of order.'
}
if ($e.z -ne 7 -or $e.x -lt 60128 -or $e.x -gt 60216 -or $e.y -ne 60200 -or
($e.phase -ne 'death' -and $e.health -le 0)) {
throw 'Raid position evidence left the live isolated runway.'
}
$phases[$e.phase] = $e
++$nextPhase
}
if ($e.event -eq 'lifecycle' -and $e.status -eq 'dead') {
if ($Mode -ne 'raid_recovery' -or -not $phases.ContainsKey('kill_injected') -or
$phases.ContainsKey('recovered') -or ++$dead -ne 1) {
throw 'Raid caused an unplanned production death.'
}
}
if ($e.event -eq 'terminal' -and -not ($Mode -eq 'raid_recovery' -and
$e.reason -eq 'controlled_player_dead' -and $phases.ContainsKey('kill_injected') -and
-not $phases.ContainsKey('recovered'))) {
throw "Raid controller stopped unexpectedly: $($e.reason)"
}
if ($e.event -eq 'lifecycle' -and $e.status -eq 'online' -and $e.recovered -eq $true) {
if ($Mode -ne 'raid_recovery' -or -not $phases.ContainsKey('recovered') -or
$phases.ContainsKey('removed') -or $recovered) {
throw 'Raid recovery was missing, repeated, or too late.'
}
$recovered = $true
}
if ($e.event -eq 'travel_safety') {
if ($e.action -notin @('withdraw', 'defer', 'resume', 'step_rejected') -or
-not $e.reason -or $null -eq $e.danger -or [double]$e.danger -lt 0) {
throw 'Raid safety telemetry lacks its action/danger/reason contract.'
}
if ($phases.ContainsKey('activated') -and -not $phases.ContainsKey('removed')) {
if ($e.action -eq 'withdraw' -and [double]$e.danger -gt 0.08) { $withdrawal = $true }
if ($recovered -and $e.action -in @('withdraw', 'defer', 'step_rejected')) { $recoverySafety = $true }
}
if ($phases.ContainsKey('removed') -and -not $phases.ContainsKey('arrived') -and
$e.action -eq 'resume') { $resume = $true }
}
}
if ($nextPhase -ne $phaseOrder.Count -or -not $withdrawal -or -not $resume) {
throw 'Raid travel needs a real spawn, observed withdrawal, guarded clearance, resume, and arrival.'
}
if ($phases.start.x -ne 60160 -or $phases.start.y -ne 60200 -or $phases.start.health_max -ne 185 -or
$phases.start.goal_x -ne 60208 -or $phases.start.goal_y -ne 60200 -or $phases.start.goal_z -ne 7 -or
$phases.activated.x -lt $phases.start.x + 8 -or $phases.activated.spawn_x -ne 60169 -or
$phases.activated.spawn_y -ne 60200 -or $phases.activated.spawn_z -ne 7 -or
$phases.spawned.count -ne 1 -or $phases.spawned.target_id -le 0 -or
$phases.withdrew.from_x -lt $phases.activated.x -or
$phases.withdrew.from_x - $phases.withdrew.x -lt 2 -or $phases.guarded.guard_ms -lt 5000 -or
$phases.arrived.x -ne $phases.start.goal_x -or $phases.arrived.y -ne $phases.start.goal_y -or
$phases.arrived.elapsed_ms -le $phases.removed.elapsed_ms) {
throw 'Raid fixture did not prove ordinary outbound, retreat, live-threat guard, and post-removal displacement.'
}
foreach ($phase in @('withdrew', 'guarded', 'removed')) {
if ($phases[$phase].target_id -ne $phases.spawned.target_id) { throw 'Raid fixture changed its threat identity.' }
}
if ($Mode -eq 'raid_recovery') {
if ($dead -ne 1 -or -not $recovered -or -not $recoverySafety -or
$phases.death.intentional -ne $true -or $phases.recovered.target_id -ne $phases.spawned.target_id -or
$phases.recovered.x -ne $phases.start.x -or $phases.recovered.y -ne $phases.start.y -or
$phases.arrived.deaths -ne 1) {
throw 'Raid recovery needs one intentional normal death and renewed safety against the same live threat.'
}
} elseif ($dead -ne 0 -or $phases.arrived.deaths -ne 0) {
throw 'Raid travel did not remain death-free.'
}
}
25 changes: 25 additions & 0 deletions scripts/playerbot-gameplay/scenarios-raid.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
if ($RaidTravel) {
foreach ($raidMode in @('raid_travel', 'raid_recovery')) {
Invoke-Scenario -Name $raidMode -DefaultTimeoutSeconds 180 -Body {
$previousRaidCatalog = $env:PLAYERBOT_RAID_CATALOG
$previousSpeedBonus = $env:PLAYERBOT_SPEED_BONUS
try {
Invoke-Compose down --volumes --remove-orphans
$env:PLAYERBOT_RAID_CATALOG = './tests/playerbot-gameplay/raids/raids.xml'
$env:PLAYERBOT_SPEED_BONUS = '0'
$env:PLAYERBOT_GAMEPLAY_MODE = $raidMode
$env:PLAYERBOT_HUNT_DURATION_SECONDS = '900'
$env:PLAYERBOT_RELOG_DELAY_SECONDS = '1'
$env:PLAYERBOT_MAX_CONSECUTIVE_DEATHS = '2'
Invoke-Compose up --detach
$logs = Wait-ForLog -Pattern ('PLAYERBOT_GAMEPLAY_TEST ' + $raidMode.ToUpperInvariant() +
'_PASS|PLAYERBOT_GAMEPLAY_TEST RAID_FIXTURE_FAILED|"event":"terminal"(?![^\r\n]*"reason":"controlled_player_dead")')
Assert-RaidTravelEvents -Logs $logs -Mode $raidMode
}
finally {
$env:PLAYERBOT_RAID_CATALOG = $previousRaidCatalog
$env:PLAYERBOT_SPEED_BONUS = $previousSpeedBonus
}
}
}
}
Loading