Room scripts

Host a RosikBall room run by your own code — like HaxBall's headless host.

Anyone can run a room with a script: it welcomes players, picks the teams, starts and stops games, answers !commands, keeps statistics, records replays — whatever you write. The room runs where your script runs: in a browser tab on your computer, or on a server around the clock. It is free and needs no account.

Hosted rooms 24/7 are rooms RosikBall's servers run for premium players: always open (asleep while empty, awake as soon as someone joins), set up in My rooms on the room list — name, place, rules, admins, Discord — with the standard script below, or a script of their own. You do not need one to run a scripted room yourself.

Quick start: a room in a browser tab

  1. Open rosikball.com/?headless=1 — a headless page: it hosts a room but has no player of its own and draws nothing. It waits for your script.
  2. Open the browser's console (F12 → Console).
  3. Paste your script and press Enter. For instance:
const room = RosikRoom.create({ roomName: 'My first room', maxPlayers: 10, public: true });

room.onRoomLink = (link, code) => console.log('Room open:', code, link);
room.onPlayerJoin = (p) => {
    room.sendAnnouncement(`Welcome, ${p.name}!`, p.id, { color: '#ffd166', bold: true });
    // the first two players get a team each, then the game starts
    const red = room.getPlayerList().filter(x => x.team === 'red').length;
    const blue = room.getPlayerList().filter(x => x.team === 'blue').length;
    if (red === 0) room.setPlayerTeam(p.id, 'red');
    else if (blue === 0) { room.setPlayerTeam(p.id, 'blue'); room.startGame(); }
};

The room appears on the room list (or not, with public: false: then share its code or invite link). Keep the tab open: closing it closes the room. A headless page keeps running in a background tab. You can also run a script on the normal game page (you keep your own player and host with it).

24/7 on your computer or a server

A small Node program keeps a scripted room running in a headless browser and restarts it if it ever stops — within two minutes the room gets its code back. It saves the replays your script records.

  1. Install Node.js 20 or newer.
  2. Download into one folder: run-room.mjs, headless-room.mjs, package.json and your script (start from standard-room.js).
  3. In that folder: npm install, then on Linux npx playwright-core install --with-deps chromium.
  4. Run it: node run-room.mjs standard-room.js
