⚽ 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
Metadata
Release files for haxball-python 0.1.2
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| haxball_python-0.1.2.tar.gz | 73.5 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| haxball_python-0.1.2-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 146.7 kB
Release files / haxball_python-0.1.2.tar.gz
| Download URL | haxball_python-0.1.2.tar.gz |
|---|---|
| Size | 73.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
f523fb00fe2f4d839ba16f12b4fb5584a983b28bc7f5199773a5e7035b471bb8
|
|
BLAKE2b-256 checksum How to use checksums |
07af4e1e8f18faeefacb0c651ae8a17f3ad66e6f27ebb03f8d27e023f9766c45
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is 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}
|
Release files / haxball_python-0.1.2-py3-none-any.whl
| Download URL | haxball_python-0.1.2-py3-none-any.whl |
|---|---|
| Size | 73.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
fc23258b15f01d154a0796a512b091db84453eb8d057ee48be314cd3a783917f
|
|
BLAKE2b-256 checksum How to use checksums |
80428d74c43576ce51d846c3d201ce60271524e23bd0f6041f7d50688c6b260a
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is 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}
|