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
22 changes: 18 additions & 4 deletions docs/playerbots.md

Large diffs are not rendered by default.

25 changes: 24 additions & 1 deletion docs/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,26 @@ The Knight scenarios `spell_training_low_supplies` and `spell_training_low_suppl

Paladin healing has exact scenarios `paladin_spell_training` (paid learning, casting, and restart), `paladin_healing_economy` (spell choice and reserve), `paladin_mana_healing`, `paladin_healing_fallback`, and `paladin_supply_restock`. They seed Bot One as a Paladin, omit the other three bots, and do not establish sustained progression. `-SpellTraining`, `-SpellUse`, and `-Healing` include the relevant Paladin cases alongside Knight checks.

`-Scenario paladin_spear_restock` checks paid restocking and hunt resumption in fresh level-8 and level-16 runs; `paladin_spear_break` checks equipping a backpack spare, ending the hunt at the reserve, and restocking. `equipment_purchase_spear` checks supply recovery from zero spears at startup in both hand orientations with only 39 gold, plus a zero-funds case that must stop after one unsuccessful service rather than loop or hunt unarmed. `paladin_spear_last_break` checks losing the final spear after hunting starts. Funded cases must buy three spears, equip them, and resume hunting without a terminal event. `paladin_spear_recovery_income` starts at 128/185 health with one spear and six gold. It requires a verified departure-healing potion use, a planner-selected stock-fitting recovery hunt, area arrival, combat, and coin looting without a reserve-one interruption. It controls corpse gold, not weapon breakage or sustainable economics. These scenarios use normalized gear and capacity, not the normal seed's capacity budget. `lua scripts/test-playerbot-spear-fixture.lua` checks their setup and verifiers without Docker.
`-Scenario paladin_spear_restock` checks paid restocking and hunt resumption in fresh level-8 and level-16 runs; `paladin_spear_break` checks equipping a backpack spare, ending the hunt at the reserve, and restocking. `equipment_purchase_spear` checks supply recovery from zero spears at startup in both hand orientations with only 39 gold, plus a zero-funds case requiring a validated positive-net fist hunt, arrival and coin loot, or a bounded recovery-planning failure. A readiness bypass alone cannot pass. `paladin_spear_last_break` checks losing the final spear after hunting starts. Funded cases must buy three spears, equip them, and resume hunting without a terminal event. `paladin_spear_recovery_income` starts at 128/185 health with one spear and six gold. It requires a verified departure-healing potion use, a planner-selected stock-fitting recovery hunt, area arrival, combat, and coin looting without a reserve-one interruption. It controls corpse gold, not weapon breakage or sustainable economics. These scenarios use normalized gear and capacity, not the normal seed's capacity budget. `lua scripts/test-playerbot-spear-fixture.lua` checks their setup and verifiers without Docker.

Recovery checks separate policy from engine integration:

| Non-Docker check | Covers |
| --- | --- |
| `sh server/tests/playerbot_contracts.sh` | Legal loadouts, skills, receipts, space and gear protection, sliced scoring, net income, safety and no-progress bounds |
| `sh server/tests/playerbotsurvival_contracts.sh` | Suppressed routine spending; emergency healing preserved |
| `pwsh -File scripts/test-playerbot-recovery-assertions.ps1` | Valid recovery evidence or bounded rejection in the zero-funds scenario |
| `lua server/tests/playerbot_recovery_fixture_contracts.lua`; `pwsh -File server/tests/playerbot_recovery_fixture_assertions.ps1` | Positive fixture setup, round-trip assertions, negative controls and fail-fast waits |

These checks do not execute engine hand moves. With a rebuilt image, run both positive round trips:

```powershell
pwsh -File scripts/test-playerbot-gameplay.ps1 -Scenario recovery_fists,recovery_melee -SkipBuild
```

The [recovery overlay](../server/compose.playerbot-recovery.yaml) keeps real Thais terrain, depot and shops. It substitutes four renewing 20-HP quarry spawns (`ignoreSpawnBlock`), normalizes skills, gear, capacity and food, sets spear breakage to 100%, and starts with one equipped spear and zero carried/bank gold. Genuine combat deaths yield fixed 2,000-gold corpses; hit/damage rolls and item APIs remain normal.

Both scenarios require matching safe, positive-net planning, verified temporary hands and live departure checks, area arrival, damage/death, normal corpse loot, depot return, gear restoration and paid restocking to three spears. Any terminal event fails immediately; each deadline is 900 seconds. Three-return exhaustion has policy coverage, not a live fixture. Passing proves the controlled pipeline, not natural profitability or unattended recovery.

Ranged positioning (#235) has `ranged_position_control` (the Paladin fights like a melee class), `ranged_position` (open room: observed displacement, more than half the fight at distance 2–3, and less damage than the control), and `ranged_position_corner` (a shallow wall blocks immediate retreats; must fall back with `retreat_blocked`). Select them with `-Focused -RangedPosition`; exact `-Scenario ranged_position` automatically includes its control. These disposable fixtures use an isolated synthetic arena with a 64-tile retreat runway, normalize spear hits/damage/breakage and chaser damage, count damage through health-change events rather than sampled health loss, and require the chaser's defeat and subsequent coin loot. They do not test wall sliding or natural combat rolls. Non-Docker fixture checks are `lua server/tests/playerbot_ranged_fixture_contracts.lua` and `pwsh -File server/tests/playerbot_ranged_fixture_assertions.ps1`.

Expand All @@ -59,6 +78,7 @@ Run the assertion scripts relevant to the change:
pwsh -File scripts/test-playerbot-navigation-assertions.ps1
pwsh -File scripts/test-playerbot-cycle-assertions.ps1
pwsh -File scripts/test-playerbot-readiness-assertions.ps1
pwsh -File scripts/test-playerbot-recovery-assertions.ps1
pwsh -File scripts/test-playerbot-log-parsing.ps1
pwsh -File scripts/test-playerbot-runtime.ps1
pwsh -File scripts/test-playerbot-scenario-isolation.ps1
Expand All @@ -81,6 +101,7 @@ sh server/tests/playerbothunttiming_contracts.sh
sh server/tests/playerbotdamage_contracts.sh
sh server/tests/playerbotrouting_contracts.sh
sh server/tests/playerbottrainer_contracts.sh
sh server/tests/playerbotserviceroute_contracts.sh
sh server/tests/playerbotselllootapproach_contracts.sh
sh server/tests/playerbotselllootbasket_contracts.sh
sh server/tests/playerbotselllootfilter_contracts.sh
Expand Down Expand Up @@ -111,6 +132,8 @@ The Maealil keyword check loads the actual NPC script and keyword handler. It ve

A zero exit status and each script's explicit pass marker are the pass signal. These checks prove policy, arithmetic, parsing, and fixture contracts; they do not execute an ordinary live world.

The [service-route contracts](../server/tests/playerbotserviceroute_contracts.cpp) exercise production sliced requests and quote checks with the real workflow, navigation runtime and planning budget. They cover walking-only sales/food, NPC-trip rejection, continuations, detours, interruptions, stale evidence and bounded search failure. Use the `CPPFLAGS` above. Graph and movement dispatch are synthetic; `-Focused -SellLoot` checks loaded-world routing, dialogue and sales.

### Depot liquidation timing

Follow one bot's depot visit across `sell_loot_plan` records: route work can span several turns. Pending calls for one candidate are merged until the scan reaches another candidate, finishes, or 5 seconds pass; `slices` counts the merged calls, and times and route counters are sums. `route_validations` stays per call, matching `route_validation_budget`. `snapshot_us` measures synchronous candidate construction; `walking_us` and `npc_us` are nested within `elapsed_us`, so do not add them. A budget denial reports `budget_wait_us` instead, at most once per 5 seconds; `suppressed_denials` counts the denials since the previous record. `sell_loot_approach` and `sell_loot_candidate` show approach retries and rejections; `action_result` with `action: "sell_loot_provider_route"` shows the execution route, quote, and rejection reason. `sell_loot_defer` marks an abandoned trip. The planning budget does not impose a hard latency bound on synchronous work.
Expand Down
92 changes: 92 additions & 0 deletions scripts/playerbot-gameplay/assertions-readiness.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,16 @@ function Assert-CombatReadinessEvents {
if ($Mode -eq "upgrade") {
$equip = @($events | Where-Object { $_.event -eq "action_result" -and $_.action -eq "equip_readiness" -and $_.result -eq "success" -and $_.item_id -eq 2384 })
if ($equip.Count -ne 1) { throw "Carried legal weapon was not equipped and verified." }
}
if ($Mode -eq "upgrade_full_bag") {
# A full source container blocks the engine swap, so the worn weapon goes to the depot first.
$deposit = @($events | Where-Object {
$_.action -eq "deposit" -and $_.result -eq "requested" -and $_.item_id -eq 2382 -and $_.source_slot -eq 6
})
$equip = @($events | Where-Object { $_.action -eq "equip_readiness" -and $_.result -eq "success" -and $_.item_id -eq 2384 })
if ($deposit.Count -lt 1 -or $equip.Count -ne 1 -or $deposit[0].sequence -gt $equip[0].sequence) {
throw "Full-bag carried upgrade did not deposit the worn weapon before equipping."
}
}
if ($Mode -eq "food_capacity") {
$initial = $readiness[0]
Expand Down Expand Up @@ -219,6 +229,40 @@ function Assert-EquipmentPurchaseEvents {
}
}

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

$events = @(ConvertFrom-PlayerbotLogs -Logs $Logs)
for ($i = 0; $i -lt $events.Count; $i++) { $events[$i] | Add-Member -NotePropertyName log_index -NotePropertyValue $i -Force }
$terminal = @($events | Where-Object { $_.event -eq "terminal" })
if ($terminal.Count -gt 0) { throw "Unfunded armor fixture stopped: $($terminal[-1].reason)." }
$equips = @($events | Where-Object {
$_.event -eq "action_result" -and $_.action -eq "equip_equipment" -and $_.result -eq "success" -and $_.slot -eq 4
})
if ($equips.Count -ne 1 -or $Logs -notmatch '(?m)^PLAYERBOT_GAMEPLAY_TEST EQUIPMENT_BUY_UNFUNDED_ARMOR_PASS\r?$') {
throw "Unfunded armor repair lacks a verified armor equip and Lua inventory pass."
}
$equip = $equips[0]
$buys = @($events | Where-Object {
$_.event -eq "action_result" -and $_.action -eq "buy_equipment" -and $_.result -eq "success" -and
$_.item_id -eq $equip.item_id -and $_.npc_id -eq $equip.npc_id
})
if ($buys.Count -ne 1 -or $buys[0].log_index -ge $equip.log_index) {
throw "Unfunded armor equip lacks one matching earlier paid purchase."
}
$buy = $buys[0]
$repair = @($events | Where-Object {
$_.event -eq "equipment_offer_candidate" -and $_.result -eq "feasible" -and $_.rule -eq "fills_readiness_gap" -and
$_.item_id -eq $buy.item_id -and $_.npc_id -eq $buy.npc_id -and $_.price -eq $buy.price -and
$_.price -gt 0 -and $_.price -le 42 -and $_.reserve -gt 42 -and
$_.carried_money -eq 42 -and $_.bank_balance -eq 0 -and $_.log_index -lt $buy.log_index
})
if ($repair.Count -lt 1 -or $buy.carried_before -ne 42 -or $buy.bank_before -ne 0 -or $buy.bank_after -ne 0 -or
$null -eq $buy.carried_after -or $buy.carried_after -ne (42 - $buy.price)) {
throw "Missing armor was not bought from exactly 42 gold below the supply reserve with the normal payment delta."
}
}

function Assert-EquipmentToolReplenishmentEvents {
param([string]$Logs, [switch]$Nested)

Expand Down Expand Up @@ -450,6 +494,54 @@ function Assert-SpearRecoveryIncomeEvents {
}
}

function Assert-UnfundedAlternativeRecoveryEvents {
param([string]$Logs)
$events = @(ConvertFrom-PlayerbotLogs -Logs $Logs)
for ($i = 0; $i -lt $events.Count; $i++) { $events[$i] | Add-Member -NotePropertyName log_index -NotePropertyValue $i -Force }
$start = @($events | Where-Object {
$_.event -eq 'spear_fixture' -and $_.scenario -eq 'spear_unfunded' -and $_.phase -eq 'start' -and
$_.spears -eq 0 -and $_.carried_gold -eq 0 -and $_.bank_gold -eq 0 -and $_.right_item_id -eq 2525
})
if ($start.Count -ne 1) { throw 'Alternative recovery lacks the zero-funded Paladin seed.' }
$selected = @($events | Where-Object { $_.event -eq 'recovery_loadout' -and $_.result -eq 'selected' })
$terminal = @($events | Where-Object { $_.event -eq 'terminal' })
if ($selected.Count -eq 0) {
if ($terminal.Count -ne 1 -or $terminal[0].reason -notin @('recovery_no_safe_income_outing', 'hunt_planning_incomplete', 'recovery_loadout_unavailable')) {
throw 'No safe alternative must finish with a bounded recovery failure, not the normal spear-readiness gate.'
}
$after = @($events | Where-Object { $_.log_index -gt $terminal[0].log_index -and $_.event -ne 'spear_fixture' })
$hunts = @($events | Where-Object { $_.action -eq 'hunt_cycle' })
if ($after.Count -or $hunts.Count) { throw 'Recovery executed an unproved outing or emitted after terminal.' }
return
}
$selection = $selected[0]
if ($terminal.Count -or $selection.left_item_id -ne 0 -or $selection.right_item_id -ne 2525 -or
$selection.expected_net_gold_per_minute -le 0) { throw 'Unfunded fists lack a positive-income selection with the original shield.' }
$proof = @($events | Where-Object {
$_.event -eq 'hunt_region_candidate' -and $_.candidate_phase -eq 'route_validation' -and
$_.recovery_loadout_id -eq $selection.loadout_id -and $_.region_id -eq $selection.region_id -and
$_.planning_pass -eq $selection.planning_pass -and $_.scoring_revision -eq $selection.scoring_revision -and
$_.route_validated -and $_.suitable -and
$_.supply_recovery -and $_.recovery_net_gold_per_minute -gt 0 -and $_.log_index -lt $selection.log_index
})
$equipped = @($events | Where-Object {
$_.event -eq 'recovery_loadout' -and $_.result -eq 'equipped' -and $_.left_item_id -eq 0 -and
$_.right_item_id -eq 2525 -and $_.log_index -gt $selection.log_index
})
$area = @($events | Where-Object { $_.event -eq 'hunt_area_entered' -and $equipped.Count -and $_.log_index -gt $equipped[0].log_index })
$loot = @($events | Where-Object {
$_.action -eq 'loot' -and $_.result -eq 'success' -and $_.coin_gold_acquired -gt 0 -and
$area.Count -and $_.log_index -gt $area[0].log_index
})
$weaponBuys = @($events | Where-Object {
$_.action -eq 'buy_throwing_weapons' -and $_.result -eq 'success' -and
(-not $loot.Count -or $_.log_index -lt $loot[0].log_index)
})
if (-not $proof.Count -or -not $equipped.Count -or -not $area.Count -or -not $loot.Count -or $weaponBuys.Count) {
throw 'Recovery needs route proof, authoritative hands, area arrival and normal coin looting before any spear purchase.'
}
}

function Assert-ZeroSpearRecoveryEvents {
param([string]$Logs, [ValidateSet("startup", "last_break", "mirrored", "unfunded")][string]$Mode)

Expand Down
Loading