OptionWhat it does
--browser=chromiumThe browser to use: msedge (Windows' default), chrome, or chromium (Linux, after the install step).
--replays=replaysWhere room.saveReplay() writes .rbr files.
--state=file.jsonWhere the room's code and reclaim key are kept (a restart gets the same code back).
--headedShow the browser window.

A room uses about half a gigabyte of memory and little CPU; every player costs some upload bandwidth.

Creating the room

const room = RosikRoom.create({
    roomName: 'Friday 4v4',   // shown on the room list
    password: '',             // '' = none
    maxPlayers: 8,            // 2–24
    public: true,             // on the room list (false: code / invite link only)
    field: 'classic',         // 'classic' | 'easy' | 'small' | 'big' | 'rounded' | 'bigeasy' | 'bigrounded' | 'futsal' | 'sixman' | 'basketball' | 'pingpong'
    map: null,                // or a custom map (a .rbmap's object): played instead of the field
    timeLimit: 3,             // minutes, 0 = none
    scoreLimit: 3,            // goals, 0 = none
    teamsLock: false,         // true: only admins (and the script) move players
    discord: 'discord.gg/abc123', // your community: a Discord invite, shown in the room with the Discord logo
    admins: []                // account ids (player.account) who are admin as soon as they join
});

A scripted room starts stopped: your script starts the games (room.startGame()). One room per page.

Every command at a glance

Everything a script can call or hear, grouped; each links to where it is explained. id is a player's id, team is 'red', 'blue' or (for players) 'spec'. The same API works in a browser tab, with the runner and in a hosted room's sandbox (where actions are rationed).

GroupCommands
The roomRosikRoom.create(config)
ReadinggetPlayerList() getPlayer(id) getScores() getBallPosition() isGameRunning() getTeamCaptain(team) getRoomCode() getRoomLink() getMap() getScoreboard() reclaimKey
PlayerssetPlayerTeam(id, team) setPlayerAdmin(id, on) kickPlayer(id, reason, ban) clearBans() mutePlayer(id, minutes) unmutePlayer(id) setPlayerAfk(id, on) setTeamCaptain(team, id) setRoomAdmins(accountIds) setTeamsLock(on)
GamesstartGame() stopGame() pauseGame(on) setTimeLimit(minutes) setScoreLimit(goals) setField(name) setMap(map)
Room settingssetRoomName(name) setPublic(on) setPassword(text) setMaxPlayers(n) setDiscord(link)
ChatsendAnnouncement(text, toId, style)
The fieldgetGoals() getZones() getPlayerDiscProperties(id) setPlayerDiscProperties(id, props) getBallProperties() setBallProperties(props) setObject(id, object) removeObject(id) clearObjects() getObjects() setScoreboard(entries) setGoalMessages(on) setPlayerAvatar(id, avatar) setPlayerColor(id, color) scoreGoal(team, scorerId)
BotsaddBot(team) removeBot(id) setBotTarget(id, point) setBotAim(id, point)
ReplaysstartRecording() await stopRecording() saveReplay(bytes, name)
EventsonRoomLink onPlayerJoin onPlayerLeave onPlayerKicked onPlayerChat onPlayerTeamChange onPlayerAdminChange onPlayerAfkChange onTeamsLockChange onGameStart onGameStop onGamePause onGameUnpause onTeamGoal onPlayerBallKick onPlayerBallTouch onTeamVictory onPositionsReset onGameTick
Hosted roomswindow.__applyRoomConfig = (cfg) => { … } (the owner saved new settings)

An action returns true when it is queued (it applies at the next tick), false when it cannot be (a player who is not in the room, a room that has closed, a hosted room's limit reached); a wrong argument throws, saying why.

Players

Every player your script gets is a snapshot (read it again for fresh values):

{ id, name, team,          // team: 'red' | 'blue' | 'spec'
  avatar, admin, owner, bot,
  color,                   // the disc's own colour ('#rrggbb', setPlayerColor), null: the team's
  verified,                // logged in: the name is their account's nickname
  account,                 // their account's id (checked by our server) — the identity to trust; null for guests
  country,                 // two letters, from their connection (never chosen by the player)
  afk,                     // away from the keyboard (see AFK players)
  captain,                 // 'red' | 'blue' while they pick their team, else null
  roomAdmin,               // one of the admins the room's owner named
  position }               // {x, y, z} on the pitch, null for spectators

Names can be anything a player types: show them as text, never as HTML.

Events

Set the handlers you need: room.onPlayerJoin = (player) => { … }. A handler that throws is logged and skipped; the room keeps running.

HandlerWhen
onRoomLink(link, code)the room is open (its invite link and code)
onPlayerJoin(player)someone joined (as a spectator)
onPlayerLeave(player, reason)left, timed out, kicked or banned
onPlayerKicked(player, reason, ban, by)before the leave
onPlayerChat(player, text, {scope, to})a message — return false to drop it (for !commands). scope: 'all', 'team' (only the sender's team reads it) or 'private' (only to, a player, and the sender)
onPlayerTeamChange(player, byPlayer)moved to a team or the spectators; byPlayer is the captain who picked them, else null
onPlayerAdminChange(player)admin given or taken
onPlayerAfkChange(player, afk, by)away or back — by: 'self' (typed /afk), 'idle', 'back' (pressed a key), 'script'
onTeamsLockChange(locked)the teams were locked or unlocked
onGameStart() / onGameStop()a game started / was stopped
onGamePause() / onGameUnpause()paused / play resumed after the countdown
onTeamGoal(team, scorer, assist, ownGoal, where)a goal (a shot on target that a defender deflects in is the shooter's); where says which goal it went into: {goal, zone, defendedBy, x, y, z} — goal = its index in room.getGoals() (zone: a goal zone's, in room.getZones()), x, y, z the ball
onPlayerBallKick(player)a kick
onPlayerBallTouch(player)a disc begins touching the ball — a kick, a block or a bump (who hit it last)
onTeamVictory({winner, red, blue})a game was won
onPositionsReset()after a goal, back to the kickoff
onGameTick(tick)every gameplay tick, 120 per second — keep it light

Actions

Actions are the room host's own: they apply at the next tick, with the same rules as the room menu (the field and the limits change only while no game runs).

room.setPlayerTeam(id, 'red')          room.setPlayerAdmin(id, true)
room.kickPlayer(id, 'reason', ban)     room.clearBans()
room.mutePlayer(id, minutes | null)    room.unmutePlayer(id)
room.startGame()   room.stopGame()     room.pauseGame(true | false)
room.setTimeLimit(minutes)   room.setScoreLimit(goals)   room.setField('big')
room.setMap(mapObjectOrText)           // a custom map (.rbmap): checked at once (throws why); its limits come with it
room.setField('custom')                // back to the room's custom map (the last setMap, or create's map)
room.setTeamsLock(true)                room.setPassword('secret' | '')
room.setRoomName('Friday 4v4')         room.setPublic(true | false)     room.setMaxPlayers(10)
room.setDiscord('discord.gg/abc123' | '')
room.setTeamCaptain('red', id | null)  // see Captains
room.setPlayerAfk(id, true | false)    // see AFK players
room.setRoomAdmins([accountId, …])     // see Admins
room.addBot('blue')   room.removeBot(id)
room.setBotTarget(id, { x, z } | null) room.setBotAim(id, { x, z } | null)   // see Bots
room.sendAnnouncement(text, toId = null, { color: '#ffd166', bold: true })
room.sendAnnouncement(text, toId = null, { type: 'warning' })
room.startRecording()   const bytes = await room.stopRecording();   room.saveReplay(bytes, 'name')

Announcements are system lines in the chat, to everyone (toId = null) or one player. With a type — 'info', 'success', 'warning' or 'error' — the announcement also pops up in the bottom-right corner for a few seconds, in that kind's colour; line: false makes it a popup only. Admins can send the same from the chat: /announce warning the room restarts in 5 minutes, or to one player with @name: /announce success @Ana you are captain. A player about to be set AFK gets a warning popup from the game itself, three seconds before.

Reading the room

room.getPlayerList()everyone in the room (the headless page itself is not a player)
room.getPlayer(id)one player, or null
room.getScores(){red, blue, time, timeLimit, scoreLimit, overtime, phase}
room.getBallPosition(){x, y, z}
room.isGameRunning()a game is on (playing, paused or between goals)
room.getTeamCaptain(team)the team's captain's id, or null
room.getRoomCode() / room.getRoomLink()the code and the invite link
room.getMap(){field, name, author}: the field in use, a built-in id or 'custom'
room.getScoreboard()your scoreboard as setScoreboard left it, or null (the score shows)
room.reclaimKeythe key that gets the room's code back after a restart (the runner keeps it for you; not in a hosted room)

The field's own readings — getGoals, getZones, getPlayerDiscProperties, getBallProperties, getObjects — are in the next section.

The field: goals, discs, objects, a scoreboard

The room plays Red against Blue, as HaxBall does. Anything else — a goal for each player, lives, rounds, more teams — is your script's: it can read the field, move discs, put objects of its own on the pitch and show its own scoreboard. Coordinates are metres: x along the field, z across it, y up (HaxBall's x and y are our x and z, divided by 30).

room.getGoals()the field's goals, in order: {index, team, a, b, middle, normal, width, height, depth} — team defends it, a / b its posts and middle as {x, z}, normal points into the net
room.getZones()the field's zones (a basket's goal zone…): {index, effect, team, from, x, z, bottom, top}
room.getPlayerDiscProperties(id){x, y, z, xspeed, yspeed, zspeed, radius}, or null for a spectator
room.setPlayerDiscProperties(id, props)moves the disc: any of x, y, z, xspeed, yspeed, zspeed (a teleport — no goal is crossed on the way)
room.getBallProperties() / room.setBallProperties(props)the same for the ball
room.setObject(id, object)puts a 3D object of your own on the field — or changes it, by its id (letters, digits, _ - . :). Everyone sees it, it stops what it stops, replays keep it.
room.removeObject(id) / room.clearObjects() / room.getObjects()(a field change removes them all; at most 64)
room.setScoreboard(entries)your own scoreboard in place of "Red 0 : 0 Blue", on every player's screen: [{text, color, faded}] (a dot of the colour; faded = greyed and struck through) or plain strings; null brings the score back.
room.setGoalMessages(false)turns off the game's own goal texts — the "GOAL! RED scores (Ana)" banner and the chat's goal line — for a script that announces its goals or points its own way; true turns them back on. They are on unless your script says so.
room.setPlayerAvatar(id, avatar)a player's avatar (two characters at most: letters, a number, an emoji)
room.setPlayerColor(id, '#rrggbb')a player's disc in a colour of its own instead of their team's, on every screen and in replays (a free-for-all, a marked player…); null: the team's colour again
room.scoreGoal(team, scorerId)a goal for 'red' or 'blue' as if the ball had gone in — the score, the kickoff, the limits and the recap as for any goal (scorerId or null): points your script decides (ping pong, targets, a field without goals). Only while a game is on; onTeamGoal hears it with where.script.

A field may have no goals at all (a room script scores with scoreGoal), and a field's physics may make the discs float at a height (the editor's Physics tab, "Players float this high": no gravity, no jumping) — the Ping pong field does both.

