Screeps: Arena

Screeps: Arena

Awful documentation and awful API
I'm trying to get into this game. I've downloaded the demo and followed the tutorials, which explained some very basic concepts but didn't really explain anything about how the engine works.

Anyway, I got the "Challenge" at the end of the tutorial, and after some poking around, I observe that my starting `StructureSpawn` starts with 500/1000 energy, and spawning a creep from it costs 50 energy. Here is the code which does this:

import { getObjectsByPrototype } from '/game/utils'; import { StructureSpawn } from '/game/prototypes'; import { MOVE } from '/game/constants'; const creepMap = {}; export function loop() { spawnCreep("Andy") } export function spawnCreep(name) { const mySpawn = getObjectsByPrototype(StructureSpawn)[0] if (name in creepMap) { return } else { const creepMaybe = mySpawn.spawnCreep([MOVE]) const creep = creepMaybe.object const error = creepMaybe.error console.log("Creep " + creep) console.log("Error " + error) if (creep && !error) { creepMap[name] = creep } } }

There is some slight complication with this code, where I am maintaining a map of creep names -> creep objects, with the idea being that I can call this function to spawn creeps with unique names, and for the code which creates a creep to only run if the map doesn't already have a creep in it. Also, I only add to the map if the result of the spawn was not an error because, based upon the documentation, the result of calling

StructureSpawn.spawnCreep(body)

is either an object on an error code. I log both the possibilities out and see this in my logs:

Creep [object Object] Error undefined

Side note at this point; I absolutely detest javascript, but I'll bear with it to learn a cool game such as this, as long as the documentation is sensible and not full of gotchas.

Anyway, so far so good right?

Well, my
StructureSpawn
still has 450 energy left, that should be enough to spawn another 9 creeps, so how about we just try spawning 1 more and seeing what happens? Here is the code for that:

import { getObjectsByPrototype } from '/game/utils'; import { StructureSpawn } from '/game/prototypes'; import { MOVE } from '/game/constants'; const creepMap = {}; export function loop() { spawnCreep("Andy") spawnCreep("Dave") } export function spawnCreep(name) { const mySpawn = getObjectsByPrototype(StructureSpawn)[0] if (name in creepMap) { return } else { const creepMaybe = mySpawn.spawnCreep([MOVE]) const creep = creepMaybe.object const error = creepMaybe.error console.log("Creep " + creep) console.log("Error " + error) if (creep && !error) { creepMap[name] = creep } } }

As you can see, this code is identical to the first code, with the addition of a second call to
spawnCreep
passing a new unique name "Dave". In my logs I see:

Creep [object Object] Error undefined Creep [object Object] Error undefined

Indicating both calls to mySpawn.spawnCreep([MOVE]) succeeded.

However, I don't see 2 creeps in the game world. And the structures energy remains at 450... so, the second creep didn't spawn... but... I got no error, in fact I even got an object back. What the hell is going on?

Ok, I go back to the tutorial code for spawning creeps. I'm rather confused at this point. Anyway, the tutorial code shows that a call to
creep.moveTo(location)
is made. Perhaps there is some undocumented "gotcha" about the spawn spot needing to be clear of other creeps for a spawn to be successful, but where this failure condition also doesn't result in an error being returned from the
spawnCreep
call. I mean, that would be absolutely awful API design and documentation, but I suppose this is what the tutorial code is doing, so let's go ahead and try that.

So, I now update my code to the following:

import { getObjectsByPrototype } from '/game/utils'; import { StructureSpawn } from '/game/prototypes'; import { MOVE, TOP } from '/game/constants'; const creepMap = {}; export function loop() { spawnCreep("Andy") spawnCreep("Dave") } export function spawnCreep(name) { const mySpawn = getObjectsByPrototype(StructureSpawn)[0] if (name in creepMap) { return } else { const creepMaybe = mySpawn.spawnCreep([MOVE]) const creep = creepMaybe.object const error = creepMaybe.error creep .move(TOP) console.log("Creep " + creep) console.log("Error " + error) if (creep && !error) { creepMap[name] = creep } } }

Adding a call to
move(TOP)
after my creep has spawned. I run the code. Nothing happens, the creep doesn't move. I see no errors.... what the hell? Getting annoyed now.

I move the call to
move
to inside the
if
as this is a closer match to the tutorial code (although why this should make any difference is honestly beyond me... once I have an instance of a creep, calling move on it should move it, or at the very least queue up the move to be taken on the next update loop surely?). So now my code looks like:


