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 asubstarting withbot:, so nobody can pose as a bot.isBot(player), from@irt/clientor@irt/room, tells them apart. - Behaviours are declared by name in
botsindefineRoom, androom.bots.add(name, { name?, status?, data? })adds a bot running one.startruns when the bot is added and again after each wake;tickruns every tick before the stepper, in the order the bots were added. Both must be synchronous. - Input. With a stepper,
ticksetsbot.inputand the stepper gets it under the bot’s player ID, so a bot’s car moves by the samestepas a person’s, and clients predict and interpolate it like any other player’s. The room doesn’t runparseInputon it, since it’s your own code. Without a stepper, a behaviour changes entities itself. bot.datais 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.
onWritedoesn’t run for a bot’s entities: the room writes them directly. room.bots.remove(bot)ends the bot’s presence and runsonPlayerLeavefor it before returning, so code that cleans up after a player cleans up after bots too. Bots never causeonJoinoronLeave, 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
datastops 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 theirticks asbots. - No player data. Bots have no player data, own no collection
entries and submit no scores, since those outlast the room.
room.playerDatathrows for a bot, and the data service answers a bot’s ID withE_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.