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.

spacetraders app - trades

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 assigns 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 spawns and assigns to context a new Actor for each ship that has not yet already been spawned.

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

spawning 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 assigned to context on the parent machine. In the example above I’ve assigned 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 429s 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.

spacetraders app - ships