diff --git a/src/roleHandlers/BaseHandler.interface.ts b/src/roleHandlers/BaseHandler.interface.ts deleted file mode 100644 index a47b3dd..0000000 --- a/src/roleHandlers/BaseHandler.interface.ts +++ /dev/null @@ -1,12 +0,0 @@ -export abstract class RoleHandler { - public static destroy(_creepMemory: CreepMemory, _state: GameState): void { - // Default implementation does nothing - // Subclasses should override this method - } - - public static run(_creep: Creep, state: GameState): GameState { - // Default implementation returns state unchanged - // Subclasses should override this method - return state; - } -} diff --git a/src/roleHandlers/base.handler.ts b/src/roleHandlers/base.handler.ts new file mode 100644 index 0000000..4065ac7 --- /dev/null +++ b/src/roleHandlers/base.handler.ts @@ -0,0 +1,9 @@ +export abstract class RoleHandler { + public static destroy(_creepMemory: CreepMemory, _state: GameState): void { + throw Error('not implemented'); + } + + public static run(_creep: Creep, _state: GameState): GameState { + throw Error('not implemented'); + } +} diff --git a/src/roleHandlers/harvester.handler.ts b/src/roleHandlers/harvester.handler.ts index 2a60b68..1fa114c 100644 --- a/src/roleHandlers/harvester.handler.ts +++ b/src/roleHandlers/harvester.handler.ts @@ -1,4 +1,4 @@ -import { RoleHandler } from './BaseHandler.interface'; +import { RoleHandler } from './base.handler'; import { SourceDestination } from '@/types/creeps'; import { getSourceById, getSpawnById } from '@/utils/funcs/getById'; import { diff --git a/src/roleHandlers/index.ts b/src/roleHandlers/index.ts index 70fba6f..e1fb485 100644 --- a/src/roleHandlers/index.ts +++ b/src/roleHandlers/index.ts @@ -1,4 +1,4 @@ -import { RoleHandler } from './BaseHandler.interface'; +import { RoleHandler } from './base.handler'; import HarvesterHandler from './harvester.handler'; import UpgraderHandler from './upgrader.handler'; diff --git a/src/roleHandlers/upgrader.handler.ts b/src/roleHandlers/upgrader.handler.ts index 2c97533..85222fa 100644 --- a/src/roleHandlers/upgrader.handler.ts +++ b/src/roleHandlers/upgrader.handler.ts @@ -1,4 +1,4 @@ -import { RoleHandler } from './BaseHandler.interface'; +import { RoleHandler } from './base.handler'; import { SourceDestination } from '@/types/creeps'; import { getControllerById, getSourceById } from '@/utils/funcs/getById'; import { diff --git a/tests/roleHandlers/harvester.handler.test.ts b/tests/roleHandlers/harvester.handler.test.ts index 0fc791a..f871d90 100644 --- a/tests/roleHandlers/harvester.handler.test.ts +++ b/tests/roleHandlers/harvester.handler.test.ts @@ -119,6 +119,32 @@ describe('HarvesterHandler', () => { expect(memory.previousDestination).toBeUndefined(); }); + it('releases both destination and previousDestination source spots when both are set', () => { + const { state, memory } = createFixture({ + memory: { + destination: { type: 'source', id: 'src1', sourceSpot: 0 }, + previousDestination: { + type: 'source', + id: 'src1', + sourceSpot: 2, + }, + }, + }); + state.sourcesStates['src1'].spots[0] = PositionSpotStatus.OCCUPIED; + state.sourcesStates['src1'].spots[2] = PositionSpotStatus.OCCUPIED; + + HarvesterHandler.destroy(memory, state); + + expect(state.sourcesStates['src1'].spots[0]).toBe( + PositionSpotStatus.EMPTY + ); + expect(state.sourcesStates['src1'].spots[2]).toBe( + PositionSpotStatus.EMPTY + ); + expect(memory.destination).toBeUndefined(); + expect(memory.previousDestination).toBeUndefined(); + }); + it('does nothing when destination is not a source', () => { const { state, memory } = createFixture({ memory: { @@ -142,6 +168,21 @@ describe('HarvesterHandler', () => { expect(() => HarvesterHandler.destroy(memory, state)).not.toThrow(); }); + + it('does not throw when destination source ID is missing from state', () => { + const { state, memory } = createFixture({ + memory: { + destination: { + type: 'source', + id: 'nonexistent', + sourceSpot: 0, + }, + }, + }); + + expect(() => HarvesterHandler.destroy(memory, state)).not.toThrow(); + expect(memory.destination).toBeUndefined(); + }); }); describe('validateCreepMemory (via run)', () => { @@ -185,7 +226,7 @@ describe('HarvesterHandler', () => { }); it('clears spawn destination when energy is empty', () => { - const { state, creep, mockSpawn } = createFixture({ + const { state, creep, mockSpawn, mockSource } = createFixture({ memory: { destination: { type: 'spawn', id: 'spawn1' }, }, @@ -193,6 +234,8 @@ describe('HarvesterHandler', () => { storeCapacity: 50, }); mockSpawn(); + // Explicitly return null so onFindNewSource finds nothing + mockSource(null); HarvesterHandler.run(creep, state); @@ -224,6 +267,19 @@ describe('HarvesterHandler', () => { expect(creep.memory.destination).toBeUndefined(); }); + + it('marks the assigned source spot as OCCUPIED in state', () => { + const { state, creep, mockSource } = createFixture(); + mockSource(); + + HarvesterHandler.run(creep, state); + + const spotIndex = (creep.memory.destination as any)?.sourceSpot; + expect(spotIndex).toBeDefined(); + expect(state.sourcesStates['src1'].spots[spotIndex]).toBe( + PositionSpotStatus.OCCUPIED + ); + }); }); describe('onSourceDestination', () => { @@ -258,6 +314,26 @@ describe('HarvesterHandler', () => { expect(creep.moveTo).toHaveBeenCalled(); }); + it('moves to the computed spot position (not source pos) when not in range', () => { + const { state, source, creep, mockSource } = createFixture({ + memory: { + destination: { type: 'source', id: 'src1', sourceSpot: 0 }, + }, + }); + mockSource(); + (creep.harvest as jest.Mock).mockReturnValue( + (global as any).ERR_NOT_IN_RANGE + ); + + HarvesterHandler.run(creep, state); + + expect(creep.moveTo).toHaveBeenCalledWith( + // spot 0 → delta (-1,-1) from source pos (5,5) → (4,4) + expect.objectContaining({ x: 4, y: 4, roomName: 'W1N1' }), + expect.objectContaining({ reusePath: 10 }) + ); + }); + it('logs and returns when source is not found', () => { const { state, creep, mockSource } = createFixture({ memory: { @@ -331,4 +407,15 @@ describe('HarvesterHandler', () => { expect(creep.transfer).not.toHaveBeenCalled(); }); }); + + describe('run', () => { + it('returns the state object', () => { + const { state, creep, mockSource } = createFixture(); + mockSource(null); + + const result = HarvesterHandler.run(creep, state); + + expect(result).toBe(state); + }); + }); }); diff --git a/tests/roleHandlers/upgrader.handler.test.ts b/tests/roleHandlers/upgrader.handler.test.ts index 2ddaf8b..6b7908f 100644 --- a/tests/roleHandlers/upgrader.handler.test.ts +++ b/tests/roleHandlers/upgrader.handler.test.ts @@ -125,6 +125,32 @@ describe('UpgraderHandler', () => { expect(memory.previousDestination).toBeUndefined(); }); + it('releases both destination and previousDestination source spots when both are set', () => { + const { state, memory } = createFixture({ + memory: { + destination: { type: 'source', id: 'src1', sourceSpot: 0 }, + previousDestination: { + type: 'source', + id: 'src1', + sourceSpot: 3, + }, + }, + }); + state.sourcesStates['src1'].spots[0] = PositionSpotStatus.OCCUPIED; + state.sourcesStates['src1'].spots[3] = PositionSpotStatus.OCCUPIED; + + UpgraderHandler.destroy(memory, state); + + expect(state.sourcesStates['src1'].spots[0]).toBe( + PositionSpotStatus.EMPTY + ); + expect(state.sourcesStates['src1'].spots[3]).toBe( + PositionSpotStatus.EMPTY + ); + expect(memory.destination).toBeUndefined(); + expect(memory.previousDestination).toBeUndefined(); + }); + it('does nothing when destination is not a source', () => { const { state, memory } = createFixture({ memory: { @@ -148,6 +174,21 @@ describe('UpgraderHandler', () => { expect(() => UpgraderHandler.destroy(memory, state)).not.toThrow(); }); + + it('does not throw when destination source ID is missing from state', () => { + const { state, memory } = createFixture({ + memory: { + destination: { + type: 'source', + id: 'nonexistent', + sourceSpot: 0, + }, + }, + }); + + expect(() => UpgraderHandler.destroy(memory, state)).not.toThrow(); + expect(memory.destination).toBeUndefined(); + }); }); describe('validateCreepMemory (via run)', () => { @@ -173,7 +214,7 @@ describe('UpgraderHandler', () => { }); it('transitions from source to controller destination when energy is full', () => { - const { state, controller, creep, mockSource, mockController } = + const { state, controller, creep, mockController } = createFixture({ memory: { destination: { @@ -186,7 +227,7 @@ describe('UpgraderHandler', () => { storeCapacity: 50, controllerInRoom: { id: 'ctrl1' } as StructureController, }); - mockSource(); + // getSourceById is not called in this path — only getControllerById is needed mockController(controller); UpgraderHandler.run(creep, state); @@ -230,6 +271,13 @@ describe('UpgraderHandler', () => { UpgraderHandler.run(creep, state); expect(creep.memory.destination).toBeUndefined(); + // previousDestination is set to the old source before checking room.controller, + // so the source spot can be released on the next tick + expect(creep.memory.previousDestination).toEqual({ + type: 'source', + id: 'src1', + sourceSpot: 0, + }); }); }); @@ -257,6 +305,19 @@ describe('UpgraderHandler', () => { expect(creep.memory.destination).toBeUndefined(); }); + + it('marks the assigned source spot as OCCUPIED in state', () => { + const { state, creep, mockSource } = createFixture(); + mockSource(); + + UpgraderHandler.run(creep, state); + + const spotIndex = (creep.memory.destination as any)?.sourceSpot; + expect(spotIndex).toBeDefined(); + expect(state.sourcesStates['src1'].spots[spotIndex]).toBe( + PositionSpotStatus.OCCUPIED + ); + }); }); describe('onSourceDestination', () => { @@ -291,6 +352,26 @@ describe('UpgraderHandler', () => { expect(creep.moveTo).toHaveBeenCalled(); }); + it('moves to the computed spot position (not source pos) when not in range', () => { + const { state, creep, mockSource } = createFixture({ + memory: { + destination: { type: 'source', id: 'src1', sourceSpot: 0 }, + }, + }); + mockSource(); + (creep.harvest as jest.Mock).mockReturnValue( + (global as any).ERR_NOT_IN_RANGE + ); + + UpgraderHandler.run(creep, state); + + expect(creep.moveTo).toHaveBeenCalledWith( + // spot 0 → delta (-1,-1) from source pos (5,5) → (4,4) + expect.objectContaining({ x: 4, y: 4, roomName: 'W1N1' }), + expect.objectContaining({ reusePath: 10 }) + ); + }); + it('logs and returns when source is not found', () => { const { state, creep, mockSource } = createFixture({ memory: { @@ -365,4 +446,15 @@ describe('UpgraderHandler', () => { expect(creep.upgradeController).not.toHaveBeenCalled(); }); }); + + describe('run', () => { + it('returns the state object', () => { + const { state, creep, mockSource } = createFixture(); + mockSource(null); + + const result = UpgraderHandler.run(creep, state); + + expect(result).toBe(state); + }); + }); });