Bot recovery relocates into a dungeon, because nearest_bank treats plane 0 as one place #138

Closed
opened 2026-08-06 14:20:32 +00:00 by sickday · 1 comment
Owner

When a leg fails, the brain's ladder calls Bot.Travel.recover/2, which relocates the bot to the bank nearest its anchor:

def recover(ctx, anchor) do
  case Bot.Sites.nearest_bank(anchor) do
    nil -> Act.relocate(ctx, Movement.spawn_position())
    bank -> Act.relocate(ctx, bank.stand)
  end
  ...

Nothing in that path asks whether the answer is anywhere the bot could have walked. nearest_bank/1 filters by plane and then minimises chebyshev distance:

def nearest_bank({plane, _x, _y} = position) do
  table().banks
  |> Enum.filter(fn bank -> elem(bank.position, 0) == plane end)
  |> Enum.min_by(&chebyshev(&1.position, position), fn -> nil end)
end

The plane filter does not separate the surface from underground. Caves, dungeons and mines are plane 0 at high y, so an underground bank is an ordinary candidate for a surface bot and vice versa. Chebyshev across that gap is a number with no physical meaning.

Measured

A runecrafter bot anchored at an essence rock, which is the natural anchor for a kind that spawns at its resource:

nearest_bank({0, 2891, 4847})   # the rock, inside the Rune Essence mine
=> %{position: {0, 2798, 5170}, stand: {0, 2797, 5171}, booth_id: 20325, action: 2}

1878 tiles from the loop, in a dungeon with no walked route to anywhere the bot works. Traced live, second by second:

 10s  {0, 2886, 4850}  in the mine, at the portal
 12s  {0, 2683, 3326}  out of the mine, on the surface, as designed
 16s  {0, 2686, 3324}  three tiles toward its destination, 297 to go
 20s-68s               stationary — the long leg is #123
 70s  {0, 2797, 5171}  recovery fires and teleports it underground

The bot never comes back. Every subsequent leg starts from a tile with no route out, so the recovery is strictly worse than the failure it is recovering from: before it, a wedged bot is standing somewhere real and an operator can see where; after it, the bot is in a dungeon and the original wedge is invisible.

Two things this is not

It is not #123. #123 is why the leg failed; this is what happened because it failed, and it would fire for any failure on the ladder.

It is not specific to Runecrafting. Any kind whose anchor sits somewhere without a sensible nearby bank hits it — the mine is simply the first anchor in the tree that is underground. Bot.Kinds now anchors :runecrafter at the mine's exit rather than at the rock, which makes that one kind safe and leaves the hazard exactly where it was.

Shape of a fix

Something that makes "nearest" mean reachable. Cheapest is a sanity gate: refuse a bank whose distance exceeds what a bot could plausibly have walked, and fall through to Movement.spawn_position() — which is at least somewhere with routes. Better is asking whether a route exists at all, which is what "nearest" was always trying to approximate.

Worth checking the same predicate elsewhere while this is open: same_plane_bank/2 already has a recorded bug in the opposite direction — redwood trees on planes 1 and 2 never looking a plane down to the guild's ground-floor chest. Both are the same mistake, which is treating plane as if it partitioned the world into places you can walk between.

When a leg fails, the brain's ladder calls `Bot.Travel.recover/2`, which relocates the bot to the bank nearest its **anchor**: ```elixir def recover(ctx, anchor) do case Bot.Sites.nearest_bank(anchor) do nil -> Act.relocate(ctx, Movement.spawn_position()) bank -> Act.relocate(ctx, bank.stand) end ... ``` Nothing in that path asks whether the answer is anywhere the bot could have walked. `nearest_bank/1` filters by plane and then minimises chebyshev distance: ```elixir def nearest_bank({plane, _x, _y} = position) do table().banks |> Enum.filter(fn bank -> elem(bank.position, 0) == plane end) |> Enum.min_by(&chebyshev(&1.position, position), fn -> nil end) end ``` **The plane filter does not separate the surface from underground.** Caves, dungeons and mines are plane 0 at high `y`, so an underground bank is an ordinary candidate for a surface bot and vice versa. Chebyshev across that gap is a number with no physical meaning. ## Measured A runecrafter bot anchored at an essence rock, which is the natural anchor for a kind that spawns at its resource: ``` nearest_bank({0, 2891, 4847}) # the rock, inside the Rune Essence mine => %{position: {0, 2798, 5170}, stand: {0, 2797, 5171}, booth_id: 20325, action: 2} ``` 1878 tiles from the loop, in a dungeon with no walked route to anywhere the bot works. Traced live, second by second: ``` 10s {0, 2886, 4850} in the mine, at the portal 12s {0, 2683, 3326} out of the mine, on the surface, as designed 16s {0, 2686, 3324} three tiles toward its destination, 297 to go 20s-68s stationary — the long leg is #123 70s {0, 2797, 5171} recovery fires and teleports it underground ``` The bot never comes back. Every subsequent leg starts from a tile with no route out, so the recovery is **strictly worse than the failure it is recovering from**: before it, a wedged bot is standing somewhere real and an operator can see where; after it, the bot is in a dungeon and the original wedge is invisible. ## Two things this is not It is not #123. #123 is why the leg failed; this is what happened *because* it failed, and it would fire for any failure on the ladder. It is not specific to Runecrafting. Any kind whose anchor sits somewhere without a sensible nearby bank hits it — the mine is simply the first anchor in the tree that is underground. `Bot.Kinds` now anchors `:runecrafter` at the mine's exit rather than at the rock, which makes that one kind safe and leaves the hazard exactly where it was. ## Shape of a fix Something that makes "nearest" mean reachable. Cheapest is a sanity gate: refuse a bank whose distance exceeds what a bot could plausibly have walked, and fall through to `Movement.spawn_position()` — which is at least somewhere with routes. Better is asking whether a route exists at all, which is what "nearest" was always trying to approximate. Worth checking the same predicate elsewhere while this is open: `same_plane_bank/2` already has a recorded bug in the opposite direction — redwood trees on planes 1 and 2 never looking a plane *down* to the guild's ground-floor chest. Both are the same mistake, which is treating `plane` as if it partitioned the world into places you can walk between.
Author
Owner
Moved to https://forge.home.arpa/Revenant/Server/issues/45
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
Revenant/Server#138
No description provided.