import { getObjectsByPrototype } from '/game/utils'; import { StructureSpawn } from '/game/prototypes'; import { MOVE, TOP } from '/game/constants'; const creepMap = {}; export function loop() { spawnCreep("Andy") spawnCreep("Dave") } export function spawnCreep(name) { const mySpawn = getObjectsByPrototype(StructureSpawn)[0] if (name in creepMap) { creepMap[name].move(TOP) return } else { const creepMaybe = mySpawn.spawnCreep([MOVE]) const creep = creepMaybe.object const error = creepMaybe.error console.log("Creep " + creep) console.log("Error " + error) if (creep && !error) { creepMap[name] = creep } } }

And now the creep moves... ok, this isn't documented, and makes 0 sense at all to me, but progress I suppose?! However, my "Dave" creep isn't spawning, even though the spawn spot is clear. But I still have no errors. So, just to be super clear about this;
StructureSpawn.spawnCreep(body)
is not returning an error, IS returning an object, but no creep is spawned...

So basically, the API is completely broken and undocumented?

Great.... guess I'll not be buying this then. What the hell?
Last edited by ShottyMonster; Apr 13, 2022 @ 6:56am
< >
Showing 1-6 of 6 comments
Tigga Apr 13, 2022 @ 7:08am 
It seems a few people have this, or a similar issue.

It feels like maybe the earlier tutorials didn't hammer quite enough on the concept of ticks and how the game processes things. When you call spawnCreep, while an object is returned immediately, that doesn't mean your creep has spawned. Nothing in the game happens in the same tick as your code executes. Instead think of it as:

In your tick:
Your code registers intents to do all these actions

After your code has executed:
The backend processes these and changes the game state

In your next tick:
You can see the changed game state and interact with it.

You're trying to stack things together on one tick and not waiting for your intents to process.

I would argue that the API is not completely broken and undocumented, it's just a bit confusing that spawnCreep returns an object. FWIW this is super-useful later down the line, which is why I suspect this inconsistency exists (it's not done like this anywhere else).

What's happening in your case is that you're invalidating the first object by overwriting the spawnCreep intent with a new spawnCreep intent. It doesn't work as a queue. I do agree this could be better documented. It's not completely broken.
ShottyMonster Apr 13, 2022 @ 7:50am 
Originally posted by Tigga:
It seems a few people have this, or a similar issue.

It feels like maybe the earlier tutorials didn't hammer quite enough on the concept of ticks and how the game processes things. When you call spawnCreep, while an object is returned immediately, that doesn't mean your creep has spawned. Nothing in the game happens in the same tick as your code executes. Instead think of it as:

In your tick:
Your code registers intents to do all these actions

After your code has executed:
The backend processes these and changes the game state

In your next tick:
You can see the changed game state and interact with it.

You're trying to stack things together on one tick and not waiting for your intents to process.

I would argue that the API is not completely broken and undocumented, it's just a bit confusing that spawnCreep returns an object. FWIW this is super-useful later down the line, which is why I suspect this inconsistency exists (it's not done like this anywhere else).

What's happening in your case is that you're invalidating the first object by overwriting the spawnCreep intent with a new spawnCreep intent. It doesn't work as a queue. I do agree this could be better documented. It's not completely broken.

So, you can only have 1 "active" intent on each game "object" at a time? I.e. you can only have 1 "spawn" intent active on each "spawn structure". They don't queue up either... is an intent guaranteed to be completed (either success|error) by the start of the next tick or can they take an indeterminate amount of time to complete (kind of like a future?).

And then, the obvious question; how can I know when it's "safe" to call "spawn" again and be sure it won't overwrite my old "spawn" intent? Is there a "hasActiveIntent" flag I can query on different game objects?
ShottyMonster Apr 13, 2022 @ 7:52am 
Originally posted by Tigga:
It seems a few people have this, or a similar issue.

It feels like maybe the earlier tutorials didn't hammer quite enough on the concept of ticks and how the game processes things. When you call spawnCreep, while an object is returned immediately, that doesn't mean your creep has spawned. Nothing in the game happens in the same tick as your code executes. Instead think of it as:

In your tick:
Your code registers intents to do all these actions

After your code has executed:
The backend processes these and changes the game state

In your next tick:
You can see the changed game state and interact with it.

