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
Contents
The gamehoster-bots/ folder holds one directory per bot personality; each holds a
settings file and two bodies.
| Name | Type | Description |
|---|---|---|
gamehoster-bot.json | file | The bot's settings — today just its likelihood of being the one picked. |
gamehoster-bot-join.js | file | Optional. Runs once when this bot is added; sets up its private memory. |
gamehoster-bot-update.js | file | The 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 }
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 })
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 does — Gamehoster.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
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:
| Member | What it does |
|---|---|
Gamehoster.Bot.id | This 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):
| Setting | Meaning |
|---|---|
botsMax | The 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. |
botsZeroAt | The 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)
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.