From 3922f367a222ca6a350ec8ac8335b2a9143d81c8 Mon Sep 17 00:00:00 2001 From: uwap Date: Wed, 31 Dec 2025 15:51:05 +0100 Subject: [PATCH] Add a Task system --- src/Constants/BodyPartCosts.ts | 12 ----- src/Proto/index.ts | 96 +++++++++++++++++++++++++++++++++ src/Tasks/Build.ts | 34 ++++++++++++ src/Tasks/Harvest.ts | 49 +++++++++++++++++ src/Tasks/Repair.ts | 34 ++++++++++++ src/Tasks/Task.ts | 97 ++++++++++++++++++++++++++++++++++ src/Tasks/Transfer.ts | 41 ++++++++++++++ src/Tasks/Upgrade.ts | 30 +++++++++++ src/Tasks/Withdraw.ts | 55 +++++++++++++++++++ src/Tasks/index.ts | 56 ++++++++++++++++++++ src/Workers/worker.test.ts | 16 ------ src/screeps-profiler.d.ts | 7 +++ 12 files changed, 499 insertions(+), 28 deletions(-) delete mode 100644 src/Constants/BodyPartCosts.ts create mode 100644 src/Proto/index.ts create mode 100644 src/Tasks/Build.ts create mode 100644 src/Tasks/Harvest.ts create mode 100644 src/Tasks/Repair.ts create mode 100644 src/Tasks/Task.ts create mode 100644 src/Tasks/Transfer.ts create mode 100644 src/Tasks/Upgrade.ts create mode 100644 src/Tasks/Withdraw.ts create mode 100644 src/Tasks/index.ts delete mode 100644 src/Workers/worker.test.ts create mode 100644 src/screeps-profiler.d.ts diff --git a/src/Constants/BodyPartCosts.ts b/src/Constants/BodyPartCosts.ts deleted file mode 100644 index bb7af76..0000000 --- a/src/Constants/BodyPartCosts.ts +++ /dev/null @@ -1,12 +0,0 @@ -const BodyPartCosts = { - [MOVE]: 50, - [WORK]: 100, - [CARRY]: 50, - [ATTACK]: 80, - [RANGED_ATTACK]: 150, - [HEAL]: 250, - [CLAIM]: 600, - [TOUGH]: 10, -}; - -export default BodyPartCosts; diff --git a/src/Proto/index.ts b/src/Proto/index.ts new file mode 100644 index 0000000..6ce56d7 --- /dev/null +++ b/src/Proto/index.ts @@ -0,0 +1,96 @@ +declare global { + interface RoomMemory { + sources: { + [id: Id]: SourceMemory; + }; + spawn: Id | null; + _spawnCacheTimeout?: number; + } + + interface SourceMemory { + container: Id | null; + } + + interface Source { + memory: SourceMemory; + get container(): StructureContainer | null; + } + + interface Room { + get sources(): Source[]; + get spawn(): StructureSpawn | null; + } +} + +Object.defineProperty(Room.prototype, "sources", { + get: function (this: Room) { + if (this == Room.prototype || this == undefined) return undefined; + if (!this.memory.sources) { + this.memory.sources = {}; + const sources = this.find(FIND_SOURCES); + for (const source of sources) { + this.memory.sources[source.id] = { + container: null, + }; + } + } + return Object.keys(this.memory.sources).map(Game.getObjectById); + }, + enumerable: true, + configurable: true, +}); + +Object.defineProperty(Room.prototype, "spawn", { + get: function (this: Room) { + if (this == Room.prototype || this == undefined) return undefined; + if (!this.memory.spawn) { + if (this.memory._spawnCacheTimeout == null + || this.memory._spawnCacheTimeout > Game.time) { + const spawns = this.find(FIND_MY_SPAWNS); + if (spawns.length > 0) { + this.memory.spawn = spawns[0].id; + } + else { + this.memory._spawnCacheTimeout = Game.time + 100; + } + } + } + return this.memory.spawn == null + ? null + : Game.getObjectById(this.memory.spawn); + }, + enumerable: false, + configurable: true, +}); + +Object.defineProperty(Source.prototype, "memory", { + get: function (this: Source) { + return this.room.memory.sources[this.id]; + }, + set: function (this: Source, mem: SourceMemory) { + this.room.memory.sources[this.id] = mem; + }, + enumerable: false, + configurable: true, +}); + +Object.defineProperty(Source.prototype, "container", { + get: function (this: Source) { + if (this.memory.container == null + || Game.getObjectById(this.memory.container) == null) { + const containers = this.pos.findInRange(FIND_STRUCTURES, 1, { + filter: (s: Structure) => s.structureType === STRUCTURE_CONTAINER, + }) as StructureContainer[]; + if (containers.length > 0) { + this.memory.container = containers[0].id; + } + } + return this.memory.container == null + ? null + : Game.getObjectById(this.memory.container); + }, + enumerable: false, + configurable: true, +}); + +export default {}; diff --git a/src/Tasks/Build.ts b/src/Tasks/Build.ts new file mode 100644 index 0000000..b7b6791 --- /dev/null +++ b/src/Tasks/Build.ts @@ -0,0 +1,34 @@ +import profiler from "screeps-profiler"; +import { TaskData, TaskStatus, TaskType } from "./Task"; + +export const Build + = (target: ConstructionSite): TaskData => ({ + target, + targetPos: target.pos, + type: TaskType.Build, + options: {}, + data: {}, + }); + +export const runBuild = profiler.registerFN((creep: Creep): TaskStatus => { + const task = creep.task; + if (task == null) { + return TaskStatus.DONE; + } + + if (creep.store.getUsedCapacity(RESOURCE_ENERGY) === 0) { + return TaskStatus.DONE; + } + + const target = task.target as ConstructionSite; + if (target == null && task.targetPos.roomName === creep.room.name) { + return TaskStatus.DONE; + } + + if (target == null + || creep.build(target) === ERR_NOT_IN_RANGE) { + creep.travelTo(task.targetPos); + return TaskStatus.IN_PROGRESS; + } + return TaskStatus.DONE; +}, "runBuild"); diff --git a/src/Tasks/Harvest.ts b/src/Tasks/Harvest.ts new file mode 100644 index 0000000..0498854 --- /dev/null +++ b/src/Tasks/Harvest.ts @@ -0,0 +1,49 @@ +import profiler from "screeps-profiler"; +import { TaskData, TaskStatus, TaskType } from "./Task"; + +interface HarvestOptions { + stopWhenFull: boolean; +} + +interface HarvestData { + resource: ResourceConstant; +}; + +const defaultOptions: HarvestOptions = { + stopWhenFull: true, +}; + +export const Harvest + = (target: Source | Mineral, + opts: Partial = {}): TaskData => ({ + target, + targetPos: target.pos, + type: TaskType.Harvest, + options: { ...defaultOptions, ...opts }, + data: { + resource: target instanceof Source + ? RESOURCE_ENERGY + : target.mineralType, + }, + }); + +export const runHarvest = profiler.registerFN((creep: Creep): TaskStatus => { + const task = creep.task; + if (task == null) { + return TaskStatus.DONE; + } + + const target = task.target as Source | Mineral | null; + const opts = task.options as HarvestOptions; + const data = task.data as HarvestData; + + if (opts.stopWhenFull && creep.store.getFreeCapacity(data.resource) == 0) { + return TaskStatus.DONE; + } + + if (target == null + || creep.harvest(target) === ERR_NOT_IN_RANGE) { + creep.travelTo(task.targetPos); + } + return TaskStatus.IN_PROGRESS; +}, "runHarvest"); diff --git a/src/Tasks/Repair.ts b/src/Tasks/Repair.ts new file mode 100644 index 0000000..41fcf98 --- /dev/null +++ b/src/Tasks/Repair.ts @@ -0,0 +1,34 @@ +import profiler from "screeps-profiler"; +import { TaskData, TaskStatus, TaskType } from "./Task"; + +export const Repair + = (target: Structure): TaskData => ({ + target, + targetPos: target.pos, + type: TaskType.Repair, + options: {}, + data: {}, + }); + +export const runRepair = profiler.registerFN((creep: Creep): TaskStatus => { + const task = creep.task; + if (task == null) { + return TaskStatus.DONE; + } + + if (creep.store.energy === 0) { + return TaskStatus.DONE; + } + + const target = task.target as Structure; + if (target == null && task.targetPos.roomName === creep.room.name) { + return TaskStatus.DONE; + } + + if (target == null + || creep.repair(target) === ERR_NOT_IN_RANGE) { + creep.travelTo(task.targetPos); + return TaskStatus.IN_PROGRESS; + } + return TaskStatus.DONE; +}, "runRepair"); diff --git a/src/Tasks/Task.ts b/src/Tasks/Task.ts new file mode 100644 index 0000000..c4ea1ea --- /dev/null +++ b/src/Tasks/Task.ts @@ -0,0 +1,97 @@ +import { + packId, packPos, unpackId, unpackPos, +} from "../../deps/screeps-packrat/src/packrat"; + +export enum TaskType { + Harvest, + Upgrade, + Withdraw, + Transfer, + Build, + Repair, +} + +export enum TaskStatus { + DONE, + IN_PROGRESS, +} + +declare global { + interface CreepMemory { + task?: string; + } + interface Creep { + readonly isIdle: boolean; + task: TaskData | null; + _task?: TaskData | null; + } + interface RoomObject { + readonly assignedCreeps: Creep[]; + } +} + +export interface TaskData { + type: TaskType; + targetPos: RoomPosition; + target: RoomObject & _HasId | null; + options: object; + data: object; +} + +const packTaskData = (td: TaskData): string => + String.fromCharCode(td.type + 65) + + packPos(td.targetPos) + + JSON.stringify({ o: td.options, d: td.data, t: td.target?.id }); + +const unpackTaskData = (s: string): TaskData => { + const { o: options, d: data, t: targetId } = JSON.parse(s.substring(3)); + const target = Game.getObjectById(targetId as Id); + return { + type: s.charCodeAt(0) - 65, + targetPos: unpackPos(s.substring(1, 3)), + target, + options, + data, + }; +}; + +Object.defineProperty(Creep.prototype, "isIdle", { + get: function (this: Creep) { + return this.memory.task == null; + }, + configurable: true, +}); + +Object.defineProperty(Creep.prototype, "task", { + get: function (this: Creep) { + if (this._task != null) { + return this._task; + } + if (this.memory.task == null) { + return null; + } + this._task = unpackTaskData(this.memory.task); + return this._task; + }, + set: function (this: Creep, task: TaskData | null) { + this._task = task; + if (task != null) { + this.memory.task = packTaskData(task); + } + else { + delete this.memory.task; + } + }, + configurable: true, +}); + +Object.defineProperty(RoomObject.prototype, "assignedCreeps", { + get: function (this: RoomObject) { + if ("id" in this) { + return Object.values(Game.creeps) + .filter(x => x.task?.target?.id == this.id); + } + return []; + }, + configurable: true, +}); diff --git a/src/Tasks/Transfer.ts b/src/Tasks/Transfer.ts new file mode 100644 index 0000000..2e07d96 --- /dev/null +++ b/src/Tasks/Transfer.ts @@ -0,0 +1,41 @@ +import { TaskData, TaskStatus, TaskType } from "./Task"; + +interface TransferOptions { + // The amount of resources to transfer + amount: number | null; + // The type of resource to transfer + resource: ResourceConstant; +} + +const defaultOptions: TransferOptions = { + amount: null, + resource: RESOURCE_ENERGY, +}; + +export const Transfer + = (target: Structure | Creep | PowerCreep, + opts: Partial = {}): TaskData => ({ + target, + targetPos: target.pos, + type: TaskType.Transfer, + options: { ...defaultOptions, ...opts }, + data: {}, + }); + +export const runTransfer = (creep: Creep): TaskStatus => { + const task = creep.task; + if (task == null) { + return TaskStatus.DONE; + } + + const target = task.target as Structure | Creep | PowerCreep; + const opts = task.options as TransferOptions; + + if (target == null + || creep.transfer( + target, opts.resource, opts.amount ?? undefined) === ERR_NOT_IN_RANGE) { + creep.travelTo(task.targetPos); + return TaskStatus.IN_PROGRESS; + } + return TaskStatus.DONE; +}; diff --git a/src/Tasks/Upgrade.ts b/src/Tasks/Upgrade.ts new file mode 100644 index 0000000..5c2a4dd --- /dev/null +++ b/src/Tasks/Upgrade.ts @@ -0,0 +1,30 @@ +import profiler from "screeps-profiler"; +import { TaskData, TaskStatus, TaskType } from "./Task"; + +export const Upgrade + = (target: StructureController): TaskData => ({ + target, + targetPos: target.pos, + type: TaskType.Upgrade, + options: {}, + data: {}, + }); + +export const runUpgrade = profiler.registerFN((creep: Creep): TaskStatus => { + const task = creep.task; + if (task == null) { + return TaskStatus.DONE; + } + + if (creep.store.energy === 0) { + return TaskStatus.DONE; + } + + const target = task.target as StructureController | null; + + if (target == null + || creep.upgradeController(target) === ERR_NOT_IN_RANGE) { + creep.travelTo(task.targetPos); + } + return TaskStatus.IN_PROGRESS; +}, "runUpgrade"); diff --git a/src/Tasks/Withdraw.ts b/src/Tasks/Withdraw.ts new file mode 100644 index 0000000..ad4fd6f --- /dev/null +++ b/src/Tasks/Withdraw.ts @@ -0,0 +1,55 @@ +import { TaskData, TaskStatus, TaskType } from "./Task"; + +interface WithdrawOptions { + // The maximum number of resources the creep should carry after the withdraw. + limit: number | null; + // The amount of resources to withdraw + amount: number | null; + // The type of resource to withdraw + resource: ResourceConstant; +} + +const defaultOptions: WithdrawOptions = { + limit: null, + amount: null, + resource: RESOURCE_ENERGY, +}; + +export const Withdraw + = (target: Structure | Tombstone | Ruin, + opts: Partial = {}): TaskData => ({ + target, + targetPos: target.pos, + type: TaskType.Withdraw, + options: { ...defaultOptions, ...opts }, + data: {}, + }); + +export const runWithdraw = (creep: Creep): TaskStatus => { + const task = creep.task; + if (task == null) { + return TaskStatus.DONE; + } + + const target = task.target as Structure | Tombstone | Ruin; + const opts = task.options as WithdrawOptions; + + if (opts.limit != null + && creep.store.getUsedCapacity(opts.resource) >= opts.limit) { + return TaskStatus.DONE; + } + const capacity = creep.store.getFreeCapacity(opts.resource); + const amount = Math.min(opts.amount ?? capacity, + opts.limit ?? capacity - creep.store.getUsedCapacity(opts.resource)); + + if (amount <= 0) { + return TaskStatus.DONE; + } + + if (target == null + || creep.withdraw(target, opts.resource, amount) === ERR_NOT_IN_RANGE) { + creep.travelTo(task.targetPos); + return TaskStatus.IN_PROGRESS; + } + return TaskStatus.DONE; +}; diff --git a/src/Tasks/index.ts b/src/Tasks/index.ts new file mode 100644 index 0000000..8df88c2 --- /dev/null +++ b/src/Tasks/index.ts @@ -0,0 +1,56 @@ +import { Harvest, runHarvest } from "./Harvest"; +import { runUpgrade, Upgrade } from "./Upgrade"; + +import { TaskType, TaskStatus, TaskData } from "./Task"; +import { runWithdraw, Withdraw } from "./Withdraw"; +import { runTransfer, Transfer } from "./Transfer"; +import { Build, runBuild } from "./Build"; +import { Repair, runRepair } from "./Repair"; +import profiler from "screeps-profiler"; +export { TaskType, TaskStatus } from "./Task"; + +declare global { + interface Creep { + run: (generator?: (creep: Creep) => TaskData | null) => void; + } +} + +const runTask = profiler.registerFN((creep: Creep): TaskStatus => { + switch (creep.task?.type) { + case TaskType.Harvest: + return runHarvest(creep); + case TaskType.Upgrade: + return runUpgrade(creep); + case TaskType.Withdraw: + return runWithdraw(creep); + case TaskType.Transfer: + return runTransfer(creep); + case TaskType.Build: + return runBuild(creep); + case TaskType.Repair: + return runRepair(creep); + default: + return TaskStatus.DONE; + } +}, "runTask"); + +Creep.prototype.run = function (generator?: (creep: Creep) => TaskData | null) { + const status = runTask(this); + if (status === TaskStatus.DONE) { + this.task = null; + if (generator != null) { + this.task = generator(this); + runTask(this); + } + } +}; + +export default profiler.registerObject({ + Harvest, + Upgrade, + Withdraw, + Transfer, + Build, + Repair, + ...TaskStatus, +}, "Tasks"); diff --git a/src/Workers/worker.test.ts b/src/Workers/worker.test.ts deleted file mode 100644 index 667389c..0000000 --- a/src/Workers/worker.test.ts +++ /dev/null @@ -1,16 +0,0 @@ -import BodyPartCosts from "../Constants/BodyPartCosts"; -import * as Workers from "./index"; - -describe("Test Creep Workers", () => { - console.log(Workers.Clerk); - for (const [moduleName, worker] of Object.entries(Workers)) { - test(`${moduleName}: Body parts cost calculation is correct`, () => { - for (let cost = 0; cost < 1500; cost++) { - expect( - worker.bodyDefinition(cost) - .map(x => BodyPartCosts[x]).reduce((x, y) => x + y, 0), - ).toBeLessThanOrEqual(cost); - } - }); - } -}); diff --git a/src/screeps-profiler.d.ts b/src/screeps-profiler.d.ts new file mode 100644 index 0000000..d6d2261 --- /dev/null +++ b/src/screeps-profiler.d.ts @@ -0,0 +1,7 @@ +declare module "screeps-profiler" { + export const registerFN: (f: T, n?: string) => T; + export const registerObject: (f: T, n?: string) => T; + export const registerClass: (f: T, n?: string) => T; + export const enable: () => void; + export const wrap: (f: () => void) => () => void; +};