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
62 changes: 42 additions & 20 deletions docs/playerbots.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,10 @@ development account `bot-one`, character `Bot One`, and `player_bots` registry.
Provisioning is idempotent: it does not restore spent money, replace occupied
equipment slots, or take over an unrelated same-named or deleted character.

A new Bot One starts at level 1 with the development hunt skills, starter
equipment, backpack, rope, shovel, 100 gp bank balance, and two 100-gp backpack
stacks. Global `freePremium` supplies premium access.
A new Bot One starts as a level 8 Knight at the Thais temple with development
hunt skills, plate equipment, sword, shield, backpack, rope, shovel, five small
health potions, one meat, 100 gp bank balance, and two 100-gp backpack stacks.
Global `freePremium` supplies premium access.

Inspect setup and startup with:

Expand Down Expand Up @@ -60,16 +61,18 @@ interrupted families cool down for 60 seconds.
Callers provide destinations, not ordered transition checkpoints. The bounded
navigator searches loaded map state within a 192-tile margin around its
endpoints and stops after 100,000 expanded nodes. It supports ordinary floor
changes, configured ladders, rope and shovel holes, direct-use holes, simple
teleports, and unlocked doors. Cardinal movement costs 10; diagonal movement
costs 30. Failed steps are excluded for 10 seconds. Repeated A-B oscillation
changes, configured ladders, rope and shovel holes, direct-use holes, and
unlocked doors. Arbitrary map teleports are not navigation edges. 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.

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
Hunt regions come from all loaded hostile spawns on floors 6 through 15. The
shared cache groups overlapping eight-tile spawn kernels through spatial
buckets. Each controller considers regions within 200 weighted tiles of its
town temple and 300 weighted tiles of its current position, then scores them in
bounded batches using health, equipment, weapon, defense, skill, cooldowns, and
observed performance. It validates suitable candidates 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.
Expand All @@ -81,9 +84,20 @@ minutes. Completed hunts update a per-controller XP correction after at least
30 seconds and one kill; the sample is clamped to `0.25` through `2.0` and uses
a 65/35 rolling blend. This state resets on relog or restart.

The planner is a bounded Rookgaard prototype, not whole-map navigation.
Regression fixtures use fixed destinations and do not validate dynamic region
selection.
When an attacked monster leaves normal positional or creature visibility, or
the attack association is lost, the bot clears chase and pursues a reachable
tile adjacent to its last observed position. It never updates this goal from a
hidden creature's live state.
Visible targets may update the approach goal and can be reacquired within six
tiles. Pursuit lasts at most five seconds and six tiles of Chebyshev displacement.
Reaching the approach point or exhausting either budget returns to patrol and
suppresses that target for ten seconds. A 60-second traversal combat timeout
retains its separate 120-second suppression.

The map-derived region planner and bounded pursuit are prototypes, not
whole-map hierarchical navigation or general creature memory. Regression
fixtures use fixed destinations unless their focused mode explicitly exercises
dynamic planning or pursuit.

## Survival, service, and loot

Expand All @@ -94,17 +108,18 @@ fullness limit.

Service NPCs require an exact `playerbot_service` XML tag of `shop`, `banker`,
or `oracle`. Shops publish their loaded offers to the bot; untagged shops and
tagged shops without offers are ignored. Reachable providers remain explicitly
allowlisted while navigation is bounded. The bot greets the selected NPC,
tagged shops without offers are ignored. Providers must remain within 200
weighted tiles of the registered town temple. The bot greets the selected NPC,
treats a private reply as focus acknowledgement, and opens the normal trade
window. Reply text is not interpreted.

The service cycle sells known surplus, restores five small health potions and
one meat, deposits carried money, and withdraws 100 gp. Hunting ends after the
configured duration or below 30 oz free capacity. Remaining top-level backpack
loot is dropped at the fake depot south of `(32105, 32195, 8)`. This is a public
world tile, not private or durable storage; equipped items, the root backpack,
currency, rope, shovel, and supply reserves are retained.
loot is moved through a reachable town depot locker into that player's real
depot chest. Nested containers are opened and deposits are verified through
normal item movement. Equipped items, the root backpack, currency, rope,
shovel, and supply reserves are retained.