An object is a map editor object: {shape: 'box' | 'cylinder' | 'sphere' | 'ring' | 'net', at: [x, z], y, w, d, h, yaw, tilt, stops: 'all' | 'ball' | 'players' | 'none', bounce, opacity, color: '#rrggbb'} — y its middle's height; a box is w along × h × d, turned by yaw degrees. A wall across a goal's mouth, from getGoals():

const g = room.getGoals()[2];
room.setObject('shut-2', {
    shape: 'box', at: [g.middle.x + g.normal.x * 0.2, g.middle.z + g.normal.z * 0.2], y: (g.height + 0.3) / 2,
    w: g.width + 0.5, d: 0.3, h: g.height + 0.3, yaw: Math.atan2(-(g.b.z - g.a.z), g.b.x - g.a.x) * 180 / Math.PI,
    stops: 'all', color: '#5a6270', opacity: 0.75
});

Bots

room.addBot('red') adds a bot to a team (it takes one of the room's places: a person joining a full room makes the newest bot leave); room.removeBot(id) takes it away. A bot plays football on its own: it chases the ball, shoots at the other team's nearest goal and covers its own when a teammate is closer to the ball. Two orders change that, for games your script invents:

room.setBotTarget(id, {x, z})where the bot waits: it goes to that point and stays there, and plays the ball only while the ball is within 2.5 m of it (a keeper in front of a goal, a defender of a zone). Whoever is closer, it goes for those balls itself. null: it chases the ball again.
room.setBotAim(id, {x, z})where the bot sends the ball, instead of the other team's goal (a player's goal in 6man, a target, a teammate). null: the goal again.

