Skip to content

Commit 6143f84

Browse files
authored
fix: prevent mineral and squad infinite loops (#746)
This fix addresses two critical infinite loop issues that waste CPU: 1. Mineral Creep Infinite Loop ---------------------------- Mineral creeps would infinitely loop trying to transfer resources from the terminal when insufficient amounts (1-4 units) were available. Changes: - Update validation threshold from === 0 to < LAB_REACTION_AMOUNT (5) in both setStatePrepareReactionLab1WithResource and setStatePrepareReactionLab2WithResource to match room cleanup logic - Add reaction cleanup in handleWithdrawFromSource to delete room.memory.reaction when resources are insufficient, preventing immediate re-setting of the same failed state - Enhance validation check to also verify resource amount meets LAB_REACTION_AMOUNT threshold during withdrawal 2. Squad Bouncing at Room Borders -------------------------------- Squad creeps (both siege and heal) would bounce infinitely between room exits, logging "Reached end of handling() why?" every tick. Root cause: When creeps reached target room border, they would: - Enter target room -> set routing.reached = true - Move back across border due to border tile instability - Delete routing.reached flag (not in target room anymore) - Routing system restarts -> creep moves forward again - Infinite loop continues Changes: - Add explicit if/else logic in squadsiege.action to separate traveling vs in-target-room behavior - Add border protection: only reset routing.reached if not at border tile (using isBorder check) to prevent bouncing - Don't execute siege() when traveling (not in target room) - Apply same fix to squadheal.action for consistency Both fixes ensure consistent validation across code paths and make the systems self-healing by clearing invalid states immediately.
1 parent 686d315 commit 6143f84

File tree

4 files changed

+298
-10
lines changed

4 files changed

+298
-10
lines changed

src/role_mineral.js

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -348,7 +348,7 @@ function setStatePrepareReactionLab1WithResource(creep) {
348348
return false;
349349
}
350350
// Check if terminal actually has the required resource
351-
if (!creep.room.terminal || !creep.room.terminal.store[reaction.result.first] || creep.room.terminal.store[reaction.result.first] === 0) {
351+
if (!creep.room.terminal || !creep.room.terminal.store[reaction.result.first] || creep.room.terminal.store[reaction.result.first] < LAB_REACTION_AMOUNT) {
352352
return false;
353353
}
354354
creep.data.state = {
@@ -378,7 +378,7 @@ function setStatePrepareReactionLab2WithResource(creep) {
378378
return false;
379379
}
380380
// Check if terminal actually has the required resource
381-
if (!creep.room.terminal || !creep.room.terminal.store[reaction.result.second] || creep.room.terminal.store[reaction.result.second] === 0) {
381+
if (!creep.room.terminal || !creep.room.terminal.store[reaction.result.second] || creep.room.terminal.store[reaction.result.second] < LAB_REACTION_AMOUNT) {
382382
return false;
383383
}
384384
creep.data.state = {
@@ -492,9 +492,13 @@ function handleWithdrawFromSource(creep) {
492492
}
493493
creep.moveToMy(source.pos);
494494
const resource = creep.data.state.getResource(source);
495-
if (!resource) {
496-
creep.log(`No resource available from ${source}, clearing state`);
495+
if (!resource || !source.store[resource] || source.store[resource] < LAB_REACTION_AMOUNT) {
496+
creep.log(`No sufficient resource available from ${source}, clearing state and reaction`);
497497
delete creep.data.state;
498+
// Also clear the room's reaction to prevent immediate re-setting
499+
if (creep.room.memory.reaction) {
500+
delete creep.room.memory.reaction;
501+
}
498502
return true;
499503
}
500504
const response = creep.withdraw(source, resource);

src/role_squadheal.js

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,15 +47,14 @@ roles.squadheal.preMove = function(creep, directions) {
4747
}
4848
};
4949

50-
// TODO need to check if it works
5150
roles.squadheal.action = function(creep) {
5251
creep.selfHeal();
5352
if (creep.room.name !== creep.memory.routing.targetRoom) {
54-
// creep.log('Not in room');
53+
// Not in target room yet - traveling
5554
if (creep.hits < creep.hitsMax) {
5655
creep.moveRandom();
57-
} else {
58-
// creep.log('delete?');
56+
} else if (!creep.pos.isBorder(-1)) {
57+
// Only reset routing if not at border to prevent bouncing
5958
delete creep.memory.routing.reached;
6059
}
6160
return true;

src/role_squadsiege.js

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,15 +76,20 @@ roles.squadsiege.preMove = function(creep, directions) {
7676
return false;
7777
};
7878

79-
// TODO need to check if it works
8079
roles.squadsiege.action = function(creep) {
8180
creep.say('action');
81+
8282
if (creep.room.name !== creep.memory.routing.targetRoom) {
83+
// Not in target room yet - traveling
8384
if (creep.hits < creep.hitsMax) {
8485
creep.moveRandom();
85-
} else {
86+
} else if (!creep.pos.isBorder(-1)) {
87+
// Only reset routing if not at border to prevent bouncing
8688
delete creep.memory.routing.reached;
8789
}
90+
return true; // Don't execute siege when traveling
8891
}
92+
93+
// In target room - execute siege action
8994
return creep.siege();
9095
};

test/test.js

Lines changed: 280 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -467,6 +467,74 @@ describe('Mineral System', () => {
467467
// With the fix, state should be set because terminal has the resource
468468
// (This test will pass after we implement the fix)
469469
});
470+
471+
it('mineral creep does not set state when terminal has insufficient resources (1-4 units)', () => {
472+
const room = new Room('W1N1', 5000);
473+
room.terminal = {
474+
id: 'terminal-id',
475+
store: {
476+
[RESOURCE_HYDROGEN]: 3, // Insufficient (< LAB_REACTION_AMOUNT of 5)
477+
[RESOURCE_OXYGEN]: 100,
478+
},
479+
};
480+
room.storage = {
481+
id: 'storage-id',
482+
store: {
483+
energy: 10000,
484+
},
485+
};
486+
room.memory.reaction = {
487+
result: {
488+
result: RESOURCE_HYDROXIDE,
489+
first: RESOURCE_HYDROGEN,
490+
second: RESOURCE_OXYGEN,
491+
},
492+
labs: ['lab0-id', 'lab1-id', 'lab2-id'],
493+
};
494+
495+
const creep = {
496+
name: 'mineral1',
497+
role: 'mineral',
498+
room: room,
499+
data: {},
500+
store: {
501+
getUsedCapacity: () => 0,
502+
getCapacity: () => 50,
503+
},
504+
carry: {},
505+
say: () => {},
506+
log: () => {},
507+
moveRandom: () => {},
508+
};
509+
510+
// Mock labs with lab1 needing hydrogen
511+
global.Game.getObjectById = (id) => {
512+
if (id === 'lab1-id') {
513+
return {
514+
id: 'lab1-id',
515+
store: {
516+
[RESOURCE_HYDROGEN]: 0, // Empty, needs refill
517+
getCapacity: () => 3000,
518+
},
519+
};
520+
}
521+
if (id === 'lab2-id') {
522+
return {
523+
id: 'lab2-id',
524+
store: {
525+
[RESOURCE_OXYGEN]: 1500,
526+
getCapacity: () => 3000,
527+
},
528+
};
529+
}
530+
return null;
531+
};
532+
533+
roles.mineral.action(creep);
534+
535+
// State should NOT be set because terminal has insufficient resources (< LAB_REACTION_AMOUNT)
536+
assert.equal(creep.data.state, undefined);
537+
});
470538
});
471539

472540
describe('Creep Spawning', () => {
@@ -680,3 +748,215 @@ describe('Trapped Detection', () => {
680748
assert.equal(isTrapped, false, 'Should not count no-data as blocked when recently stagnant');
681749
});
682750
});
751+
752+
describe('Squad Roles', () => {
753+
beforeEach(() => {
754+
// Setup Memory.squads for tests
755+
global.Memory.squads = {};
756+
});
757+
758+
it('squadsiege does not delete routing.reached when at border position', () => {
759+
const room = new Room('W1N1', 5000);
760+
761+
const creep = {
762+
name: 'squadsiege-1',
763+
memory: {
764+
role: 'squadsiege',
765+
routing: {
766+
targetRoom: 'W1N2',
767+
reached: true,
768+
},
769+
},
770+
room: room,
771+
pos: {
772+
x: 0, // Border position
773+
y: 25,
774+
roomName: 'W1N1',
775+
isBorder: () => true, // At border
776+
},
777+
hits: 1000,
778+
hitsMax: 1000,
779+
say: () => {},
780+
moveRandom: () => {},
781+
siege: () => true,
782+
};
783+
784+
roles.squadsiege.action(creep);
785+
786+
// routing.reached should still be true because at border
787+
assert.equal(creep.memory.routing.reached, true);
788+
});
789+
790+
it('squadsiege deletes routing.reached when not in target room and not at border', () => {
791+
const room = new Room('W1N1', 5000);
792+
793+
const creep = {
794+
name: 'squadsiege-1',
795+
memory: {
796+
role: 'squadsiege',
797+
routing: {
798+
targetRoom: 'W1N2',
799+
reached: true,
800+
},
801+
},
802+
room: room,
803+
pos: {
804+
x: 25, // Not border position
805+
y: 25,
806+
roomName: 'W1N1',
807+
isBorder: () => false, // Not at border
808+
},
809+
hits: 1000,
810+
hitsMax: 1000,
811+
say: () => {},
812+
moveRandom: () => {},
813+
siege: () => true,
814+
};
815+
816+
roles.squadsiege.action(creep);
817+
818+
// routing.reached should be deleted because not at border and not in target room
819+
assert.equal(creep.memory.routing.reached, undefined);
820+
});
821+
822+
it('squadsiege does not call siege() when not in target room', () => {
823+
const room = new Room('W1N1', 5000);
824+
let siegeCalled = false;
825+
826+
const creep = {
827+
name: 'squadsiege-1',
828+
memory: {
829+
role: 'squadsiege',
830+
routing: {
831+
targetRoom: 'W1N2',
832+
},
833+
},
834+
room: room,
835+
pos: {
836+
x: 25,
837+
y: 25,
838+
roomName: 'W1N1',
839+
isBorder: () => false,
840+
},
841+
hits: 1000,
842+
hitsMax: 1000,
843+
say: () => {},
844+
moveRandom: () => {},
845+
siege: () => {
846+
siegeCalled = true;
847+
return true;
848+
},
849+
};
850+
851+
roles.squadsiege.action(creep);
852+
853+
// siege() should NOT be called when traveling (not in target room)
854+
assert.equal(siegeCalled, false);
855+
});
856+
857+
it('squadsiege calls siege() when in target room', () => {
858+
const room = new Room('W1N1', 5000);
859+
let siegeCalled = false;
860+
861+
const creep = {
862+
name: 'squadsiege-1',
863+
memory: {
864+
role: 'squadsiege',
865+
routing: {
866+
targetRoom: 'W1N1', // Same as current room
867+
},
868+
},
869+
room: room,
870+
pos: {
871+
x: 25,
872+
y: 25,
873+
roomName: 'W1N1',
874+
isBorder: () => false,
875+
},
876+
hits: 1000,
877+
hitsMax: 1000,
878+
say: () => {},
879+
siege: () => {
880+
siegeCalled = true;
881+
return true;
882+
},
883+
};
884+
885+
roles.squadsiege.action(creep);
886+
887+
// siege() SHOULD be called when in target room
888+
assert.equal(siegeCalled, true);
889+
});
890+
891+
it('squadheal does not delete routing.reached when at border position', () => {
892+
const room = new Room('W1N1', 5000);
893+
894+
const creep = {
895+
name: 'squadheal-1',
896+
memory: {
897+
role: 'squadheal',
898+
routing: {
899+
targetRoom: 'W1N2',
900+
reached: true,
901+
},
902+
},
903+
room: room,
904+
pos: {
905+
x: 0, // Border position
906+
y: 25,
907+
roomName: 'W1N1',
908+
isBorder: () => true, // At border
909+
findClosestByRange: () => null,
910+
},
911+
hits: 1000,
912+
hitsMax: 1000,
913+
selfHeal: () => {},
914+
say: () => {},
915+
moveRandom: () => {},
916+
moveTo: () => {},
917+
squadHeal: () => {},
918+
creepLog: () => {},
919+
};
920+
921+
roles.squadheal.action(creep);
922+
923+
// routing.reached should still be true because at border
924+
assert.equal(creep.memory.routing.reached, true);
925+
});
926+
927+
it('squadheal deletes routing.reached when not in target room and not at border', () => {
928+
const room = new Room('W1N1', 5000);
929+
930+
const creep = {
931+
name: 'squadheal-1',
932+
memory: {
933+
role: 'squadheal',
934+
routing: {
935+
targetRoom: 'W1N2',
936+
reached: true,
937+
},
938+
},
939+
room: room,
940+
pos: {
941+
x: 25, // Not border position
942+
y: 25,
943+
roomName: 'W1N1',
944+
isBorder: () => false, // Not at border
945+
findClosestByRange: () => null,
946+
},
947+
hits: 1000,
948+
hitsMax: 1000,
949+
selfHeal: () => {},
950+
say: () => {},
951+
moveRandom: () => {},
952+
moveTo: () => {},
953+
squadHeal: () => {},
954+
creepLog: () => {},
955+
};
956+
957+
roles.squadheal.action(creep);
958+
959+
// routing.reached should be deleted because not at border and not in target room
960+
assert.equal(creep.memory.routing.reached, undefined);
961+
});
962+
});

0 commit comments

Comments
 (0)