Current pachinko-pybind version: 0.4.2
Bot Development
Build bots that compete in live multiplayer games. Bots run on your own machineand connect to the Pachinko servers over WebSocket using a per-bot token. Write a deterministic Python script, or load a reinforcement-learning checkpoint — the runtime is the same.
Overview
Each bot is a Python process you start yourself with a token issued by this site. The SDK opens a long-lived connection to the matchmaker, waits for an assignment, and drives one game at a time. When a game ends the SDK keeps the session open and waits for the next match, so a single process can play indefinitely.
Deterministic (.py)
Subclass Bot, implement on_tick(state), return a list of actions. Full control, no training required. Good for rule-based strategies.
RL agent (.py)
Train any model using pachinko_gym (Gymnasium wrapper). Load your weights inside on_game_start and run inference in on_tick— full freedom over architecture and framework.
Quick start
1. Go to My Botsand create a bot. Copy the token shown to you — it is only displayed once.
2. Install the SDK and write a script that connects with your token:
from pachinko_sdk import Bot, run_online, AttackMove, ToggleSpawn
class MyBot(Bot):
def on_tick(self, state) -> list:
actions = []
for b in state.my_buildings:
if b.kind in ("squarracks", "circhery", "triables") and not b.spawn_toggle:
actions.append(ToggleSpawn(b.id, True))
if state.enemy_buildings and state.idle_units():
t = state.enemy_buildings[0]
for u in state.idle_units():
actions.append(AttackMove(u.id, t.x, t.y))
return actions
if __name__ == "__main__":
run_online(MyBot(),
orchestrator_url="https://pachinko-rts.com/orchestrator",
bot_token="bot_...paste_your_token_here...")3. Run it. The bot stays connected and plays match after match until you stop it. Watch it in action on the Spectate page.
Installation
One package, both modules
pip install pachinko-pybindPython 3.9+ required. pachinko-pybind is a single pre-built wheel that ships two Python modules: pachinko_sdk (the live-server client used by every bot) and pachinko_gym(Gymnasium environment used for offline RL training). The game simulation runs in compiled Rust under the hood — no source build needed. requests, websocket-client, and numpy install automatically.
Optional: training extras
For RL training you'll also want a learning library. The examples below use stable-baselines3 / sb3-contrib:
pip install "pachinko-pybind[train]"Versions & upgrades
Keep pachinko-pybind up to date — current version 0.4.2:
pip install -U pachinko-pybindDeterministic bots
Create a .py file, subclass Bot, and implement on_tick. The SDK handles all communication with the game.
Minimal example
from pachinko_sdk import Bot, run_online, AttackMove, ToggleSpawn
class MyBot(Bot):
def on_tick(self, state) -> list:
actions = []
# Keep all military buildings spawning
for b in state.my_buildings:
if b.kind in ("squarracks", "circhery", "triables"):
if not b.spawn_toggle:
actions.append(ToggleSpawn(b.id, True))
# Attack-move idle units toward the nearest enemy building
if state.enemy_buildings:
target = state.enemy_buildings[0]
for unit in state.idle_units():
actions.append(AttackMove(unit.id, target.x, target.y))
return actions
if __name__ == "__main__":
run_online(MyBot(),
orchestrator_url="https://pachinko-rts.com/orchestrator",
bot_token="bot_...your_token...")Counter-unit production
Count the visible enemy unit types and spawn the counter. Square beats Triangle, Triangle beats Circle, Circle beats Square.
def on_tick(self, state) -> list:
actions = []
# Count visible enemy types
sq = sum(1 for u in state.enemy_units if u.unit_type == "square")
ci = sum(1 for u in state.enemy_units if u.unit_type == "circle")
tr = sum(1 for u in state.enemy_units if u.unit_type == "triangle")
if tr > max(sq, ci):
primary = "squarracks" # squares beat triangles
elif sq > max(ci, tr):
primary = "circhery" # circles beat squares
else:
primary = "triables" # triangles beat circles
for b in state.my_buildings:
if b.kind in ("squarracks", "circhery", "triables"):
actions.append(ToggleSpawn(b.id, b.kind == primary))
return actionsBuying upgrades
# Upgrade types: "spawn_rate", "weapon_damage", "armor", "move_speed", "mining_rate"
from pachinko_sdk import Upgrade
def on_tick(self, state) -> list:
actions = []
RESERVE = 600 # keep this much hematite in reserve
for b in state.my_buildings:
if b.kind == "squarracks":
if b.upgrades["spawn_rate"] < 5 and state.resources > 300 + RESERVE:
actions.append(Upgrade(b.id, "spawn_rate"))
if b.kind == "hematite":
if b.upgrades["mining_rate"] < 5 and state.resources > 400 + RESERVE:
actions.append(Upgrade(b.id, "mining_rate"))
return actionsAPI reference
Bot lifecycle methods
| Name | Type | Description |
|---|---|---|
| on_game_start(data) | None | Called once at game start. data includes the GameStart payload (map_seed, players, num_players). |
| on_tick(state) | list[Action] | Called every tick. Return a list of action objects. |
| on_mulligan(obs) | MulliganDecision | Called once when the mulligan phase opens, then again after every reroll. Default: accept. |
| on_bet_action(obs) | BetDecision | Called whenever the server asks this bot for a betting action. Default: call (goes all-in if the call exceeds the bankroll). |
| on_game_over(data) | None | Called when the game ends. data is the GameOver payload, or {reason: 'ws_closed' | 'ws_eof'} if the server dropped the WS without a GameOver. |
GameState fields
| Name | Type | Description |
|---|---|---|
| tick | int | Current game tick (24 ticks per second). |
| resources | int | Your current hematite balance. |
| my_units | list[Unit] | All your living units. |
| my_buildings | list[Building] | All your buildings. |
| enemy_units | list[Unit] | Visible enemy units. |
| enemy_buildings | list[Building] | Visible enemy buildings. |
| resource_nodes | list[ResourceNode] | All hematite nodes on the map. |
| idle_units() | list[Unit] | Helper: your units with no current order. |
| units_of_type(t) | list[Unit] | Helper: filter by type string. |
| nearest_enemy(x, y) | Unit | None | Helper: closest visible enemy unit to a point. |
| military_buildings() | list[Building] | Helper: your squarracks/circhery/triables. |
| enemy_military_buildings() | list[Building] | Helper: visible enemy military buildings. |
Unit fields
| Name | Type | Description |
|---|---|---|
| id | int | Unique entity ID. |
| x, y | float | World position (0–2500 on each axis). |
| unit_type | str | "square" | "circle" | "triangle" |
| hp / max_hp | int | Current and maximum hit points. |
| has_target | bool | True if the unit is currently executing an order. |
| attack_moving | bool | True if the unit is attack-moving. |
Building fields
| Name | Type | Description |
|---|---|---|
| id | int | Unique entity ID. |
| x, y | float | World position. |
| kind | str | "squarracks" | "circhery" | "triables" | "hematite" | "arrow_tower" |
| hp / max_hp | int | Current and maximum hit points. |
| spawn_toggle | bool | Whether this building is currently spawning units. |
| upgrades | dict | Keys: spawn_rate, weapon_damage, armor, move_speed, mining_rate. Values: int (0–5). |
ResourceNode fields
| Name | Type | Description |
|---|---|---|
| x, y | float | World position. |
| richness | float | Income rate multiplier. |
| control | str | "neutral" | "own" | "enemy" |
Actions
| Name | Type | Description |
|---|---|---|
| Move(unit_id, x, y) | — | Move to position without attacking. |
| AttackMove(unit_id, x, y) | — | Move and auto-attack enemies encountered. |
| Attack(unit_id, target_id) | — | Direct attack on a specific enemy unit. |
| Halt(unit_id) | — | Cancel current order. |
| ToggleSpawn(building_id, on) | — | Enable or disable unit production. |
| Upgrade(building_id, upgrade_type) | — | Purchase one upgrade level. upgrade_type: see Building fields. |
| Patrol(unit_id, x, y) | — | Patrol back and forth to position. |
| AttackMoveGroup(unit_ids, x, y) | — | Attack-move a list of units together. |
| MoveGroup(unit_ids, x, y) | — | Move a list of units to the same position. |
| HaltGroup(unit_ids) | — | Halt a list of units. |
Betting + mulligan overview
A match is wrapped in poker-style betting: one round before the RTS phase opens and one mid-game. The pot aggregates across roundsand is paid out at the end of the game — nothing is awarded between rounds. RTS-eliminated bots keep betting until they fold or run out of bankroll.
If you only care about the RTS layer, do nothing — the base Bot class auto-accepts every mulligan and auto-calls every betting decision (going all-in if the call exceeds your bankroll). To play the betting game, override on_mulligan and/or on_bet_action.
from pachinko_sdk import (
Bot, BetDecision, BetObservation,
MulliganDecision, MulliganObservation,
)
class MyBot(Bot):
def on_mulligan(self, obs: MulliganObservation) -> MulliganDecision:
# Reroll the two lowest-rarity items if we can afford it.
from pachinko_sdk import Rarity
cheap = [i for i, item in enumerate(obs.items) if item.rarity == Rarity.COMMON]
return MulliganDecision.reroll(cheap[:2]) if cheap else MulliganDecision.accept()
def on_bet_action(self, obs: BetObservation) -> BetDecision:
# Fold to aggression if anyone has raised this round; otherwise call.
from pachinko_sdk import BetActionKind
raised = any(ev.action == BetActionKind.RAISE for ev in obs.history_this_round)
if raised and obs.to_call > obs.me.bankroll * 0.1:
return BetDecision.fold()
return BetDecision.call()MulliganObservation fields
| Name | Type | Description |
|---|---|---|
| items | list[StartingItem] | Your 5 dealt items: kind, rarity, unit_type, value. |
| bankroll | int | Chips remaining (rerolls cost big_blind each). |
| big_blind | int | Cost per rerolled index. |
| reroll_cost | int | Alias for big_blind. |
StartingItem fields
| Name | Type | Description |
|---|---|---|
| kind | ItemKind | Enum: ExtraProjectile, Ballistics, DamageBoost, SpeedBoost, ArmorBoost, SpawnRateBoost, HpBoost, CircleRangeBoost, CircleAttackSpeedBoost, CircleSplashDamage, CleaveAttack, ResourceRateBoost, HematiteBonus, HealthRegen, VisionBoost. |
| rarity | Rarity | Common, Uncommon, Rare, Legendary. |
| unit_type | UnitType | None | Square / Circle / Triangle for unit-targeted kinds; None otherwise. |
| value | float | Kind-specific magnitude (multiplier or flat bonus). |
| raw | dict | Original wire dict, in case you need a field this dataclass omits. |
BetObservation fields
| Name | Type | Description |
|---|---|---|
| round | int | 0 = pre-game round, 1+ = mid-game rounds. |
| my_player_id | int | Your slot id. |
| my_seat | int | Your index in this round's action_order. |
| action_order | list[int] | Player slots in the order they act this round. |
| pot | int | Cumulative chips in the pot (across all rounds). |
| current_bet | int | Total committed needed to stay in the round. |
| players | list[PlayerBetState] | Per-slot snapshot: bankroll, committed, status. |
| my_items | list[StartingItem] | Your mulligan items, retained from the pre-game phase. |
| history_this_round | list[BetEvent] | Every BetUpdate seen this round, in order. |
| history_prior_rounds | list[list[BetEvent]] | Closed rounds, oldest first. |
| to_call | int (property) | Chips needed to call (0 means a check is free). |
| can_check | bool (property) | True if to_call == 0. |
| me | PlayerBetState (property) | Shorthand for players[my_player_id]. |
BetEvent fields
| Name | Type | Description |
|---|---|---|
| player | int | Slot of the player who acted. |
| action | BetActionKind | Ante, Check, Bet, Call, Raise, Fold, AllIn. |
| amount | int | Chips put in for this action (0 for check/fold). |
| pot_after | int | Total pot after this action. |
| current_bet_after | int | current_bet after this action. |
Decision constructors
| Name | Type | Description |
|---|---|---|
| BetDecision.call() | BetDecision | Call (or check if obs.to_call == 0). Goes all-in if the call exceeds bankroll. |
| BetDecision.fold() | BetDecision | Fold. |
| BetDecision.raise_(amount) | BetDecision | Raise by `amount` chips on top of current_bet. |
| BetDecision.all_in() | BetDecision | All-in. |
| MulliganDecision.accept() | MulliganDecision | Lock in your current items. |
| MulliganDecision.reroll([0, 2]) | MulliganDecision | Reroll items at the listed indices (0..=4). Costs big_blind each. |
RL bots (Gymnasium)
RL bots are just regular .py bot scripts — the same format as deterministic bots. You train your model offline using pachinko_gym, then load the weights inside on_game_start and call your model in on_tick. You choose the architecture, the framework (PyTorch, JAX, anything), and the inference code. The game does not care how you produce the actions.
Train offline
pachinko_gym ships inside the same pachinko-pybind wheel you already installed for the live SDK. Use any RL library. The example below uses MaskablePPO from sb3-contrib, but any algorithm or framework works.
from pachinko_gym import PachinkoSeriesEnv
from sb3_contrib import MaskablePPO
env = PachinkoSeriesEnv()
model = MaskablePPO("MultiInputPolicy", env, verbose=1)
model.learn(total_timesteps=5_000_000)
model.save("my_rl_bot") # saves my_rl_bot.zip (SB3 format)Observation space
The environment exposes a Dict observation with PointNet-style feature arrays — the same information available in on_tick:
| Name | Type | Description |
|---|---|---|
| own_units | (N, 7) float32 | x, y, type, hp, max_hp, has_target, attack_moving |
| enemy_units | (M, 5) float32 | x, y, type, hp, max_hp |
| own_buildings | (B, 8) float32 | x, y, kind, hp, max_hp, spawn_toggle, upgrade levels |
| resources | (1,) float32 | Normalised hematite balance |
| resource_nodes | (K, 4) float32 | x, y, richness, control |
Write your bot script
After training, write a normal bot script that loads your model and runs inference each tick. The example below uses SB3, but the pattern works for any framework.
import numpy as np
from pachinko_sdk import Bot, run_online, AttackMove, ToggleSpawn
from sb3_contrib import MaskablePPO
from pachinko_gym import obs_from_state, actions_from_output # your own helpers
class RLBot(Bot):
def on_game_start(self, data: dict) -> None:
# Load weights once at game start - not every tick
self.model = MaskablePPO.load("my_rl_bot.zip")
def on_tick(self, state) -> list:
obs = obs_from_state(state) # convert GameState to numpy obs dict
action_vec, _ = self.model.predict(obs, deterministic=True)
return actions_from_output(action_vec, state) # decode to SDK actions
if __name__ == "__main__":
run_online(RLBot(),
orchestrator_url="https://pachinko-rts.com/orchestrator",
bot_token="bot_...your_token...")You write obs_from_state and actions_from_output yourself — this is intentional. It keeps your observation encoding and action decoding exactly consistent between training and inference, and lets you iterate on them freely without being locked into any fixed interface.
Operating notes
Command rate limit
The server enforces a token-bucket rate limit per player: 10 commands per second sustained, with a burst allowance of up to 20 commands. Commands beyond the burst cap are silently dropped — they do not error, the game just ignores them. Returning a large list from on_tick is fine occasionally (the burst absorbs it), but flooding hundreds of actions every tick will result in most being discarded. Prioritise and keep lists concise.
Token security
Treat your bot token like a password. Anyone with it can play matches as your bot and lose its bankroll. If a token leaks, rotate it from My Bots — the old token stops working immediately.
Logging and debugging
The SDK uses standard Python logging. Enable info-level output to watch the connect / poll / match lifecycle:
import logging
logging.basicConfig(level=logging.INFO)Watch your bot live on the Spectate page once a match starts.