# BotWars bot API Everything a bot can use: the `botwars` package (`engine/botwars/`). A bot is one Python file with one class that subclasses `Bot`. The game creates one instance per match and calls its methods; the bot answers by calling commands. ```python from botwars import Bot class MyBot(Bot): def on_tick(self, me, enemies): self.forward(8) # drive at full speed self.turn_radar(45) # sweep the radar if enemies: self.aim_at(enemies[0]) # point the turret at the first enemy seen self.fire(2) ``` Contents: [How a bot runs](#how-a-bot-runs) · [Units and angles](#units-and-angles) · [Commands](#commands) · [Your tank: `me`](#your-tank-me) · [Enemies: scans](#enemies-scans) · [Events](#events) · [Helpers](#helpers) · [Other attributes](#other-attributes) · [Debug drawing](#debug-drawing) · [Output, errors and time](#output-errors-and-time) · [Rules cheat sheet](#rules-cheat-sheet) · [Bot file rules](#bot-file-rules) ## How a bot runs 1. **Load.** The file must define exactly one `Bot` subclass; anything else is a load error. 2. **Start.** The game creates the instance and calls `on_start(config)` once per match. Set up your state here (`self.target = None`, …). 3. **Every tick** (30 per second of playback), for each round: - the events since your last tick are delivered first, each to its hook (`on_hit_wall`, `on_bullet_hit`, …); - then `on_tick(me, enemies)` is called with your tank and the enemies your radar saw. 4. **Commands are for this tick only.** Whatever you ask for during the calls above happens in this tick, then the commands reset. If you don't call `forward()` on a tick, your target speed is 0 and the tank slows to a stop. Calling a command twice in one tick keeps the last value. 5. **State persists.** Attributes you set on `self` live for the whole match, across ticks and rounds. The example above calls commands every tick; that is the normal pattern. ## Units and angles | Quantity | Unit | |---|---| | Position, distance | arena units; the arena is 1000 × 1000 by default, origin at the **bottom-left**, y grows **up** | | Speed | units per tick | | Angle, heading | degrees, `0` = east (+x), **counterclockwise** positive, headings in `[0, 360)` | | Turn commands | degrees for this tick: positive turns counterclockwise (left), negative clockwise (right) | | Energy, damage, power, heat | plain numbers (a tank starts with 100 energy) | ## Commands Call these on `self`. Values outside the legal range are clamped, not rejected. | Command | Meaning | Range (default rules) | |---|---|---| | `forward(speed)` | Target speed. The tank accelerates toward it (+1 per tick), or brakes (−2 per tick). Negative drives in reverse. | −4 … 8 | | `turn(degrees)` | Turn the **body**. The real limit shrinks with speed: `10 − 0.75 × abs(speed)` degrees per tick (speed at the start of the tick). | −10 … 10 | | `turn_turret(degrees)` | Turn the **turret** (where bullets go). Independent of the body. | −20 … 20 | | `turn_radar(degrees)` | Turn the **radar**. Independent of body and turret. | −45 … 45 | | `fire(power)` | Fire a bullet along the turret. 0 or less: don't fire. It only fires if the gun is cool (`me.gun_heat == 0`), `me.energy > power`, and the tank isn't disabled; otherwise it silently does nothing. | 0.1 … 3 | Turning the body does **not** carry the turret or radar along: each part keeps its own heading. ## Your tank: `me` `on_tick(self, me, enemies)` gets `me` (also available as `self.me`): | Attribute | Meaning | |---|---| | `me.x`, `me.y` | Position of your tank's centre | | `me.heading` | Body heading (degrees) | | `me.turret_heading` | Turret heading: the direction your next bullet goes | | `me.radar_heading` | Radar heading | | `me.speed` | Current speed (negative while reversing) | | `me.energy` | Energy left. At exactly 0 the tank is disabled; below 0 it is destroyed. | | `me.gun_heat` | Gun heat; the gun fires only at 0 | | `me.disabled` | `True` at 0 energy: no moving, turning or firing until a refund lifts energy above 0 | ## Enemies: scans `enemies` lists the enemies your radar swept over **in the previous tick** (empty when it saw nothing). A turning radar sweeps the arc between its old and new heading; a still radar sees a 10° beam. Each entry has: | Attribute | Meaning | |---|---| | `id` | The enemy's slot number (stable for the match) | | `x`, `y` | Its position when scanned | | `heading`, `speed`, `energy` | Its body heading, speed and energy when scanned | | `distance` | Distance from you | | `bearing` | Absolute direction from you to it, in degrees (same frame as `me.heading`) | The radar crosses an enemy only now and then while it sweeps, so most ticks `enemies` is empty. Keep the last sighting on `self` if you want to act on it every tick: ```python if enemies: self.target = enemies[0] ``` ## Events Override the hooks you need; the defaults do nothing. Event hooks run before `on_tick` in the same tick. Each event object `e` also has `e.type` (the event name) and `e.bot` (your slot). | Hook | When | Payload | |---|---|---| | `on_start(self, config)` | Once, before the first tick | `config`: the match rules (see `self.config`) | | `on_round_start(self, round)` | A round begins | `round`: 0-based round number | | `on_hit_by_bullet(self, e)` | A bullet hit you | `e.shooter` (slot), `e.bullet` (id), `e.power`, `e.damage` (energy you lost) | | `on_bullet_hit(self, e)` | Your bullet hit a tank | `e.bullet` (id), `e.target` (slot), `e.damage` | | `on_bullet_missed(self, e)` | Your bullet is gone without hitting | `e.bullet` (id), `e.reason`: `"wall"` (left the arena) or `"bullet"` (collided with another bullet) | | `on_hit_wall(self, e)` | You drove into a wall (your speed drops to 0) | `e.damage` (0 when wall damage is off) | | `on_hit_tank(self, e)` | You collided with a tank | `e.other` (slot), `e.damage` (each tank takes it) | | `on_death(self)` | You were destroyed | — | | `on_round_end(self, result)` | A round ended | `result.round`, `result.ticks`, `result.winner` (slot or `None` for no winner), `result.reason` (`"elimination"`, `"deadlock"`, `"inactivity"` or `"time_limit"`), `result.scores` (points per slot this round) | `on_death` and `on_round_end` arrive with the next tick (the next round's first), or, for the last round, when the match ends. ## Helpers | Helper | Returns / does | |---|---| | `self.angle_to(x, y)` | Absolute direction from your tank to a point, degrees in `[0, 360)` | | `self.distance_to(x, y)` | Distance from your tank to a point | | `Bot.normalize_angle(angle)` | `angle` wrapped into `(-180, 180]` — the short way round | | `self.turn_toward(x, y)` | Calls `turn(...)` with the body turn toward a point (still limited per tick) | | `self.aim_at(target)` | Calls `turn_turret(...)` toward anything with `.x` and `.y` (a scan, or your own object) | A typical "is the gun lined up?" check: ```python off = abs(self.normalize_angle(self.angle_to(t.x, t.y) - me.turret_heading)) if off < 2 and me.gun_heat == 0: self.fire(3) ``` ## Other attributes | Attribute | Meaning | |---|---| | `self.config` | The match rules, as attributes: every rules field, including all of [the cheat sheet](#rules-cheat-sheet) (`self.config.arena_width`, `self.config.max_turret_turn`, …) | | `self.tick` | Tick number within the current round, from 0 | | `self.round` | Current round, from 0 | | `self.me` | The same object as `on_tick`'s `me` | | `self.random` | A `random.Random` seeded for this bot and match. The global `random` module is seeded the same way, so a bot's randomness repeats exactly in a replay. | | `self.debug` | Debug drawing (below) | ## Debug drawing Draw on the replay to see what your bot is thinking; shapes last one tick and show when the **Debug** overlay is on. | Call | Draws | |---|---| | `self.debug.line(x1, y1, x2, y2, color="#ffffff")` | A line between two arena points | | `self.debug.circle(x, y, r, color="#ffffff")` | A circle | | `self.debug.text(x, y, s, color="#ffffff")` | Text (first 200 characters) | Colours are `"#rrggbb"`; anything else becomes white. Up to 200 shapes per tick are kept. ```python if self.target: self.debug.line(me.x, me.y, self.target.x, self.target.y, "#ff6b6b") ``` ## Output, errors and time - **`print()`** works: output is shown in the Console and the Inspector for that tick (up to 4096 bytes per tick). - **An exception** in a hook or `on_tick` shows its traceback in the Console; that tick counts as a **miss** (your tank does nothing), and the bot keeps running. - **Time:** each tick must answer within 200 ms, or it is a miss. More than 30 misses in a row forfeits the match. A bot stuck for over a second is stopped and forfeits. Starting (`on_start` plus the first tick) has 2 s. - A reply larger than 64 KB (mostly debug shapes) is a miss. ## Rules cheat sheet Default values; every one is a field of `self.config` and can differ per match (the workbench's ⚙ rules, or a lesson). | Rule | Default | Config field | |---|---|---| | Arena | 1000 × 1000 | `arena_width`, `arena_height` | | Rounds per match | 3 | `rounds` | | Ticks per round (time limit) | 3000 | `max_ticks_per_round` | | Round ends after this many ticks with no tank damage (0 = never) | 450 | `stalemate_ticks` | | Tank radius | 18 | `tank_radius` | | Top speed forward / reverse | 8 / 4 | `max_forward_speed`, `max_reverse_speed` | | Acceleration / braking per tick | 1 / 2 | `acceleration`, `deceleration` | | Body turn per tick | `10 − 0.75 × abs(speed)` | `body_turn_base`, `body_turn_per_speed` | | Turret / radar turn per tick | 20 / 45 | `max_turret_turn`, `max_radar_turn` | | Radar beam / range | 10° / 1200 | `radar_beam_width`, `radar_range` | | Start energy | 100 | `start_energy` | | Fire power | 0.1 … 3 | `fire_power_min`, `fire_power_max` | | Cost of a shot | its power | — | | Bullet speed | `20 − 3 × power` (power 3 is slowest: 11 per tick) | `bullet_speed_base`, `bullet_speed_per_power` | | Bullet damage | `4 × power + 2 × max(0, power − 1)` (power 1: 4, power 3: 16) | `bullet_damage_per_power`, `bullet_damage_bonus` | | Energy back on a hit | `3 × power` | `hit_refund_per_power` | | Gun heat per shot | `1 + power / 5` | `heat_base`, `heat_per_power` | | Gun cooling per tick / heat at the start | 0.1 / 3.0 (first shot after 30 ticks) | `gun_cooling_rate`, `start_gun_heat` | | Ram damage (to each tank) | 0.6 | `ram_damage` | | Wall damage | `0.5 × abs(speed)` | `wall_damage_enabled`, `wall_damage_per_speed` | **Scoring:** 1 point per point of bullet damage dealt; 50 survival points shared by the tanks still alive at each death; a kill bonus of 20% of the damage dealt to the destroyed tank; 10 points to the last survivor. The match goes to the highest total score (ties: more round wins). ## Bot file rules - **One bot class:** exactly one `Bot` subclass per file. - **Imports:** the Python standard library, and `numpy` (loaded only when your file imports it). No network or page access: bots run in a sandbox. - **Metadata (optional):** plain-string lines at the top level of the file name and describe the bot. The workbench keeps them in step with the bot's name and the `.bwbot` export. ```python __botname__ = "Rammer" # the bot's name: 1–32 letters, digits, spaces, _ or - __author__ = "your name" # up to 64 characters __version__ = "1.2" # up to 16 __date__ = "2026-10-09" # up to 32 __about__ = "Chases and rams." # up to 200 ```