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
78 changes: 70 additions & 8 deletions scripts/test-playerbot-gameplay.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ param(
[switch]$HuntRegionPlanning,
[switch]$CombatReadiness,
[switch]$Depot,
[switch]$MainlandLoop,
[switch]$Focused,
[switch]$SkipBuild,
[switch]$KeepStack
Expand Down Expand Up @@ -272,7 +273,7 @@ function Assert-OracleDepartureEvents {
$events = @(ConvertFrom-PlayerbotLogs -Logs $Logs)
if ($Restart) {
$restored = @($events | Where-Object {
$_.event -eq "lifecycle" -and $_.status -eq "online" -and $_.objective -eq "departure_complete"
$_.event -eq "lifecycle" -and $_.status -eq "online" -and $_.objective -eq "service"
})
if ($restored.Count -lt 1) {
throw "The persisted Oracle departure state was not restored."
Expand All @@ -295,13 +296,12 @@ function Assert-OracleDepartureEvents {
$goalResult = @($events | Where-Object {
$_.event -eq "goal_result" -and $_.goal -eq "oracle_departure" -and $_.result -eq "success"
})
$stopped = @($events | Where-Object {
$_.event -eq "state_transition" -and $_.to -eq "stopped"
$continued = @($events | Where-Object {
$_.event -eq "objective_transition" -and $_.to -eq "service" -and $_.reason -eq "departure_complete"
})
$wrongStoppedCount = if ($InterruptedByRestart) { $stopped.Count -lt 1 } else { $stopped.Count -ne 1 }
if ($candidate.Count -lt 1 -or $selection.Count -lt 1 -or $result.Count -ne 1 -or
$goalResult.Count -ne 1 -or $wrongStoppedCount) {
throw "The bot did not complete and verify the selected Oracle departure: candidate=$($candidate.Count), selection=$($selection.Count), result=$($result.Count), goalResult=$($goalResult.Count), stopped=$($stopped.Count)."
$goalResult.Count -ne 1 -or $continued.Count -ne 1) {
throw "The bot did not complete and continue after the selected Oracle departure: candidate=$($candidate.Count), selection=$($selection.Count), result=$($result.Count), goalResult=$($goalResult.Count), continued=$($continued.Count)."
}
}

Expand Down Expand Up @@ -1102,13 +1102,51 @@ function Assert-DepotRecoveryEvents {
}
}

function Assert-MainlandLoopEvents {
param([string]$Logs, [int]$MinimumCycles = 3, [int]$MinimumDeposits = 2)

$events = @(ConvertFrom-PlayerbotLogs -Logs $Logs)
$hunts = @($events | Where-Object {
$_.event -eq "action_result" -and $_.action -eq "hunt_cycle" -and $_.result -eq "started"
})
$deposits = @($events | Where-Object {
$_.event -eq "action_result" -and $_.action -eq "deposit" -and $_.result -eq "complete" -and $_.depot_id -eq 2
})
$realDepot = @($events | Where-Object {
$_.event -eq "action_result" -and $_.action -eq "depot_discover" -and $_.result -eq "success" -and
$_.locker.x -eq 32344 -and $_.locker.y -eq 32218 -and $_.locker.z -eq 5
})
$trainingRoomDepot = @($events | Where-Object {
$_.event -eq "action_result" -and $_.action -eq "depot_discover" -and $_.result -eq "success" -and
$_.locker.x -eq 32276 -and $_.locker.y -eq 32218 -and $_.locker.z -eq 11
})
$selection = @($events | Where-Object {
$_.event -eq "hunt_region_selection" -and $_.result -eq "selected" -and
[Math]::Abs($_.center.x - 32369) + [Math]::Abs($_.center.y - 32241) +
20 * [Math]::Abs($_.center.z - 7) -le 200
})
$cheeseDeposit = @($events | Where-Object {
$_.event -eq "action_result" -and $_.action -eq "deposit" -and $_.result -in @("success", "partial") -and
$_.item_id -eq 2696
})
$rookService = @($events | Where-Object {
$_.event -eq "npc_reply" -and $_.npc_name -in @("Billy", "Willie", "Lily", "Paulie")
})
$terminal = @($events | Where-Object { $_.event -eq "terminal" })
if ($hunts.Count -lt $MinimumCycles -or $deposits.Count -lt $MinimumDeposits -or $realDepot.Count -lt 1 -or
$trainingRoomDepot.Count -ne 0 -or
$selection.Count -lt 1 -or $cheeseDeposit.Count -lt 1 -or $rookService.Count -ne 0 -or $terminal.Count -ne 0) {
throw "Mainland loop failed. hunts=$($hunts.Count), deposits=$($deposits.Count), realDepot=$($realDepot.Count), trainingRoomDepot=$($trainingRoomDepot.Count), localSelections=$($selection.Count), cheeseDeposits=$($cheeseDeposit.Count), rookService=$($rookService.Count), terminal=$($terminal.Count)."
}
}

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
$PickupProgression -or $GoalArbitration -or $OracleDeparture -or $StaminaProjection -or $HuntRegionPlanning -or
$CombatReadiness -or $Depot
$CombatReadiness -or $Depot -or $MainlandLoop
if ($Focused -and -not $focusedScenarioRequested) {
throw "-Focused requires at least one focused scenario switch."
}
Expand Down Expand Up @@ -1140,6 +1178,30 @@ try {
}
}

if ($MainlandLoop) {
Invoke-Scenario -Name "mainland_loop" -DefaultTimeoutSeconds 600 -Body {
Invoke-Compose down --volumes --remove-orphans
$env:PLAYERBOT_GAMEPLAY_MODE = "mainland"
$env:PLAYERBOT_HUNT_DURATION_SECONDS = "10"
Invoke-Compose up --detach
Wait-ForLog -Pattern 'PLAYERBOT_GAMEPLAY_TEST MAINLAND_START' | Out-Null
$loopLogs = Wait-ForLog -Pattern '"action":"hunt_cycle","result":"started","cycle":3'
Assert-MainlandLoopEvents -Logs $loopLogs

$restartLineCount = @((Get-ServerLogs) -split "`r?`n").Count
Invoke-Compose stop server
Invoke-Compose up --detach server
$restartLogs = ""
for ($attempt = 0; $attempt -lt 300; $attempt++) {
Start-Sleep -Seconds 1
$restartLogs = ((Get-ServerLogs) -split "`r?`n" | Select-Object -Skip $restartLineCount) -join "`n"
if ($restartLogs -match '"action":"deposit","result":"complete","depot_id":2' -and
$restartLogs -match '"action":"hunt_cycle","result":"started","cycle":1') { break }
}
Assert-MainlandLoopEvents -Logs $restartLogs -MinimumCycles 1 -MinimumDeposits 1
}
}

if ($Depot) {
Invoke-Scenario -Name "real_depot" -DefaultTimeoutSeconds 240 -Body {
Invoke-Compose down --volumes --remove-orphans
Expand Down Expand Up @@ -1439,7 +1501,7 @@ try {
Invoke-Compose stop server
Invoke-Compose up --detach server
Wait-ForLog -Pattern 'PLAYERBOT_GAMEPLAY_TEST ORACLE_DEPARTURE_RESTART_PASS' | Out-Null
$restartLogs = Wait-ForLog -Pattern '"objective":"departure_complete"'
$restartLogs = Wait-ForLog -Pattern 'PLAYERBOT_GAMEPLAY_TEST ORACLE_DEPARTURE_RESTART_PASS'
Assert-OracleDepartureEvents -Logs $restartLogs -Restart
}

Expand Down
1 change: 1 addition & 0 deletions server/data/npc/Frodo.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
<health now="100" max="100" />
<look type="128" head="58" body="68" legs="109" feet="115" addons="0" />
<parameters>
<parameter key="playerbot_service" value="shop" />
<parameter key="module_shop" value="1" />
<parameter key="shop_buyable" value="bread,2689,4;ham,2671,8;cheese,2696,6;meat,2666,5;mug of milk,2012,5,6;" />
</parameters>
Expand Down
3 changes: 3 additions & 0 deletions server/data/npc/Naji.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,7 @@
<npc name="Naji" script="Naji.lua" walkinterval="2000" floorchange="0">
<health now="100" max="100" />
<look type="129" head="57" body="113" legs="95" feet="113" addons="0" />
<parameters>
<parameter key="playerbot_service" value="banker" />
</parameters>
</npc>
1 change: 1 addition & 0 deletions server/data/npc/Sam.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
<health now="100" max="100" />
<look type="131" head="38" body="113" legs="67" feet="95" addons="0" />
<parameters>
<parameter key="playerbot_service" value="shop" />
<parameter key="module_shop" value="1" />
<parameter key="shop_buyable" value="
axe,2386,20;
Expand Down
4 changes: 3 additions & 1 deletion server/data/npc/Xodet.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@
<health now="100" max="100" />
<look type="130" head="19" body="86" legs="87" feet="95" addons="0" />
<parameters>
<parameter key="playerbot_service" value="shop" />
<parameter key="module_shop" value="1" />
<parameter key="shop_buyable" value="
blank rune,2260,10;
great health potion,7591,190;
great mana potion,7590,120;
great spirit potion,8472,190;
health potion,7618,45;
health potion,7618,45;
small health potion,8704,20;
mana potion,7620,50;
moonlight rod,2186,1000;
necrotic rod,2185,5000;
Expand Down
22 changes: 13 additions & 9 deletions server/schema/insertPlayerbots.sql
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ INSERT INTO `accounts` (`name`, `password`, `type`, `premium_ends_at`, `email`,
VALUES ('bot-one', SHA1('bot-one'), 1, 0, '', 0)
ON DUPLICATE KEY UPDATE `id` = `id`;

-- This idempotent kit remains valid through the deterministic Rookgaard-to-Knight
-- transition. Focused gameplay fixtures add any stronger equipment they need.
-- Bot One starts ready for the first mainland hunt. Focused Rookgaard fixtures
-- replace this state when they exercise the earlier progression stages.

CREATE TABLE IF NOT EXISTS `player_bots` (
`player_id` int NOT NULL,
Expand All @@ -21,11 +21,11 @@ INSERT INTO `players` (
`cap`, `sex`, `stamina`, `skill_sword`, `skill_shielding`, `balance`
)
SELECT
'Bot One', 1, `id`, 1, 0, 150,
150, 0, 68, 76, 78,
39, 128, 0, 2, 0, 0,
0, 0, 100, 6, 32097, 32219, 7,
400, 1, 2520, 70, 60, 100
'Bot One', 1, `id`, 8, 4, 185,
185, 4200, 68, 76, 78,
39, 128, 0, 2, 0, 35,
35, 0, 100, 2, 32369, 32241, 7,
470, 1, 2520, 70, 60, 100
FROM `accounts`
WHERE `name` = 'bot-one'
AND NOT EXISTS (SELECT 1 FROM `players` WHERE `name` = 'Bot One');
Expand Down Expand Up @@ -62,8 +62,12 @@ SET @bot_next_sid = (
INSERT INTO `player_items` (`player_id`, `pid`, `sid`, `itemtype`, `count`, `attributes`)
SELECT @bot_player_id, `loadout`.`pid`, @bot_next_sid + `loadout`.`offset`, `loadout`.`itemtype`, `loadout`.`count`, ''
FROM (
SELECT 4 AS `pid`, 1 AS `offset`, 2650 AS `itemtype`, 1 AS `count`
UNION ALL SELECT 6, 2, 2382, 1
SELECT 1 AS `pid`, 1 AS `offset`, 2457 AS `itemtype`, 1 AS `count`
UNION ALL SELECT 4, 2, 2463, 1
UNION ALL SELECT 5, 3, 2525, 1
UNION ALL SELECT 6, 4, 2376, 1
UNION ALL SELECT 7, 5, 2647, 1
UNION ALL SELECT 8, 6, 2643, 1
) AS `loadout`
WHERE NOT EXISTS (
SELECT 1 FROM `player_items`
Expand Down
14 changes: 8 additions & 6 deletions server/src/playerbotcombat.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -541,7 +541,7 @@ bool PlayerBotController::selectHuntRegion(Player& player, const Position& posit
cancelHuntRegionPlanning();
}
if (!huntRegionPlanning) {
PlayerBotHuntRegionScan scan = huntRegionPlanner.beginScan();
PlayerBotHuntRegionScan scan = huntRegionPlanner.beginScan(player);
HuntRegionPlanning planning;
planning.regions.reserve(scan.candidateCount);
planning.reason = reason;
Expand All @@ -550,6 +550,7 @@ bool PlayerBotController::selectHuntRegion(Player& player, const Position& posit
planning.snapshotTimeUs = scan.snapshotTimeUs;
planning.clusteringTimeUs = scan.clusteringTimeUs;
planning.cacheRevision = scan.revision;
planning.candidateIndices = std::move(scan.candidateIndices);
planning.totalCandidates = static_cast<uint32_t>(scan.candidateCount);
planning.playerPosition = player.getPosition();
planning.playerLevel = player.getLevel();
Expand All @@ -571,7 +572,8 @@ bool PlayerBotController::selectHuntRegion(Player& player, const Position& posit
scoredThisTurn < huntRegionScoringCandidatesPerTurn && planning.nextScoringCandidate < planning.totalCandidates;
++scoredThisTurn, ++planning.nextScoringCandidate) {
PlayerBotHuntRegion region;
if (!huntRegionPlanner.score(player, planning.cacheRevision, planning.nextScoringCandidate, excludedRegions,
if (!huntRegionPlanner.score(player, planning.cacheRevision,
planning.candidateIndices[planning.nextScoringCandidate], excludedRegions,
huntRegionPerformance, huntDurationSeconds, region)) {
emitHuntRegionPlanning(planning, position, "stale_revision");
cancelHuntRegionPlanning();
Expand Down Expand Up @@ -768,9 +770,6 @@ void PlayerBotController::processTraversal(Player* player, const Position& curre
processReadinessEquipment(player, currentPosition);
return;
}
if (cyclePhase == CyclePhase::Hunt && !ensureCombatReady(player, currentPosition, "readiness_continuous_check")) {
return;
}
if (cyclePhase != CyclePhase::Hunt || progressionObjective == ProgressionObjective::OracleDeparture) {
if (defensiveTargetId != 0) {
processDefensiveCombat(player, currentPosition);
Expand All @@ -785,6 +784,9 @@ void PlayerBotController::processTraversal(Player* player, const Position& curre
processProgression(player, currentPosition);
return;
}
if (cyclePhase == CyclePhase::Hunt && !ensureCombatReady(player, currentPosition, "readiness_continuous_check")) {
return;
}
if (cyclePhase == CyclePhase::Hunt && !testPolicy.fixedFixtureRoute && !activeHuntRegion && !huntRegionPlanning) {
startHunt(player, currentPosition, "hunt_region_restart");
return;
Expand All @@ -804,7 +806,7 @@ void PlayerBotController::processTraversal(Player* player, const Position& curre
if (cyclePhase == CyclePhase::Hunt &&
(std::chrono::steady_clock::now() >= huntDeadline || player->getFreeCapacity() < returnCapacityThreshold)) {
const char* reason = player->getFreeCapacity() < returnCapacityThreshold ? "capacity" : "hunt_deadline";
if (testPolicy.progressionEnabled) {
if (testPolicy.progressionEnabled && !hasCompletedRookgaardDeparture(*player)) {
finishHuntAndSelectGoal(player, currentPosition, reason);
return;
} else {
Expand Down
14 changes: 6 additions & 8 deletions server/src/playerbotcontroller.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ const PlayerBotTestPolicy& playerbot::testPolicyFromEnvironment()
std::strcmp(gameplayMode, "stamina_boundary") != 0 &&
std::strcmp(gameplayMode, "stamina_normal") != 0 &&
std::strcmp(gameplayMode, "hunt_planning") != 0 &&
std::strcmp(gameplayMode, "mainland") != 0 &&
std::strcmp(gameplayMode, "depot") != 0;
const char* depotRestartPhase = std::getenv("PLAYERBOT_DEPOT_RESTART_PHASE");
const DepotRestartCheckpoint depotRestartCheckpoint = !depotRestartPhase ? DepotRestartCheckpoint::None :
Expand Down Expand Up @@ -87,23 +88,20 @@ void PlayerBotController::start(const Position& position, bool recovered, uint32
Player* controlledPlayer = g_game.getPlayerByID(playerId);
const bool departureComplete = controlledPlayer && hasCompletedRookgaardDeparture(*controlledPlayer);
const bool departureRequired = controlledPlayer && requiresRookgaardDeparture(*controlledPlayer);
const bool useGoalSelector = controlledPlayer && (departureRequired || (!recovered && testPolicy.progressionEnabled));
if (useGoalSelector && !departureComplete && !selectTopLevelGoal(*controlledPlayer, position, "startup")) {
const bool useGoalSelector = controlledPlayer && !departureComplete &&
(departureRequired || (!recovered && testPolicy.progressionEnabled));
if (useGoalSelector && !selectTopLevelGoal(*controlledPlayer, position, "startup")) {
return;
}
std::ostringstream lifecycle;
lifecycle << "\"status\":\"online\",\"message\":\"Playerbot online\""
<< ",\"recovered\":" << (recovered ? "true" : "false")
<< ",\"recovery_count\":" << recoveryCount
<< ",\"objective\":" << jsonString(departureComplete ? "departure_complete" :
useGoalSelector ? topLevelGoalName(activeGoal) :
<< ",\"objective\":" << jsonString(useGoalSelector ? topLevelGoalName(activeGoal) :
(startInHunt ? "hunt" : "service"))
<< ",\"step_speed\":" << (g_game.getPlayerByID(playerId) ? g_game.getPlayerByID(playerId)->getSpeed() : 0);
emit("lifecycle", position, lifecycle.str());
if (departureComplete) {
setStage(ScenarioStage::Stopped, position);
return;
} else if (useGoalSelector) {
if (useGoalSelector) {
// The selected goal initialized its own executor state.
} else if (startInHunt) {
activeGoal = TopLevelGoal::Hunt;
Expand Down
1 change: 1 addition & 0 deletions server/src/playerbotcontroller.h
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,7 @@ class PlayerBotController : public std::enable_shared_from_this<PlayerBotControl
};

std::vector<PlayerBotHuntRegion> regions;
std::vector<size_t> candidateIndices;
std::string reason;
std::chrono::steady_clock::time_point started;
size_t nextCandidate = 0;
Expand Down
5 changes: 3 additions & 2 deletions server/src/playerbotdeparture.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -173,9 +173,10 @@ void PlayerBotController::finishOracleDeparture(Player* player, const Position&
serviceTargetId = 0;
clearNavigation();
if (std::strcmp(result, "success") == 0) {
setStage(ScenarioStage::Stopped, position);
if (player) {
say(*player, "Rookgaard departure complete. Awaiting mainland behavior.");
say(*player, "Rookgaard departure complete. Starting mainland service.");
beginService(player, position, "departure_complete");
schedule(navigationInterval);
}
return;
}
Expand Down
Loading