Python bridge for the HaxBall Headless Host API
Project description
⚽ haxball-python
A faithful, type-safe Python bridge for the official HaxBall Headless Host API.
Drive a real HaxBall headless room from Python using Playwright. No reverse-engineering, no modified runtimes — just the official JavaScript host, automated from Python.
✨ Features
- Three API layers — choose your abstraction level:
- Native — raw
HBInitcalls, maximum control - Room — async Python methods mirroring the official
RoomObjectAPI - Extended — high-level module/command system with player management
- Native — raw
- Full event system — all 19 official events with Python callbacks (sync or async)
- Type-safe — Pydantic models for players, scores, disc properties, and config
- Automatic token prompt —
init()asks for token interactively if not provided - Playwright-based — reliable browser automation, works headlessly on VPS
- Disc property reflection — read/write physics properties of any disc in the game
📦 Installation
pip install haxball-python
playwright install chromium
📋 Requirements
- Python 3.10+
- Playwright 1.50+
- Pydantic 2.7+
🚀 Quick Start
🏟️ Basic Room
import asyncio
from haxball import HaxballClient, HaxBallConfig
async def main():
async with HaxballClient() as client:
room = await client.init(HaxBallConfig(
room_name="Haxball.Py",
player_name="Bot",
max_players=16,
public=False,
no_player=True,
))
@room.on_player_join
def on_join(player):
print(f"join: {player.id} {player.name}")
@room.on_player_leave
def on_leave(player):
print(f"leave: {player.id} {player.name}")
@room.on_room_link
def on_link(url: str):
print("room link:", url)
await room.set_default_stadium("Big")
await room.set_score_limit(5)
await room.set_time_limit(0)
print("players:", await room.get_player_list())
await room.wait_for_room_link(timeout=180)
await asyncio.sleep(3600)
asyncio.run(main())
🔌 Extended Room (Modules & Commands)
import asyncio
from haxball import HaxBallConfig
from haxball.extended import HaxballClientExtended
from haxball.module import Module, module, module_command, event
from haxball.command import CommandExecInfo
from haxball.player import Player
@module
class AdminModule(Module):
@module_command(name="adm", aliases=["admin"], usage="adm", desc="Claims admin status", roles=[])
async def claim_admin(self, info: CommandExecInfo):
if info.player.admin:
info.player.reply("You are already an admin!", color=0xFFFF00)
return
info.player.admin = True
info.player.reply("You are now an administrator!", color=0x00FF00)
@event
async def on_player_join(self, player: Player):
await self.room.send(f"Welcome to the room, {player.name}!", color=0x00FFFF)
@event
async def on_player_leave(self, player: Player):
await self.room.send(f"Goodbye, {player.name}!", color=0xFF0000)
async def main():
config = HaxBallConfig(
room_name="Extended Haxball Room",
player_name="BotExtended",
max_players=10,
public=False,
no_player=False,
headless=True,
)
client = HaxballClientExtended()
room = await client.init(config)
room.module(AdminModule)
@room.native.on_room_link
def on_link(url: str):
print("room link:", url)
room.set_stadium("Big")
await room.native.set_score_limit(5)
await room.native.set_time_limit(0)
print(await room.native.wait_for_room_link(timeout=120))
await asyncio.sleep(3600)
asyncio.run(main())
⚡ Native Engine (Raw HBInit)
import asyncio
from haxball._hbinit import HaxballJS
async def main():
HBInit = await HaxballJS({"debug": True})
room = await HBInit({
"roomName": "Haxball.Py Native Engine",
"maxPlayers": 16,
"public": False,
"noPlayer": True,
})
@room.on_room_link
def on_link(url):
print(f">>> ROOM LINK: {url} <<<")
@room.on_player_join
def on_join(player):
print(f"Player joined: {player.name} (id={player.id})")
@room.on_player_leave
def on_leave(player):
print(f"Player left: {player.name} (id={player.id})")
await room.set_default_stadium("Big")
await room.set_score_limit(5)
await room.set_time_limit(0)
await room.wait_for_room_link(timeout=120)
await asyncio.sleep(3600)
asyncio.run(main())
🏗️ Architecture
┌─────────────────────────────────────────┐
│ Your Python Code │
├─────────────────────────────────────────┤
│ Room RoomExtended HBInit │ ← API layers
├─────────────────────────────────────────┤
│ BrowserBridge │ ← Playwright automation
├─────────────────────────────────────────┤
│ Playwright (Chromium) │ ← Browser engine
├─────────────────────────────────────────┤
│ HaxBall Headless Host (JS) │ ← Official runtime
└─────────────────────────────────────────┘
The project respects the official HaxBall headless host as the single source of truth. All API calls are forwarded as-is to the JavaScript RoomObject via Playwright's evaluate(), and events are bridged back through a message-passing channel injected into the page.
⚙️ Configuration Reference
⚙️ HaxBallConfig
| Field | Type | Default | Description |
|---|---|---|---|
room_name (alias roomName) |
str |
required | Room display name |
player_name (alias playerName) |
str | None |
None |
Bot player name (auto-set if no_player=False) |
password |
str | None |
None |
Room password |
max_players (alias maxPlayers) |
int |
16 |
Max players (1–50) |
public |
bool |
True |
Whether the room appears in the lobby |
no_player (alias noPlayer) |
bool |
True |
Whether the host joins as a player |
token |
str |
required (prompted) | HaxBall authentication token |
geo |
GeoConfig | None |
None |
Geolocation override |
prefix |
str |
"!" |
Extended API command prefix |
proxy_server |
str | None |
None |
HTTP proxy for the browser |
headless |
bool |
True |
Run browser headless |
browser_executable_path |
str | None |
None |
Custom Chromium path |
browser_channel |
str | None |
None |
Browser channel (e.g., "chrome") |
browser_args |
list[str] |
[] |
Extra Chromium CLI args |
timeout_ms |
int |
30000 |
Browser operation timeout |
headless_host_url |
str |
https://html5.haxball.com/headless |
Headless host URL |
🌍 GeoConfig
| Field | Type | Default |
|---|---|---|
code |
str |
"xx" |
lat |
float |
0.0 |
lon |
float |
0.0 |
🛠️ Room API
📡 Methods
All methods return Any (the JSON-decoded result from the JS host).
| Python method | JS equivalent | Description |
|---|---|---|
send_chat(message, target_id?) |
sendChat |
Send a chat message |
set_player_admin(id, admin) |
setPlayerAdmin |
Grant/revoke admin |
set_player_team(id, team) |
setPlayerTeam |
Set player's team (0=spec, 1=red, 2=blue) |
kick_player(id, reason?, ban?) |
kickPlayer |
Kick or ban a player |
clear_ban(id) |
clearBan |
Clear ban for a player |
clear_bans() |
clearBans |
Clear all bans |
set_score_limit(limit) |
setScoreLimit |
Set goals to win |
set_time_limit(minutes) |
setTimeLimit |
Set match time limit |
set_default_stadium(name) |
setDefaultStadium |
Set stadium by name |
set_custom_stadium(contents) |
setCustomStadium |
Set custom stadium (HBS) |
set_teams_lock(locked) |
setTeamsLock |
Lock/unlock teams |
set_team_colors(team, angle, text, colors) |
setTeamColors |
Set team colors |
start_game() |
startGame |
Start the match |
stop_game() |
stopGame |
Stop the match |
pause_game(state) |
pauseGame |
Pause/unpause |
get_player(id) → Player | None |
getPlayer |
Get player info |
get_player_list() → list[Player] |
getPlayerList |
Get all players |
get_scores() → Scores | None |
getScores |
Get scoreboard |
set_password(password) |
setPassword |
Set/clear room password |
set_require_recaptcha(required) |
setRequireRecaptcha |
Toggle recaptcha |
reorder_players(ids) |
reorderPlayers |
Reorder player list |
send_announcement(msg, color?, style?) |
sendAnnouncement |
Send colored announcement |
set_kick_rate_limit(min, rate, burst, by?) |
setKickRateLimit |
Configure kick rate limits |
set_player_avatar(id, avatar) |
setPlayerAvatar |
Set player avatar |
set_disc_properties(id, props) |
setDiscProperties |
Set disc physics |
get_disc_properties(id) → DiscProperties |
getDiscProperties |
Get disc physics |
set_player_disc_properties(id, props) |
setPlayerDiscProperties |
Set player's disc physics |
get_player_disc_properties(id) → DiscProperties |
getPlayerDiscProperties |
Get player's disc physics |
get_disc_count() → int |
getDiscCount |
Total disc count |
wait_for_room_link(timeout?) → str |
— | Blocks until the room link is available |
🔔 Events
| Python event | JS event | Payload |
|---|---|---|
on_player_join |
onPlayerJoin |
Player |
on_player_leave |
onPlayerLeave |
Player |
on_team_victory |
onTeamVictory |
Scores |
on_player_chat |
onPlayerChat |
Player, str (message) |
on_player_ball_kick |
onPlayerBallKick |
Player |
on_team_goal |
onTeamGoal |
int (team) |
on_game_start |
onGameStart |
Player (byPlayer) |
on_game_stop |
onGameStop |
Player (byPlayer) |
on_player_admin_change |
onPlayerAdminChange |
Player, bool (admin) |
on_player_team_change |
onPlayerTeamChange |
Player, int (team) |
on_player_kicked |
onPlayerKicked |
Player, str (reason), bool (ban) |
on_game_tick |
onGameTick |
— |
on_game_pause |
onGamePause |
bool (paused) |
on_game_unpause |
onGameUnpause |
— |
on_positions_reset |
onPositionsReset |
— |
on_player_activity |
onPlayerActivity |
Player |
on_stadium_change |
onStadiumChange |
str (stadium name) |
on_room_link |
onRoomLink |
str (URL) |
on_kick_rate_limit_set |
onKickRateLimitSet |
int (min), int (rate), int (burst) |
on_teams_lock_change |
onTeamsLockChange |
bool (locked) |
Events can be bound via decorator or direct assignment:
# Decorator (preferred)
@room.on_player_join
def handler(player):
pass
# Assignment
room.on_player_join = lambda p: print(p.name)
Both sync and async callbacks are supported.
🔌 Extended API
The HaxballClientExtended / RoomExtended layer adds a modern, intuitive API on top of the native room:
Live Properties
Instead of calling verbose methods on the native room, you interact directly with Player and Disc objects:
| What | Old way | New way |
|---|---|---|
| Grant admin | await room.set_player_admin(id, True) |
player.admin = True |
| Move team | await room.set_player_team(id, 1) |
player.team = 1 |
| Change radius | await room.set_player_disc_properties(id, {"radius": 15}) |
player.radius = 15 |
| Kick player | await room.kick_player(id, "bye") |
player.kick("bye") |
| Ban player | await room.kick_player(id, "spam", True) |
player.ban("spam") |
| Send whisper | await room.send_chat("hi", id) |
player.reply("hi") |
| Set disc physics | await room.set_disc_properties(id, {...}) |
disc.x = 10; disc.radius = 5 |
| Get IP | — | player.ip |
Player Properties
| Property | Type | Description |
|---|---|---|
player.id |
int |
Player's unique ID (read-only) |
player.name |
str |
Player's name (read-only) |
player.admin |
bool |
Get/set admin status |
player.team |
int |
Get/set team (0=spec, 1=red, 2=blue) |
player.auth |
str | None |
Public ID (read-only) |
player.conn |
str | None |
Connection identifier (read-only) |
player.ip |
str | None |
Decoded IP address (read-only) |
player.position |
Position | None |
Get/set position on the map |
player.roles |
list |
Permission roles |
Disc Properties (also on Player)
player.x, player.y, player.radius, player.xspeed, player.yspeed, player.xgravity, player.ygravity, player.b_coeff, player.inv_mass, player.damping, player.c_mask, player.c_group, player.color
Setting any of these immediately syncs to the native room.
RoomExtended Methods
| Method | Description |
|---|---|
room.send(msg, color?, style?, target_id?) |
Send announcement or whisper |
room.set_stadium(name | dict) |
Set stadium (name or HBS JSON) |
room.lock_teams() / room.unlock_teams() |
Lock/unlock team switching |
room.enable_captcha() / room.disable_captcha() |
Toggle captcha requirement |
room.start() / room.stop() |
Start/stop the game |
room.pause() / room.unpause() |
Pause/unpause the game |
room.start_recording() / await room.stop_recording() |
Replay recording |
room.unban(id) / room.unban_all() |
Unban players |
room.clear_password() / room.password = "..." |
Room password management |
await room.is_game_in_progress() |
Check if a match is running |
await room.scores |
Get current scores |
await room.ball |
Get the ball disc (Disc object with live props) |
room.command(options) |
Register a command |
room.remove_command(name) |
Remove a command |
room.module(ModuleClass) |
Load a module |
Private Messages
await room.send("Only player #3 can see this", target_id=3)
player.reply("Only this player can see this")
player.reply(...) is a convenience wrapper for room.send(..., target_id=player.id).
Logging and Hidden Commands
Extended rooms log room events with RoomLogger by default. Disable this with
room.logging = False, or use RoomLogger().log_event(...) directly when you
need the same formatting outside a room.
Commands registered with room.command(...) or @module_command(...) hide the
player's original chat message by default, including configured single-character
commands sent without the prefix, such as t hello. Pass delete_message=False
to leave a command message visible.
📦 Module Example
from haxball.module import Module, module, module_command, event
@module
class MyModule(Module):
@event
async def on_player_join(self, player):
await self.room.send(f"Welcome, {player.name}!")
@module_command(name="ping", usage="ping", desc="Pong!", delete_message=False)
async def ping(self, info):
info.player.reply("Pong!")
⚡ Native Engine
The native engine (HaxballJS) is a thin wrapper that obtains the HBInit function from the HaxBall page and exposes it as a Python callable. This is the lowest-level API — you pass the exact same object you would pass in JavaScript.
from haxball._hbinit import HaxballJS
HBInit = await HaxballJS({"debug": True})
room = await HBInit({"roomName": "...", "maxPlayers": 16, ...})
The returned room object mirrors the JS RoomObject — it has the same methods, events, and behavior. Python names are already mapped to their JS equivalents (e.g., set_default_stadium → setDefaultStadium).
🐛 Error Handling
❌ HaxballBridgeError
Raised when communication with the browser fails (timeout, crash, etc.).
⏰ HaxballTimeoutError
Raised when a browser operation exceeds the configured timeout.
💡 Example
from haxball.errors import HaxballBridgeError
try:
room = await client.init(config)
except HaxballBridgeError as e:
print(f"Bridge error: {e}")
✅ Compatibility
📋 Implemented Methods
send_chat, set_player_admin, set_player_team, kick_player, set_score_limit, set_time_limit, set_default_stadium, set_custom_stadium, set_teams_lock, set_team_colors, start_game, stop_game, pause_game, get_player, get_player_list, get_scores, clear_ban, clear_bans, set_password, set_require_recaptcha, reorder_players, send_announcement, set_kick_rate_limit, set_player_avatar, set_disc_properties, get_disc_properties, set_player_disc_properties, get_player_disc_properties, get_disc_count
📝 Notes
- A valid HaxBall token is required (you can get one at https://www.haxball.com/headless)
no_player=Trueis recommended when running 24/7 bots- Proxy support is available through
proxy_serverin the config - Browser args (e.g.,
--disable-web-security) can be passed viabrowser_args
🧪 Development
git clone https://github.com/Fusionzx/haxball-python
cd haxball-python
pip install -e ".[dev]"
playwright install chromium
ruff check .
pytest
📄 License
This project is a wrapper around the official HaxBall Headless Host API. It is not affiliated with or endorsed by HaxBall.
See the MIT License, contribution guide, code of conduct, and security policy.
© 2026 haxball-python contributors
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file haxball_python-0.1.2.tar.gz.
File metadata
- Download URL: haxball_python-0.1.2.tar.gz
- Upload date:
- Size: 73.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.11.7 {"installer":{"name":"uv","version":"0.11.7","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":null,"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f523fb00fe2f4d839ba16f12b4fb5584a983b28bc7f5199773a5e7035b471bb8
|
|
| MD5 |
d36db2e0d30daf26a87993c20479654c
|
|
| BLAKE2b-256 |
07af4e1e8f18faeefacb0c651ae8a17f3ad66e6f27ebb03f8d27e023f9766c45
|
File details
Details for the file haxball_python-0.1.2-py3-none-any.whl.
File metadata
- Download URL: haxball_python-0.1.2-py3-none-any.whl
- Upload date:
- Size: 73.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.11.7 {"installer":{"name":"uv","version":"0.11.7","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":null,"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
fc23258b15f01d154a0796a512b091db84453eb8d057ee48be314cd3a783917f
|
|
| MD5 |
328a7cfe1c59d9fe6ed3e070367330d3
|
|
| BLAKE2b-256 |
80428d74c43576ce51d846c3d201ce60271524e23bd0f6041f7d50688c6b260a
|