Spaces:
Paused
Paused
Fix hauler ping-pong loop when its source is itself a drop-off (#27)
Browse filesA Foundry/Refinery/Arsenal is both a haul-able collection point and a
valid forward drop-off. Once a worker loaded cargo there, it was also
the nearest drop-off (distance ~0, closer than a far-off CC), so
updateHaul delivered the load straight back into the pile it just
took it from, then reloaded β forever, with zero net progress.
nearestGatherDrop now takes an excludeId so updateHaul's "toDrop"
phase can exclude the job's own source building from the drop-off
search.
Co-authored-by: Claude <noreply@anthropic.com>
- engine/gather.js +9 -3
- engine/haul.js +5 -1
- test/haul.test.js +21 -0
engine/gather.js
CHANGED
|
@@ -117,12 +117,18 @@ export function updateGather(state, unit, dt) {
|
|
| 117 |
// isGatherDropOff). A PLAYER's forward drop-off has a finite intake buffer, so a FULL one is
|
| 118 |
// skipped and the gatherer reroutes to the next-nearest with room (the Command Center never
|
| 119 |
// fills). AI drop-offs are the bottomless treasury as before β so AI gather routing is
|
| 120 |
-
// byte-identical. Closest wins, deterministic Map order breaks ties.
|
| 121 |
-
/
|
| 122 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 123 |
let best = null, bestD = Infinity;
|
| 124 |
for (const b of state.buildings.values()) {
|
| 125 |
if (b.owner !== owner || b.constructing || !isGatherDropOff(b.type)) continue;
|
|
|
|
| 126 |
if (owner === "player" && !BUILDINGS[b.type].isCommandCenter && storeRoom(b) <= 0) continue; // full forward buffer β skip
|
| 127 |
const d = Math.hypot(b.x - x, b.y - y);
|
| 128 |
if (d < bestD) { bestD = d; best = b; }
|
|
|
|
| 117 |
// isGatherDropOff). A PLAYER's forward drop-off has a finite intake buffer, so a FULL one is
|
| 118 |
// skipped and the gatherer reroutes to the next-nearest with room (the Command Center never
|
| 119 |
// fills). AI drop-offs are the bottomless treasury as before β so AI gather routing is
|
| 120 |
+
// byte-identical. Closest wins, deterministic Map order breaks ties. `excludeId`, when given,
|
| 121 |
+
// skips that one building β a HAUL job's own source is ALSO a valid drop-off (a Foundry/Refinery/
|
| 122 |
+
// Arsenal is both a haul-able collection point AND a forward drop-off), so without this a worker
|
| 123 |
+
// standing right on it after loading would just find itself the nearest "drop", unload straight
|
| 124 |
+
// back into the pile it just took the load from, and reload β never actually leaving (engine/
|
| 125 |
+
// haul.js updateHaul passes its own source id here).
|
| 126 |
+
/** @param {State} state @param {string} owner @param {number} x @param {number} y @param {string} [excludeId] @returns {Building|null} */
|
| 127 |
+
export function nearestGatherDrop(state, owner, x, y, excludeId) {
|
| 128 |
let best = null, bestD = Infinity;
|
| 129 |
for (const b of state.buildings.values()) {
|
| 130 |
if (b.owner !== owner || b.constructing || !isGatherDropOff(b.type)) continue;
|
| 131 |
+
if (excludeId && b.id === excludeId) continue;
|
| 132 |
if (owner === "player" && !BUILDINGS[b.type].isCommandCenter && storeRoom(b) <= 0) continue; // full forward buffer β skip
|
| 133 |
const d = Math.hypot(b.x - x, b.y - y);
|
| 134 |
if (d < bestD) { bestD = d; best = b; }
|
engine/haul.js
CHANGED
|
@@ -349,7 +349,11 @@ export function updateHaul(state, unit, dt) {
|
|
| 349 |
return;
|
| 350 |
}
|
| 351 |
if (order.phase === "toDrop") {
|
| 352 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 353 |
if (!drop) { unit.order = null; return; } // nowhere with room to deliver (and no CC) β hold the load, idle
|
| 354 |
if (reached(unit, drop)) {
|
| 355 |
depositHaul(state, unit, drop);
|
|
|
|
| 349 |
return;
|
| 350 |
}
|
| 351 |
if (order.phase === "toDrop") {
|
| 352 |
+
// Exclude the producer this load just came FROM: a Foundry/Refinery/Arsenal is both a valid
|
| 353 |
+
// haul source AND a valid forward drop-off, so without this a worker standing right on it
|
| 354 |
+
// after loading would find itself the nearest "drop", unload straight back into the same
|
| 355 |
+
// pile, then reload β an infinite loop that never actually leaves (see nearestGatherDrop).
|
| 356 |
+
const drop = nearestGatherDrop(state, unit.owner, unit.x, unit.y, order.buildingId);
|
| 357 |
if (!drop) { unit.order = null; return; } // nowhere with room to deliver (and no CC) β hold the load, idle
|
| 358 |
if (reached(unit, drop)) {
|
| 359 |
depositHaul(state, unit, drop);
|
test/haul.test.js
CHANGED
|
@@ -64,6 +64,27 @@ test("a hauler prefers a forward drop-off (Refinery/Foundry/Arsenal) over the Co
|
|
| 64 |
"not one trip detoured all the way to the distant Command Center while the Refinery had room");
|
| 65 |
});
|
| 66 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 67 |
test("a hauler falls back to the Command Center once the closer forward drop-off's buffer is full", () => {
|
| 68 |
const { s, cc, workers } = base(1);
|
| 69 |
const rig = makeBuilding("plasmarig", "player", cc.x + 900, cc.y);
|
|
|
|
| 64 |
"not one trip detoured all the way to the distant Command Center while the Refinery had room");
|
| 65 |
});
|
| 66 |
|
| 67 |
+
test("a hauler whose SOURCE is itself a forward drop-off (Foundry/Refinery/Arsenal) doesn't loop delivering back to itself", () => {
|
| 68 |
+
// Regression: a Foundry/Refinery/Arsenal is both a haul-able collection point (workers dump raw
|
| 69 |
+
// gathers there) AND a valid drop-off in its own right. Once a worker loads a cargo right there,
|
| 70 |
+
// it used to be the NEAREST drop-off too (distance ~0, closer than the far-off CC) β so it
|
| 71 |
+
// "delivered" straight back into the very pile it just took from, then reloaded, forever, never
|
| 72 |
+
// making any progress toward the Command Center.
|
| 73 |
+
const { s, cc, workers } = base(1);
|
| 74 |
+
const foundry = makeBuilding("foundry", "player", cc.x + 900, cc.y);
|
| 75 |
+
foundry.store = { ore: 60 };
|
| 76 |
+
s.buildings.set(foundry.id, foundry);
|
| 77 |
+
const w = workers[0];
|
| 78 |
+
w.x = foundry.x; w.y = foundry.y;
|
| 79 |
+
w.order = { type: "haul", buildingId: foundry.id };
|
| 80 |
+
const before = s.players.player.resources.ore || 0;
|
| 81 |
+
|
| 82 |
+
for (let i = 0; i < 6000 && (storeTotal(foundry) > 0 || (w.cargo && w.cargo.qty > 0)); i++) updateHaul(s, w, 0.05);
|
| 83 |
+
|
| 84 |
+
assert.equal(storeTotal(foundry), 0, "the foundry's whole buffer was hauled away instead of endlessly recycling");
|
| 85 |
+
assert.ok(near((s.players.player.resources.ore || 0) - before, 60, 1e-3), "and it actually reached the treasury");
|
| 86 |
+
});
|
| 87 |
+
|
| 88 |
test("a hauler falls back to the Command Center once the closer forward drop-off's buffer is full", () => {
|
| 89 |
const { s, cc, workers } = base(1);
|
| 90 |
const rig = makeBuilding("plasmarig", "player", cc.x + 900, cc.y);
|