Add a Task system

This commit is contained in:
uwap 2025-12-31 15:51:05 +01:00
parent 216d44ab5c
commit 3922f367a2
12 changed files with 499 additions and 28 deletions

View file

@ -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;

96
src/Proto/index.ts Normal file
View file

@ -0,0 +1,96 @@
declare global {
interface RoomMemory {
sources: {
[id: Id<Source>]: SourceMemory;
};
spawn: Id<StructureSpawn> | null;
_spawnCacheTimeout?: number;
}
interface SourceMemory {
container: Id<StructureContainer> | 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 {};

34
src/Tasks/Build.ts Normal file
View file

@ -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");

49
src/Tasks/Harvest.ts Normal file
View file

@ -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<MineralConstant>,
opts: Partial<HarvestOptions> = {}): 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<MineralConstant> | 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");

34
src/Tasks/Repair.ts Normal file
View file

@ -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");

97
src/Tasks/Task.ts Normal file
View file

@ -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<RoomObject & _HasId>);
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,
});

41
src/Tasks/Transfer.ts Normal file
View file

@ -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<TransferOptions> = {}): 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;
};

30
src/Tasks/Upgrade.ts Normal file
View file

@ -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");

55
src/Tasks/Withdraw.ts Normal file
View file

@ -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<WithdrawOptions> = {}): 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;
};

56
src/Tasks/index.ts Normal file
View file

@ -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");

View file

@ -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);
}
});
}
});

7
src/screeps-profiler.d.ts vendored Normal file
View file

@ -0,0 +1,7 @@
declare module "screeps-profiler" {
export const registerFN: <T extends Function>(f: T, n?: string) => T;
export const registerObject: <T extends Object>(f: T, n?: string) => T;
export const registerClass: <T extends Object>(f: T, n?: string) => T;
export const enable: () => void;
export const wrap: (f: () => void) => () => void;
};