Bots

A bot is a player the server plays. It lives in an instance with a real ship, real player state, and a place in the roster, and it is streamed to everyone else exactly like a person — it just has no socket on the other end. The engine drives it: it pads a thin arena with bots and eases them out again as humans arrive. Every bot definition lives in the game's gamehoster-bots/ folder, one directory per bot personality, and the whole point of the design is that the game's own logic does not change: the tick, the command handlers, join, leave and view all run unaltered, because to them a bot simply is a player.

A bot acts on the world through one channel only: it sends the game's own commands, the very same ones a mouse or key would, through Gamehoster.Bot.Send(type, params). They enter the same per-tick queue as human input and run through the same handler. A bot never writes player, entity or instance state directly — its bodies are firewalled to no writes at all — so it can only ever do what a real player could do.

<gamehoster-config-contentRoot>/
  game.gamehoster.org/
    gamehoster-games/
      asteroids/
        gamehoster-bots/
          ace/
            gamehoster-bot.json
            gamehoster-bot-join.js
            gamehoster-bot-update.js
          drifter/
            gamehoster-bot.json
            gamehoster-bot-join.js
            gamehoster-bot-update.js
The gamehoster-bots/ folder inside a game: one directory per personality, each with a likelihood, a join body and an update body.

Contents

The gamehoster-bots/ folder holds one directory per bot personality; each holds a settings file and two bodies.

NameTypeDescription
gamehoster-bot.jsonfileThe bot's settings — today just its likelihood of being the one picked.
gamehoster-bot-join.jsfileOptional. Runs once when this bot is added; sets up its private memory.
gamehoster-bot-update.jsfileThe bot's brain: reads the world and issues commands, on the bot timer.

gamehoster-bot.json

The bot's settings. Today it carries a single field, gamehoster-bot-likelihood: the weight with which this personality is chosen when the engine adds a bot.

{ "gamehoster-bot-likelihood": 2 }
asteroids · gamehoster-bots/ace/gamehoster-bot.json

When a new bot is needed the engine sums every personality's likelihood, picks a random number from 0 up to that total, and walks the personalities accumulating their weights: the first whose running total passes the number is the one added. So with ace: 2, drifter: 3, sniper: 1 (total 6) a new bot is a drifter three times in six, an ace twice, a sniper once. A likelihood of 0 means the personality never appears; if every likelihood is 0 the game runs with no bots at all. The draw uses ordinary, server-only randomness — bot choice is not the seeded, client-reproducible Gamehoster.Random().

gamehoster-bot-join.js

Runs once, the moment this bot is added to an instance. By the time it runs the bot is already a fully set-up player: the engine has run the game's own gamehoster-player-join.js for it, so its ship has a spawn point, a colour, a heading — everything a human's would. The bot-join body sets up only what is unique to the bot: its private memory, the scratch its update reads and writes each tick, through Gamehoster.Bot.Persistent.Set(value). It writes no game state (that already happened), so like the update it is firewalled to no game writes.

// The ace: an aggressive bot that will often break off to hunt a nearby ship. Set up
// only this bot's PRIVATE memory — the scratch its per-update brain reads and writes.
Gamehoster.Bot.Persistent.Set({ attackChance: 0.4, wasAlive: true, facing: null, target: null })
asteroids · gamehoster-bots/ace/gamehoster-bot-join.js

The join body is optional: a bot with no memory to prepare can omit it, and its Gamehoster.Bot.Persistent.Get() simply returns undefined until its update sets it.

gamehoster-bot-update.js

The bot's brain, run on the bot timer — a clock of its own, set by gamehoster-game-botRate (default 10/s), independent of the game tick. Each run it reads the world exactly as the tick doesGamehoster.Player.All(), Gamehoster.Entity.All(type), Gamehoster.Instance.State.Get, and the instance's own Gamehoster.Instance.Persistent.Get() — and then acts by sending commands. Because a bot's id is also its player id, it reads its own ship with Gamehoster.Player.State.Get(Gamehoster.Bot.id, …), and the command it sends runs through the handler with Gamehoster.Context.playerId already set to it.

