Commanding the Fleet with XState Actors
In the previous post I introduced SpaceTraders API, a space trading a game for programmers exposed via REST API, and documented my first foray into using XState to automate a single spaceship. In this post I’ll look into automating many spaceships with XState Actors.
My implementation against the API has come pretty far since last writing, with most endpoints implemented and a UI allowing review and control of different game concepts.
An Actor Per Spaceship
XState’s Actors functionality lets a developer spawn
and stop
child state machines as they please. A great feature of Actors is syncing child state to the parent which solved my previous issue of exposing child machine state context to React.
At any one time, each spaceship is assigned a specific strategy, and a separate XState Machine
is implemented for each:
- Trade
- Fly between two locations buying/selling goods for profit
- Halt
- Do nothing but wait
- Probe
- Fly to a location and periodically query it for intelligence
During startup the parent state machine queries the SpaceTraders API for the user’s list of ships and assign
s the result to the machine’s context.
[States.GetShips]: {
invoke: {
src: (c) => api.getShips(c.token!, c.username!),
onDone: {
target: States.GetStrategies,
actions: assign<Context>({
ships: (c, e: any) => (e.data as api.GetShipsResponse).ships,
}),
},
},
},
Another state, SpawnShips
, then spawn
s and assign
s to context
a new Actor for each ship that has not yet already been spawn
ed.
spawnShips: assign<Context>({
actors: (c, e: any) => {
const alreadySpawnedShipIds = c.actors.map(
(actor) => actor.state.context.id
);
const toSpawn: Ship[] = c.ships!.filter((s: Ship) => {
const alreadySpawned = alreadySpawnedShipIds.find((id) => id === s.id);
if (alreadySpawned) {
return false;
}
return true;
});
if (toSpawn.length) console.warn(`Spawning ${toSpawn.length} actor(s)`);
return [...c.actors, ...toSpawn.map(spawnShipMachine(c))] as any;
},
});
spawn
ing is done via XState’s assign
function which is synchronous, so if any data from asynchronous functions is needed during the spawn
, this data must be collected beforehand and placed in the machine’s context
.
Periodically these machines check whether they should change strategy. If a strategy change is needed, they will transition to a state of type=final
, terminating the child instance. The parent machine will spawn
a new machine for the new strategy on its next cycle.
Syncing Child State
The result of a spawn
is assign
ed to context
on the parent machine. In the example above I’ve assign
ed to a property I named actors
. Child machine state can then be synced to that parent property and I can expose it to my React components.
export const ShipComponent = ({ ship: actor }: Props) => {
if (!actor) return <CircularProgress size={48} />;
return (
<>
{actor.state ? (
<Box>
<Typography variant="h6">Name</Typography>
<Typography>{actor.state.context.shipName}</Typography>
</Box>
) : (
<Box>
<CircularProgress size={48} />
</Box>
)}
</>
);
};
A Machine Per Actor
Each Actor is itself a state machine, so it could spawn
its own Actors if needed, or, call other machines. Both the tradeMachine
and probeMachine
actors above have the need to send spaceships to a given location. This means buying fuel to get there, and waiting during flight progress. The travelToLocationMachine
supplies this functionality, and the actors as parent machines call it using the Invoke Services api.
The Actors make API requests whenever they need to and to avoid hitting 429
s due to rate limiting my API implementation uses bottleneck to throttle outgoing requests.
So How Many Spaceships?
Using this approach I’ve automated up to 80 spaceships at one time. This means 80 actors all executing state machines, controlling a single spaceship each. Rate limiting on the Spacetraders API is the limiting factor here and I’m yet to see any evidence that XState couldn’t handle many more than this. As the game continues to be developed I’m certain there will be occasion to command even larger fleets. If you think automating spaceships could be fun, come chat on Discord with myself and others building against the SpaceTraders API.