Orders are for bots only (a person throws), stay until changed, and end when the bot leaves. The 6man scripts give every bot its own goal to guard and the leader's goal to shoot at:

room.setBotTarget(id, { x: g.middle.x - g.normal.x * 2, z: g.middle.z - g.normal.z * 2 });   // 2 m in front of its goal
room.setBotAim(id, { x: leader.middle.x, z: leader.middle.z });

Captains

Name a team's captain with room.setTeamCaptain('red', id) (they must be on that team). A captain can open the room menu (Esc) and drag a spectator who is not AFK onto their own team — never someone from the other team; the room checks every pick. onPlayerTeamChange(player, byPlayer) tells you who picked whom. room.setTeamCaptain('red', null) ends it. The standard script uses this for its picking turns (below).

AFK players

A player on a team who presses no key for 30 seconds while the ball is in play is marked AFK; so is a player who types /afk, or one your script marks with room.setPlayerAfk(id, true). Everyone sees "💤 AFK" on their card. Pressing a key in play, typing /afk again, or clicking the tag on their own card brings them back. player.afk and onPlayerAfkChange let your script react (move them out, skip them…).

Admins and owners

Replays

room.startRecording() records the game; await room.stopRecording() gives the file's bytes and room.saveReplay(bytes, 'name') keeps them — a download in a browser tab, a file in the replays folder with the runner. Anyone can watch a .rbr file with ▶ Watch replay on the room list.

Examples

Commands

room.onPlayerChat = (player, text) => {
    if (!text.startsWith('!')) return true;          // a normal message: shown
    if (text === '!score') {
        const s = room.getScores();
        room.sendAnnouncement(`Red ${s.red} : ${s.blue} Blue`, player.id);
    } else if (text === '!bb') {
        room.kickPlayer(player.id, 'bye!', false);
    }
    return false;                                     // commands are not shown in the chat
};

Goals and statistics

const goals = new Map();                                   // account (or name) → goals
room.onTeamGoal = (team, scorer, assist, ownGoal) => {
    if (!scorer || ownGoal) return;
    const key = scorer.account || scorer.name;
    goals.set(key, (goals.get(key) || 0) + 1);
    room.sendAnnouncement(`⚽ ${scorer.name} (${goals.get(key)} today)`);
};

6man: a goal for each player