Item value comes from tagged shop offers. Currency uses intrinsic value; other
loot must have a known buyer. Corpse contents are ranked by value per weight,
Expand Down Expand Up @@ -134,7 +149,7 @@ next decision after restart.
An unpromoted level 8 through 10 player can select the live tagged Oracle. The
bot derives a route, says `hi`, `yes`, `thais`, `knight`, `yes`, then verifies
vocation `4`, town `2`, and the registered Thais temple position. It remains
server-owned but stops because mainland behavior is not implemented.
server-owned and continues directly into mainland service.

## Recovery and configuration

Expand Down Expand Up @@ -186,6 +201,13 @@ target changes, and action failures are emitted at most once per 60 seconds;
`summary.suppressed_events` counts omissions. Counters cover one in-memory
controller lifetime. Docker retains three 10 MiB server log files.

Target pursuit uses `action_result` with `action="target_pursuit"`.
`result="started"` includes `target_id` and `last_seen_position`;
`result="reacquired"` includes `target_id`; and `result="abandoned"` includes
`target_id` plus `reason`. Current abandonment reasons are
`last_seen_position_reached` and `pursuit_budget_exhausted`.
`state_transition` exposes entry to and exit from `target_pursuit`.

When `GOD Admin` is online, selected objectives and verified transactions also
appear as private messages and orange status text. JSONL remains authoritative.

