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
186 changes: 144 additions & 42 deletions docs/playerbots.md

Large diffs are not rendered by default.

164 changes: 130 additions & 34 deletions docs/testing.md

Large diffs are not rendered by default.

89 changes: 76 additions & 13 deletions scripts/playerbot-gameplay/assertions-progression.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -321,10 +321,35 @@ function Assert-HuntRegionPlanningEvents {

$events = @(ConvertFrom-PlayerbotLogs -Logs $Logs)
$scored = @($events | Where-Object {
$_.event -eq "hunt_region_scan" -and $_.phase -in @("scoring_started", "scored")
$_.event -eq "hunt_region_scan" -and $_.phase -eq "scored"
})
$build = @($scored | Where-Object { $_.cache -eq "build" })
$hit = @($scored | Where-Object { $_.cache -eq "hit" })
$planningStarted = @($events | Where-Object {
$_.event -eq 'hunt_region_scan' -and $_.phase -eq 'planning_started'
})
$startedBuild = @($planningStarted | Where-Object { $_.cache -eq 'build' })
$startedHit = @($planningStarted | Where-Object { $_.cache -eq 'hit' })
$completedScoring = @($scored | Where-Object {
$_.candidate_count -gt 0 -and $_.scored_candidate_count -eq $_.candidate_count -and
$_.suitable_candidate_count -ge 1 -and $_.route_candidate_count -ge 1
})
$completedBuildScoring = @($completedScoring | Where-Object { $_.cache -eq 'build' })
$buildLifecycleValid = $completedBuildScoring.Count -ge 1 -and @($completedBuildScoring | Where-Object {
$completion = $_
@($startedBuild | Where-Object {
$_.candidate_count -eq $completion.candidate_count -and
$_.snapshot_time_us -eq $completion.snapshot_time_us -and
$_.clustering_time_us -eq $completion.clustering_time_us
}).Count -ge 1
}).Count -eq $completedBuildScoring.Count
$cacheReuseValid = $startedHit.Count -ge 2 -and @($startedHit | Where-Object {
$hitStart = $_
$hitStart.candidate_count -le 0 -or $hitStart.scored_candidate_count -ne 0 -or
$hitStart.route_candidate_count -ne 0 -or $hitStart.snapshot_time_us -ne 0 -or
$hitStart.clustering_time_us -ne 0 -or
@($completedScoring | Where-Object { $_.candidate_count -eq $hitStart.candidate_count }).Count -lt 1
}).Count -eq 0
$transportYields = @($events | Where-Object { $_.event -eq 'hunt_region_scan' -and $_.phase -eq 'transport_yield' })
$scoringYields = @($events | Where-Object { $_.event -eq 'hunt_region_scan' -and $_.phase -eq 'scoring_yield' })
$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" })
$topologyScans = @($events | Where-Object {
Expand All @@ -339,8 +364,14 @@ function Assert-HuntRegionPlanningEvents {
$completed = @($events | Where-Object {
$_.event -eq "hunt_region_scan" -and $_.phase -eq "selected" -and $_.decision_latency_us -gt 0
})
$selectedScan = if ($completed.Count -gt 0) { $completed[$completed.Count - 1] } else { $null }
$selectedCandidate = if ($selection) { @($candidates | Where-Object { $_.region_id -eq $selection.region_id }) } else { @() }
$reachableCandidates = @($candidates | Where-Object { $_.suitable -and $_.reachable -and $_.route_validated })
$validatedVariantCount = @($reachableCandidates | Select-Object -ExpandProperty atlas_variant_id -Unique).Count
$finiteRouteQueue = $selectedScan -and $selectedScan.route_candidate_policy -eq 'all_cheap_viable_ranked' -and
$selectedScan.route_candidate_count -gt 0 -and $selectedScan.route_candidate_count -le $selectedScan.candidate_count -and
$validatedVariantCount -le $selectedScan.route_candidate_count -and $selectedScan.transport_offer_count -ge 0 -and
$selectedScan.transport_arrival_count -ge 1
$budgetCandidates = @($reachableCandidates | Where-Object { $_.supply_budget_fits })
$minimumPotions = if ($reachableCandidates.Count -gt 0) { ($reachableCandidates | Measure-Object -Property supply_expected_potions -Minimum).Minimum } else { $null }
$preferredCandidates = if ($budgetCandidates.Count -gt 0) { $budgetCandidates } else {
Expand All @@ -363,18 +394,18 @@ function Assert-HuntRegionPlanningEvents {
$expectedThreshold = [Math]::Max(1, [Math]::Ceiling($expectedHealthLoss / $reserve.minimum_potion_healing))
$reserveFormulaValid = $reserve.return_threshold -eq $expectedThreshold
}
if ($build.Count -lt 2 -or $hit.Count -lt 1 -or $cancelled.Count -ne 1 -or $staleRevision.Count -ne 1 -or
$topologyScans.Count -lt 1 -or $routeValidations.Count -lt 1 -or -not $selection -or $selectedCandidate.Count -ne 1 -or
if (-not $buildLifecycleValid -or -not $cacheReuseValid -or $transportYields.Count -lt 1 -or $scoringYields.Count -lt 1 -or
$cancelled.Count -ne 1 -or $staleRevision.Count -ne 1 -or $topologyScans.Count -lt 1 -or $routeValidations.Count -lt 1 -or -not $selection -or $selectedCandidate.Count -ne 1 -or
$bestScore -eq $null -or [Math]::Abs($selectedCandidate[0].score - $bestScore) -gt 0.01 -or
$reachableCandidates.Count -gt 8 -or $selection.selection_rule -ne $selectionRule -or
-not $finiteRouteQueue -or $selection.selection_rule -ne $selectionRule -or
-not $selectedCandidate[0].route_validated -or
($budgetCandidates.Count -gt 0 -and -not $selectedCandidate[0].supply_budget_fits) -or
($budgetCandidates.Count -eq 0 -and $selectedCandidate[0].supply_expected_potions -ne $minimumPotions) -or
-not $selectedCandidate[0].topology_reachable -or $selectedCandidate[0].topology_travel_steps -lt 1 -or
-not $selectedCandidate[0].reachable -or $selectedCandidate[0].route_danger_cost -lt 0 -or
$outsideLocalFixture.Count -lt 1 -or $completedTopology.Count -lt 1 -or
-not $reserveFormulaValid) {
throw "Hunt planning telemetry was incomplete. build=$($build.Count), hit=$($hit.Count), cancelled=$($cancelled.Count), stale=$($staleRevision.Count), topology_scans=$($topologyScans.Count), route_validations=$($routeValidations.Count), selection=$($selection.Count), outside=$($outsideLocalFixture.Count), completed=$($completedTopology.Count)."
throw "Hunt planning telemetry was incomplete. started_build=$($startedBuild.Count), started_hit=$($startedHit.Count), completed_scoring=$($completedScoring.Count), build_lifecycle=$buildLifecycleValid, cache_reuse=$cacheReuseValid, transport_yields=$($transportYields.Count), scoring_yields=$($scoringYields.Count), cancelled=$($cancelled.Count), stale=$($staleRevision.Count), topology_scans=$($topologyScans.Count), route_candidates=$($selectedScan.route_candidate_count), validated_variants=$validatedVariantCount, route_validations=$($routeValidations.Count), selection=$($selection.Count), outside=$($outsideLocalFixture.Count), completed=$($completedTopology.Count)."
}
}

Expand Down Expand Up @@ -409,6 +440,38 @@ function Assert-HuntAreaArrivalEvents {
}
}

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

$events = @(ConvertFrom-PlayerbotLogs -Logs $Logs)
$selection = @($events | Where-Object { $_.event -eq 'hunt_region_selection' -and $_.result -eq 'selected' }) | Select-Object -Last 1
if (-not $selection) { throw 'Remote hunt did not select a region.' }
$candidate = @($events | Where-Object {
$_.event -eq 'hunt_region_candidate' -and $_.region_id -eq $selection.region_id -and $_.route_validated
}) | Select-Object -Last 1
$entry = @($events | Where-Object { $_.event -eq 'hunt_area_entered' -and $_.region_id -eq $selection.region_id }) | Select-Object -First 1
$depot = @($events | Where-Object { $_.event -eq 'action_result' -and $_.action -eq 'depot_discover' -and $_.result -eq 'success' }) | Select-Object -Last 1
$travel = @($events | Where-Object { $_.event -eq 'npc_travel' -and $_.result -eq 'success' })
$boatTravel = @($travel | Where-Object { $_.npc_name -like 'Captain *' })
$return = @($events | Where-Object { $_.event -eq 'action_result' -and $_.action -eq 'return' -and $_.result -eq 'started' })
$terminal = @($events | Where-Object { $_.event -eq 'terminal' })
$remoteDistance = if ($candidate) {
[Math]::Max([Math]::Abs($candidate.center.x - 32360), [Math]::Abs($candidate.center.y - 31782))
} else { 0 }
$depotMatchesPreflight = $candidate -and $depot -and
$depot.approach.z -eq $candidate.exit_depot_destination.z -and
[Math]::Max([Math]::Abs($depot.approach.x - $candidate.exit_depot_destination.x),
[Math]::Abs($depot.approach.y - $candidate.exit_depot_destination.y)) -le 2
$darashiaRegion = $candidate -and $candidate.center.x -ge 33100 -and $candidate.center.x -le 33350 -and
$candidate.center.y -ge 32300 -and $candidate.center.y -le 32650
if (-not $candidate -or -not $darashiaRegion -or $candidate.topology_reachable -or -not $candidate.outbound_npc_travel -or
$candidate.outbound_fare -le 0 -or $candidate.exit_npc_travel -or $remoteDistance -le 100 -or
-not $entry -or $travel.Count -lt 1 -or $boatTravel.Count -lt 1 -or $return.Count -lt 1 -or
-not $depotMatchesPreflight -or $terminal.Count -ne 0) {
throw "Remote hunt integration failed. candidate=$([bool]$candidate), darashia=$darashiaRegion, distance=$remoteDistance, travel=$($travel.Count), boat_travel=$($boatTravel.Count), entry=$([bool]$entry), return=$($return.Count), depot_match=$depotMatchesPreflight, terminal=$($terminal.Count)."
}
}

function Assert-SupplyBudgetEvents {
param([string]$Logs)
$fixtures = @(ConvertFrom-PlayerbotLogs -Logs $Logs | Where-Object {
Expand All @@ -434,7 +497,7 @@ function Assert-AdaptiveChallengeEvents {
$events = @(ConvertFrom-PlayerbotLogs -Logs $Logs)
$frontier = @($events | Where-Object { $_.event -eq "hunt_challenge_frontier" })
$idle = @($frontier | Where-Object { $_.result -eq "insufficient_active_combat" -and $_.active_combat_seconds -eq 0 -and $_.kills -eq 0 })
$noKill = @($frontier | Where-Object { $_.result -eq "insufficient_active_combat" -and $_.active_combat_seconds -ge 30 -and $_.kills -eq 0 })
$noKill = @($frontier | Where-Object { $_.result -eq "insufficient_active_combat" -and $_.active_combat_seconds -ge 60 -and $_.kills -eq 0 })
$escalated = @($frontier | Where-Object { $_.result -eq "escalated" })
$invalidEscalation = @($escalated | Where-Object {
$_.active_combat_seconds -lt $_.minimum_active_combat_seconds -or $_.kills -lt $_.minimum_kills
Expand Down Expand Up @@ -463,10 +526,10 @@ function Assert-AdaptiveChallengeEvents {
if ($idle.Count -ne 1 -or $noKill.Count -ne 1 -or $invalidEscalation.Count -ne 0 -or
$escalated.Count -ne 3 -or $backoff.Count -ne 2 -or $deathBackoff.Count -ne 1 -or $hold.Count -ne 2 -or
[Math]::Abs($escalated[0].frontier_before - 0.20) -gt 0.001 -or
[Math]::Abs($escalated[1].frontier_after - 0.25) -gt 0.001 -or
[Math]::Abs($escalated[2].frontier_after - 0.225) -gt 0.001 -or
[Math]::Abs($backoff[0].frontier_after - 0.20) -gt 0.001 -or
[Math]::Abs($deathBackoff[0].frontier_after - 0.175) -gt 0.001 -or
[Math]::Abs($escalated[1].frontier_after - 0.40) -gt 0.001 -or
[Math]::Abs($escalated[2].frontier_after - 0.40) -gt 0.001 -or
[Math]::Abs($backoff[0].frontier_after - 0.30) -gt 0.001 -or
[Math]::Abs($deathBackoff[0].frontier_after - 0.30) -gt 0.001 -or
$hold[0].hold_qualifying_hunts -ne 1 -or $hold[1].hold_qualifying_hunts -ne 0 -or
$fixture.Count -ne 1 -or -not $fixture[0].recovery_spell_legal -or $fixture[0].recovery_spell_casts -lt 1 -or
$fixture[0].equipment_pressure_after -gt $fixture[0].equipment_pressure_before -or
Expand All @@ -475,7 +538,7 @@ function Assert-AdaptiveChallengeEvents {
-not $fixture[0].zero_health_lethal -or -not $fixture[0].helper_scope_exhausted -or
$fixture[0].capacity_before_grace -or -not $fixture[0].capacity_at_grace -or -not $fixture[0].capacity_cycle_reset -or
$fixture[0].knight_route_reserve -ne 4 -or $fixture[0].rook_route_reserve -ne 9 -or
$fixture[0].high_health_route_reserve -ne 12 -or $fixture[0].high_health_restock_target -ne 13 -or
$fixture[0].high_health_route_reserve -ne 12 -or $fixture[0].high_health_restock_target -ne 20 -or
-not $fixture[0].net_value_loss_rejected -or -not $fixture[0].currency_priority_override -or
-not $fixture[0].currency_hunt_capacity_excluded -or
-not $fixture[0].large_restock_batched -or
Expand Down
2 changes: 1 addition & 1 deletion scripts/playerbot-gameplay/assertions-readiness.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ function Assert-CombatReadinessEvents {
if ($Mode -eq "supplies") {
$service = @($events | Where-Object { $_.event -eq "combat_readiness" -and $_.selected_recovery -eq "service" })
$potions = @($events | Where-Object {
$_.action -eq "buy_potions" -and $_.result -eq "success" -and $_.item_id -eq 7618 -and $_.count -eq 9
$_.action -eq "buy_potions" -and $_.result -eq "success" -and $_.item_id -eq 7618 -and $_.count -eq 19
})
$food = @($events | Where-Object { $_.action -eq "buy_meat" -and $_.result -eq "success" })
if ($service.Count -lt 1 -or $potions.Count -lt 1 -or $food.Count -ne 0) { throw "Missing healing supplies did not select potion-only service recovery." }
Expand Down
27 changes: 27 additions & 0 deletions scripts/playerbot-gameplay/scenarios-progression.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,22 @@
}

if ($HuntRegionPlanning) {
Invoke-Scenario -Name "supply_recovery" -DefaultTimeoutSeconds 360 -Body {
Invoke-Compose down --volumes --remove-orphans
$env:PLAYERBOT_GAMEPLAY_MODE = "supply_recovery"
$env:PLAYERBOT_HUNT_DURATION_SECONDS = "2400"
Invoke-Compose up --detach
Wait-ForLog -Pattern 'PLAYERBOT_GAMEPLAY_TEST SUPPLY_RECOVERY_START' | Out-Null
$logs = Wait-ForLog -Pattern 'PLAYERBOT_GAMEPLAY_TEST SUPPLY_RECOVERY_INCOME_PASS'
if ($logs -notmatch '"event":"hunt_region_selection".*"result":"selected"' -or
$logs -notmatch '"event":"hunt_area_entered"' -or
$logs -notmatch '"action":"loot","result":"success","item_id":2148') {
throw "Broke-bot income must follow actual selected-region combat, not a reward or fixture grant."
}
if ($logs -match '"from_goal":"service","to_goal":"service"') {
throw "Broke bot repeated Service instead of earning hunt income."
}
}
Invoke-Scenario -Name "hunt_region_planning" -DefaultTimeoutSeconds 180 -Body {
Invoke-Compose down --volumes --remove-orphans
$env:PLAYERBOT_GAMEPLAY_MODE = "hunt_planning"
Expand All @@ -140,6 +156,17 @@
$arrivalLogs = Wait-ForLog -Pattern '"event":"target_changed".*"reason":"visible_monster"'
Assert-HuntAreaArrivalEvents -Logs $arrivalLogs
}
Invoke-Scenario -Name "remote_hunt" -DefaultTimeoutSeconds 600 -Body {
Invoke-Compose down --volumes --remove-orphans
$env:PLAYERBOT_GAMEPLAY_MODE = "remote_hunt"
$env:PLAYERBOT_HUNT_DURATION_SECONDS = "900"
Invoke-Compose up --detach
Wait-ForLog -Pattern 'PLAYERBOT_GAMEPLAY_TEST HUNT_MAINLAND_LOADOUT_PASS remote_hunt' | Out-Null
Wait-ForLog -Pattern 'PLAYERBOT_GAMEPLAY_TEST REMOTE_HUNT_START' | Out-Null
Wait-ForLog -Pattern '"event":"hunt_area_entered"' | Out-Null
$remoteLogs = Wait-ForLog -Pattern '"event":"action_result".*"action":"depot_discover".*"result":"success"'
Assert-RemoteHuntEvents -Logs $remoteLogs
}
}

if ($CombatReadiness) {
Expand Down
8 changes: 4 additions & 4 deletions scripts/test-playerbot-gameplay.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -73,10 +73,10 @@ $scenarioCatalog = @(
"real_depot_restart_deposit", "real_depot_restart_depart", "real_depot_partial_move", "real_depot_rejected_move",
"pickup_progression", "pickup_progression_bundle", "pickup_progression_nested", "pickup_progression_resume",
"pickup_progression_nested_resume", "pickup_progression_space", "goal_arbitration", "goal_arbitration_interrupt",
"stamina_bonus_projection", "stamina_boundary_projection", "stamina_normal_projection", "hunt_region_planning",
"stamina_bonus_projection", "stamina_boundary_projection", "stamina_normal_projection", "hunt_region_planning", "supply_recovery",
"combat_readiness_ready", "combat_readiness_upgrade", "combat_readiness_missing_weapon", "combat_readiness_supplies",
"combat_readiness_no_food", "combat_readiness_low_wealth", "combat_readiness_food_capacity",
"combat_readiness_retention", "hunt_area_arrival", "equipment_offer_shadow_upgrade", "equipment_offer_shadow_unaffordable",
"combat_readiness_retention", "hunt_area_arrival", "remote_hunt", "equipment_offer_shadow_upgrade", "equipment_offer_shadow_unaffordable",
"equipment_offer_shadow_no_upgrade", "equipment_purchase", "equipment_purchase_resume", "equipment_purchase_provider_moved", "equipment_purchase_provider_unreachable", "equipment_purchase_space",
"equipment_purchase_rejected", "adaptive_challenge", "mainland_equipment_reward", "oracle_departure",
"oracle_level_eight_interrupt", "oracle_level_eight_recovery",
Expand All @@ -94,8 +94,8 @@ foreach ($scenarioName in $scenarioCatalog) {
throw "Duplicate gameplay scenario name: $scenarioName"
}
}
if ($scenarioCatalog.Count -ne 90) {
throw "The gameplay scenario catalog must contain 90 scenarios; found $($scenarioCatalog.Count)."
if ($scenarioCatalog.Count -ne 92) {
throw "The gameplay scenario catalog must contain 92 scenarios; found $($scenarioCatalog.Count)."
}
$requestedScenarioNames = @($Scenario | ForEach-Object { $_ -split ',' } | Where-Object { $_ })
$exactScenarioSelection = $requestedScenarioNames.Count -gt 0
Expand Down
19 changes: 14 additions & 5 deletions scripts/test-playerbot-hunt-fixture.lua
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ SKILL_FIST, SKILL_CLUB, SKILL_SWORD, SKILL_AXE, SKILL_DISTANCE, SKILL_SHIELD, SK
ITEM_GOLD_COIN = 2148
CreatureEvent = function() return {register = function() end} end
Town = function(id) assert(id == 4); return {} end
Game = {getExperienceForLevel = function(level) assert(level == 8); return seeded.experience end}
local experienceByLevel = {[8] = seeded.experience, [15] = 37800}
Game = {getExperienceForLevel = function(level) return assert(experienceByLevel[level]) end}
PlayerbotGameplayFixture = {}
local root = 'server/tests/playerbot-gameplay/includes/'
dofile(root .. 'constants.inc')
Expand Down Expand Up @@ -58,11 +59,16 @@ local function login(selectedMode, fault, starter)
equip(CONST_SLOT_AMMO, 2050) -- firstitems.lua runs before the fixture
if fault == 'tool' then inventory[2120] = 0 end
local vocation, health, position = 4, 1, nil
local level, experience = seeded.level, seeded.experience
local player = {
getName = function() return F.botName end,
getId = function() return 3 end,
getLevel = function() return seeded.level end,
getExperience = function() return seeded.experience end,
getLevel = function() return level end,
getExperience = function() return experience end,
addExperience = function(_, amount)
experience = experience + amount
if experience >= experienceByLevel[15] then level = 15 end
end,
getVocation = function() return {getId = function() return vocation end} end,
setVocation = function(_, value) assert(value == 4, 'hunt fixture restored Rookgaard'); vocation = value; return true end,
setTown = function() return true end,
Expand Down Expand Up @@ -105,12 +111,15 @@ local function login(selectedMode, fault, starter)
assert(skills[skill] == wanted and tries[skill] == 0)
end
assert(health == seeded.health and position.x == seeded.posx and position.y == seeded.posy and position.z == seeded.posz)
assert(level == (mode == 'remote_hunt' and 15 or 8))
assert(inventory[7618] == 10 and inventory[8704] == 0)
assert(suppressed == (mode == 'hunt_planning' and 1 or 0))
assert(output[1]:find('HUNT_MAINLAND_LOADOUT_PASS ' .. mode, 1, true))
assert(output[2] == 'PLAYERBOT_GAMEPLAY_TEST ' .. (mode == 'hunt_planning' and 'HUNT_PLANNING_START' or 'HUNT_AREA_ARRIVAL_START'))
local start = mode == 'hunt_planning' and 'HUNT_PLANNING_START' or
mode == 'hunt_area_arrival' and 'HUNT_AREA_ARRIVAL_START' or 'REMOTE_HUNT_START'
assert(output[2] == 'PLAYERBOT_GAMEPLAY_TEST ' .. start)
end
for _, selectedMode in ipairs({'hunt_planning', 'hunt_area_arrival'}) do
for _, selectedMode in ipairs({'hunt_planning', 'hunt_area_arrival', 'remote_hunt'}) do
login(selectedMode, nil, false)
login(selectedMode, nil, true)
assert(login(selectedMode, 'unequipped'):find('did not equip seeded item', 1, true))
Expand Down
Loading