Two complete scripts play HaxBall's 6man rules on the 6man field (or any field with two goals or more): every player owns a goal and has 3 points, every goal let in costs one, at 0 you are out — the last player standing wins. They show what the field functions above can do: each goal gets a floor marker in its owner's colour (setObject), each disc an avatar of the same colour, everyone is placed in front of their own goal after every reset (setPlayerDiscProperties), the points are on the scoreboard (setScoreboard), the game's "RED scores" texts are off while it plays (setGoalMessages(false)), and onTeamGoal's where.goal says whose goal it was. Each disc is in its owner's colour (setPlayerColor).

Ping pong

pingpong.js plays ping pong on the Ping pong field (a table of 3D objects in the middle of the hall, discs floating at its height): it serves — the ball dropped over the server's half — and judges every rally from the ball's flight (onGameTick + getBallProperties: a bounce is its vertical speed turning up over the table) and the hits (onPlayerBallTouch, onPlayerBallKick): two bounces on one side, a ball hit onto one's own side, a ball not returned or off the table give the point to the other side through scoreGoal — so the score panel, the kickoff and the score limit (first to 11) work as for football.

The standard room

standard-room.js is the script every hosted room runs by default: players wait in line, captains pick the teams (the first in line who is not AFK captains an empty team; a short team picks one player at a time, the next in line is picked for a captain who waits 20 seconds), the game starts by itself, winners stay and the losers go to the back of the line, idle players sit out, !afk, !back, !bb, and every game is recorded. Change its ROOM and RULES at the top, or use it as a starting point for your own.

Your hosted room's script

A hosted room runs the standard script until its owner gives it one of their own, for behaviour the room's settings do not offer. In My rooms, 📜 Script → ✎ Edit opens the script the room runs (the standard one, to start from); change it, or 📂 Load a file… you wrote elsewhere. ✓ Check compiles it (an error names its line: click it to go there) and tries it on a simulated room — players join, chat, play and win a game. Only a script that passes can be saved: Save and run switches the running room to it at once, and its players stay. Every save is a version (the last five are kept: Run this one goes back to one), ↺ Standard script goes back to the standard rules, 🗒 Log shows what the script logged (console.log) and its errors.

The script settings of the room (ROOM, RULES in the standard script) come from ⚙ Settings; when the owner saves new settings, a script that sets window.__applyRoomConfig = (cfg) => { … } hears them.

The sandbox and its limits

A hosted room's script runs in a sandbox on our servers: the RosikRoom API and plain JavaScript, nothing else — no network (fetch, WebSocket and the like do not exist there), no storage, no WebAssembly. Reading the room is instant; actions go to the room. So that one room cannot hurt the others, it is rationed:

WhatLimit
All actions togethera burst of 120, then 20 a second
sendAnnouncementa burst of 40, then 8 a second
setRoomName, setPublic, setPassword, setMaxPlayers, setDiscord, setRoomAdminsa burst of 10, then 1 a second
setField, setMap, addBot, removeBot, startRecording, stopRecordinga burst of 6, then 1 every 2 seconds
setObject, removeObject, clearObjects, setScoreboard, setPlayerAvatar, setGoalMessages, setPlayerColor, setBotTarget, setBotAima burst of 60, then 8 a second
saveReplaya burst of 3, then 20 an hour; RosikBall replays only, 16 MB each; the room keeps the newest 40 files
An action's arguments16,000 characters (a map for setMap: 192,000)
Timers waiting at once500
Script size64 KB
Processor35% of the room's on average over 30 seconds (📜 Script shows what it uses now)
Memorythe room's page within 60% of the room's memory

An action over its limit is refused: it returns false and the log says how many were. The room replaces the script with the standard one — without disconnecting anyone, and telling the players — when it stops answering for 4 seconds (an endless loop; it has 15 seconds to start), throws more than 50 errors in a minute, keeps asking for more than 600 refused actions in a minute, or goes over the processor or memory limit. 📜 Script then says why; fix it, Check it and save it again. Scripts you run yourself (in a browser tab or with the runner) are not rationed: they use your own machine.

What a script cannot do

A script runs on the host's machine and can do exactly what the room's host can — nothing more for anyone else. It cannot change the physics (every player's game predicts the same match), see other players' screens, or touch their accounts. Keep handlers light, especially onGameTick: a slow script slows the room for everyone. Please respect the terms: no impersonating RosikBall or other players, no spam on the room list.