Expand Down
12 changes: 11 additions & 1 deletion docs/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,26 +31,36 @@ pwsh -File scripts/test-playerbot-gameplay.ps1
```

The baseline covers NPC discovery, sale and reserve purchase, bank deposit and
withdrawal, fake-depot handling, and return to hunting. Add switches that match
withdrawal, fixture-depot handling, and return to hunting. Add switches that match
the changed behavior:

| Switch | Coverage |
| ------ | -------- |
| `-FullNavigation` | Complete fixed A-B-C-B-A fixture route and temporary blockage recovery. |
| `-TargetPursuit` | Visible-only last-seen pursuit, bounded reacquisition, and out-of-budget abandonment. |
| `-CorpseLoot` | Non-corpse, empty, guaranteed-loot, and container death items; open-before-inspect ordering. |
| `-DeathTelemetry` | Death context, exponential relog, fresh controller state, abandonment, and removal. |
| `-Healing` | Potion verification, threshold recovery, missing-stock service, purchase, and resume behavior. |
| `-ValueLoot` | Value-per-weight replacement under constrained capacity and bank-funded purchase. |
| `-PickupProgression` | Nested and multi-root reward inspection, claim verification, upgrades, restart recovery, and space rejection. |
| `-GoalArbitration` | Pickup, service, hunt, and critical-healing precedence across safe boundaries. |
| `-OracleDeparture` | Tagged Oracle route, dialogue, vocation/town/position verification, and restart persistence. |
| `-StaminaProjection` | Premium bonus, low-stamina penalty, and ordinary stamina projections. |
| `-HuntRegionPlanning` | Cached scanner batching, threat rejection, reachability, cooldowns, and observed correction. |
| `-CombatReadiness` | Equipment, supplies, capacity, service recovery, upgrades, and restart reconstruction. |
| `-Depot` | Real locker/chest discovery, nested deposits, move verification, retries, and restart checkpoints. |
| `-MainlandLoop` | Two real Thais hunt/depot cycles, local services, restart recovery, and teleport exclusion. |

Navigation or looting changes require at least:

```powershell
pwsh -File scripts/test-playerbot-gameplay.ps1 -FullNavigation -CorpseLoot
pwsh -File scripts/test-playerbot-gameplay.ps1 -TargetPursuit -Focused
```

`-TargetPursuit` runs successful `target_pursuit` reacquisition and bounded
`target_pursuit_abandon` fallback scenarios.

Use `-Focused` with one or more scenario switches to skip the baseline. Use
`-SkipBuild` only with a known-current `angelion-server:latest` image; it does
not prove that the image matches the worktree. `-KeepStack` preserves the final
Expand Down
92 changes: 91 additions & 1 deletion scripts/test-playerbot-gameplay.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ param(
[ValidateRange(30, 3600)]
[int]$TimeoutSeconds = 300,
[switch]$FullNavigation,
[switch]$TargetPursuit,
[switch]$CorpseLoot,
[switch]$DeathTelemetry,
[switch]$Healing,
Expand Down Expand Up @@ -977,6 +978,73 @@ function Assert-NavigationEvents {
}
}

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

$events = @(ConvertFrom-PlayerbotLogs -Logs $Logs)
$started = @($events | Where-Object {
$_.event -eq "action_result" -and $_.action -eq "target_pursuit" -and $_.result -eq "started"
})
$reacquired = @($events | Where-Object {
$_.event -eq "action_result" -and $_.action -eq "target_pursuit" -and $_.result -eq "reacquired"
})
$defeated = @($events | Where-Object {
$_.event -eq "target_changed" -and $_.reason -eq "target_defeated"
})
$terminal = @($events | Where-Object { $_.event -eq "terminal" })
$firstPlan = @($events | Where-Object {
$_.event -eq "action_result" -and $_.action -eq "plan" -and $_.result -eq "success"
}) | Select-Object -First 1
$lastSeenPlanDistance = $started.Count -eq 1 -and $firstPlan.Count -eq 1 ?
[Math]::Max([Math]::Abs($started[0].last_seen_position.x - $firstPlan[0].destination.x),
[Math]::Abs($started[0].last_seen_position.y - $firstPlan[0].destination.y)) : 99
$distance = $started.Count -eq 1 -and $reacquired.Count -eq 1 ?
[Math]::Max([Math]::Abs($started[0].position.x - $reacquired[0].position.x),
[Math]::Abs($started[0].position.y - $reacquired[0].position.y)) : 0
if ($started.Count -ne 1 -or $reacquired.Count -ne 1 -or $defeated.Count -lt 1 -or
$started[0].target_id -ne $reacquired[0].target_id -or
$reacquired[0].target_id -ne $defeated[0].previous_target_id -or
$distance -lt 1 -or $distance -gt 6 -or $lastSeenPlanDistance -gt 1 -or $terminal.Count -ne 0) {
throw "Target pursuit failed. started=$($started.Count), reacquired=$($reacquired.Count), defeated=$($defeated.Count), distance=$distance, lastSeenPlanDistance=$lastSeenPlanDistance, terminal=$($terminal.Count)."
}
}

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

$events = @(ConvertFrom-PlayerbotLogs -Logs $Logs)
$started = @($events | Where-Object {
$_.event -eq "action_result" -and $_.action -eq "target_pursuit" -and $_.result -eq "started"
})
$abandoned = @($events | Where-Object {
$_.event -eq "action_result" -and $_.action -eq "target_pursuit" -and $_.result -eq "abandoned" -and
$_.reason -in @("last_seen_position_reached", "pursuit_budget_exhausted")
})
$reacquired = @($events | Where-Object {
$_.event -eq "action_result" -and $_.action -eq "target_pursuit" -and $_.result -eq "reacquired"
})
$routeUnavailable = @($events | Where-Object {
$_.event -eq "action_result" -and $_.action -eq "navigate" -and $_.result -eq "failed" -and
$_.reason -eq "route_unavailable"
})
$terminal = @($events | Where-Object { $_.event -eq "terminal" })
$firstPlan = @($events | Where-Object {
$_.event -eq "action_result" -and $_.action -eq "plan" -and $_.result -eq "success"
}) | Select-Object -First 1
$lastSeenPlanDistance = $started.Count -eq 1 -and $firstPlan.Count -eq 1 ?
[Math]::Max([Math]::Abs($started[0].last_seen_position.x - $firstPlan[0].destination.x),
[Math]::Abs($started[0].last_seen_position.y - $firstPlan[0].destination.y)) : 99
$distance = $started.Count -eq 1 -and $abandoned.Count -eq 1 ?
[Math]::Max([Math]::Abs($started[0].position.x - $abandoned[0].position.x),
[Math]::Abs($started[0].position.y - $abandoned[0].position.y)) : 0
if ($started.Count -ne 1 -or $abandoned.Count -ne 1 -or
$started[0].target_id -ne $abandoned[0].target_id -or $distance -gt 6 -or
($distance -eq 0 -and $routeUnavailable.Count -lt 1) -or $lastSeenPlanDistance -gt 1 -or
$reacquired.Count -ne 0 -or $terminal.Count -ne 0) {
throw "Target pursuit fallback failed. started=$($started.Count), abandoned=$($abandoned.Count), distance=$distance, routeUnavailable=$($routeUnavailable.Count), lastSeenPlanDistance=$lastSeenPlanDistance, reacquired=$($reacquired.Count), terminal=$($terminal.Count)."
}
}

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

Expand Down Expand Up @@ -1144,7 +1212,7 @@ if (-not (Get-Command docker -ErrorAction SilentlyContinue)) {
throw "Docker is required to run the playerbot gameplay suite."
}

$focusedScenarioRequested = $FullNavigation -or $CorpseLoot -or $DeathTelemetry -or $Healing -or $ValueLoot -or
$focusedScenarioRequested = $FullNavigation -or $TargetPursuit -or $CorpseLoot -or $DeathTelemetry -or $Healing -or $ValueLoot -or
$PickupProgression -or $GoalArbitration -or $OracleDeparture -or $StaminaProjection -or $HuntRegionPlanning -or
$CombatReadiness -or $Depot -or $MainlandLoop
if ($Focused -and -not $focusedScenarioRequested) {
Expand Down Expand Up @@ -1542,6 +1610,28 @@ try {
}
}

if ($TargetPursuit) {
Invoke-Scenario -Name "target_pursuit" -DefaultTimeoutSeconds 60 -Body {
Invoke-Compose down --volumes --remove-orphans
$env:PLAYERBOT_GAMEPLAY_MODE = "target_pursuit"
$env:PLAYERBOT_HUNT_DURATION_SECONDS = "900"
Invoke-Compose up --detach
Wait-ForLog -Pattern 'PLAYERBOT_GAMEPLAY_TEST TARGET_PURSUIT_HIDDEN' | Out-Null
Wait-ForLog -Pattern '"action":"target_pursuit","result":"reacquired"' | Out-Null
$pursuitLogs = Wait-ForLog -Pattern '"reason":"target_defeated"'
Assert-TargetPursuitEvents -Logs $pursuitLogs
}
Invoke-Scenario -Name "target_pursuit_abandon" -DefaultTimeoutSeconds 60 -Body {
Invoke-Compose down --volumes --remove-orphans
$env:PLAYERBOT_GAMEPLAY_MODE = "target_pursuit_abandon"
$env:PLAYERBOT_HUNT_DURATION_SECONDS = "900"
Invoke-Compose up --detach
Wait-ForLog -Pattern 'PLAYERBOT_GAMEPLAY_TEST TARGET_PURSUIT_HIDDEN' | Out-Null
$pursuitLogs = Wait-ForLog -Pattern '"action":"target_pursuit","result":"abandoned"'
Assert-TargetPursuitAbandonEvents -Logs $pursuitLogs
}
}

if ($CorpseLoot) {
Invoke-Scenario -Name "corpse" -DefaultTimeoutSeconds 60 -Body {
Invoke-Compose down --volumes --remove-orphans
Expand Down
Loading