// An Asteroids bot's brain (excerpt). It reads rocks and players like the tick does and
// acts ONLY by sending the game's own aim/thrust commands — there is no fire command, a
// living ship auto-fires, so "shoot that rock" just means "point where the bullet meets it".
const me   = Gamehoster.Bot.id
const here = Gamehoster.Context.instanceId
const s    = Gamehoster.Player.All().get(me)          // our own ship — a bot is a player
if (!s || !s.alive) { Gamehoster.Bot.Send("thrust", { on: false }); return }

const mem = Gamehoster.Bot.Persistent.Get() || {}     // our private memory
// … read Entity.All("rock"), pick a heading, dodge, lead a shot …
Gamehoster.Bot.Send("aim",    { angle: desiredAngle }) // the SAME command a mouse sends
Gamehoster.Bot.Send("thrust", { on: thrusting })
Gamehoster.Bot.Persistent.Set(mem)                     // remember for next tick
asteroids · gamehoster-bots/ace/gamehoster-bot-update.js (excerpt)

A command a bot sends lands in the same queue as human input and is applied on the next tick, through the game's unmodified handler. That is the only way a bot changes anything: it can read everything a tick can read and remember its own state, but it cannot touch player, entity or instance state except by playing.

Gamehoster.Bot

Inside a bot body — join or update — the whole of the read surface the tick has is present (Instance, Player, Entity, Tick(), Entropy(), …). What is unique to a bot lives under Gamehoster.Bot:

MemberWhat it does
Gamehoster.Bot.idThis bot's id — which is also its player id. Same as Gamehoster.Context.botId and Gamehoster.Context.playerId.
Gamehoster.Bot.Persistent.Get()This bot's live private memory object (server-only, never sent, never firewalled). undefined until set.
Gamehoster.Bot.Persistent.Set(value)Replace this bot's memory. Mirrors Instance.Persistent.
Gamehoster.Bot.All()Read-only Map(botId → { id, type, persistent }) of every bot in this instance — the bot analogue of Player.All() / Entity.All(), for coordinating between bots.
Gamehoster.Bot.Send(type, params)The one way a bot acts: queue a command as this bot into the normal per-tick flow. Runs through the game's own handler next tick.

Padding: how many bots, and when

The engine keeps each instance's bot count moving toward a target it computes from two instance settings and the number of human players present (bots don't count toward the target they drive):

SettingMeaning
botsMaxThe most bots an instance holds — the number present when a lone human is in the arena, and the number an unattended (zero-human) arena is kept warm at.
botsZeroAtThe human count at which the target reaches zero: a full arena of people needs no bots.

Between those two points the target eases linearly. With botsMax = 20 and botsZeroAt = 20 the arena sheds one bot per human who joins; with botsZeroAt = 40 it sheds one bot per two humans. An arena with no humans at all is kept full, at botsMax — an unattended game is meant to keep running, so an idle instance stays warmed up rather than draining to empty. That keeps the game monitorable at any hour and means the first player to arrive drops into a lively arena instead of watching it fill from nothing. (Pair this with gamehoster-game-warmInstances to also guarantee at least one such arena always exists, so a game is never dark even with zero players connected.)

target(humans) = botsMax                              when humans is 0  (kept warm)
               = max(0, botsMax − ⌊humans · botsMax / botsZeroAt⌋)   otherwise
               (never more than capacity − humans, so a human can always take a slot)
The padding target, recomputed every bot pass from the live human count.

Each bot pass the population moves by one toward that target: it adds a bot (picked by likelihood) if it is short, or removes a random one if it is over, never more than one per pass, so bots ease in and out rather than popping. Adding a bot runs the game's own join for it; removing one runs the game's own leave, so its ship and any in-flight entities are cleaned up exactly as a leaving human's would be.

Humans take priority. If every instance is full when a person joins, the engine frees a random bot to make room, so a real player never has to open an empty new arena while bots sit in a full one.

Why the game logic never changes

The whole facility rests on one fact: a bot is a player, just a socketless one. It sits in the instance roster with ordinary player state, so the tick, the per-player update, the view and the delta protocol all treat it as a player with nothing special added; other viewers see it as one. Its commands arrive in the same queue and run through the same handlers. Nothing in a game's own definition needs a line about bots — a game gets them purely by adding a gamehoster-bots/ folder and the two instance settings. See it live in the Asteroids example.