Rooms

Bots

Players the room drives itself, which take seats, appear in presence, and give their seat to a person who joins a full room.

A bot is a player that your room’s code drives instead of a person: an opponent for someone playing alone, or a seat filled so a game can start. A bot is in presence like anyone else, owns entities, and sends input to your stepper, but it has no connection. It runs inside the room, so it costs no traffic and doesn’t keep the room awake.

A creature or hazard that isn’t a player doesn’t need a bot. Make it an entity the room owns and move it in onTick.

// src/room.ts
import { defineRoom, defineSchema, defineStepper, entity } from "@irt/room";

const Car = entity("car", { owner: "string", x: "f32" });

export default defineRoom({
  schema: defineSchema({ entities: [Car] }),
  stepper: defineStepper<number>({
    step(world, inputs) {
      for (const car of world.all(Car)) car.x += inputs.get(car.owner) ?? 0;
    },
  }),
  bots: {
    racer: {
      start(room, bot) {
        if (![...room.entities.all(Car)].some((c) => c.owner === bot.player)) room.entities.create(Car, { owner: bot.player, x: 0 });
      },
      tick(room, bot) {
        bot.input = bot.data.fast ? 2 : 1;
      },
    },
  },
  onCreate(room) {
    room.bots.add("racer", { name: "Rival", data: { fast: true } });
  },
  onJoin(room, conn) {
    room.entities.create(Car, { owner: conn.player, x: 0 });
  },
  onPlayerLeave(room, player) {
    for (const car of room.entities.all(Car)) if (car.owner === player) room.entities.delete(car);
  },
});

A bot

  • Its player ID is bot: and a number the room gives out in order, never reused while the room exists. No player token can have a sub starting with bot:, so nobody can pose as a bot. isBot(player), from @irt/client or @irt/room, tells them apart.
  • Behaviours are declared by name in bots in defineRoom, and room.bots.add(name, { name?, status?, data? }) adds a bot running one. start runs when the bot is added and again after each wake; tick runs every tick before the stepper, in the order the bots were added. Both must be synchronous.
  • Input. With a stepper, tick sets bot.input and the stepper gets it under the bot’s player ID, so a bot’s car moves by the same step as a person’s, and clients predict and interpolate it like any other player’s. The room doesn’t run parseInput on it, since it’s your own code. Without a stepper, a behaviour changes entities itself.
  • bot.data is the behaviour’s own JSON, at most 4 KB (MAX_BOT_DATA), saved in room state.
  • Ownership and private state work by player ID, as for people. onWrite doesn’t run for a bot’s entities: the room writes them directly.
  • room.bots.remove(bot) ends the bot’s presence and runs onPlayerLeave for it before returning, so code that cleans up after a player cleans up after bots too. Bots never cause onJoin or onLeave, which take a connection.
  • A behaviour that throws, or returns a promise, has the error logged and its bot removed, and the room keeps running. So does a bot whose data stops being JSON or grows over 4 KB.
  • Limits. A room holds at most 64 bots (MAX_BOTS). Bots count toward presence’s 1,024 entries. The tick profile shows their ticks as bots.
  • No player data. Bots have no player data, own no collection entries and submit no scores, since those outlast the room. room.playerData throws for a bot, and the data service answers a bot’s ID with E_INVALID. Voice leaves bots out.

Seats

room.maxPlayers is the room’s seat limit: its maxPlayers, or its room size’s connection limit. People and bots together fill at most that many, and room.bots.add throws when they already do.

A person can always take a bot’s seat. When a person joins a room that people and bots fill, the room removes the bot added last in the join itself: onPlayerLeave runs for the bot, then onJoin for the person, before the person’s first state update. Give the bot’s entities to the person in onJoin if the game should carry on from where the bot was.

The router counts only people, so the room list and quick join see a room with one person and three bots as having one player. Quick join still prefers rooms with more people.

Hibernation and wake

Bots are room state: their IDs, names, statuses, behaviours and data are saved and come back after a wake or a room host crash. Before the room hibernates, onPlayerLeave runs for every person in presence but not for bots. On wake each bot is back in presence before onWake, and its start runs after onWake.

Bots hold no connection, so a room with only bots in it hibernates 30 seconds after its last connection closes, like any room. A game that should finish without people ends itself in onLeave or onPlayerLeave.

Simulating and testing

A room’s bots are room code, so irt simulate and @irt/testing run them with nothing more to set up. Their simulated bots are client connections, a different thing: spawn them into a room whose bots fill it to test that people take their seats. A Node client can play a room as an external bot, connecting like a person with its own token, for a game whose logic lives only in its client.

View this page as Markdown