fix(issue-3): astar() returns empty path on failure instead of raw destination (#21)

* fix(navigation): astar() returns [] on failure instead of raw destination

Issue #3: When A* could not find a route, it returned [{x: endX, y: endY}]
(the raw destination) as a one-element path. The movement layer treated
this as a valid waypoint and steered agents in a straight line toward the
target, letting them pass through walls and furniture.

Changes:
- navigation.ts: Both failure exits in astar() now return [] instead of
  [{x: ex, y: ey}]. This applies to:
    * findFree() returning null (start or end hopelessly blocked)
    * start and end resolving to the same free cell (already arrived)
    * open list exhausted with no route to the end
- RetroOffice3D.tsx: Movement-layer waypoint fallback changed from
  agent.targetX/Y to agent.x/agent.y when path is empty. An agent with
  no route now stays put rather than walking through obstacles.

Tests added (tests/unit/navigation.astarFallback.test.ts):
- astar returns [] for an unreachable destination (thick wall barrier)
- astar returns [] when the entire grid is blocked (findFree fails)
- astar returns a valid path for reachable destinations (regression)
- astar returns [] when start and end snap to the same grid cell
- movement-layer simulation: empty path keeps agent at current position

* fix(navigation): preserve final waypoint for same-cell reachable targets

astar() was returning [] for both true pathfinding failures and same-cell
reachable targets, causing the movement layer to treat both cases identically
(agent stays put). A destination within the same nav cell is still reachable
— the agent should take the final fine-grained step to the exact pixel.

Fix: return [{ x: ex, y: ey }] for same-cell case so the movement layer
can settle the agent onto the exact interaction point.

---------

Co-authored-by: Neo (subagent) <neo@openclaw.local>
Co-authored-by: Neo <neo@openclaw.ai>
This commit is contained in:
robotica4us-collab
2026-03-27 13:19:39 -05:00
committed by GitHub
parent b091b9087a
commit 450f4873f6
3 changed files with 228 additions and 5 deletions
+4 -3
View File
@@ -1828,10 +1828,11 @@ function useAgentTick(
agent.status === "working" && agent.state !== "sitting"
? baseSpeed * WORKING_WALK_SPEED_MULTIPLIER
: baseSpeed;
// Move toward the first waypoint; fall back to the raw target when empty.
// Move toward the first waypoint. An empty path means astar found no route —
// the agent stays put instead of walking through walls toward the raw target.
const path = agent.path ?? [];
const wpX = path.length > 0 ? path[0].x : agent.targetX;
const wpY = path.length > 0 ? path[0].y : agent.targetY;
const wpX = path.length > 0 ? path[0].x : agent.x;
const wpY = path.length > 0 ? path[0].y : agent.y;
const dx = wpX - agent.x,
dy = wpY - agent.y;
const dist = Math.hypot(dx, dy);
+6 -2
View File
@@ -168,11 +168,15 @@ export function astar(
let { c: ec, r: er } = toCell(ex, ey);
const startFree = findFree(sc, sr);
const endFree = findFree(ec, er);
if (!startFree || !endFree) return [{ x: ex, y: ey }];
if (!startFree || !endFree) return [];
sc = startFree.c;
sr = startFree.r;
ec = endFree.c;
er = endFree.r;
// Same nav cell: start and end are close enough that A* has no grid edges
// to traverse. The destination is still reachable — return a single-waypoint
// path to the exact target pixel so the movement layer can make the final
// fine-grained adjustment instead of staying put.
if (sc === ec && sr === er) return [{ x: ex, y: ey }];
const nodeCount = GRID_COLS * GRID_ROWS;
@@ -292,7 +296,7 @@ export function astar(
}
}
return [{ x: ex, y: ey }];
return [];
}
export const getDeskLocations = (items: FurnitureItem[]) =>