You're trying to stack things together on one tick and not waiting for your intents to process.

I would argue that the API is not completely broken and undocumented, it's just a bit confusing that spawnCreep returns an object. FWIW this is super-useful later down the line, which is why I suspect this inconsistency exists (it's not done like this anywhere else).

What's happening in your case is that you're invalidating the first object by overwriting the spawnCreep intent with a new spawnCreep intent. It doesn't work as a queue. I do agree this could be better documented. It's not completely broken.

How do you "wait for your intent to process"? is there some state I can query?
Tigga Apr 13, 2022 @ 7:58am 
All intents are processed at the end of the tick and are ready by your next tick. By "wait" I mean "wait until next tick".

Everything is synchronous. Both sides generate intents, there's a sync point, all intents are processed, there's a sync point. An intent can fail for a number of reasons - for example if two creeps try to move to the same tile, only one will succeed. If you have two spawns they could both try to spawn in the same tick, and it's possible one wouldn't succeed as the other had "stolen" the energy.

I would recommend not saving the objects, you can pick them up from getObjectsByPrototype next tick if the intent succeeded.
Last edited by Tigga; Apr 13, 2022 @ 8:13am
Tigga Apr 13, 2022 @ 8:11am 
Originally posted by ShottyMonster:
So, you can only have 1 "active" intent on each game "object" at a time? I.e. you can only have 1 "spawn" intent active on each "spawn structure".
You can only have one active intent of a given type. You can't tell a creep to move left and right at the same time either. It'll only process the last one.

Creeps can do multiple intents in the same tick. For example, you can move and heal. Or move and attack. There's another undocumented problem here where some intents are mutually exclusive and some are not. Hopefully the devs will document this separately for Arena but as far as I understand it's the same as World right now. https://docs.screeps.com/simultaneous-actions.html

In general do _not_ use docs.screeps.com for Arena docs. Again... something that could probably be better. But once you know you know :)
Last edited by Tigga; Apr 13, 2022 @ 8:18am
ShottyMonster Apr 13, 2022 @ 9:03am 
Originally posted by Tigga:
Originally posted by ShottyMonster:
So, you can only have 1 "active" intent on each game "object" at a time? I.e. you can only have 1 "spawn" intent active on each "spawn structure".
You can only have one active intent of a given type. You can't tell a creep to move left and right at the same time either. It'll only process the last one.

Creeps can do multiple intents in the same tick. For example, you can move and heal. Or move and attack. There's another undocumented problem here where some intents are mutually exclusive and some are not. Hopefully the devs will document this separately for Arena but as far as I understand it's the same as World right now. https://docs.screeps.com/simultaneous-actions.html

In general do _not_ use docs.screeps.com for Arena docs. Again... something that could probably be better. But once you know you know :)


So, something like this seems like a sensible way of keeping track of what resources I've got available each tick without re-issuing intents to game objects that have already received one (and I can think of a way I would encode the more complex edge cases you linked me to later down the line if/when I need to).

This code will spawn up to 4 creeps. Every loop, I get hold of the ids of spawn structures and creeps that I own using map. Then, because I am guaranteed that the previous ticks intents have all been processed, each of these ids can be used once this tick for something, so when I want to use them, I "shift" the id out of the list (so it won't be re-used this tick) and use the object that id relates to for "something".

This seems like a pretty solid starting point for organising my tick code.

import { getObjectsByPrototype, getObjectById } from '/game/utils'; import { StructureSpawn, Creep } from '/game/prototypes'; import { MOVE, TOP } from '/game/constants'; export function loop() { const spawnIdsWithoutIntents = getObjectsByPrototype(StructureSpawn) .filter(structureSpawn => structureSpawn.my ) .map(structureSpawn => structureSpawn.id ) const creepIdsWithoutIntents = getObjectsByPrototype(Creep) .filter(creep => creep.my ).map(creep => creep.id ) if (creepIdsWithoutIntents.length < 5) { spawnCreep(spawnIdsWithoutIntents) } } function spawnCreep( spawnIds ) { const firstNoneBusySpawnId = spawnIds.shift() const spawn = getObjectById(firstNoneBusySpawnId) if (spawn) { const creep = spawn.spawnCreep([MOVE]).object } }
Last edited by ShottyMonster; Apr 13, 2022 @ 9:06am
< >
Showing 1-6 of 6 comments
Per page: 1530 50