Skip to main content

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.

Português | Español


✨ Features

  • Three API layers — choose your abstraction level:
    • Native — raw HBInit calls, maximum control
    • Room — async Python methods mirroring the official RoomObject API
    • Extended — high-level module/command system with player management
  • 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 promptinit() 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_stadiumsetDefaultStadium).


🐛 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=True is recommended when running 24/7 bots
  • Proxy support is available through proxy_server in the config
  • Browser args (e.g., --disable-web-security) can be passed via browser_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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

haxball_python-0.1.2.tar.gz (73.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

haxball_python-0.1.2-py3-none-any.whl (73.2 kB view details)

Uploaded Python 3

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

Hashes for haxball_python-0.1.2.tar.gz
Algorithm Hash digest
SHA256 f523fb00fe2f4d839ba16f12b4fb5584a983b28bc7f5199773a5e7035b471bb8
MD5 d36db2e0d30daf26a87993c20479654c
BLAKE2b-256 07af4e1e8f18faeefacb0c651ae8a17f3ad66e6f27ebb03f8d27e023f9766c45

See more details on using hashes here.

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

Hashes for haxball_python-0.1.2-py3-none-any.whl
Algorithm Hash digest
SHA256 fc23258b15f01d154a0796a512b091db84453eb8d057ee48be314cd3a783917f
MD5 328a7cfe1c59d9fe6ed3e070367330d3
BLAKE2b-256 80428d74c43576ce51d846c3d201ce60271524e23bd0f6041f7d50688c6b260a

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page