Almaatla Claude commited on
Commit
fca5d94
Β·
unverified Β·
1 Parent(s): 26fa5a2

Fix hauler ping-pong loop when its source is itself a drop-off (#27)

Browse files

A 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>

Files changed (3) hide show
  1. engine/gather.js +9 -3
  2. engine/haul.js +5 -1
  3. 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
- /** @param {State} state @param {string} owner @param {number} x @param {number} y @returns {Building|null} */
122
- export function nearestGatherDrop(state, owner, x, y) {
 
 
 
 
 
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
- const drop = nearestGatherDrop(state, unit.owner, unit.x, unit.y);
 
 
 
 
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);