# War3AI / OpenWar3 Full documentation > Source https://war3ai.com/en. An open API for Warcraft III 1.27, built for AI agents. When writing a bot, use only the Game methods listed in the "API catalog" at the end of this file. --- # Overview > OpenWar3 docs: what it is, what it can do; quickstart, your first bot, write a bot with an LLM, API and protocol, gateway and MCP — and which page to start with for your situation. **OpenWar3** is War3AI's open interface layer: a runtime injected into Warcraft III 1.27, plus a Python SDK. - Every **50 ms**, the runtime pushes the complete state of the entire map into shared memory: every player's resources and food; every unit's HP and mana, order, current target, ability cooldowns, buffs and inventory; items on the ground, trees, production queues, and time of day. There's also an **event stream**: units appearing and dying, every single hit of damage, production completing… - External programs issue **semantic commands** with **about one frame** of latency: move, attack, gather, build, train, cast, learn skills, revive, use items, buy… Every command returns a **receipt** that says whether the engine accepted it and, if not, the reason code. - You only say *what* to do: units by four-character code and abilities by order string, the same names the game uses. *How* it gets done is the runtime's job. So an LLM needs no low-level knowledge and never has to look at the screen. Once it has read the docs, it can write a bot that runs an economy and fights, then revise it on its own from the receipts and events after it plays. It's not just for matches: the [canvas](https://war3ai.com/en/docs/canvas/) draws your own panels and markers on the game screen, [UI & input](https://war3ai.com/en/docs/ui-input/) makes the buttons you draw clickable and your hotkeys work, the [JASS channel](https://war3ai.com/en/docs/jass/) calls the game's 1291 functions from outside, and in RPG maps you can bring along an [AI companion](https://war3ai.com/en/docs/companion/). A finished AI can be packaged as a [scheme](https://war3ai.com/en/docs/schemes/) to switch to with one click, or export and share; a whole new set of gameplay can be written as a [gameplay mod](https://war3ai.com/en/docs/mods/). You can connect without writing Python, too: the [gateway](https://war3ai.com/en/docs/gateway/) lets any language or browser page call the same APIs over WebSocket / JSON, and the [MCP server](https://war3ai.com/en/docs/mcp/) lets agents such as Claude Code call tools directly to read the game and issue commands. - [Quickstart](https://war3ai.com/en/docs/quickstart/): Set up your environment, start a game with one command, and watch the example bot take over. - [Write a bot with an LLM](https://war3ai.com/en/docs/ai-bot/): No programming required: copy the prompt, describe your strategy, and hand it to an agent. - [Mental model](https://war3ai.com/en/docs/concepts/): Snapshots, commands, receipts, events, ticks. Spend five minutes here before you write a bot. - [API catalog](https://war3ai.com/en/api/): Every API, each labeled with its test status, latency tier and underlying mechanism. ## Pick your path | You | Start here | Then | |---|---|---| | Play Warcraft, don't code | [Quickstart](https://war3ai.com/en/docs/quickstart/) → [Write a bot with an LLM](https://war3ai.com/en/docs/ai-bot/) | If you get stuck, see the [FAQ](https://war3ai.com/en/docs/faq/) | | Know Python | [Your first bot](https://war3ai.com/en/docs/first-bot/) → [Mental model](https://war3ai.com/en/docs/concepts/) → [Fifteen rules](https://war3ai.com/en/docs/rules/) | [Pro strategy cookbook](https://war3ai.com/en/docs/cookbook/), [Example bots](https://war3ai.com/en/docs/examples/) | | Build coding agents / automation | [Autonomous agent loop](https://war3ai.com/en/docs/agent-loop/) | [Receipts and reason codes](https://war3ai.com/en/docs/reason-codes/), [`llms-full.txt`](https://war3ai.com/en/llms-full.txt) | | Want an LLM making in-game decisions | [LLM strategy coach](https://war3ai.com/en/docs/llm-coach/) | [Speech bubbles and local models](https://war3ai.com/en/docs/speech/) | | Want an agent to operate the game directly (Claude Code, etc.) | [LLM calls tools directly (MCP)](https://war3ai.com/en/docs/mcp/) | [UI & input](https://war3ai.com/en/docs/ui-input/) | | Use another language (JS, C#, Go, Rust…) | [Gateway](https://war3ai.com/en/docs/gateway/) | Lower level: [W3P protocol](https://war3ai.com/en/docs/protocol/) | | Want AIs from different people to play each other | [Fair mode](https://war3ai.com/en/docs/fair-mode/) | [Arena](https://war3ai.com/en/arena/) | | Want to build your own gameplay in RPG / custom maps | [Gameplay mods](https://war3ai.com/en/docs/mods/) | [UI & input](https://war3ai.com/en/docs/ui-input/), [Canvas](https://war3ai.com/en/docs/canvas/), [JASS channel](https://war3ai.com/en/docs/jass/), [RPG companion](https://war3ai.com/en/docs/companion/) | | Want to share your AI with others | [AI schemes](https://war3ai.com/en/docs/schemes/) | [Farsight console](https://war3ai.com/en/docs/console/) | ## What's in the repository ```text start.bat The single entry point: deploys from scratch + opens Farsight; stop.bat stops everything sdk/python/ Interface layer. openwar3/ is the public facade (Game + Bot); start here brains/ Decision layer examples/ hello_bot (economy) → rush_bot (army) → macro_bot (macro) → micro_bot (micro + creeping); buddy (RPG companion); mod_hero_roguelike / mod_endless_defense (gameplay mods) xwar3/ Reference brain: strategy layer (seconds) + reflex layer (4 processes) + win-rate model console/ Farsight web console (FastAPI + React) gateway/ Gateway (WebSocket / JSON) + JS client + browser demo page director/ Auto camera, overhead health bars speech/ Overhead chat bubbles + local LLM runtime/ Multi-instance orchestration (each game restarts with your settings) data/ order-ids.txt; tools to extract data from your own game schemes/ Your AI schemes (mine/) and schemes shared by others (installed/); not checked into the repo tools/ play.py (start a game with one command), run_scheme.py (scheme runner), war3_mcp.py (MCP server), run_tests.py, live verification scripts docs/ API catalog api.json (generated from code), protocol, manual ``` The only thing between the runtime and your code is a versioned [W3P protocol](https://war3ai.com/en/docs/protocol/). The Python SDK is the easiest route, but any other language can connect by following the protocol. ## What an API's test status means Every API in the catalog is labeled with one of three statuses: - **Verified in live games**: the underlying path (action ID, argument shape, the effect read back) has been verified in real games and is guarded by a verification script. - **Experimental**: a newly added API that already works on a test instance and is still being verified item by item in live games. You can use it, but API details may still change. - **Inferred / not fully tested**: the underlying mechanism copies what the engine itself does (for example, the equivalent JASS function), but it hasn't been checked item by item in a game yet. Check the receipt before relying on it. > **Note** > > Only **Warcraft III 1.27** (The Frozen Throne) is supported for now. Versions 1.24–1.28 share the same engine structure, and multi-version support is phase P4 of the [roadmap](https://war3ai.com/en/roadmap/). 1.29 and later, as well as Reforged, use a different engine and are out of scope. --- # Quickstart > Double-click start.bat to install everything, set your game folder in Farsight, and start a game to watch an example Bot take over. About 15 minutes. ## What you need | | Requirement | Notes | |---|---|---| | OS | Windows 10 / 11, 64-bit | Only Windows is supported for now | | Game | Warcraft III **1.27a** (The Frozen Throne, `Game.dll` 1.27.0.52240) | A client you legally own; no game files on disk are modified | **Nothing else needs to be installed first.** `start.bat` downloads exactly one thing: Python 3.13 (the official portable package, about 14 MB), into the repository's `bin\env\`: no administrator rights, no changes to the system PATH, and it switches to mirrors automatically on networks in mainland China. If your computer already has a working copy of Python, it uses that. PowerShell is the copy built into Windows; Farsight's web UI ships pre-built with the repository, so Node.js isn't needed. ## Installation 1. **Get the code** ```bash git clone https://github.com/OPENXXAI/OpenWar3AI.git ``` Or [download the zip](https://github.com/OPENXXAI/OpenWar3AI/archive/refs/heads/main.zip) and extract it. The runtime (the injection DLL and the launcher) ships with the repository; no separate download needed. 2. **Double-click `start.bat`** The first time, it will: - Download Python 3.13; - Install the Python packages, and check and install the runtime files; - Download AMAI and generate the strategy data the reference brain needs (AMAI has a custom license, so the generated files are not committed to git; a failure here only affects the reference brain); - Open Farsight's home page, **Control Center**, at `http://127.0.0.1:8866`. Every step prints its result, and a step that fails tells you how to fix it. After that, each double-click just runs a check of a second or two and opens Farsight. The black window closes by itself after a few seconds: Farsight keeps running in the background, and closing the browser doesn't stop it. 3. **Set the game folder in Control Center** At the top of Control Center, use **Find automatically**, or **Browse…** to pick your Warcraft III folder yourself. Farsight checks the game version and **extracts data from your own copy of the game** (unit table, abilities, items, counter table… Blizzard's files are not distributed with the code). If the version isn't 1.27a, it warns you. Maps and the next game's settings are relative to this folder (maps in any folder under `\Maps` can be picked); to change the folder later, use the **Settings** page. 4. **Start a game and let the example Bot take over** The easiest way is Farsight's **Instances & Setup** page: tick an instance number, pick an AI scheme and click **Start test**. You can also use the command line: ```bash python tools/play.py --bot brains/examples/hello_bot.py ``` This command starts a game instance, injects the runtime, starts the game automatically, and then launches the Bot. **Once you see peasants heading to mine and the hall starting to train peasants, it works.** The `python` in the command is the one recorded in `openwar3.json`; the one `start.bat` installed itself is at `bin\env\python\python.exe`. ## start.bat and stop.bat ```bash start.bat # deployment check + open Farsight start.bat setup # full check: reinstall Python packages, retry AMAI start.bat restart # restart only the Farsight backend (games and services are unaffected) start.bat node # also installs a copy of Node.js (only needed for previewing the website; not needed otherwise) start.bat 5 6 # also start tests on instances 5 and 6 (game + reference brain) stop.bat # stop everything completely; stop.bat --keep-llm keeps the local model in VRAM ``` The gateway, speech bubbles and the local LLM are also started and stopped from Farsight's **Control Center**, so there are no other scripts to hunt down. **To stop everything completely**, double-click `stop.bat`, or click **Stop all** in the top-right corner of Control Center: game instances, AIs, the gateway, bubbles, the local model this system uses and the Farsight backend all stop in turn. The MCP server is managed by clients such as Claude and is not stopped. > **Config file** > > `openwar3.json` is written automatically by `start.bat` and Farsight; it stores only local paths and is not committed to git. To change ports or the local LLM's address and model name, follow `openwar3.example.json` and include only the entries that differ from it. ## play.py options ```bash python tools/play.py --bot my_bot.py --inst 9 --race 2 --enemy-race 1 --difficulty 3 --speed 200 python tools/play.py --bot my_bot.py --inst 9 --attach # the game is already running; only attach the Bot python tools/play.py --bot my_bot.py --fair # fair mode: can only see what's in vision ``` | Option | Default | Description | |---|---|---| | `--bot` | Required | Path to the Bot file (the file must contain a subclass of `Bot`) | | `--inst` | `9` | Instance number. Don't reuse the number of an instance that's already running (Farsight's Instances & Setup page shows which numbers are in use) | | `--race` | `1` | Our race: 1 Human, 2 Orc, 3 Undead, 4 Night Elf | | `--enemy-race` | `0` | Opponent's race | | `--difficulty` | `2` | Computer opponent difficulty: 2 Easy, 3 Normal, 4 Insane | | `--speed` | `100` | Game speed (percent, 200 = 2×) | | `--map` | `default_map` from the config | Map | | `--attach` | | Don't start a game; only attach to an instance that's already running | | `--hz` | `5` | How many times per second `on_tick` is called | | `--minutes` | `60` | Maximum run time in minutes (wall clock) | | `--fair` | | [Fair mode](https://war3ai.com/en/docs/fair-mode/) | | `--player` | | Which player number to command as (for AI vs. AI) | > **Warning** > > Don't start the game with `--minimize`: **the game simulation stops while the window is minimized** (the clock doesn't move), and the Bot will wait forever for the game to start. You can also skip `play.py` and use the SDK's command line to attach directly to an instance that's already running: ```bash python -m openwar3 run brains/examples/hello_bot.py --inst 5 # run a Bot python -m openwar3 status --inst 5 # connect and print snapshot / fast lane status python -m openwar3 catalog # print the API reference ``` ## Once it's running - [Write your first Bot](https://war3ai.com/en/docs/first-bot/): Start with a 10-line minimal Bot and add training units and attacks step by step. - [Let an LLM write it for you](https://war3ai.com/en/docs/ai-bot/): Copy the prompt template and describe your strategy in plain words. ## Self-check ```bash python tools/run_tests.py # SDK / reference brain / reflex layer / console / speech bubbles / examples, one subprocess per suite ``` Offline tests don't require the game to be running. Farsight's **Control Center** also has an **Environment check** that shows whether each part is installed. --- # Your First Bot > Start with a 10-line minimal Bot, then add training peasants, building for food, producing an army, heroes and attacking — and finally learn to read receipts. A Bot is a class that subclasses `openwar3.Bot`. You only override the hooks you need; `g` (`Game`) handles "observing" and "acting". ## The minimal Bot ```python title="my_bot.py" from openwar3 import Bot class MyBot(Bot): def on_start(self, g): # called once after the game starts g.message("I'm here") def on_tick(self, g): # about 5 times per second for w in g.idle_workers(): g.gather(w, g.nearest(g.gold_mines(), w)) ``` ```bash python tools/play.py --bot my_bot.py ``` Idle peasants head to the nearest gold mine. The four hooks: | Hook | When it's called | |---|---| | `on_start(g)` | Once after the game starts, before the first tick | | `on_tick(g)` | Every tick (5 times per second by default). If a tick runs over, the next one is pushed back automatically — they never pile up | | `on_event(g, ev)` | Before each `on_tick`; hands you every event since the last tick, one by one | | `on_end(g, reason)` | Once when the game ends (the game process is gone / we have no units left / stopped manually) | > **Tip** > > An exception in `on_tick` doesn't end the game: the runner prints the stack trace and continues with the next tick; it only stops after **20 consecutive ticks with errors**. ## Adding an economy: training peasants, building for food ```python from openwar3 import Bot class Economy(Bot): def on_tick(self, g): res = g.resources() # None if it can't be read, not 0 halls = g.my_buildings({"htow", "hkee", "hcas"}) if res is None or not halls: return home = halls[0] # 1. Idle peasants go mine gold for w in g.idle_workers(): mine = g.nearest(g.gold_mines(), w) if mine: g.gather(w, mine) # 2. Train peasants: only 1 in the queue at a time (a full queue locks gold up in it) if len(g.my_workers()) < 15 and not g.queue(home): g.train(home, "hpea") # 3. Food almost capped: find a peasant who isn't building and build a Farm near the hall if res["food_cap"] - res["food_used"] <= 6: builder = next((w for w in g.my_workers() if not g.is_constructing(w)), None) if builder: g.build_near(builder, "hhou", home.x, home.y) ``` Three things worth noting: - **Only train when `g.queue(home)` is empty.** Issuing a train order every tick fills the 7-slot queue and locks up your gold (measured: the hall queued 4 peasants with 300 gold locked in the queue, and the opening was much slower). - **`build_near` instead of hard-coded coordinates.** It searches outward from near to far for a spot that fits and tracks the result across ticks; it does nothing when you can't afford it. Hard-coded coordinates may well land right in a forest. - **Don't pick peasants who are already building.** A Human Farm takes 35 seconds to build; if you send the worker away partway through, construction stops. The complete version that works for all four races is `brains/examples/hello_bot.py`: 5 per mine, extra workers chop trees once the mine is full, and it resumes stalled construction. ## Adding Barracks, a hero and attacks ```python from openwar3 import Bot WAVE = 8 class Rush(Bot): def on_start(self, g): self.attacking = False def on_tick(self, g): halls = g.my_buildings({"htow", "hkee", "hcas"}) if not halls: return home = halls[0] # Hero: have an altar but no hero -> revive first, train only if revive fails (heroes are unique; training again after death is rejected) altars = g.my_buildings({"halt"}) if altars and not g.my_heroes(): if not g.revive(altars[0]): g.train(altars[0], "Hpal") for h in g.my_heroes(): info = g.hero_info(h) if info and info["skill_points"]: g.learn(h, "AHhb") # Holy Light # Barracks keep producing Footmen (only 1 in the queue at a time) for b in g.my_buildings({"hbar"}): if not g.queue(b): g.train(b, "hfoo") # Attack once a wave is ready; go home after heavy losses army = g.my_army() if len(army) >= WAVE: self.attacking = True elif len(army) < WAVE // 2: self.attacking = False if self.attacking: target = g.nearest([e for e in g.enemies() if g.is_building(e)], home) if target: idle = [u for u in army if not g.order_of(u)] # only order idle units g.attack_move(idle, target.x, target.y) ``` For the full version, see `brains/examples/rush_bot.py` (it subclasses `hello_bot` and builds the Barracks / altar if missing). ## Reading receipts Every command returns a receipt. `if r:` means "the engine accepted it"; when it wasn't accepted, `r.reason` says why: ```python r = g.train(barracks, "hfoo") if not r: print(r.reason) # rejected(人口不够) (= not enough food) print(r.verdict) # 3 ``` Common reason codes: `3` not enough food, `8` not enough gold, `9` not enough lumber, `32` queue full, `183` missing prerequisite, `221` no such item / under construction / you already have this hero, `1001` target not visible. For the full table, see [Receipts and reason codes](https://war3ai.com/en/docs/reason-codes/). > **Accepted ≠ done** > > A receipt only tells you "the engine accepted this command". The engine also accepts a build spot inside a forest on the spot, and the worker only fails once it gets there; spells can be interrupted. To see the actual effect, look at snapshots and events: for buildings use `build_near` (it tracks whether the foundation appears), and for spells check whether `g.cooldown()` shows a cooldown. ## Next steps - [Mental Model](https://war3ai.com/en/docs/concepts/): Snapshots, commands, events, ticks, batches — why it's designed this way. - [Pro Playbook Cookbook](https://war3ai.com/en/docs/cookbook/): 21 recipes: saturated mining, never food blocked, focus fire, pulling back wounded units, night creeping… --- # Write a Bot with an LLM > No coding required: you explain how you want it to play, and the LLM writes the code. Copy the prompt template, describe your strategy, run it, then have the model revise it. This is for people who play Warcraft but don't code, and for developers who want to save time. The whole process is a conversation: **you describe the strategy → the model writes code → you play a game → you tell the model what happened → it revises**. > **Tip** > > First set up your environment with [Quickstart](https://war3ai.com/en/docs/quickstart/) and get `hello_bot` running (you'll see peasants go mine gold). That way, when something goes wrong, you can tell whether it's the environment or the Bot. ## 1. Prepare materials for the model How well the model writes depends mostly on whether it has read the right materials. Pick the option that matches your tool: | What you're using | How to give it the materials | |---|---| | **A coding agent that can read your repo** (Claude Code, Cursor, Codex, etc.) | Open it in the repo directory and have it read `docs/BOT_HANDBOOK_ZH.md`, `docs/api.json` and one example first (`brains/examples/macro_bot.py` for economy, `micro_bot.py` for fighting) | | **A chat model with web access** | Have it read [`https://war3ai.com/llms-full.txt`](https://war3ai.com/en/llms-full.txt) first — the entire site's docs are in that one file | | **Web chat without web access** | Paste the handbook, [`api.json`](https://war3ai.com/en/api.json) and one example file after your prompt | | **A local model** (LM Studio, Ollama) | Same as above. A context window of 32K tokens or more is recommended; otherwise the handbook and the API reference won't fit | If you want a particular pro technique, also paste the matching recipe from the [Pro Playbook Cookbook](https://war3ai.com/en/docs/cookbook/). ## 2. Copy this prompt Replace "The strategy I want" at the end with your own words — the more specific, the better: ```text You are writing an AI (in Python) for Warcraft III 1.27. Use only the Game methods listed in api.json; don't invent methods that don't exist. Follow the style of rush_bot.py: subclass openwar3.Bot and implement on_start(g) and on_tick(g). Rules: - on_tick is called about 5 times per second and must be fast (don't sleep inside it). - Values that can't be read are None, not 0 — check before using them. - Commands return a receipt (Receipt); `if r:` means "the engine accepted it"; when it wasn't accepted, `r.reason` says why (not enough food, not enough gold, target not visible, you already have this hero…) — try again next tick or try something else. - To attack a specific enemy, use g.attack(units, enemy); the enemy must be in vision — ones you can't see are rejected. - A dead hero must be revived with g.revive(altar); you can't train another one. - Build with g.build_near(worker, building_code, x, y): it finds a spot that fits, tracks the result, and does nothing when you can't afford it. - To know "what just happened" (who died, who took damage, a hero leveled up, an item dropped), implement on_event(g, ev). - Don't re-issue the same command to the same unit every tick (it interrupts what the unit is doing); give orders to "idle" units. - Only assign workers from idle_workers() to harvesting. At most 5 workers per gold mine. - Queue only 1 unit at a time in training (queue the next when g.queue(building) is empty); to detect being food blocked, check g.production(building).blocked. - When a tick issues many commands, wrap them in with g.batch(): (it waits on the game thread only once). - To choose what to attack, use g.time_to_kill(my_group, enemy) (accounts for counters and armor); to choose where to go, use g.path_distance (returns None if unreachable). - In fair mode you can only see what's in vision; for enemies you saw earlier, use g.last_seen(). - Units are four-character codes (Human Peasant hpea, Footman hfoo, Barracks hbar…), spells are order strings (thunderbolt Storm Bolt, blizzard Blizzard, holybolt Holy Light…, full table in data/order-ids.txt), and learning skills uses four-character codes (AHtb, AHbz…). The strategy I want: ``` ### How to describe your strategy clearly Models struggle most with vague requests. Instead of "play more aggressively", this kind of information is much more useful: - **Race and heroes**: which hero first, and the skill order (e.g. Archmage: Water Elemental, Blizzard, Water Elemental…). - **Build order**: at which peasant count to build Barracks, when to tier up, how many Barracks. - **Army composition**: Footmen + Riflemen? At what count do you move out? - **Attack and retreat conditions**: how many units before attacking, what hero HP triggers a retreat, go home and rebuild after taking heavy losses. - **Creeping**: creep or not, when (after nightfall?), only camps you can beat? - **Fair or not**: if you plan to enter the Arena later, say "only use enemies visible in vision". ## 3. Run it Save the model's code as `brains/my_bot.py`, then: ```bash python tools/play.py --bot brains/my_bot.py --race 1 --difficulty 2 ``` To see results faster, add `--speed 200` (2× game speed). ## 4. Have it revise - **It errors out**: paste the **entire error** back to the model verbatim and say "fix this". - **It plays badly**: describe **what you saw in the game**, not what you guess the cause is. For example: "the hero just stands at home", "units trickle in one at a time", "peasants crowd onto one mine". - **You want to add a new tactic**: add one thing at a time, play a game to confirm nothing broke, then add the next. > **Note** > > A coding agent that can run commands itself can take over steps 3 and 4 as well: play a game, read the logs and receipts, change the code, run again. For how to give it enough information, see [Agent self-iteration](https://war3ai.com/en/docs/agent-loop/). ## 5. Common problems | Symptom | Most likely cause | |---|---| | Nothing moves | Wrong instance number (`--inst`), or the game hasn't started yet | | Peasants don't mine | Orders were given to peasants who were already working; only assign from `idle_workers()` | | Houses never get built | Use `build_near` instead of hard-coded coordinates; check whether the receipt's `reason` says you can't afford it | | The hero never comes out | Check the `train` receipt: not enough food? Or did the hero die (use `revive`)? | | The hero doesn't cast spells | The skill wasn't learned (`learn`) or there's no mana; after casting, check whether `cooldown()` shows a cooldown | | Units twitch tick after tick | Commands are being re-issued every tick; only order idle units | | No units come out and gold keeps rising | You're food blocked: check `g.production(barracks).blocked` | | The model used methods that don't exist | Stress "only use methods in api.json" again in the prompt, and paste the complete api.json | ## Going further - Every API and the underlying mechanism behind each one: [API reference](https://war3ai.com/en/api/); - The reference brain (`brains/xwar3/strategy`) is a complete AI that expands, creeps and attacks. You can have the model read it for ideas, but it uses lower-level APIs, so copying it directly isn't recommended; - Once you're on the [Arena](https://war3ai.com/en/arena/), you'll only be able to see enemies in vision — add `--fair` now to hold yourself to that, and you won't need changes later. --- # Agent Self-Iteration > Let a coding agent play games, read the results, change the code and play again on its own. It needs a command that runs unattended, a structured game report, and a clear goal. In [Write a Bot with an LLM](https://war3ai.com/en/docs/ai-bot/), the "play a game → observe → tell the model" step is done by you. A coding agent that can run commands (Claude Code, Codex, Cursor's agent mode, etc.) can take over that step too, closing the loop: ```text change code ──► play a game (unattended) ──► read game report ──► find the one thing that matters most ──┐ ▲ │ └──────────────────────────────────────────────────────────────────────────────────────────────────────┘ ``` For this loop to actually converge, the agent needs three things. ## 1. A command that runs unattended ```bash python tools/play.py --bot brains/my_bot.py --speed 200 --minutes 10 --fair ``` - `--minutes` guarantees the game ends (in wall-clock minutes), so the agent never gets stuck in a game; - `--speed 200` uses 2× game speed to save time — but inside the Bot, **wait by the game clock** (`g.clock()`), not with a wall-clock `sleep`; - `--fair` makes it follow Arena rules from day one: it can only see what's in vision; - When the run ends, the terminal prints the end reason, e.g. `我方没有单位了` ("we have no units left") or `到时间了` ("time's up"); whatever the Bot itself `print`s also shows up in the terminal. > **Warning** > > The game simulation stops while the window is minimized. Have the agent start the game in the default windowed mode, and make sure it doesn't use the same instance number as the one you're using (`--inst`). ## 2. A structured game report Terminal output is for humans. What the agent should read is a JSON file: what happened, what failed, and why. The SDK already gives you all the raw material — receipts carry reason codes, and the event stream carries production completions and casualties. Just collect them: ```python title="recorder.py" import collections, json, time from openwar3 import Bot class Recorder(Bot): """Adds a game report to a Bot. Subclass it, then call super() in your own on_start / on_event.""" def on_start(self, g): self.rejects = collections.Counter() # "train hfoo: rejected(人口不够)" -> count (= not enough food) self.timeline = [] # [game seconds, category, four-character code]: train / research / build / upgrade completed self.lost = collections.Counter() # what we lost self.killed = collections.Counter() # what we killed def check(self, r, what): """Wrap a command to record rejection reasons: self.check(g.train(b, "hfoo"), "train hfoo")""" if r is not None and not r: self.rejects[f"{what}: {r.reason}"] += 1 return r def on_event(self, g, ev): me = g.me() if ev.kind == "production.done" and ev.owner == me: self.timeline.append([round(ev.clock), ev.done_kind, ev.done_code]) elif ev.kind == "unit.died": (self.lost if ev.owner == me else self.killed)[ev.type] += 1 def on_end(self, g, reason): report = {"reason": reason, "timeline": self.timeline, "lost": self.lost, "killed": self.killed, "rejects": self.rejects.most_common(10)} try: # the game may already have exited; skip it if it can't be read report |= {"clock": g.clock(), "resources": g.resources(), "army": len(g.my_army()), "workers": len(g.my_workers())} except Exception: pass with open(f"run_{int(time.time())}.json", "w", encoding="utf-8") as f: json.dump(report, f, ensure_ascii=False, indent=1) ``` Questions this report can answer: | Signal | Where it comes from | What it tells you | |---|---|---| | Most frequent rejection reasons | Receipt `reason` / `verdict` | Constantly food blocked (3), ordering things you can't afford (8 / 9), attacking targets in the fog of war (1001), training a hero that already died (221) | | Production timeline | `production.done` events (with game seconds taken) | When the first hero came out, when you tiered up, whether the Barracks kept producing; compare with pro players' openings | | Casualties on both sides | `unit.died` events | Whether you keep feeding units, how many times the hero died, whether creeping paid off | | End reason | `on_end(g, reason)` | `我方没有单位了` ("we have no units left") = loss; `到时间了` ("time's up") = no winner yet | | Final army and resources | One snapshot read at `on_end` | Gold piling up unspent = production can't keep up; too few workers = the economy never took off | > **Note** > > Programmatic win/loss detection is one of the [Arena](https://war3ai.com/en/arena/)'s foundation experiments and is still on the roadmap. For now, you can treat `我方没有单位了` ("we have no units left") as a loss, and approximate a win as "every visible enemy building is destroyed". ## 3. A clear goal and a few constraints Give the agent the following, adjusted to your goal: ```text Goal: make brains/my_bot.py reliably beat the Easy computer on Echo Isles (Human vs random race). Each round: 1. Run python tools/play.py --bot brains/my_bot.py --speed 200 --minutes 10 --fair 2. Read the terminal output and the latest run_*.json: end reason, production timeline, most frequent rejection reasons, casualties on both sides 3. Find the ONE problem that affects the result most, and change only that; write the reason for the change and the data behind it in a code comment 4. Go back to step 1. If there's no improvement for 3 games in a row, stop and tell me the report and your assessment Constraints: - Only use methods in docs/api.json; don't invent APIs - Don't re-issue the same command to the same unit every tick; only order idle units - Keep --fair (only use enemies visible in vision) - Before changing code, run python tools/run_tests.py to make sure the examples aren't broken ``` ## Habits that help the loop converge faster - **Change one thing at a time.** If you change three things at once and win, you don't know which one helped; if you lose, you don't know which one broke it. - **Play enough games to compare.** The same matchup has a lot of randomness; two games only reveal very large differences. Judge "did it improve" by the trend over at least several games. - **Fix rejections before tuning strategy.** The most frequent rejection reason in the receipts is often the Bot's biggest bug. - **Write your reasoning into comments.** The next round's agent (or the next conversation) can read from the comments why the code is the way it is, and won't revert fixes. - **Offline tests as a safety net.** Write unit tests for key logic that don't need a running game (the example Bots' tests are in `brains/examples/tests/`), and have the agent run them after every change. --- # LLM as Strategy Coach > Hand "what to save for, where to put workers, attack or hold this minute" to an LLM, and let the rule layer only execute and veto. The reference brain already works this way; this page explains the pattern and the pitfalls. Once your Bot grows past a certain point, you'll notice the economy rules are stacked one on top of another: a rule for how many lumberjacks, a rule for 5 workers per mine, a rule to halve lumber when there's too much, a rule to send more to gold when gold is short and lumber is plentiful… Each rule is correct on its own, yet together they produce situations like "the mine is short of workers while every peasant is chopping trees" — situations **no single rule is responsible for**. Judgments like "look at the whole picture and set priorities" were never a good fit for `if / else`, but they're exactly what LLMs are good at. The reference brain (`brains/xwar3/strategy/brain/coach.py`) uses the layering below. ## Layers ```text LLM (advisor) Once every 20 game seconds, async, never blocks a tick Input: a one-page snapshot of the game (resources, food, worker distribution, mines, unit types, tech, heroes, enemy intel, recent events) Output: strict JSON — a one-line diagnosis + worker split + what to build first + this minute's posture + things to avoid │ ▼ whitelist + min/max clamping + veto Rule layer (Bot, every tick) Translates advice into "biases" on existing abilities: worker split, build / train priority, attack posture │ ▼ Execution layer (SDK / reflex layer) Issues orders, reads receipts, micro ``` ## Output contract Make the model output JSON with a fixed set of fields — no additions, no omissions: ```json { "diagnosis": "One sentence: the biggest problem in the game, which must be backed by the input data", "workers": { "gold": 10, "lumber": 6 }, "priority": ["hpea", "hhou", "hbar"], "posture": "creep", "avoid": ["Don't research Iron Plating first when lumber is short"] } ``` | Field | How the rule layer uses it | Reference brain's clamp | |---|---|---| | `workers` | Target number of workers on gold and on lumber | Gold 2 ~ 25, lumber 1 ~ 20; the sum can't exceed the total number of peasants | | `priority` | Priority order for training / building / research | At most 4; only four-character codes that appear in the "allowed codes" table are accepted | | `posture` | This minute's posture | Must be one of `attack` `defend` `creep` `expand` `recover` `hold` | | `avoid` | Things not to do this minute | At most 2 | | `diagnosis` | Only used for logs and the console display | — | Use one prompt per race, covering only that race's specific trade-offs (Human's cooperative building and Militia, Orc's Burrows, Undead's Haunted Gold Mine, Night Elf's Entangled Gold Mine…). Put the common rules in a shared section — don't copy them four times. ## Four hard constraints The reference brain learned each of these the hard way: 1. **The advisor never issues unit orders directly.** It can't see what's happening on a 150 ms timescale, and it hallucinates. It only changes targets and priorities; who goes where and who attacks what is still decided by the rule layer and the reflex layer — command authority can only have one owner. 2. **Async.** One advisor call takes about 1 second and runs on a background thread; the latest result wins, and it **never blocks a tick**. If the model isn't up, times out, or answers garbage, act as if this layer doesn't exist and fall back to pure rules. Advice that's too old (older than 3 intervals) isn't used either. 3. **Whitelist + clamping.** Every field must map to an existing ability, and numeric values are clamped to a sensible range. Anything unrecognized is **counted and then discarded**, not silently ignored. 4. **Count everything.** How many times you asked, how many succeeded, how many timed out, how many were clamped, how many times each field was adopted — publish all of it along with the last input sent to the model. Otherwise "is this layer actually doing anything?" is a question you can't answer. > **Things that degrade safely are the easiest to degrade silently** > > The advisor is designed so that "failure = act as if this layer doesn't exist", so when the model service isn't running, the Bot behaves exactly like pure rules and nothing looks wrong from the outside. The reference brain once went a whole day with the advisor unable to connect on all 6 instances, and nobody noticed. Always publish "last successful call time" and "last failure reason" — that's exactly what the "strategy coach" page in the [Farsight console](https://war3ai.com/en/docs/console/) is for. ## Implementing it in your own Bot Below is a minimal skeleton that works with any OpenAI-compatible API (LM Studio, Ollama, or a cloud API) and uses only the standard library: ```python title="coached_bot.py" import collections, json, threading, urllib.request from openwar3 import Bot BASE = "http://127.0.0.1:1234/v1" # LM Studio / Ollama / any OpenAI-compatible service MODEL = "your-model" POSTURES = {"attack", "defend", "creep", "expand", "recover", "hold"} SYSTEM = """You are a Warcraft III macro coach. You handle only economy and strategy, not micro. Output JSON only, with fixed fields: {"diagnosis": one sentence, "workers": {"gold": integer, "lumber": integer}, "priority": [four-character codes, at most 4, only from allowed], "posture": one of six, "avoid": [at most 2]} Base everything on the game data you are given; don't make up anything that isn't in the data.""" def ask(state: dict) -> dict: body = {"model": MODEL, "temperature": 0.3, "max_tokens": 260, "messages": [{"role": "system", "content": SYSTEM}, {"role": "user", "content": json.dumps(state, ensure_ascii=False)}]} req = urllib.request.Request(f"{BASE}/chat/completions", json.dumps(body).encode(), {"Content-Type": "application/json"}) with urllib.request.urlopen(req, timeout=8) as r: text = json.load(r)["choices"][0]["message"]["content"] return json.loads(text[text.index("{"): text.rindex("}") + 1]) class CoachedBot(Bot): EVERY = 20.0 # game seconds: macro decisions happen on a scale of minutes, no need to ask every tick allowed = {"hpea", "hfoo", "hrif", "hkni", "hhou", "hbar", "hbla", "Rhme", "Rhar"} def on_start(self, g): self.plan, self.plan_at, self.asked_at, self.busy = {}, -1e9, -1e9, False self.stats = collections.Counter() def summary(self, g) -> dict: # read the snapshot on the main thread; the background thread never touches g res = g.resources() or {} return {"clock": round(g.clock() or 0), "gold": res.get("gold"), "lumber": res.get("lumber"), "food": [res.get("food_used"), res.get("food_cap")], "workers": len(g.my_workers()), "idle_workers": len(g.idle_workers()), "army": collections.Counter(u.type for u in g.my_army()), "enemy_seen": collections.Counter(u.type for u, _t, _age in g.last_seen(max_age=90)), "night": g.is_night(), "allowed": sorted(self.allowed)} def consult(self, state, now): try: p = ask(state) self.stats["ok"] += 1 posture = p.get("posture") if posture not in POSTURES: self.stats["bad_posture"] += 1 # count, then discard — never silently posture = "hold" self.plan = {"gold": min(25, max(2, int(p["workers"]["gold"]))), # clamp "lumber": min(20, max(1, int(p["workers"]["lumber"]))), "priority": [c for c in p.get("priority", []) if c in self.allowed][:4], "posture": posture} self.plan_at = now except Exception as e: # timeout / garbage answer: act as if this layer doesn't exist self.stats[f"error:{type(e).__name__}"] += 1 finally: self.busy = False def on_tick(self, g): now = g.clock() or 0.0 if not self.busy and now - self.asked_at >= self.EVERY: self.busy, self.asked_at = True, now threading.Thread(target=self.consult, args=(self.summary(g), now), daemon=True).start() plan = self.plan if now - self.plan_at <= 3 * self.EVERY else {} # don't use advice that's too old # ↓ rule layer: with an empty plan, follow the default rules; with a plan, only adjust the split, priorities and posture — the actual orders are still decided by rules ... ``` ## Choosing a model | Situation | Recommendation | |---|---| | Local, needs to be fast | MoE models (which activate only a small fraction of parameters per call) are much faster than dense models of the same size. The reference brain uses Qwen3.6-35B-A3B (LM Studio, Q4): median **1.09 s**, slowest 1.45 s, and 5/5 outputs parse directly with `json.loads` | | Local "thinking" models | **You must turn off the thinking section**, otherwise all the tokens go to thinking and no JSON ever comes out. LM Studio ignores `/no_think`; the reference brain switched to `/v1/completions`, builds the ChatML itself, and pre-fills an empty `` followed by a `{` | | Cloud models | Latency is usually higher, but this layering is async by design; macro decisions are measured in minutes, so a few seconds of latency is fine | > **Note** > > The same model can also voice your units: see [Speech bubbles and local models](https://war3ai.com/en/docs/speech/). If you want the model to issue orders directly every tick (instead of acting as an advisor), wait for the [Arena](https://war3ai.com/en/arena/) JSON gateway. --- # LLM calls tools directly (MCP) > tools/war3_mcp.py is an MCP server. Hook it up to Claude Code, Claude Desktop or any MCP-capable client, and the LLM can read the game, issue commands, talk to the player on screen, ask the player with pop-up cards and look at screenshots — no code to write first. `tools/war3_mcp.py` is an **MCP server** (stdio). Claude Code, Claude Desktop, agent frameworks for local models — hook it up to any MCP-capable client, and the LLM can **directly** read the game, issue commands, talk to the player on the game screen, ask the player questions and look at screenshots, with no code to write first. Besides writing bots, coaching, and making units talk, this is one more way to connect: **the LLM itself is the one using the tools**. ## Hook it up ```bash claude mcp add war3 -- python \tools\war3_mcp.py --inst 9 # Claude Code; replace with your openwar3 folder ``` For other clients, write the config in this format: ```json {"mcpServers": {"war3": {"command": "python", "args": ["\\tools\\war3_mcp.py", "--inst", "9"]}}} ``` It connects to the game only on the first tool call, so the game can be started later; if the game is closed and restarted, the next call reconnects automatically. Add `--role` to limit what the LLM can do: | Role | Can use | |---|---| | `dev` (default) | All tools, including `war3_jass` | | `player --player N` | Can command only player N's units and sees only its vision (fair mode); no JASS | | `observer` | Read-only; can't draw on the screen or make units talk; the runtime rejects its commands outright | The `player` role has the same limits as on the [gateway](https://war3ai.com/en/docs/gateway/): no ending the game, changing the speed or pausing, no APIs that reveal other players' hands, and queries that take a player number can only query its own player. A few limits: a tool result is at most 200,000 characters, and anything beyond that is truncated with a hint on how to narrow the request; `war3_ask_player` waits at most 120 seconds; a screenshot's `scale` is between 0.1 and 1. ## Tools | Tool | What it does | |---|---| | `war3_overview` | One-page overview: time, resources, food, counts of each of our unit types, heroes (HP, mana, level, cooldowns), visible enemy unit types, production. **Call this first** | | `war3_units` | Unit list (`owner` is me / enemy / creep / all; filter with `types`); use `addr` to issue commands | | `war3_events` | What happened since the last call: deaths, level-ups, spell casts, production finished, chat, the player clicking a button… (the noisiest kinds are left out by default) | | `war3_call` | Call any public API (`move`, `attack_move`, `train`, `build`, `cast`, `learn`, `ui.button`, `canvas.text`…); write units as `{"unit": addr}` | | `war3_api` | Look up APIs: search names and descriptions by keyword | | `war3_toast` / `war3_say` | A line of text at the top of the screen / a line over a unit's head | | `war3_ask_player` | Shows the player a few choice cards in the middle of the screen, waits for a click, and returns which one was picked (can pause the game) | | `war3_screenshot` | Screenshot of the game (PNG; works even when the window is covered, and doesn't steal focus) | | `war3_jass` | Run a JASS snippet (dev only; changes to the world only in single-player games) | What you can build with it: - **Play buddy / coach**: `war3_overview` to read the game, `war3_toast` to give advice on screen; - **Ask the player mid-game**: `war3_ask_player` pops up three cards, and whichever one the player clicks is what happens; - **Commentary**: `war3_events` reads what happened, and `war3_say` lets the units say it themselves; - **Command a force directly**: `player` role + `war3_call`, which can only move its own units; - **Check the UI by looking**: `war3_screenshot` takes a shot to see whether the buttons you drew are laid out right. ## What a conversation looks like ```text You: Take a look at the game, then ask me on screen: expand, build army, or tier up next? → war3_overview {} ← One-page overview: game time, gold 500, food 10/12, ours htow 1 · hpea 5 · Hpal 1, no enemies seen, nothing in production → war3_ask_player {"question": "Next?", "options": ["Build army", "Expand", "Tier up"], "pause": true} ← {"picked": 1, "option": "Expand"} Model: You picked expand. First I'll use war3_units to find an idle peasant, then look for the nearest gold mine… ``` ## Measured 2026-09-25: - Our own MCP client against a real game, 7/7: handshake → list tools (10) → `war3_overview` (`htow` 1, `hpea` 5) → `war3_units` → `war3_toast` → `war3_screenshot` (PNG of about 200,000 bytes) → `war3_ask_player` (three cards, simulated click on the second → `{"picked": 1, "option": "Expand"}`). - Hooked up in Claude Code 2.1: it started the server and handshook on its own, status `connected`, and all 10 tools showed up in its tool list as `mcp__war3__*`. ## Implementation - Newline-delimited JSON-RPC 2.0 (`initialize` / `tools/list` / `tools/call` / `ping`), protocol version 2025-06-18, compatible with 2025-03-26 and 2024-11-05. - Tool errors go in the result per MCP convention (`isError: true`); the connection stays open. - Shares the same role whitelists, unit argument format and "one-page overview" with the [gateway](https://war3ai.com/en/docs/gateway/). - Logs go to stderr; stdout carries only the protocol. --- # Mental Model > Snapshots, commands, receipts, events, ticks and batches. Understand these six concepts and you'll understand why the API looks the way it does, and how to write fast code. ## Snapshots: reads with zero wait Every **50 ms**, the runtime samples the entire world on the game thread and writes it into shared memory. What `g.snapshot()` gives you is a **complete, self-consistent** world: - 16 player slots: gold, lumber, food, food cap, total resources harvested, race; - Up to 1024 units: type, owner, coordinates, HP / mana (with maximums), current order and order target, **what it is actually attacking** (task target), hero level / XP / skill points, and each player's visibility of it; - Up to 256 unit details: 12 abilities (level, cooldown remaining), 8 buffs, 6 inventory slots; - Ground items, trees (refreshed every 2 seconds), production table (training / research / construction / upgrade progress), game clock, in-game time of day. Reading one takes about **0.4 ms** (Python parsing), without waiting on the game thread. So: **read as much as you like**. APIs like `g.units()`, `g.my_army()`, `g.cooldown()` and `g.inventory()` all pull from the same snapshot, so calling them many times in one tick costs little. > **Tip** > > The publish period is adjustable: `g.set_publish_period(ms)`, 16 ~ 1000 ms. One sample takes about 0.5 ~ 0.9 ms on the game thread, so even 33 ms is fine. There's a single value shared machine-wide; the last write wins. ## Commands: writes, about one frame `g.move / attack / gather / build / train / cast …` are handed to the game thread for execution. The runtime executes client-submitted commands in batches inside the game thread's **event dispatch**, so a command waits about **one frame** (about 0.1 ms when it lands in an event cluster; otherwise it waits for the next dispatch). - Commands accept **a single unit or a list**; units in a list are all ordered in the same frame; - Adding `queue='after'` is a Shift-queue: do this after finishing the current task; - You can pass the unit objects you got from the snapshot; the SDK verifies identity by **handle pair** (addresses get reused by new units, handles don't). ## Receipts: every command has one ```python r = g.build(worker, "hbar", x, y) if r: # the engine accepted it ... else: r.reason # 'rejected(金不够)' (= not enough gold) r.verdict # 8 r.exec_us # microseconds this command took on the game thread ``` Receipts are read back **in the same frame**: the unit's order before and after the command, the engine function's return value, and the feasibility check's reason code. A receipt answers "did the engine accept this command, and if not, why", but it **does not answer** "did it eventually succeed" — for that, look at snapshots and events. For all status codes and reason codes, see [Receipts and reason codes](https://war3ai.com/en/docs/reason-codes/). ## Events: what happened `on_event(g, ev)` runs before each `on_tick` and hands you, one by one, every event since the last tick: | Event | Meaning | |---|---| | `unit.appeared` / `unit.died` / `unit.removed` | A unit appeared, died, or disappeared (entering a gold mine, being converted, or a corpse decaying also count as disappearing — not the same as dying) | | `unit.damaged` / `order.changed` / `owner.changed` | Lost HP, changed order, changed owner | | `hero.levelup` | A hero leveled up | | `item.appeared` / `item.removed` | A ground item appeared, was picked up, or was used | | `damage` | Engine-level: **every single hit**. Source unit, attack type, damage type, actual HP lost, pre-armor damage | | `killed` | Engine-level: this hit killed it, with the killer | | `production.done` | Training / research / construction / upgrade finished, with the four-character code and game seconds taken. Also emitted for opponents | | `spell.cast` | A unit cast a spell: the spell's four-character code, level, cooldown in seconds, cast point | | `message` | A line appeared in an on-screen message frame: game hints ("You need more farms"), chat (`.chat` has the sender and text), system messages | | `selection.changed` / `player.left` | The local player's selection changed / a player left or was removed after being defeated | | `game.started` / `game.ended` | A new game started / left the game | Input events such as canvas button clicks, hotkeys and ground clicks are covered in [UI & input](https://war3ai.com/en/docs/ui-input/). > **Warning** > > The event stream is **global**: opponents' production completions and creep deaths are all in it. Filter by `ev.owner` or unit handle. ## Ticks: the Bot's rhythm By default, `on_tick` is called 5 times per second (wall clock). A tick's cost is basically just your own computation: snapshots have zero wait, and commands take about one frame. If a tick runs past its period, the next one is pushed back automatically — they never pile up. - **Don't wait by wall clock at 2× game speed.** To wait 3 game seconds, watch for `g.clock()` to advance by 3 — don't `sleep(1.5)`. - **Don't `sleep` inside `on_tick`.** If you need to "do it in a bit", record the current game time and check again next tick. ## Batches: dozens of commands, one wait When a tick issues many commands, wrap them in `with g.batch():`: ```python with g.batch(): g.attack(melee, target_a) g.attack(ranged, target_b) g.move(wounded, home.x, home.y) g.cast(hero, "thunderclap") # the whole batch is submitted when the block ends: executed in the same frame, waiting on the game thread only once ``` - Commands inside the block return `Pending`, which becomes a receipt when the block ends; reading it before then raises an error; - If an exception is raised inside the block, **the whole batch is discarded** (half a set of commands is more dangerous than none); - Measured with 8 moves: 68 ~ 99 ms one at a time, **6.5 ~ 10 ms** as a batch. The same idea applies to queries: `g.can_do_many([(u, code), ...])` and `g.tech_many([...])` ask about many things at once. ## Reading what you just wrote Within the same tick, the snapshot doesn't yet reflect the commands you just issued (it catches up on the next publish). Two pieces of logic can end up fighting over the same worker: one just sent it to build a farm, while the other looks at the snapshot and thinks it's still idle. `g.order_of(u)` solves this: until the snapshot catches up, it uses the new order from the receipt. **To decide whether a unit is idle, use `g.order_of(u)`, not `u.order`.** `g.idle_workers()` already excludes workers that were given a job this tick. ## Latency tiers | Tier | Channel | Latency | Used for | |---|---|---|---| | 0 | Pushed snapshot + event stream | About 0.4 ms per read; fresh data every 50 ms | All "observe" APIs | | 1 | Fast lane | About 1 frame; median 0.06 ms with 6 processes in parallel | All commands and queries (SDK default) | | 2 | Control channel | 20 ~ 40 ms | Fallback, and a few UI-type operations (game speed, speech bubbles, messages) | | 3 | [Gateway](https://war3ai.com/en/docs/gateway/) (WebSocket / JSON) | Tier 1 + about 1 ms | Any language, browsers, LLMs, programs on another machine | Every API in the [API reference](https://war3ai.com/en/api/) is labeled with the tier it uses. --- # Fifteen rules > Every one was learned the hard way in real games. Check your bot against them and you'll skip most of your debugging. > **Tip** > > Give this page to an LLM together with [`api.json`](https://war3ai.com/en/api.json), and the bot it writes will avoid a lot of detours. ## Reading state ### 1. Missing data is `None`, not 0 `resources()`, `time_of_day()`, `production()` and `cooldown()` can all return `None` (while loading, when a unit has no details, when a building isn't producing…). Check before you use the value: ```python res = g.resources() if res is None: return ``` ### 2. Identify units by handle, not by address Addresses get reused by new units: an old address may point to a freshly spawned unit. To remember a unit across ticks, store `u.handle` and get it back with `g.unit(handle)`. ### 3. The event stream is global `production.done` and `unit.died` include events from your opponent and from creeps. Filter by `ev.owner` (or by the building handle): ```python if ev.kind == "production.done" and ev.owner == g.me(): ... ``` ### 4. Workers inside a gold mine aren't in the snapshot The moment a worker enters a gold mine, it disappears from the snapshot (`unit.removed`; it isn't dead). To count the workers on each mine, **keep your own tally** and don't prune it based on the snapshot, or you'll send extra workers to a full mine. ## Issuing commands ### 5. A receipt saying "accepted" ≠ done The engine accepts a build spot in a forest immediately too; it only fails once the worker gets there. Spells can be interrupted. Check the effects in snapshots and events: build with `build_near` (it tracks whether the foundation appears), and after casting, check whether `g.cooldown()` has started. ### 6. You can't attack what you can't see Targeted commands on enemies in the fog are rejected with reason code **1001**. To chase an enemy into the fog, `attack_move` to where it was last seen. ### 7. Only give orders to idle units Reissuing the same command to the same unit every tick interrupts it: soldiers twitch in place and peasants' gather cycle resets to zero. Check whether a unit is idle with `g.order_of(u)` (which includes what you issued this tick), not the snapshot's `u.order` (the snapshot hasn't caught up yet). ### 8. Shift can only insert right after the current order The engine has no "append to the end": sending B and then C with `queue='after'` gives you A, C, B. To walk a sequence of points, use `g.path(units, points)`; to have one worker build several structures in a row, use `g.build_queue(worker, plan)`. Both insert in reverse order and handle this for you. ### 9. Send each tick's commands as one batch Sending dozens of commands one by one means waiting on the game thread dozens of times; wrap them in `with g.batch():` and you wait only once. ## Economy and production ### 10. At most 5 workers per mine More doesn't increase income. Scale your worker target with the number of mines: 5 on gold per mine, plus a few on lumber. ### 11. Queue only 1 unit at a time Filling all 7 slots locks your money in the queue (in testing, a town hall queued 4 peasants, tying up 300 gold and slowing the opening considerably). Queue the next one when `g.queue(b)` is empty. ### 12. Check the production table for food blocks `g.production(b).blocked` means something is queued but hasn't started, usually because there isn't enough food. It's one step ahead of "build when food is nearly capped": when you lose a chunk of your army in a fight and the queue stalls while you rebuild, you'll know right away. ### 13. Heroes are unique, and you can't tier up while the town hall queue is busy - A dead hero can only come back through `g.revive(altar)`; training it again is rejected (221). Reviving also costs food (a hero takes 5). - You can't upgrade the town hall while its queue still has something in it (reason code 185, "building is busy"). ## Time and space ### 14. At 2× speed, don't wait on the wall clock To wait 3 game seconds, watch for `g.clock()` to advance by 3, not `sleep(1.5)`. At higher game speeds, the engine clock runs faster than the wall clock. ### 15. Don't use straight-line distance on island or forest maps Use `g.path_distance(a, b)` (ground A* that routes around forests, cliffs and buildings) to choose creep camps and expansions; it returns `None` when the target is unreachable. The spot that's nearest in a straight line may be across the sea. ## One more: write for fair mode Under `--fair`, you only see the units, items, production and events within your vision; that's exactly the Arena's rule. Write for fair mode now, and you won't need to change anything when you move to the [Arena](https://war3ai.com/en/arena/). See [Fair mode](https://war3ai.com/en/docs/fair-mode/). --- # Fair mode > A client injected into the game can read the whole map. Fair mode limits your bot to what's within its vision, just like a human player and just like the Arena rules. This project's observation capability comes from the fact that the client holds the state of every player: the snapshot contains every unit on the map, including enemies in the fog. That's handy for debugging, but unfair in a match. **Fair mode** makes the SDK filter everything by your vision: ```bash python tools/play.py --bot my_bot.py --fair python -m openwar3 run my_bot.py --inst 5 --fair ``` ```python from openwar3 import Game, run g = Game(inst=5, fair=True) # use Game directly run(MyBot, inst=5, fair=True) # or hand it to the runner ``` ## What gets filtered | Content | In fair mode | |---|---| | Units | All of yours + the enemy and neutral units you can see right now | | Items on the ground | Only those within your units' vision (day and night vision are computed separately from the data tables) | | Production table | Only buildings you can see (you can't see what your opponent is training) | | Events | Your own; visible ones (or ones that were visible within the last second); damage dealt by your side | ## Where vision comes from - Every unit in the snapshot carries a **visibility mask**: bit p = player p can see it right now (only players 0–11 with units on the field count; your own units are always visible to you). `u.visible_to(g.me())` reads it directly, with zero wait. - For any point, `g.visible(x, y)` asks the engine (visible / fog / black mask) through the fast lane, at about one frame per call. When a tick needs to check many units, use `u.visible_to()` from the snapshot instead of calling `g.visible()` one unit at a time. ## Remembering enemies: `last_seen` A human player remembers "I just saw a pack of Raiders over there." The SDK remembers for you too: every time the snapshot refreshes, it records the enemy and creep units you can currently see (last position, HP, time); it removes them once it sees them die, and clears everything between games. ```python for u, t, age in g.last_seen(max_age=60): # enemies seen within the last 60 game seconds print(u.type, u.x, u.y, f"{age:.0f}s ago") heroes = [r for r in g.last_seen() if r[0].is_hero] # where the enemy heroes were last seen camps = g.last_seen(owner="creep") # creeps you've seen ``` In fair mode, this is your only source of information about your opponent, just like for a human player. Normal mode also records by vision, so the same code works in both. ## Commanding as a specific player ```bash python tools/play.py --bot my_bot.py --player 1 --attach ``` `--player N` (or `Game(player=N)`) makes the bot command as player N, and it can only command player N's units. To have two AIs fight, open two such channels in the same game. > **In local mode, fairness is a convention, not a security boundary** > > On your own machine, nothing can stop a program from reading the whole map. `--fair` is a constraint you place on yourself; real matches are guaranteed by the [Arena](https://war3ai.com/en/arena/)'s referee process: bots never touch shared memory, only receive observations that the referee has filtered by vision, can only submit actions, and every action is checked for unit ownership first. ## Why turn it on now - The Arena will use exactly these rules. Write for fair mode now, and you won't have to change a line later; - Without full-map information, you find out how good your bot really is (the reference brain currently relies heavily on full-map information, such as the computer captain's target point, which makes this a good test); - Scouting, memory and judgment written under fair mode are the AI capabilities that actually matter. --- # Pro Playbook Cookbook > Most of a top player's edge comes from dozens of small habits. This page maps common pro techniques, one by one, onto SDK code — every snippet can be pasted straight into on_tick. Conventions: `g` is the `Game`, `home` is our main base (`g.my_buildings({"htow", "hkee", "hcas"})[0]`), `now = g.clock()`. For API details, see the [API reference](https://war3ai.com/en/api/); complete runnable examples are in [Example Bots](https://war3ai.com/en/docs/examples/). > **Tip** > > When you ask an LLM to add a technique, paste it the relevant recipe along with its code. That works far better than telling it to "play more like a pro". ## I. Economy ### 1. Workers never idle, 5 per mine ```python for w in g.idle_workers(): # only assign idle ones (re-ordering busy workers interrupts harvesting) mine = g.nearest([m for m in g.gold_mines() if crew[m.addr] < 5], w) g.gather(w, mine) if mine else g.gather(w, g.trees(w.x, w.y, limit=1)[0]) ``` Track how many you've sent to each mine yourself (`crew`): workers inside a gold mine aren't in the snapshot. Full example in `hello_bot.py`. ### 2. Queue only 1 at a time, don't lock up gold ```python for b in g.my_buildings({"hbar"}): if not g.queue(b): # only queue the next one when it's empty g.train(b, "hfoo") ``` ### 3. Never get food blocked ```python stuck = any(p.blocked for _b, p in g.all_production("me")) # queued but not started = not enough food res = g.resources() if stuck or res["food_cap"] - res["food_used"] <= 6: g.build_near(builder, "hhou", home.x, home.y) ``` `blocked` fires one step earlier than "almost full": when you lose a chunk of your army in a fight and the queue stalls as you rebuild, you know immediately. ### 4. Build order + return to the mine when done (Shift-queue back to mining) ```python spot = g.build_near(w, "hbar", home.x, home.y) if spot: g.gather(w, mine, queue="after") # go back to mining when done, no need to find it again next tick ``` One peasant building several in a row: `g.build_queue(w, [("hhou", x1, y1), ("hhou", x2, y2)])`. Gold is only deducted when construction starts. ### 5. Tier-up timing, attack/armor upgrades ```python if not g.queue(hall) and g.can_do(hall, "hkee") in (0, 220): # the hall can only upgrade with an empty queue (otherwise 185) g.upgrade(hall, "hkee") p = g.production(hall) # tier-up progress if p and p.kind == "upgrade": print(f"Town hall needs {p.remaining:.0f} more seconds") for sm in g.my_buildings({"hbla"}): if not g.queue(sm): ok = [u for u, v in zip(UPS, g.can_do_many([(sm, u) for u in UPS])) if v in (0, 220)] if ok: g.research(sm, ok[0]) ``` ### 6. Taking an expansion: pick the closest mine by walking distance ```python mines = [m for m in g.gold_mines() if g.dist(m, home) > 1500 and not taken(m)] best = min(mines, key=lambda m: g.path_distance(home, m) or 1e9) # mines on islands return None -> sorted last ``` ## II. Scouting and information ### 7. See what your opponent is doing ```python for b, p in g.all_production("enemy"): # what visible enemy buildings are training / researching / upgrading print(b.type, p.kind, p.queue, f"{p.progress:.0%}" if p.progress is not None else "stuck") ``` Combine with events: `ev.kind == "production.done" and ev.owner != g.me()` — what the opponent just finished. ### 8. Remember what you've seen (fog of war) ```python for u, t, age in g.last_seen(max_age=60): # enemies seen within the last 60 game seconds (last position and HP) ... hero_seen = [r for r in g.last_seen() if r[0].is_hero] # where the enemy hero was last seen ``` In fair mode this is your only source of information about the opponent, just like a human player. ### 9. Where the computer opponent is going to attack (computer AI only) ```python plan = g.enemy_ai_plan(some_enemy_soldier) # where its computer captain is heading ``` The computer picks its target point before it leaves home — bring your army back there ahead of time. ## III. Creeping ### 10. Creeping at night ```python if g.is_night(): # 18:00 ~ 6:00: creeps are asleep (you get the first hit without being surrounded), everyone's vision is shorter ... wait = g.seconds_until(18) # game seconds until nightfall (a day is 480 seconds) ``` ### 11. Only attack camps you can beat ```python from openwar3 import combat mine = [g.stats(u) for u in army] def ttk(target): return combat.time_to_kill(mine, g.stats(target), target_hp=target.hp) or 1e9 camp = [c for c in g.creeps() if g.dist(c, center) < 600] ours = max(ttk(c) for c in camp) # how long to clear this camp (rough estimate) theirs = g.time_to_kill(camp, weakest_of_mine) or 1e9 # how long for them to kill our weakest unit if ours < theirs and g.reachable(center, camp[0]): g.attack_move(army, camp[0].x, camp[0].y) ``` Full example: `_maybe_creep` in `micro_bot.py`. ## IV. Micro ### 12. Focus fire: attack what dies fastest, not what's closest ```python target = min(visible_enemies, key=lambda e: g.time_to_kill(fighters, e) or 1e9) g.attack([u for u in fighters if (g.current_target(u) or target).handle != target.handle], target) ``` Only order the units that aren't already attacking it (`current_target`), so you don't interrupt the ones that are. ### 13. Pull back wounded units ```python for u in army: if u.hp < u.hp_max * 0.35: g.move(u, *toward(home, u, 500)) # retreat 500 toward home; don't pull the same unit again within 3 seconds ``` Detecting focus fire: in `damage` events, the same unit taking hits from multiple sources in a short window = it's surrounded. ### 14. Keep heroes alive, don't feed XP ```python for h in g.my_heroes(): if h.hp < h.hp_max * 0.4: g.move(h, home.x, home.y) g.use_item(h, slot_of(h, "phea")) # healing potion: find the slot number with inventory(h) ``` ### 15. Counters: the right unit on the right target ```python s = g.stats(u) best = max(enemies, key=lambda e: s.dps_vs(g.stats(e))) # Riflemen on Gryphon Riders (pierce vs light armor ×2), Gryphon Riders on Footmen (magic vs heavy armor ×2) ``` The counter table comes from game data: `combat.damage_multiplier("pierce", "small") == 2.0`. ### 16. Flanks and pathing: route around towers ```python route = g.walk_path(army_center, target) # corner points of the shortest ground path g.path(army, route, attack=True) # attack-move through each point in order ``` To avoid towers, mark the area around each tower as unwalkable on the pathing grid before computing the route: ```python grid = g.grid().copy() for t in towers: grid.block_area(t.x, t.y, 800) # tower range 700 + margin route = grid.path((army_x, army_y), (target.x, target.y)) ``` ### 17. Send a tick's commands as one batch ```python with g.batch(): g.attack(melee, target_a) g.attack(ranged, target_b) g.move(wounded, *home_xy) g.cast(hero, "thunderclap") ``` Dozens of commands wait on the game thread only once (measured: 8 moves went from 68 ms to 6.5 ms). ### 18. Sieging: artillery attacks the ground ```python g.attack_ground(mortars, tower.x, tower.y) # Mortar Teams / Demolishers fire at an area (behind trees, invisible units) ``` ## V. Heroes ### 19. Skill build ```python SKILLS = {"Hamg": ["AHwe", "AHbz", "AHwe", "AHbz", "AHwe", "AHmt"]} # Water Elemental, Blizzard… level-6 ultimate Mass Teleport info = g.hero_info(h) if info and info["skill_points"]: g.learn(h, SKILLS[h.type][learned_count]) # if rejected (level too low for the ultimate), wait for the next level ``` ### 20. Did the spell actually go off? ```python r = g.cast(h, "thunderbolt", target=enemy_hero) # next tick: if g.cooldown(h, "AHtb"): # on cooldown = it really went off; accepted ≠ cast ... ``` ### 21. Buying potions, teleporting home ```python g.buy(shop, "phea") # the hero is standing next to the shop g.use_item(hero, slot, x=home.x, y=home.y) # Scroll of Town Portal (point-targeted item use) ``` ## VI. Post-game review - Log each tick's decisions (`print` goes to the run window), and use `g.say(unit, "Fall back")` to see them in-game; - `production.done` events include "how many seconds it took" — build your own timeline (when your first hero came out, when you tiered up) and compare it with top players'; - Let the agent review itself: see the game report in [Agent self-iteration](https://war3ai.com/en/docs/agent-loop/). --- # Example bots > Four examples, from simple to complex. Each one runs as-is, and every piece of logic maps to an SDK capability. Plus a complete reference brain. The examples live in `brains/examples/`. Each one builds on the previous one and only adds what's new. Read them in order: | Example | What it teaches | Run it | |---|---|---| | `hello_bot.py` | Gathering (5 per mine; when a mine is full, chop lumber), training workers (only 1 in the queue), building food structures, resuming stalled foundations; works for all four races | `python tools/play.py --bot brains/examples/hello_bot.py` | | `rush_bot.py` | Barracks and altar (built with `build_near` if missing), hero first (revived when it dies), learning skills whenever points are available, gathering a wave and attack-moving | `… --bot brains/examples/rush_bot.py` | | `macro_bot.py` | Build order + returning to the mine after building (Shift), adding food the moment production is blocked, 1 unit in the barracks queue, attack/armor upgrades, tiering up and advanced units, picking targets by **actual ground distance** and following the path | `… --bot brains/examples/macro_bot.py --speed 200` | | `micro_bot.py` | Takes over combat on top of the macro: focus fire on whatever dies fastest, pull back wounded units, keep the hero alive, pick creep camps it can win at night, defend when enemies reach the base; sends each tick's commands as one batch | `… --bot brains/examples/micro_bot.py --fair` | > **Note** > > The comments in `hello_bot` and `rush_bot` record pitfalls hit in live games, such as "it always picked the first worker to build, so all 3 farms ended up as half-built foundations" and "the hardcoded barracks coordinates were right in a forest, and not a single barracks got built in 3 minutes." The comments teach more than the code. ## hello_bot: economy ```python # race -> (worker, town halls, food building) RACES = { "h": ("hpea", {"htow", "hkee", "hcas"}, "hhou"), "o": ("opeo", {"ogre", "ostr", "ofrt"}, "otrb"), "u": ("uaco", {"unpl", "unp1", "unp2"}, "uzig"), "e": ("ewsp", {"etol", "etoa", "etoe"}, "emow"), } MINE_CAP = 5 # at most 5 peasants per mine (more doesn't raise income) LUMBER_CREW = 5 # lumber crew size: 5 on gold per mine + this many on lumber = worker target ``` Three things: idle workers go mine gold (it keeps its own count of workers per mine; when a mine is full, they chop lumber); train workers when short (only 1 in the queue); when food is nearly capped, find a worker that isn't already building and put up a food building next to the town hall (for Human and Orc, it also sends workers to resume stalled foundations). ## rush_bot: army and attacks Adds three things on top of `hello_bot`: build a barracks and an altar if missing; train a hero at the altar (**revive it first if it's dead**, since heroes are unique) and learn skills whenever points are available; once 8 soldiers are gathered, attack-move them all to the enemy town hall, and come home to regroup when they're badly beaten. Only idle soldiers get orders, so fights aren't interrupted tick after tick. ## macro_bot: macro fundamentals ```python TECH = { "h": dict(order=["halt", "hbar", "hbla", "hlum"], altar="halt", hero="Hamg", skills=["AHwe", "AHbz", "AHab"], barracks="hbar", soldiers=["hfoo", "hrif", "hkni"], smith="hbla", upgrades=["Rhme", "Rhar", "Rhra", "Rhla"], tiers=["hkee", "hcas"]), ... } ``` The things pro players do every game, each mapped to an SDK capability: a build-order table + `gather(..., queue="after")` to return to the mine after building; `production().blocked` to spot food-blocked production; `g.queue` to keep only 1 unit in the barracks queue; `can_do` to ask the engine whether the next attack/armor level can be researched; tiering up and advanced units (a lesson from a live game: it stayed at tier 1 the whole time and got steamrolled at 23 minutes by tier-3 knights and gryphons); picking targets by `path_distance` and following waypoints with `path()`. ## micro_bot: once the fighting starts ```python def _fight(self, g, army, foes, home, now): ... visible = [e for e in foes if e.visible_to(me)] # targets you can't see get rejected (1001) atk = [s for s in (g.stats(u) for u in fighters) if s] target = min(visible, key=lambda e: _ttk(g, atk, e)) # the one that dies fastest, not the nearest idle_or_other = [u for u in fighters if g.current_target(u) is None or g.current_target(u).handle != target.handle] if idle_or_other: g.attack(idle_or_other, target) ``` In a live game: 1497 ticks, 3023 commands and 0 errors over 5 minutes. ## Reference brain: a complete AI `brains/xwar3/` is a complete AI that expands, creeps and attacks, organized in three layers: | Layer | Location | Cadence | What it does | |---|---|---|---| | Strategy layer | `strategy/` | Seconds | AMAI-style selection and switching among multiple strategies, build tables, counter units, hero picks; optional [LLM strategy coach](https://war3ai.com/en/docs/llm-coach/) | | Reflex layer | `reflex/` (4 separate processes) | ~100 ms | Staying alive, casting, focus fire, picking up items | | Win-rate model | `worldmodel/` | — | Can we win this fight (inference subset) | Multiple processes share units through a **claim table**, and priority decides who's in charge: manual 95 > survival 90 > dodging spells 85 > casting 80 > item pickup 70 > … > strategy 50 > worker assignment 45. Your own bot appears in the table as `bot`, with a default priority of 50. > **Warning** > > The reference brain uses the SDK's low-level layer directly (`w3cmd` / `act`) and relies heavily on full-map information. Treat it as a source of ideas; don't have an LLM copy it directly. It needs AMAI data: the first time `start.bat` deploys, it pulls it from AMAI's public repository and generates it (AMAI has a custom license, so the generated files are not committed to git; if that fails, retry with `start.bat setup`). The easiest way to start the reference brain is the [Farsight console](https://war3ai.com/en/docs/console/): on the **Instances & Setup** page, check the instance numbers and click **Start test**. --- # Debugging and performance > Why a tick is slow, why a command didn't take effect, why the game isn't moving. Troubleshoot by symptom, then confirm with the bundled live verification scripts. ## Read the receipt Every command's receipt is your first-hand clue: ```python r = g.cast(hero, "blizzard", x=tx, y=ty) if not r: print(r.reason, r.verdict) # rejected(…) and the reason code print(r.exec_us, r.engine_us) # microseconds this command took on the game thread / how much of that the engine's order function itself took ``` Normally a command takes anywhere from a few to a few hundred microseconds on the game thread. After a batch block ends, `g.last_receipts` holds the receipt for each command in that batch. ## Watch it in the game ```python g.say(unit, "Fall back") # a chat bubble pops up over the unit (doesn't affect the game) g.message("Going creeping") # prints a line in the bottom-left message area (visible only on this machine) ``` Anything you `print` shows up in the terminal running the bot. Printing the key decisions of each tick, together with speech bubbles, is much faster than reading code. ## A tick is slow First check whether it's one of these: | Cause | Fix | |---|---| | Sending commands one at a time, each waiting a frame | Wrap them in `with g.batch():`, so dozens of commands wait only once | | Calling `g.visible()` / `g.can_do()` one by one (each goes through the fast lane and waits a frame) | For visibility, use `u.visible_to()` from the snapshot; for feasibility, ask in one batch with `g.can_do_many([...])` | | Calling `sleep` or waiting inside `on_tick` | Note the game time and check again on a later tick | | Recomputing something expensive every tick (pathfinding, full-map scans) | Cache the result and recompute every few ticks. `g.grid()` has a built-in 2-second cache, and `g.stats()` caches tech levels for 5 seconds | ## The game isn't moving / the bot never gets into the game | Symptom | Most likely cause | |---|---| | Stuck on "waiting for game" | Wrong instance number; or the game window is **minimized**, and the game simulation stops while it's minimized (the clock doesn't advance) | | The game is running, but the bot's commands do nothing | Commanding someone else's units (receipt `not_owner`); or the bot connected as an observer (`forbidden`) | | Commands come back `held` | The unit is held by a higher-priority layer (the reference brain's reflex layer, or a manual command from the console), so the command wasn't sent | | Commands still go through while paused | That's normal: when paused, the engine clock stops, but event dispatch keeps running and commands execute as usual | ## Connect and check status ```bash python -m openwar3 status --inst 5 ``` This prints the connection status: game PID, world publish interval and per-capture time, fast lane counters, whether a game is in progress, unit count, and the game clock. ## Live verification scripts Start a test instance and check, item by item, that the SDK's capabilities work on your machine: ```bash python tools/sdk_live_check.py --inst 20 # everything python tools/sdk_live_check.py --inst 20 --only prod # check just one section ``` Sections: batching, time, production, queued commands, combat stats, pathfinding, fair mode. Each section issues commands in a real game, reads back the effects, and prints the number of passes. Offline tests don't need the game running: ```bash python tools/run_tests.py ``` ## Common "looks like a bug" cases - **The build receipt says accepted, but no foundation ever appears**: the engine accepts a spot in a forest immediately too; it only fails when the worker gets there. Use `build_near`, which tracks the attempt and blacklists failed spots for a while. - **The spell receipt says accepted, but the spell never went off**: it was interrupted, or the caster was out of mana. On the tick after casting, check whether `g.cooldown()` has started. - **The attack order was accepted, but the soldiers attack something else**: to attack a specific target, use `g.attack(soldier, enemy)` (right-click semantics). The raw attack order on a target only changes the order without recording the target, so the units go after something else nearby. - **The worker count doesn't add up**: workers inside a gold mine aren't in the snapshot. - **A dead hero can't be trained**: heroes are unique, so use `g.revive(altar)`. Reviving costs food, and a hero can only be revived about 3 game seconds after it dies. --- # RPG companion > Give the player an AI companion in RPG and custom maps that follows you, helps you fight, heals you when you're low, and chats with you. Four modes; subclass one class and change a few attributes to make it your own. It's not just for melee games. In RPG and custom maps, you can give yourself an **AI companion**: it follows you around, helps you fight creeps, heals you when you're low on health, and chats with you when nothing's happening — its lines can even come from a local LLM. **How you use it is up to you.** It's split into three layers of APIs, from the bottom up, and you can use any of them directly: | Layer | What it is | Good for | |---|---|---| | **JASS channel** `g.jass` | The 1291 JASS functions available to map makers, called directly by name (create units, set alliances, give items, rename, show text, revive heroes…) | Building your own gameplay | | **Convenience APIs** | `g.spawn`, `g.set_alliance`, `g.player_slots`, `g.show_text`, `g.map_data`: the common tasks, already wrapped | Writing your own helper scripts | | **Companion framework** | `openwar3.companion.Companion` + `openwar3.talk.Talk`: subclass it, change a few attributes, and you have a companion that follows you, fights alongside you, heals and chats | Just wanting a companion | > **Warning** > > Only for **single-player and self-hosted LAN** games. Creating units or setting alliances means your machine changes the world unilaterally: that's fine in a single-player game (against the computer), but in a multiplayer game it would desync the other players. So in multiplayer games the JASS channel only allows read-only functions, and the companion automatically falls back to "talk only". ## Fastest start: one click in Farsight 1. **Pick a map**: in Farsight, go to the "Instances & Setup" page → "Next game" → Map, and choose an RPG map (everything under `Scenario` and `Download` in the game folder's `Maps` is listed, e.g. `(4)WarChasers`). 2. **Pick a scheme**: in the instance card's "AI scheme" dropdown, choose **Companion example (buddy)** → "Select". 3. **Start test**: once the game is up, **play it yourself in the game window**. The companion — a paladin named "Sunny" — appears right next to you. You can also use the command line: ```bash python tools/play.py --bot brains/examples/buddy.py --inst 20 --rpg --map "\Maps\Scenario\(4)WarChasers.w3m" ``` `--rpg` (`"judge": false` in the scheme manifest) means the game isn't judged by melee rules: in RPGs heroes can revive, and there's no "you lose when all your buildings are gone". Many RPG maps stop at "Press any key to continue" after loading; when the SDK notices it's "in a game, but the game clock is stuck at 0", it presses Space by itself (`g.press_to_continue()`, which only sends a key message to the game window and doesn't steal focus). ## Write your own companion ```python from openwar3.companion import Companion from openwar3.talk import Talk class MyBuddy(Companion): mode = "ally" # mode, see the table below unit = "Hpal" # what to create: any four-character code, including the map's custom ones nickname = "Sunny" heal = ("holybolt", "AHhb", 0.55) # (cast order string, ability to learn, heal when the master's HP drops below this); None = no healing follow_distance = 350 talk = Talk(persona="A cheerful young paladin who loves cheering on its master") ``` ### Four modes | mode | Who the companion is | Notes | |---|---|---| | `ally` (default) | Takes an empty player slot and becomes your **ally** | Has its own color and name (the scoreboard and allies panel show `nickname`); you can't command it, it fights on its own. The framework sets up the alliance + shared vision automatically | | `own` | Created **under your control** | You can command it manually at any time; when you leave it alone, the AI controls it for you | | `adopt` | Takes over a unit **already on the map** | Override `adopt(g)` to return that unit (a pet or follower the map gives you) | | `voice` | Creates no unit, **only talks** | Chat and reminders; doesn't change the world, so it works in multiplayer games too | When there's no empty slot, `ally` automatically falls back to `own`; in multiplayer games, or when the unit can't be created, it falls back to `voice`. > **Note** > > An `ally` companion goes by "the best unit in that slot right now" (heroes first) instead of sticking to one specific unit. In testing, one map treated the companion as a real player, removed the paladin and handed out a map hero — the companion simply took over that hero and learned the abilities the map defined for it. When the hero dies, it prefers to revive it on the spot; if the map revives it by itself, it just keeps using it. ### What it does each tick It checks these in order and does the first one that applies: | Order | Behavior | Condition | Tunable | |---|---|---|---| | 1 | Retreat | Its own HP is below 25% and enemies are nearby: fall back behind the master | `retreat_at` | | 2 | Heal | The master's HP is below the threshold, the ability is off cooldown, and the master is within 900 range | `heal` (None turns it off) | | 3 | Assist | Enemies near the master: **attacking the master > the master's target > the nearest one** | `assist_radius`, or override `pick_target` | | 4 | Follow | Catch up when too far from the master; when far enough away, run straight back instead of staying in the fight | `follow_distance`, `leash` | | 5 | Idle chat | When there are no enemies, say a line every 1–2.5 minutes | The line table | "Enemies" are determined by the in-game alliance settings (refreshed every 20 seconds). RPG maps often have several allied players, so you can't simply treat "every player except me" as an enemy. Hooks you can override: `find_master` (who the master is; by default the local player's highest-level hero), `adopt`, `pick_target`, `on_poke` (the master right-clicked the companion), and the Bot's `on_start` / `on_tick` / `on_event` / `on_end`. The number of heals, assists, kills, follows, retreats, lines spoken and revives is recorded in `self.stats` and printed at the end. ### How to call it - **Chat commands**: type `-follow` (follow me), `-stay` (hold position here), `-heal` (heal me now) or `-hi` (say hello) in the chat box. To change the command table, edit `commands`; to change the reactions, override `on_command`. - **Right-click the companion**: triggers `on_poke`. In the example, the reaction is: if the master isn't at full health, give them a heal; otherwise say something. - **Portrait dialogue**: greetings, the master falling, the master leveling up, and the companion coming back are spoken through the game's own portrait dialogue (the portrait at the bottom switches to the companion and a subtitle appears on screen); everything else pops up as a speech bubble. - **Status panel**: a panel on the left side of the screen showing the companion's health bar, what it's doing, its mood (happy / excited / nervous / scared / sad), and its kill and heal counts. It's drawn with the [canvas](https://war3ai.com/en/docs/canvas/), so it's safe in multiplayer games too. ### Talking, and local LLMs `Talk` picks lines by event and shows them as speech bubbles; in `voice` mode, or when a bubble can't be shown, they appear in the bottom-left corner of the screen. Every line is also written to the scheme log, so you can check afterwards what it said. | Event | When | Event | When | |---|---|---|---| | `hello` | Just arrived | `master_low` | Master is low on HP | | `poke` | Master right-clicks it | `master_levelup` | Master levels up | | `fight` | A fight starts | `master_died` / `master_back` | Master falls / revives | | `kill` | Kills a creep (says the creep's name) | `buddy_low` / `buddy_died` / `buddy_back` | Companion itself is low / falls / comes back | | `healed` | Healed the master | `idle` / `item` | Idle chat / picked up an item | Lines can use placeholders such as `{master}`, `{me}`, `{map}`, `{enemy}`, `{level}` and `{item}`; to change the lines, edit `talk.lines` directly; the cooldown is in `talk.cooldown`. **Connecting a local LLM**: `Talk(llm=LocalLLM(url, model))` works with any OpenAI-compatible API (LM Studio, Ollama…). The model answers in a background thread, and the line is spoken only once the answer comes back; if the model isn't running, times out or errors, a fixed line is used instead, so the game never stalls. Requests go only to the local address you provide, and contain what happened in the game (the master's name, which creeps were killed). ## Unit names in custom maps Most units, items and heroes in RPG maps are created by the map itself (four-character codes like `HC07`, `I00A`), so they aren't in the built-in name table. `g.map_data` reads the current game's map file directly: ```python md = g.map_data md.name_of("HC07") # 'Optimus Primo' — names changed by the map take priority md.hero_names("HC07") # list of proper names md.hero_skills("OC10") # abilities the map defines for this hero md.tooltip("I00A") # tooltip text ``` Protected and optimized maps (many popular RPGs) don't include the standard object data files, so names are read from the map's text data instead. In testing, all 38 RPG / custom maps on this machine were parsed successfully, and unit names were found in 37 of them. ## Share it as a scheme A companion is just an `openwar3.Bot` subclass, so you can package it as an [AI scheme](https://war3ai.com/en/docs/schemes/) and share it with others. Add two more fields to the manifest: ```json {"id": "my-buddy", "name": "My companion", "entry": "my_buddy.py", "fair": false, "judge": false} ``` `"fair": false`: needed to use the JASS channel (create units, set alliances); `"judge": false`: don't judge wins and losses by melee rules. ## Test log 2026-09-24, test instance, WarChasers map, 2× speed: - All 18 JASS channel checks passed: player slots, round-trip conversion between units and handles, real return values, string arguments, creating a unit in an empty slot, setting alliances, renaming, removing units; calls from a player lane and wrong argument counts were both correctly rejected. - Companion: got past "Press any key to continue" by itself → appeared next to the master and said hello → followed into the circle of power for picking heroes, was given a hero by the map and took it over → followed (200–400 from the master) → fought creeps and said "Nice!" after a kill → retreated when low on HP → died, was revived by the map, and kept following. ## Not done yet 1. **It can't read arbitrary chat text the player types.** Fixed chat commands already work; for the companion to really chat freely with you, we still need access to the text itself. 2. **The companion doesn't understand a specific map's gameplay** (quests, shops, story). What it does is generic: following, assisting, healing. To make it understand a particular map, write that into your subclass for that map — `g.map_data` can look up names, and `g.jass` can call any function. That's exactly the part left for you to decide. --- # Canvas > Draw text boxes, panels, progress bars, images, circles that hug the ground and routes with arrows on the game screen. The runtime draws them itself every frame without changing game state, so it's safe in multiplayer games; use it from Python, over HTTP, or by writing shared memory directly. External programs can draw **text boxes, panels, progress bars, images, circles on the ground and routes on the ground (with arrows)** on the game screen, and the runtime draws them itself every frame. It's a good fit for your own HUD, guide lines, hints, teaching annotations and stream info boards. ## Canvas or JASS visual functions? | | Canvas (this page) | [JASS visual functions](https://war3ai.com/en/docs/jass/) | |---|---|---| | Who draws it | The runtime draws it itself | The game itself (floating text, effects, boards, portrait dialogue…) | | Multiplayer | **Safe**: drawn only on your local screen; creates no game objects and doesn't change game state | Single-player only | | Style | Anything you like: Chinese fonts, rounded corners, translucency, borders, any color, local images | The game's native look | | Following things | Follows units, world coordinates or screen positions; ground circles follow the terrain's rises and dips | Depends on the function | | Cost | Measured at 0.2–0.35 ms per frame (9 elements) | About 13 ms per call | You can use both together: JASS for effects in the game's native style, the canvas for custom panels, guide lines and hints. ## Python ```python c = g.canvas # on first use the runtime installs its draw hook (~0.1 s) c.text("title", "Hello, this is the canvas", screen=(40, 110), color=(255, 220, 80), bg=(0, 0, 0, 170), border="#C49C40", size=22, bold=True) c.panel("status", "Companion · Sunny", ["Mood: happy", "Kills: 12"], screen=(16, 330)) c.bar("hp", 0.62, unit=hero, lift=260, width=100, height=12, color=(80, 220, 80), text="62%") # follows the unit c.text("tag", "Boss ultimate incoming!", unit=boss, lift=320, color=(255, 80, 80), size=22, bold=True) c.circle("danger", (x, y), 300, color=(255, 60, 60), fill=(255, 60, 60, 60), width=3) # danger zone on the ground c.circle("aura", hero, 450, color=(80, 200, 255, 220)) # circle that follows a unit c.path("route", [(x1, y1), (x2, y2), (x3, y3)], color=(255, 220, 0), width=5, arrow=True) c.image("icon", "icon.png", screen=(40, 170), width=64, height=64) c.remove("danger"); c.hide("tag"); c.clear() # clear only removes what you drew c.expire("tag", 5) # disappears by itself after 5 seconds with c.batch(): ... # change many entries at once, with a single shared-memory write c.stats() # drawnFrames going up = it's really drawing ``` Each element is identified by a `key`: drawing the same key again updates it. **Clickable**: add `clickable=True` to a text box or panel (set the hover color with `hover=`). When it's clicked, a `ui.click` arrives in the event stream with `ev.key` set to that key, and the game never receives that click. For ready-made buttons, choice cards, hotkeys and ground clicks, see [UI & input](https://war3ai.com/en/docs/ui-input/). **Position** (give each element one of these): - `screen=(x, y)`: screen pixels; negative values count back from the right / bottom edge; `center=True` aligns by the center; - `frac=(0.5, 0.1)`: fraction of the screen; - `world=(x, y)`: world coordinates; - `unit=u`: follows a unit. Text and progress bars placed in the world or on a unit are anchored by the midpoint of their bottom edge; `lift` raises them. By default, elements in the world and on units stay clear of the HUD at the bottom and the day/night clock at the top (`over_ui=True` draws over them). **Colors** can be written as `(r, g, b)`, `(r, g, b, a)`, `"#RRGGBB"` or `"#RRGGBBAA"`. | Method | What it draws | Common parameters | |---|---|---| | `text(key, text, ...)` | Text box; use `\n` for multiple lines | `color`, `bg` background (transparent if omitted), `border`, `size`, `bold`, `shadow`, `width` (wraps at this width), `radius` for rounded corners | | `panel(key, title, [lines...], ...)` | Panel (dark translucent background, gold border) | Same as `text` | | `bar(key, 0..1, ...)` | Progress bar: health, cooldown, cast bar | `width`, `height`, `color`, `bg`, `border`, `text` | | `image(key, path, ...)` | Local image (png / jpg / bmp / gif) | `width`, `height` (original size if omitted) | | `circle(key, unit_or_point, radius, ...)` | Circle on the ground, hugging the terrain | `color` line color, `fill` fill (with alpha), `width` line width | | `path(key, [points...], ...)` | Polyline on the ground | `color`, `width`, `arrow` for an arrowhead at the end; points can be coordinates or units | ## HTTP (any language) Farsight backend (listens only on the local machine): ```http POST /api/instances/20/canvas {"set": [ {"key": "banner", "kind": "text", "text": "Canvas from HTTP", "frac": [0.5, 0.12], "center": true, "color": "#FFDC50", "bg": [0, 0, 0, 180]}, {"key": "hp", "kind": "bar", "value": 0.8, "unit": 596125988, "lift": 260, "text": "80%"}, {"key": "zone", "kind": "circle", "center": 596125988, "radius": 600, "color": [255, 200, 0], "width": 4}, {"key": "route", "kind": "path", "points": [[-4587, -9092], [-5387, -8792]], "color": "#50C8FF"} ], "remove": ["old"], "clear": false} GET /api/instances/20/canvas which elements are drawn right now + how many frames have been drawn ``` `kind` is the Python method name, and the parameter names are the same too; for a unit, pass its `addr` from the snapshot. ## Writing shared memory directly You can also skip Python and Farsight entirely: first send the semantic command `canvas_enable` once (W3P opcode 73), and the runtime creates the shared memory block `Local\War3Canvas_`: a 64-byte header + 256 entries × 112 bytes + a 64 KB text / point pool. Write it with a seqlock (sequence number goes odd → write the entries and the pool → sequence number goes even). The runtime reads it once per frame; if it reads a half-written update it keeps the previous frame, and it writes back the number of frames drawn, the element count and the fault count. The Python reference implementation is `sdk/python/w3canvas.py`, and the struct definitions are in the protocol header file; see [W3P protocol](https://war3ai.com/en/docs/protocol/). ## Several programs drawing at once Mods, Farsight, MCP and the gateway may all draw into the same game at the same time, and there's only one canvas. The rule is: **each program touches only its own elements**. - Before writing, take a named lock, read the existing elements, keep everyone else's, swap in your own, then write it back; - Each element records who drew it (process ID + an in-process sequence number). When the program that drew it exits, the element is cleaned up the next time anyone writes, and its buttons stop intercepting clicks; - Element IDs are allocated from a shared counter, so they never collide. The Python SDK already does this, and `clear()` only clears its own elements. If you write the shared memory directly, follow the same rules, or you'll wipe out other programs' elements. For layout details, see [W3P protocol](https://war3ai.com/en/docs/protocol/). ## Measurements and caveats - Measured on 2026-09-25 (1920×1080, 2× speed): 9 elements take 0.27–0.34 ms per frame at about 63 frames per second, with 0 faults; writing 9 entries takes 6 ms; while the hero moves, the circles, text and health bars that follow units keep up. Textures are redrawn only when the content changes; moving an element doesn't trigger a redraw. - It's drawn after the game UI and before the mouse cursor: it covers the game's own health bars, units and UI, and the cursor is drawn over it. It stays clear of the HUD at the bottom and the day/night clock at the top, but **does not avoid the map's own panels** (the leaderboard and countdown in the top-right corner) — don't put your own panels in the top-right. - Outside a game (main menu, score screen), elements placed at world coordinates or on units aren't drawn; elements at screen positions are drawn as usual. - A ground circle is made by projecting each of 64 points on its circumference onto the ground, so on uneven terrain its shape rises and dips with the ground — that's correct: it's drawn on the real ground. - The first time it's enabled, it installs the hook and warms up the fonts, which takes about 1 second; during that time text elements aren't drawn yet, but circles and lines are. - If a single exception occurs while drawing, nothing more is drawn for the rest of the session (the same protection as speech bubbles), and `faults` in `stats()` becomes 1. - Text, image paths and points share 64 KB in total, with at most 256 elements; image paths must be local paths the game process can read. The [AI companion](https://war3ai.com/en/docs/companion/)'s status panel is drawn with the canvas: health bar, what it's doing, mood, and kill and heal counts. --- # UI & input > Buttons and choice cards on the canvas are clickable and highlight automatically on hover; register hotkeys, pick a position by clicking the ground, read what the mouse is pointing at, and know who the local player has selected. Clicks, hotkeys, spell casts, full chat text and players leaving all go into the event stream. What the [canvas](https://war3ai.com/en/docs/canvas/) draws is now **clickable**. The runtime takes over the game window's input, and external programs can use: | Capability | In one sentence | Does the game receive it? | |---|---|---| | **Clickable canvas items** | Buttons, choice cards, panels: a click sends `ui.click`, and hovering highlights automatically | The click that lands on the button is **not received** | | **Hotkeys** | Register combinations such as `F5` or `ctrl+shift+Q`; a press sends `hotkey` | Can optionally be swallowed (along with the character it produces) | | **Ground clicks** | A click in the world sends `mouse.world` with ground coordinates | Can optionally be swallowed ("click a spot to place a tower") | | **Mouse position** | Updated every frame: screen pixels, the ground point under the cursor, the hovered canvas item | — | | **Selection** | Who the local player has selected; any change sends `selection.changed` | — | All of this is **local input + local drawing**: nothing goes into the command stream, so it's safe in multiplayer. But if your callbacks change the world (spawning units, changing stats), that part is still single-player only. ## Python: g.ui ```python ui = g.ui # on first use the runtime takes over window input ui.button("shop", "Buy a potion (50 gold)", screen=(40, 300), on_click=lambda g, ev: buy(g)) c = ui.choice("Level up! Pick a reward", [("Strength +5", "Tougher"), ("Attack speed +20%", "Hits faster"), ("Summon wolf", "One more helper")], pause=True, on_pick=lambda g, i: give(g, i)) # a row of cards mid-screen; pause=True pauses the game while choosing i = c.wait(timeout=30) # or block and wait (events keep pumping meanwhile, none are lost) ui.hotkey("F5", lambda g, ev: g.say(hero, "On it!")) # swallowed by default ui.hotkey("ctrl+shift+Q", on_press=..., swallow=False) ui.mouse(on_click, capture=True, buttons=("left", "right")) # capture ground clicks: report both left and right, and swallow them xy = ui.pick_point("Where should the tower go?") # blocking: next left click on the ground -> (x, y); Esc or timeout -> None ui.cursor() # {'screen': (x, y), 'world': (x, y, z) or None, 'hover': 'shop'} ui.toast("Wave 3 incoming!", seconds=3) ui.close() # remove your own widgets and hotkeys; window input is handed back only when no other program is using input g.close() # or disconnect entirely (you can also write with Game(...) as g:) ``` Callbacks take `(g, ev)` and fire when you call `g.events()` — the runners for bots and [gameplay mods](https://war3ai.com/en/docs/mods/) call it every tick. Clicks without a callback go into `ui.clicks`. An exception raised in a callback is only logged; it doesn't affect other callbacks or events. You can also use the canvas layer directly: `g.canvas.text(..., clickable=True, hover=color)`, then pick up clicks from the event stream, where `ev.key` is the key you gave when drawing. Drawing a clickable element turns input on automatically, so you don't need to touch `g.ui` first. **Hotkey syntax**: `F1`–`F24`, `A`–`Z`, `0`–`9`, `numpad0`–`numpad9`, `space enter esc tab backspace insert delete home end pageup pagedown left up right down`, optionally prefixed with `ctrl+`, `shift+` or `alt+`. > **Warning** > > Letters and digits without a modifier key clash with chat input and the game's own shortcuts. Prefer keys the game doesn't use, such as F5–F8, or key combinations. ## New events `g.events()` now also yields these (for all fields, see [W3P protocol](https://war3ai.com/en/docs/protocol/)): | kind | When | Convenience fields | |---|---|---| | `ui.click` | An interactive canvas item was clicked | `.key` canvas key, `.button` (`'left'` / `'right'`), `.mods` modifier keys | | `ui.hover` | The mouse moved onto / off a canvas item | `.key` (`None` when moving off) | | `hotkey` | A registered hotkey was pressed | `.key` hotkey string, `.mods` | | `mouse.world` | With ground clicks enabled, a click landed in the world | `.x .y` ground coordinates, `.button`, `.value` (1 = swallowed) | | `selection.changed` | The local player's selection changed | Get the units with `g.selection()` | | `spell.cast` | A unit cast a spell (the spell's cooldown started) | `.spell` four-character code, `.b` level, `.value` cooldown seconds, `.x .y` cast point | | `message` | A line appeared in an on-screen message frame | `.text` full text, `.frame` which frame, `.chat` (when it's chat) | | `player.left` | A player left or was removed after being defeated | `.player` | | `game.ended` | Left the game | — | ## Chat and screen messages What the player types in the chat box is read directly from the `message` event's `.chat`: ```python for ev in g.events(): if ev.kind == "message" and ev.chat and ev.chat["text"] == "-follow": ... # ev.chat = {'channel': 'All', 'sender': '', 'text': '-follow'} ``` `g.messages()` keeps its own separate cursor, and game hints ("You need more farms", "Can't build there") are in there too. When writing a bot, use it to find out why a command didn't work. ## From other languages - **Gateway**: `ui.button`, `ui.choice`, `ui.hotkey`, `ui.mouse`, `ui.cursor` and the rest are available under the same names on the [gateway](https://war3ai.com/en/docs/gateway/). A remote client can't pass callback functions, so clicks and hotkeys arrive through the event push (the `ui.click` event carries `key`). - **Writing shared memory directly**: first send the semantic command `input_enable` (W3P opcode 74), and the runtime starts taking over input. In the input block `Local\War3Input_` you write the hotkey table and mouse switches, and it writes back the mouse position, the ground point under the cursor and the hovered item. Flag bit `0x40` on a canvas item means "interactive". See [W3P protocol](https://war3ai.com/en/docs/protocol/) for the layout. ## Several programs at once Mods, Farsight, MCP and each gateway session may all place buttons and register hotkeys in one game at the same time without interfering with each other: - Each program registers its own hotkeys and ground-click switch, and the SDK merges everyone's into one table for the runtime. Each key is listed only once; events go to everyone, and each program picks out its own hotkeys by key; - `ui.close()` withdraws only your own; window input is handed back only when the last program leaves; - If a program is killed before it can clean up: the runtime checks every 2 seconds, and once all registered programs have exited, it clears the hotkeys and ground-click interception they left behind, and the buttons they drew stop intercepting clicks. ## Measured 2026-09-25, live verification on a test instance, 16/16: - Click a button → `ui.click` + callback, and the runtime's intercept count goes up by 1 (the game didn't receive that click); clicking outside the button triggers nothing; - F6 → `hotkey`; click the ground → `mouse.world` (swallowed); - Spawn a Paladin and select it → `selection.changed`, matching `g.selection()`; cast Divine Shield → `spell.cast('AHds', 1, 35.0)`; - Map text → `message`; chat → `message`, with `.chat` parsing out the sender and the text; - Defeat the computer player → `player.left`; end the game → `game.ended`. Real mouse clicks on buttons and hover highlighting were also checked one by one. ## Limits and caveats - **Position comes from the real mouse**: the game reads the position from the system cursor, so hover and `cursor()` reflect the real mouse. Interception only covers button and key presses. - **Drawn under the mouse cursor**: Warcraft draws the cursor into the frame as part of the picture every frame. The canvas and speech bubbles are both drawn just before the step where the game draws the cursor: they cover the game UI, and the cursor covers them. Only when a frame has no cursor (hidden, or during a cinematic) do they fall back to drawing in the very last step. - **System scaling**: if you write your own tests and post clicks with window messages, coordinates posted by a process that isn't DPI-aware get scaled up by the system (×1.5 measured at 150% scaling). Have your test program declare DPI awareness first. Real clicks are unaffected. - **Fonts need warming up the first time**, which takes about 1 second. During that time the buttons aren't drawn yet and can't be clicked. - **No ground clicks outside a game**: on the main menu and the score screen, `mouse.world` is neither sent nor swallowed. - If a press was swallowed and you switch to another program or drag the mouse out of the window before releasing, the state is reset too, so the next release isn't swallowed as well. - 1.27 has no functions for creating new game UI frames (they arrived in 1.31): the buttons and cards here are drawn by the runtime, so they can look however you like, but they don't appear in the game's own menu hierarchy. --- # JASS channel > The 1291 JASS functions available to map makers can now be called by name from outside the game: create units, change properties, effects, boards, dialogs, sound, camera, fog… Four ways to use it: the Farsight console, the command line, HTTP and Python. The **1291 JASS natives** that map makers can use in map scripts can now all be called by name from outside the game: create units, change properties, draw effects, pop up boards and dialogs, play sounds, move the camera, change the fog… Use them to customize the game further — RPG helpers, an [AI companion](https://war3ai.com/en/docs/companion/), homemade mini-games, debugging tools. | Usage | Good for | Where | |---|---|---| | **Farsight "JASS console" page** | Trying things by hand, tweaking as you watch | Sidebar "System → JASS console": write a script and click Run; the right side lists functions by category, click one to insert it into the script | | **Command line** | Trying things by hand, or saving a script file to run again and again | `python -m openwar3 jass --inst 20` (interactive), `-e "code"`, `my_script.j`, `--list keyword` | | **HTTP** | External programs in any language | `POST /api/instances/{n}/jass` and others (see below); the Farsight backend listens only on the local machine | | **Python** | Writing schemes, companions and tools | `g.jass.AnyFunction(...)`; common visuals and interactions are wrapped in `openwar3.visual` | > **Warning** > > Three boundaries, all dictated by how it works: > > - Only **single-player games** (against the computer on your machine) can change the world. Creating objects or changing units unilaterally on your machine would desync the other players in a multiplayer game — so in multiplayer games only read-only functions (`Get*`, `Is*`, `Count*`…) are allowed. > - It's only for your machine's own tools; calls made while connected as a player (`Game(player=N)`) or in fair mode are rejected. > - Only for single-player and self-hosted LAN games. > > To add things to the screen in a multiplayer game, use the [canvas](https://war3ai.com/en/docs/canvas/): the runtime draws it itself, and it doesn't change game state. ## Script syntax The console, the command line and HTTP all use the same scripts. One statement per line; **you can paste JASS directly** (`call` / `set` / `local`, `true` / `false` / `null`, `'Hpal'` four-character codes, `//` comments), or write it Python-style: ```text set h = hero() // built-in: our main hero local texttag t = CreateTextTag() call SetTextTagText(t, "|cffffcc00+128 Critical!|r", 0.024) call SetTextTagPosUnit(t, h, 60) call SetTextTagVelocity(t, 0, 0.03) call SetTextTagPermanent(t, false) call SetTextTagLifespan(t, 4) call SetTextTagVisibility(t, true) call PingMinimapEx(h.x, h.y + 300, 3, 255, 0, 0, false) set u = CreateUnit(Player(0), 'hfoo', h.x + 200, h.y, 270) print("created", u, "hero level", GetHeroLevel(h)) ``` - **Variables persist**: within the same instance and the same game, variables you `set` in one snippet can be used in the next; they're cleared automatically when a new game starts, and you can also clear them manually. - **Built-in functions**: `hero()` our main hero, `me()` the local player, `unit('hfoo')` finds a unit, `unit_at(x, y)`, `wait(seconds)`, `print(...)`. Units expose `.x`, `.y`, `.hp`, `.hp_max`, `.mana`, `.type`, `.owner` and `.level`, and arithmetic and comparisons are supported. - **Not supported**: `if`, `loop`, `function` — for logic, use Python's `g.jass` (it's just ordinary function calls), or write a [scheme](https://war3ai.com/en/docs/schemes/). - Errors tell you which line failed and why (no such function, wrong number of arguments, undefined variable…); statements before the error have already taken effect. Arguments and return values: | In the signature | What to pass | Notes | |---|---|---| | integer | A number; `'Hpal'` four-character codes are converted automatically | | | real | A number | The runtime converts it to the format the engine expects | | boolean | `true` / `false` | | | string | `"..."` | Chinese text and the game's color codes are supported; strings the game keeps (floating text, boards, buttons, chat commands) are copied on the spot, so it's safe | | handle | A handle held in a variable, or a unit (things like `hero()` are converted to handles automatically) | | | function (code) | Only `null` | A JASS function can't be passed in from outside; something like `TimerStart(t, 60, false, null)` works | | string return value | — | The engine returns a string table index, so the text can't be read back. For unit names, use `g.map_data.name_of` | ## Categories Functions are grouped into categories by name; the console's right panel and `--list` both use these: | Category | Count | Examples | |---|---|---| | Visual effects | 80 | Floating text, lightning links, special effects, ground images, ground splats, unit tint / scale / animations | | Interface and boards | 146 | Multiboards, leaderboards, timer windows, dialogs, quests, on-screen text, minimap pings, portrait dialogue, full-screen filters | | Camera | 44 | Camera fields, panning, camera shake | | Sound and music | 50 | Creating and playing sounds, playing music | | Fog and vision | 25 | Visibility modifiers, toggling fog of war | | Item / hero / unit | 63 / 32 / 161 | Create items, set hero level, change owner, add abilities | | Player / alliance / resources | 71 | Set alliances, change gold and lumber | | Trigger / event / timer | 62 | Create triggers, register events, timers | | Terrain / weather / destructable | 45 | Weather effects, changing terrain, creating destructables | | Game flow | 57 | Game speed, pause, time of day | | Other | … | Unit groups and regions, storage, computer AI scripts, type conversion and math, event responses… | On 2026-09-24, **94** of them were called one by one in a live game and their effects checked by eye; the rest go through the same path, just without each effect being checked individually. > **Note** > > Event response functions (`GetTriggerUnit`, `GetClickedButton`…) only have a value at the moment a trigger runs; called from outside, they return 0 or null. To find out whether something happened, use the event counters described below. ## HTTP Farsight backend (default `127.0.0.1:8866`, listens only on the local machine): ```http GET /api/jass/natives?q=TextTag&cat=visual POST /api/instances/20/jass {"code": "set h = hero()\ncall PingMinimapEx(h.x, h.y, 3, 255, 0, 0, false)"} -> {"ok": true, "rows": [...], "printed": [...], "vars": {...}} -> on error: {"ok": false, "error": "第 2 行:...", "line": 2} POST /api/instances/20/jass/call {"name": "SetUnitScale", "args": [{"unit": 599669636}, 1.4, 1.4, 1.4]} POST /api/instances/20/jass/reset clear the remembered variables ``` For a unit argument, write `{"unit": addr}`, where `addr` is the unit's `addr` in the snapshot. Measured at 60–90 ms per request. ## Python: g.jass and openwar3.visual ```python j = g.jass t = j.CreateTextTag() j.SetTextTagText(t, "Hello", 0.024) # same argument rules as scripts; unit and item objects from the snapshot can be passed directly j.signature("CreateImage") # look up a signature ``` `openwar3.visual.Visual(g)` wraps the tested, commonly used visual effects into one-liners (call `v.tick()` once per tick: it removes expired effects and moves the lines and circles that follow units; `v.clear()` removes everything): | Method | Effect | |---|---| | `float_text(text, unit_or_point, ...)` | Floating text: damage numbers, overhead hints; Chinese text and colors both work | | `link(a, b, kind)` | A line between two units that follows them: magic leash / spirit link / life drain / healing wave | | `effect(model, unit_or_point, ...)` | Special effect model: overhead, at the feet, or played once (explosion, pillar of light) | | `ring(unit_or_point, radius, color)` | Range circle on the ground: ability range, danger zone, rally point; can follow a unit | | `ping(point, color)` | Minimap ping | | `board(title, rows...)` | Multi-row board in the top-right corner (with icons); cells can be updated one by one | | `countdown(title, seconds)` | Timer window in the top-right corner; the game counts down the seconds itself | | `scene(name, line, portrait)` | Portrait dialogue: the portrait at the bottom switches to a talking unit, and a "Name: line" subtitle appears on screen | | `screen_tint(color, alpha)` | Full-screen filter (by default a red glow around the edges: a low-health warning) | | `sound(path)` / `reveal(point, radius, seconds)` / `look(unit, ...)` | Play a sound / clear the fog over an area / tint, scale, animate or flash a unit | ## Interaction: knowing what the player did without writing JASS functions Responding to the player in JASS means writing trigger functions, and a function can't be passed in from outside. The workaround: **create an empty trigger with no conditions and no actions, register only the event, and count how many times it has run.** Testing confirmed that an empty trigger still counts. | Method | Use | |---|---| | `chat_commands(["-follow", "-stay"])` → `.poll()` | Commands the player types in the chat box (exact match, or prefix match) | | `menu(title, [buttons...])` → `.clicked()` | A button menu in the middle of the screen, and which button was clicked | | `hotkeys(("left", "right", "up", "down", "esc"))` → `.poll()` | How many times the arrow keys and Esc were pressed | | `on("TriggerRegister...Event", args...)` → `.poll()` | How many times any JASS event happened: unit death, entering a region, taking damage, timers… | The limitation is that you only know how many times something happened, not who did it or what they typed. To tell who, create a separate counter for each object. That's how the [AI companion](https://war3ai.com/en/docs/companion/)'s chat commands are wired up. ## Caveats - **Clean up what you create**: floating text, links, images, boards, triggers… stay around until removed (`Visual.clear()` removes the ones it created). The game can show at most about 100 floating texts at once. - **BJ functions aren't natives**: functions like `CreateTextTagUnitBJ` are built out of natives in the map script and aren't available here — call the natives the way their implementation does. - **Some constants need converting first**: e.g. `ConvertPlayerColor(1)`, `ConvertFogState(4)` (see common.j for the values). - One call takes about 13 ms (including handle conversion); at the protocol level these are W3P opcodes 70–72; see [W3P protocol](https://war3ai.com/en/docs/protocol/). --- # Gameplay mods > A scheme doesn't have to be an AI that plays for you — it can also be a set of rules. You play in the game window yourself, and the mod sets up the start, spawns enemies, hands out rewards, gives you buttons and choice cards on screen, and decides who wins. Subclass openwar3.Mod, and one file is a whole game mode. There are two kinds of [AI schemes](https://war3ai.com/en/docs/schemes/): `kind: bot` is an AI that plays for you; `kind: mod` is **a set of rules** — you play in the game window yourself, and the mod sets the challenges: how the game starts, spawning enemies on a timer or on events, what rewards to give, which buttons and choice cards to show you on screen, and when you've won. Mods only use capabilities that already exist: [UI & input](https://war3ai.com/en/docs/ui-input/) (clickable buttons, cards, hotkeys, ground clicks), the [canvas](https://war3ai.com/en/docs/canvas/) (panels, progress bars, routes), the [JASS channel](https://war3ai.com/en/docs/jass/) (spawning units, changing stats, giving items) and the event stream (deaths, level-ups, spell casts, chat). ## Two examples Pick them in Farsight under "AI schemes" → "Built-in": | Mod | How it plays | Capabilities used | |---|---|---| | **Hero Roguelike** `builtin/hero-roguelike` | You have just one Paladin, and enemies close in from all sides, wave after wave; each time you level up, pick one of three upgrades in the middle of the screen (the game pauses while you choose); survive 10 waves to win, lose if your hero dies | `g.ui.choice` (clickable cards + pause), `hero.levelup` / `killed` / `spell.cast` events, chat `-help`, JASS for changing hero stats and giving items | | **Endless Defense** `builtin/endless-defense` | Enemies spawn at the opposite start location and charge your town hall along a red line on the ground; every wave you hold off pays gold; click the on-screen button or press F7 to call the next wave early for ×1.5 rewards; press F8 and then left-click the ground to place a free arrow tower (right-click cancels) | `g.ui.button`, `g.ui.hotkey`, `g.ui.mouse` (capturing ground clicks), canvas panel / progress bar / route, JASS for spawning enemies and adding gold | Each example is about 150 lines; the code is in `brains/examples/mod_hero_roguelike.py` and `brains/examples/mod_endless_defense.py`. ```bash python tools/play.py --bot brains/examples/mod_hero_roguelike.py --inst 9 # start a game; the mod takes over and you play in the game window ``` ## Write a mod ```python from openwar3 import Mod class Survive(Mod): name = "survive" def on_start(self, g): super().on_start(g) # single-player check + suppress the computer opponent self.foe = self.wave_player(g) # an empty-slot player as the "wave player": allied with no one, no computer AI self.every(30, self.wave) # one wave every 30 game seconds (stops while paused) g.ui.hotkey("F7", lambda g, ev: self.wave(g)) def wave(self, g): self.spawn_ring(g, self.foe, "ugho", 6, self.home(g), 1400, attack_to=self.home(g)) def on_event(self, g, ev): if ev.kind == "unit.died" and ev.type == "htow": self.finish("loss", "The town hall fell") ``` On top of `Bot`, `Mod` adds: | Method / attribute | Description | |---|---| | `on_start / on_tick / on_event / on_end` | Same as Bot; when you override `on_start` / `on_tick`, remember to call `super()` first | | `every(seconds, fn, first=)` / `after(seconds, fn)` | Timers that run on **game time**; the callback is `fn(g)` | | `finish(result, reason)` | Ends this game (`'win'` / `'loss'` / `'unknown'`): the runner stops on the next tick, a result panel is drawn in the middle of the screen, and the scheme's results record it | | `wave_player(g)` | The first empty-slot player, used as the wave player | | `spawn_ring(g, player, unit, count, center, radius, attack_to=)` | Spawns units around a ring; dozens per wave without stutter; returns JASS handles | | `alive_of(g, player)` / `attack_move_all(g, player, point)` | A player's living units / attack-move all of them to a point (call it every few seconds and the enemies will chase) | | `home(g)` / `hud(g, title, lines)` | Our town hall's position / the info panel in the top-right corner | | `neutralize_ai = True` | Suppresses the computer opponent at game start: its units are paused every 5 seconds and its gold and lumber are zeroed. Melee maps always have a computer player, and when a mod sets its own rules, it shouldn't interfere | | `single_player_only = True` | Refuses to run if there are other human players (world-changing JASS would desync them) | | `linger_s = 6` | After the result is decided, how many seconds to stay on the result screen before ending | `finish()` works on `Bot` too: a regular bot can also announce the end of a game itself. ## Package it as a scheme and share it Write `"kind": "mod"` in `scheme.json` and define a `Mod` subclass in the entry file: ```json {"id": "survive", "name": "Hold out for 10 waves", "kind": "mod", "entry": "survive.py", "class": "Survive"} ``` A mod **never uses fair mode** (it's the referee setting the challenges, so it needs to see the whole map and change the world) and **isn't judged by melee rules** (the result is reported through `finish`); `fair` / `judge` in the manifest have no effect. Exporting a zip, importing, trust and results work exactly like bot schemes; see [AI schemes](https://war3ai.com/en/docs/schemes/). A mod is code too, so someone else's mod also needs your trust before it runs for the first time. ## Measured 2026-09-25, on a test instance: - **Hero Roguelike**: the first wave spawns and the top-right panel updates; level the hero to 3 → cards pop up in the middle of the screen and the game clock stops; click cards twice → both upgrades take effect (Strength 22 → 27) and the clock resumes. - **Endless Defense**: the panel, the route on the ground and the button are all there; F8 + click the ground → a defense tower appears next to the town hall; click the button before the current wave is cleared → the hint "this wave isn't cleared yet". ## Limits - **Single-player only**: spawning units and changing stats go through the JASS channel, which would desync a multiplayer game. That comes from the lockstep model; multiplayer gameplay has to wait for a sync channel (see the [roadmap](https://war3ai.com/en/roadmap/)). - A mod sees the whole map — it's the one setting the challenges, not a player. - The computer opponent on melee maps is only "suppressed", not removed (removing it would trigger the melee victory check). --- # Farsight console > A local web console and the single entry point: set the game folder, start and stop services and game instances, set up the next game, see what the AI is thinking, issue manual commands, direct the camera, and review match history. Farsight is a web console that runs on your machine and **listens only on 127.0.0.1**. It's also the single entry point to the whole system: starting games, switching AIs, the gateway, speech bubbles and the local LLM are all controlled here, with no other scripts to hunt down. ```bash start.bat # deployment check, then open Farsight at http://127.0.0.1:8866 start.bat 5 6 # also start tests on instances 5 and 6 (game + reference brain) start.bat restart # restart only the Farsight backend (after changing server code; games and services are unaffected) stop.bat # stop everything completely ``` Change the port in `ports.console` in `openwar3.json` (default 8866). ## Control Center Farsight's home page. - **Game folder**: find it automatically or pick it yourself; Farsight checks the game version and extracts data from your own game. - **Local services**: [Gateway](https://war3ai.com/en/docs/gateway/), [Chat bubbles](https://war3ai.com/en/docs/speech/), Local LLM (LM Studio) and Website preview. Every card can start, stop and restart its service and show its logs; the section also shows whether a client has attached the [MCP](https://war3ai.com/en/docs/mcp/) server. - **Environment check**: whether Python, the runtime files, game data, AMAI data and the other parts are installed. - **Stop all** (top right): stops game instances, AIs, the gateway, bubbles, the local model this system uses and the Farsight backend, in that order; the same as double-clicking `stop.bat`. The MCP server is managed by clients such as Claude and is not stopped; the LM Studio app itself isn't closed either. ## Pages | Group | Page | What it does | |---|---|---| | Hub | Control Center | See the previous section | | Game | Overview | Summary of the current instance's game | | | Battlefield | Map view; you can issue manual commands (manual commands have the highest priority in the claim table: 95) | | | Unit data | Each unit's order, task target, mana, hero level and XP, ability cooldowns, inventory | | | AI decisions / Combat decisions | What the reference brain is thinking this tick, and the details of every combat decision | | | Strategy coach | Status of the [LLM strategy coach](https://war3ai.com/en/docs/llm-coach/): whether the model server is up, whether each instance is connected, the latest advice and the input it saw | | | Director | Auto camera, overhead health bars | | | Chat Bubbles | Make units speak, chat with the local model, peasant break room, camera dialogue, battle triggers, model settings. See [Speech bubbles and local models](https://war3ai.com/en/docs/speech/) | | | Command rate | APM and command throughput | | | Events & input | What happened in this game: spell casts, chat and screen messages, button clicks, hotkeys, ground clicks, selection, players leaving, filterable by category; alongside are the cursor position, the hovered item and the local selection. See [UI & input](https://war3ai.com/en/docs/ui-input/) | | Records | Logs / Match history | Log sources for each instance; each game's result, duration and peak army size | | | Issue notes | Press Pause/Break in the game to pause and mark the timestamp, then add a description here later | | System | Instances & Setup | Start and stop instances; set the **next game**'s map (a melee map, or an RPG / custom map), both races, difficulty and game speed; pick an AI scheme for each instance; **Start test** launches the game + AI in one click | | | AI schemes | Import, export, copy, trust and delete schemes; switch an instance's scheme (even the game in progress can be handed over right away); see each scheme's results. See [AI schemes](https://war3ai.com/en/docs/schemes/) | | | JASS console | Write a JASS script and hit Run; on the right, browse 1291 functions by category and click one to insert it into the script. Variables persist for the whole game. See [JASS channel](https://war3ai.com/en/docs/jass/) | | | Connect & extend | Gateway status and one-click start; connection URLs generated per role (dev / player / observer), MCP setup commands and config; JS and Python examples. See [Gateway](https://war3ai.com/en/docs/gateway/) and [MCP](https://war3ai.com/en/docs/mcp/) | | | Data & disk | How much disk space recordings, match history, logs and other run data take, how much was added in the last day, and what can be deleted (Farsight never deletes anything automatically) | | | Settings | Game folder, interface language, appearance (dark / light, modern / Warcraft style) and more | | | Feedback | Report a problem or make a suggestion straight to us; diagnostic information is attached only if you tick the box, and you can preview it before sending | Press Ctrl + K to open the command palette: jump to a page, switch instances, end the current game, or start a new brain. **What's new** at the bottom of the sidebar lists what was recently added to Farsight and the platform. At startup (and every 6 hours after that), Farsight asks War3AI.com whether there's a new version and lets you know if there is. When Farsight is updated, a banner appears at the top of the page; save whatever you're typing, then click **Refresh**. ## Multiple instances `runtime/farm.py` handles running multiple copies (Farsight calls it for you when starting and stopping instances): it copies the original `War3.exe` launcher to `War3-.exe` (without modifying any game file), and each instance gets its own number and directory (`bin/inst/`). When a game ends, the next one starts automatically according to `next_game.json` (which is exactly what the console's Instances & Setup page edits). > **Tip** > > Your bot connects to a specific instance with `--inst N`. The console's Instances & Setup page shows which numbers are in use, so you don't collide with the reference brain. ## Live page `http://127.0.0.1:8866/live` is a scrolling log page designed to drop into an OBS Browser Source, showing the AI's decisions and how the battle is going. ## API The console's server is a set of local REST + WebSocket APIs (instance status, next-game settings, unit details, manual commands, logs, match history, director, AI schemes, JASS calls, canvas…), and the web page is just one of its clients — programs in any language can call them directly. The API list is in the header comment of `console/server/app.py`; for how to use the scheme, JASS and canvas groups, see [AI schemes](https://war3ai.com/en/docs/schemes/), [JASS channel](https://war3ai.com/en/docs/jass/) and [Canvas](https://war3ai.com/en/docs/canvas/). --- # AI schemes > A scheme is a complete AI. Switch with one click in Farsight — even the game in progress can be taken over right away. Export a zip to share with others, import other people's schemes to test them, and every scheme's results are tracked automatically. A **scheme** = a complete AI: a folder + a manifest `scheme.json` + code. Each game instance picks one scheme; you switch with one click in Farsight, and **the new scheme can even take over the game in progress right away**. Schemes shared by others are imported into **a separate area**, kept apart from your own schemes; to change one, use "Copy to mine". ```text schemes/ mine// mine: ones you wrote, or copied from another scheme to modify (change freely; takes effect next game) installed// installed: zips shared by others are unpacked here (trust must be confirmed before the first run) brains/xwar3/ built-in: the reference brain (a full AI) brains/examples/ built-in: the four teaching examples hello / rush / macro / micro, the companion example buddy, and two gameplay mods (Hero Roguelike, Endless Defense) ``` A scheme doesn't have to be an AI that plays for you: a `kind: mod` scheme is a set of **gameplay rules** — you play, and it sets the challenges. See [Gameplay mods](https://war3ai.com/en/docs/mods/). ## Using it in Farsight The "AI schemes" page (sidebar "System → AI schemes"): | Action | What it does | |---|---| | Import scheme (zip) | Installs it into `installed/`; if the same id is already installed, it asks whether to replace it (after replacing, trust must be confirmed again) | | Use on instance… | Pick an instance + "Apply now" (stops the current AI; the new scheme takes over this game) or "Apply at next Start test" | | Copy to mine | Copies it into `mine/`, sets the author to "me" and the version to 0.1.0, and records which version of which scheme it was copied from | | Export zip | Packs it as `-.zip`; sending that to someone is how you share it | | Open folder | Opens the scheme folder in File Explorer so you can edit the code directly | | Trust | Required before someone else's scheme runs for the first time (see "Trust and safety" below) | | Recent results | Win/loss, duration and end reason for every game this scheme played | | Delete | Only "mine" and "installed" schemes can be deleted; a scheme an instance is using can't be | Instance cards also have a new "AI scheme" row: pick a scheme from the dropdown → "Switch (applies now)". When the instance isn't running, the button says "Select", and the next "Start test" launches the AI with that scheme. ## The manifest: scheme.json ```json { "format": 1, "id": "fast-rush", "name": "Three-minute rush", "version": "1.2.0", "author": "Someone", "description": "One sentence on what strategy this AI plays", "entry": "rush_bot.py", "class": "RushBot", "fair": true, "hz": 5, "races": ["human", "orc"], "license": "MIT" } ``` | Field | Required | Description | |---|---|---| | `id` | ✔ | Lowercase letters, digits, `-`, `_`; 2–41 characters | | `entry` | ✔ | A `.py` file in the scheme folder (no absolute paths, no `..`) | | `kind` | | Default `bot` (a subclass of `openwar3.Bot` that plays for you); `mod` = a [gameplay mod](https://war3ai.com/en/docs/mods/) (a subclass of `openwar3.Mod`; never uses fair mode and isn't judged by melee rules) | | `class` | | Name of the Bot (or Mod) subclass in the entry file; if omitted, the last `openwar3.Bot` subclass in the entry file is used | | `fair` | | Default `true`: sees only what's within vision, the same rule as the Arena. `false` = full-map visibility, and only then can it use the [JASS channel](https://war3ai.com/en/docs/jass/) (companions need it) | | `judge` | | Default `true`: wins and losses are judged by melee rules. Set it to `false` for RPG / companion schemes | | `hz` | | How many times per second `on_tick` is called; default 5 | | `format` | | Manifest format version, currently 1; a manifest newer than your local OpenWar3 is rejected with a prompt to update | | Others | | `name`, `version`, `author`, `description`, `races`, `license`, `homepage` and `forked_from` are for display only | The scheme folder is added to Python's module search path, so the entry file can `import` other files in the same folder. Third-party packages (numpy, torch…) aren't installed automatically — state clearly in `description` what's needed. **The smallest scheme needs just two files**: ```python # my_bot.py from openwar3 import Bot class MyBot(Bot): def on_tick(self, g): for w in g.idle_workers(): mine = g.nearest(g.gold_mines(), w) if mine: g.gather(w, mine) ``` ```json {"id": "my-first", "name": "My first AI", "entry": "my_bot.py"} ``` Put them in `schemes/mine/my-first/` and refresh Farsight to see it. An even easier starting point: pick an example under "Built-in" and click "Copy to mine". ## How schemes run, and results Schemes are run by the **scheme runner** (it's what Farsight's "Start test / Switch" launches): ```bash python tools/run_scheme.py --inst 20 --scheme builtin/micro --hours 6 ``` - Each instance has one long-running supervisor process, which **starts a child process for every game** to run the scheme: if the scheme's code crashes, the supervisor is unaffected; if you change the code of one of your own schemes, the next game automatically uses the new code. - At the end of each game one results row is recorded: scheme, version, author, win/loss, reason, game duration, error count. The win rates in Farsight are computed from these. How wins and losses are decided: | Situation | Recorded as | |---|---| | All enemy buildings are gone | Win | | All our buildings are gone (even if units are still alive — that's how melee games decide a loss) | Loss | | All our units are gone | Loss | | Ended / stopped manually in Farsight | Undecided | | At the time of switching, the game had already run for more than 60 game seconds (taken over midway) | Counted separately, **not included in the win rate** | | The game clock hasn't moved for a long time | Undecided | Once a game is decided, the runner closes the score screen, starts the next game according to the "Next game" settings, and the scheme takes over again — you can leave it running all night to build up results. Pausing doesn't end a game: while paused, the Bot keeps running as usual; only the game clock stops. ## Trust and safety **A scheme is code, and when it runs it has the same permissions as you do** (it can read and write files and access the network). So: - Schemes in `installed/` are **not trusted** by default; both Farsight and the runner refuse to run them until you click "Trust"; - Replacing an installed scheme with one that has the same id **resets trust** (a new version means new code); - Imports are checked: the zip must be at most 50 MB with at most 2000 files; no absolute paths or `..` (so nothing can be written outside the scheme folder); an invalid manifest or a missing entry file is rejected outright. > **Warning** > > Before you trust a scheme, click "Open folder" and read through the code. Only take schemes from people you trust. ## API (for scripts) | Endpoint | Description | |---|---| | `GET /api/schemes` | Scheme list + results + the scheme each instance has selected and is running | | `GET /api/schemes/results?ref=` | A scheme's last 30 games | | `POST /api/schemes/import` | Import a zip | | `GET /api/schemes/export?ref=` | Download the zip | | `POST /api/schemes/fork` | Copy to mine | | `POST /api/schemes/trust` | Trust | | `DELETE /api/schemes?ref=` | Delete (refused while an instance is using it) | | `POST /api/instances/{n}/scheme` | Change an instance's scheme: take over the current game right away, or apply at the next Start test | In Python, use the library directly: `from openwar3 import schemes` (`list_schemes`, `install_zip`, `export_zip`, `fork`, `trust`, `stats`…). ## Later: a scheme website An exported zip is the unit of sharing, so the website only needs to add a layer on top: upload from Farsight with one click; downloads from the website go through exactly the same checks as "Import scheme" and still require confirming trust; results can optionally be reported, and the website aggregates win rates by version. Farsight already has a spot reserved for the "Share to scheme website" button. See the [roadmap](https://war3ai.com/en/roadmap/) for progress. --- # Speech bubbles and local models > Pop up a speech bubble over any unit in the game, speaking as any character. Hook up a local LLM, and one line goes in while the reply appears over the unit's head. Bubbles are a presentation layer: they don't affect who wins, and they're great for streaming, commentary and debugging. - Any unit can speak as any character, and multiple units can talk at once; - Each bubble's font size, color, width, tail, opacity and typing speed can be customized individually; - Connects directly to a local LLM (LM Studio) with streaming output: the bubble updates as the text is generated. ## Using it from a bot The simplest way is the SDK's built-in `say`: ```python g.say(hero, "With me! Charge!", seconds=4) ``` ## Starting it and the UI **Easiest: Farsight's home page, Control Center.** First click **Local LLM → Start and load model** (starts the LM Studio local server and loads the configured model into VRAM), then **Chat bubbles → Start**. The cards also let you view logs, stop and restart. The UI is the **Chat Bubbles** page in Farsight's left sidebar: make units speak (pick a unit, write the text, adjust the style, chat with the model), peasant break room, camera dialogue, battle triggers and model settings, all acting on the instance selected in the top bar. You can also use the command line: ```bash python speech/speak_launch.py # start the local model server + load and warm up the model + start the bubble API python speech/speak_launch.py --restart # restart the API after changing code python speech/speak_launch.py --stop # stop the API and unload the model from VRAM ``` Every step is skipped if it's already running, so running it again has no side effects. ## HTTP API The default is `http://127.0.0.1:8872/` (the port is `ports.speech` in `openwar3.json`), and any program can call it. ### Make a unit speak: `POST /api/say` ```json { "inst": 16, "bubbles": [ { "unit": "0x14A12614", "name": "Mountain King", "text": "With me! Charge!" }, { "unit": "0x14A12924", "name": "Archmage", "text": "I'll cast Blizzard.", "style": { "font_px": 26, "text_color": "#FFE080", "bg_color": "#C0102040" } }, { "screen": [960, 110], "key": 1, "name": "Narrator", "text": "The first orc wave arrives in 30 seconds.", "style": { "tail": false, "type_ms": 0 } }, { "world": [-4684, 2644], "key": 2, "text": "Rally point", "style": { "font_px": 16 } } ] } ``` | Field | Description | |---|---| | `unit` / `world` / `screen` | Pick one: follow a unit (sits right above its health bar when it has one) / map coordinates / screen pixels (for narration) | | `name` | Speaker shown on the first line; anything you like, it doesn't have to be the unit | | `text` | Body text, wrapped automatically | | `duration_ms` | How long to show it; 0 = automatic, 3–5 seconds | | `key` | ID for world / screen bubbles; a new message with the same key replaces the old one | | `update` | If the same bubble already exists, only swap the text without resetting the timer (for streaming) | | `style` | `font_px`, `max_width_px`, `text_color`, `bg_color`, `border_color`, `tail`, `side_px`, `opacity`, `type_ms`, `font`… | Up to 32 bubbles at once; the per-frame cost averages about 0.1–0.2 ms. ### Chat with a local model: `POST /api/chat` ```json { "inst": 16, "unit": "0x14A12614", "name": "Mountain King", "persona": "You are Muradin, the Mountain King from Warcraft: boisterous and fond of ale. Reply in one or two casual sentences, under 40 words.", "message": "There's a pack of ogres up ahead. Do we charge?", "stream": true } ``` It returns `{"reply": "...", "first_token_ms": 283, "total_ms": 342}`, and by then the reply is already showing over that unit's head. Each unit remembers its last 6 rounds of conversation. ### Other endpoints | Endpoint | Description | |---|---| | `GET /api/instances` | Running games | | `GET /api/units?inst=16&mine=true&heroes=true` | Unit list (with Chinese display names, coordinates, HP) | | `POST /api/clear` | Clear one bubble or all of them | | `GET /api/llm`, `POST /api/llm` | View / change the model config (`base_url`, `model`, `max_tokens`, `temperature`) | | `POST /api/banter` | Peasant break room: workers at home take turns griping in character, with an opening roll call (every battle stat is real data) | | `POST /api/camtalk` | Camera dialogue: heroes and followers on camera talk to each other in character | | `POST /api/events` | Battle triggers: fight starts, fight ends, hero dies, tier up, building destroyed… it only speaks when something happens | ## Choosing a local model Measured on a single RTX 5090 (5 in-game lines): | Model | VRAM | Speed | One reply | Verdict | |---|---|---|---|---| | **Qwen3.6-35B-A3B** (MoE, only 3B active at a time), Q4, thinking off | 20.6 GB | ~142 tokens/s | **~0.3 s** (first token ~0.27 s) | Recommended: fast, with natural Chinese role-play | | gpt-oss-20b (MXFP4), reasoning low | 11.3 GB | ~280 tokens/s | 0.3–0.8 s | Use when VRAM is tight; Chinese is a bit flat | | Qwen3.6-27B (dense), Q4 | 17.2 GB | ~39 tokens/s | Still thinking at 5.5 s | Not suitable for real-time dialogue | - **Speed depends on the parameters active per step, not the total**: the 35B MoE activates only 3B and is 3–4× faster than the 27B dense model. - **You must turn off "thinking"**: otherwise every token goes into thinking and not a single word of reply comes back. - Bubbles type out at about 22 characters per second, so generation speed is no longer the bottleneck; what really shapes the experience is **time to first token**. > **Make the lines ring true** > > Always feed the model real battle data (games played, wins and losses, army size, resources in the bank), and state explicitly that it may use only these facts. In testing, without that constraint the model made up battles that never happened. --- # Gateway > A WebSocket / JSON gateway: the public APIs the Python SDK can call are callable from JS, C#, Go, Rust, browser pages and programs on another machine. Three roles, with a bundled JS client and browser demo page; latency is the fast lane plus about 1 ms. The gateway wraps the fast lane and the pushed state in **WebSocket / JSON**. The public APIs the Python SDK can call in the [API catalog](https://war3ai.com/en/api/) are callable from JS, C#, Go, Rust, browser pages, programs on another machine and LLMs, with the same method names and parameters. Latency is the fast lane plus about 1 ms. **Easiest: Farsight's home page, Control Center → Gateway → Start** (stop, restart, view logs and open the demo page from the same card). From the command line: ```bash python gateway/server.py # ws://127.0.0.1:8870/ws (the port is ports.gateway in openwar3.json) python gateway/server.py --open # same as above, and opens the demo page http://127.0.0.1:8870/demo once the port is listening python gateway/server.py --host 0.0.0.0 # for LAN use: a token is required automatically (bin/gateway/token.txt) python gateway/server.py --allow-origin http://localhost:5173 # lets your own web page connect too ``` ## Connections and roles Connection URL: `ws://127.0.0.1:8870/ws?inst=9&role=dev` (you can use `pid=` instead of `inst=`; add `&token=` when a token is required). | Role | Can call | Best for | |---|---|---| | `dev` | Everything: observe, command, game control, sandbox (changing the world with JASS), drawing UI | Local tools, [gameplay mods](https://war3ai.com/en/docs/mods/), companions | | `player` (add `&player=N`) | Observe, command player N's units, drawing UI; **fair mode by default**, so it sees only what's within player N's vision (`&fair=0` turns it off) | A bot or LLM playing for one player | | `observer` | Read-only (the runtime rejects its commands outright) | Spectating, commentary, data collection | `player` doesn't get: game control such as ending the game, changing the speed or pausing; `players` and `enemy_ai_plan`, which reveal other players' hands; `canvas.image`, which makes the game process open a local file; or JASS. Queries that take a player number, such as `resources`, `tech` and `stats`, can only query its own player. One connection is one session and takes one fast lane (the runtime has 16 in total). The gateway allows at most 12 sessions at once, leaving a few for bots, mods and Farsight. On disconnect, only what this session drew and its own hotkeys are removed; what other programs drew is left alone. ## Messages After connecting, you first receive `hello`: the protocol version, the role, the game's process ID, and the list of methods this role can call. After that, every request carries an `id`, and the response carries the same `id`: ```json → {"id": 1, "op": "call", "method": "units", "args": ["me"]} ← {"id": 1, "ok": true, "result": [{"addr": 123456, "type": "hpea", "owner": 0, "x": -4480, "y": 2120, "hp": 220, "hp_max": 220}]} → {"id": 2, "op": "call", "method": "move", "args": [[{"unit": 123456}], 100, 200]} → {"id": 3, "op": "call", "method": "ui.button", "args": ["shop", "Buy potion"], "kwargs": {"screen": [40, 300]}} → {"id": 4, "op": "subscribe", "state": {"hz": 4, "units": "mine"}, "events": true} ← {"type": "state", ...} {"type": "events", ...} pushed continuously from then on → {"id": 5, "op": "overview"} one-page overview: resources, unit counts, heroes, visible enemies, production → {"id": 6, "op": "jass", "code": "call PingMinimapEx(0, 0, 3, 255, 0, 0, false)"} dev only → {"id": 7, "op": "api"} method catalog (there are also ping / unsubscribe) ``` - **Unit arguments** are written as `{"unit": addr}`, where the address is the `addr` in the unit's JSON; you can add `"handle": [lo, hi]` to check that the address hasn't been reused by another unit. - **Method names** are the public methods of Game, plus `ui.*` (button / choice / toast / hotkey / mouse / cursor…), `canvas.*` (text / panel / bar / image / circle / path / remove…) and `jass.` (dev only). - A remote client can't pass callback functions: clicks and hotkeys arrive through the event push, and the `ui.click` event carries `key`. See [UI & input](https://war3ai.com/en/docs/ui-input/). - A failed call gets an error response for that call only (`ok: false` plus `error`); the connection stays open. The same goes for a message that isn't JSON. - Event JSON fields match the [W3P protocol](https://war3ai.com/en/docs/protocol/), plus convenience fields (`spell`, `key`, `text`, `chat`, `button`, `player`, `mods`). HTTP works too, which suits one-off calls and curl: `GET /api?role=player` lists the method catalog, and `POST /call` with `inst`, `role`, `method`, `args` and `kwargs` makes a single call. `/call` reuses sessions: when the game is restarted in a new process it switches to a new session automatically, and sessions idle for 10 minutes are closed. ## Clients **JS** (browser or Node 22+, zero dependencies): `gateway/clients/js/openwar3.mjs` ```js import { OpenWar3, unit } from "./openwar3.mjs"; const ow = new OpenWar3("ws://127.0.0.1:8870/ws", { inst: 9, role: "dev" }); await ow.connect(); const mine = await ow.api.units("me"); await ow.api.move(mine.slice(0, 3).map(unit), 100, 200); await ow.api.ui.button("hi", "Click me", { screen: [40, 300] }); // a trailing plain object = keyword arguments ow.on("event:ui.click", (e) => console.log("clicked", e.key)); await ow.subscribe({ state: { hz: 2, units: "mine" }, events: true }); ``` Node 20 / 21 needs `--experimental-websocket`. The full example is in `gateway/clients/js/example.mjs`. **Browser demo page** `http://127.0.0.1:8870/demo`: the game overview, a table of our units, placing a button in the game, and the event stream, all on one page. **Other languages**: any WebSocket library + the JSON above is all you need; no need to touch shared memory. **LLMs**: use the [MCP server](https://war3ai.com/en/docs/mcp/) directly; it turns the common tasks into ready-made tools. ## Measured 2026-09-25, checked item by item against a real game, 16/16 (9 for the gateway + 7 for MCP): handshake (121 methods for the dev role), `units('me')`, the one-page overview, a screen toast, placing a button; after subscribing, clicking that button in the game → `ui.click` pushed to the client; JASS; passing a bad unit reports an error for that call only; HTTP `/call` (observer role). The JS client (Node) and the browser demo page were run too: a button placed from the web page was clicked in the game, and the page's event log received `ui.click`. ## Security - By default it listens only on `127.0.0.1` and needs no token (same as Farsight). When `--host` isn't a local address, a token is required automatically; `--auth` requires one locally too. - **Other websites in a browser can't connect**: every connection a browser opens carries its origin (`Origin`), and the gateway only accepts its own demo page and the URLs given with `--allow-origin`. Programs such as Python, Node and curl send no origin and connect as usual. When listening only locally, it also checks `Host`, blocking attacks that resolve an outside domain name to the local machine. - The role is declared by the client when it connects: in local mode it's a convention, not a security boundary. For the Arena, the referee process has to decide who gets which role; see [Arena](https://war3ai.com/en/arena/). --- # W3P Protocol > The complete contract between the runtime and external programs: eight shared memory blocks, reading world state, reading events, issuing commands, receipts, lane roles, the canvas, and UI & input. Read this page if you're integrating from a language other than Python. The runtime and external programs exchange data **only through shared memory**. What follows is the whole of it. - The reference implementation is the Python `sdk/python/w3world.py` (read) and `sdk/python/w3fast.py` (write); every structure's size and offsets are defined there and pinned by tests; - **The protocol describes semantics only and is independent of the game version.** When the game version changes, the runtime adapts itself and the protocol stays the same; new fields are only ever appended to the end of a block, so old clients keep working. > **Note** > > Most people don't need this page — just use the Python SDK. You only need it if you want to integrate directly from C++ / C# / Rust / Go or another language, or you want to know what happens underneath the SDK. ## 1. Eight shared memory blocks `` is the game's process ID. | Name | Direction | Contents | Synchronization | |---|---|---|---| | `Local\War3World_` | runtime → you | World state: header + 16 players + up to 1024 units + 256 unit details + 256 ground items + extension area + production table | seqlock | | `Local\War3Trees_` | runtime → you | Up to 4096 destructables (trees, etc.), refreshed every 2 seconds | seqlock | | `Local\War3Events_` | runtime → you | Event ring, 8192 entries | each entry carries its own sequence number | | `Local\War3Map_` | runtime → you | Map: terrain cells (128 per cell, up to 256×256) + playable area bounds + start locations; computed in batches over the first few seconds of the game | seqlock (never changes once computed) | | `Local\War3Fast_` | both ways | Command lanes: 16 lanes × 16 slots; each slot holds one command + receipt; each lane has a role | single writer, single reader per slot | | `Local\War3Canvas_` | you → runtime | [Canvas](https://war3ai.com/en/docs/canvas/): 64-byte header + 256 elements × 112 bytes + 64 KB text / point pool; created only after you send `canvas_enable` once | seqlock (you write, the runtime reads every frame) | | `Local\War3Msgs_` | runtime → you | Screen message ring: full text of game hints, chat and system messages, 128 entries × 256 bytes | each entry carries its own sequence number | | `Local\War3Input_` | both ways | [UI & input](https://war3ai.com/en/docs/ui-input/): the runtime writes back the mouse position, the ground point under the cursor and the hovered item; you write the hotkey table and mouse switches; the runtime takes over input only after you send `input_enable` once | seqlock for the hotkey table | **Several clients using the canvas and input at once**: each of these two blocks exists only once, so clients writing independently would overwrite each other. The conventions below apply; follow them in your own client too: - **Canvas**: hold the named mutex `Local\War3CanvasMutex_` for a read-modify-write, replace only your own elements and leave everyone else's as they are (repacking the pool offsets); clear elements whose owner process has exited and elements with no owner. An element's `reserved[1]` = owner process ID, `reserved[2]` = in-process sequence number; element IDs are allocated from the counter at header offset 60 (starting at `0x10000`). - **Input**: each client registers its own hotkeys and mouse switches in `Local\War3InputClients_` (16-byte header + 16 clients × 528 bytes). Holding `Local\War3InputMutex_`, it updates its own entry, then merges the live clients and writes the result into the input block: hotkeys are deduplicated by "key code + modifiers", and mouse switches are combined as a union. Events go to all clients, and each one picks out its own hotkeys by "key code + modifiers". Don't send `input_enable 0` while other live clients are still in the registry. - **Runtime**: clickable elements whose owner process has exited stop intercepting clicks; every 2 seconds it checks the registry, and once all registered clients have exited, it zeroes the input block's hotkey table and mouse switches. ## 2. Reading world state (seqlock) ```text loop: s1 = block.seq (offset 8, int32) if s1 is odd: retry (the runtime is writing) copy header + players + units[unitCount] + details[detailCount] + items[itemCount] if block.seq != s1: retry ``` - **Header**: publish counter (not increasing = publishing has stopped), engine game clock, an epoch that increments by 1 each game, local player number, whether a game is in progress, game speed, publish period, microseconds spent collecting this copy on the game thread, event sequence number, per-stage timings. Clients can write `requestedPeriodMs` to request a publish period (16 ~ 1000 ms). - **Unit** (112 bytes): handle pair (**identify units by handle pair** — addresses get reused), type four-character code, owner, flags, coordinates, HP / mana (with maximums), current order + order target, task target (what it is actually attacking), hero level / XP / skill points, detail index, `visibleTo` (bit p = player p can see it right now). - **Details** (288 bytes, heroes > player units > creeps, up to 256): 12 abilities (code / level / flags / cooldown seconds remaining), 8 buff codes, 6 inventory slots. - **End-of-block extensions** (append-only, earlier offsets never move, so old clients keep working): extension area `EXT1` (in-game time of day, day/night speed, production table entry count) and production table `prods[128]` (buildings currently training / researching / constructing / upgrading, queue, total duration, elapsed time, whether stuck). **Only use them if the magic matches.** ## 3. Reading events ```text head = ring.writeSeq (offset 8) for seq in (cursor, head]: e = ring.events[(seq - 1) % 8192] if e.seq > seq: one was lost (overwritten because you read too slowly) elif e.seq != seq: not fully written yet, read it next time else: handle e ``` The event struct is 64 bytes: `seq kind clockMs addr handleLo handleHi typeId owner a b x y value extra`. - Derived by diffing two consecutive publishes (precision = publish period): `unit.appeared / unit.died / unit.removed / unit.damaged / order.changed / hero.levelup / owner.changed / item.appeared / item.removed / game.started`; - Engine-level (recorded by the runtime on the game thread as they happen, one for **every single hit**): `damage` (source, damage type, attack type, position, actual HP lost, pre-armor damage), `killed` (killer); - Derived by tracking the production table: `production.done` (four-character code of what finished, category, game seconds taken; also emitted for opponents); - Checked by the runtime on every publish: `spell.cast` (the spell's cooldown started: `a` spell four-character code, `b` level, `value` cooldown seconds, `x/y` cast point), `player.left` (`a` player number, `b` new slot state), `selection.changed` (the local player's selection; the full list is in the world block's extension area), `game.ended` (left the game); - Screen messages: `message` (`a` = message sequence number; look up the full text in the shared memory `Local\War3Msgs_`: 128 entries × 256 bytes, covering game hints, chat and system messages; `b` = message frame number); - UI & input (after `input_enable`): `ui.click` (`a` canvas item id, `b` 1 left button / 2 right button), `ui.hover`, `hotkey` (`a` hotkey id, `b` virtual-key code), `mouse.world` (`x/y` ground coordinates, `value` = 1 means it was swallowed); modifier keys are all in `extra`. ## 4. Issuing commands 1. **One client object occupies one lane**: hold `Local\War3FastMutex_`, find a free lane (or one whose owning process has died), and write the role, player number and your own pid. If the same process needs two roles, open two lanes; 2. Fill slots: semantic command flag, opcode, `args[11]`, deadline `deadlineMs`; 3. After all slots are written, mark them submitted and increment the lane's `submitSeq` by 1; 4. Wait for the `Local\War3FastDone__` event (or poll), read the receipts, and return the slots. The runtime executes commands in batches inside the game thread's event dispatch: each drain has a time budget of **4 ms** (real high-resolution timer); anything over budget is left for the next dispatch. **Slots past their deadline are never executed** — so you never get "old commands running again after unpausing". `args` indices: `0..2` unit (address, handle lo, handle hi), `3` order ID or four-character code, `4..6` target, `7/8` x / y (float bits), `9` extra (player number / slot number / toggle / queue flag), `10` mode (0 no target / 1 point / 2 target). ### Opcodes | Opcode | Name | Description | |---|---|---| | 1 | `point` | Point order for a unit (move / attack-move / patrol / attack ground / point-targeted spell). extra bit0 = queue (insert after the current order) | | 2 | `target` | Target order for a unit (right-click attack / harvest / repair / unit-targeted spell / pick up item); the target must be visible | | 3 | `immediate` | No-target command (stop / hold position / train / research / upgrade / no-target spell) | | 4 | `build` | Worker constructs a building (coordinates aligned to 32) | | 5 | `learn` | Hero learns an ability | | 6 | `use_item` | Use inventory slot extra | | 7 | `revive` | Revive a hero at the altar | | 8 | `rally` | Rally point (point / target) | | 9 | `buy` | A shop sells an item to a nearby hero | | 10 | `item_drop` | Let go of an item: give it to a teammate, sell it to a shop (`code` = the receiving unit), or drop it on the ground | | 20 ~ 25 | Queries | `q_tech` tech count, `q_feasible` feasibility, `q_visible` visibility, `q_mine_gold` gold left in a mine, `q_captain` computer captain, `q_dead_heroes` dead hero list | | 30 | `pause` | Pause / resume | | 40 ~ 50 | Camera | Read camera state, set field, look at a point, follow, reset, rotate, bounds, smoothing, UI visibility, clean view, fog | | 60 ~ 63 | HUD | Quest button text, quest panel title and description, refresh, read whether the panel is open | | 70 | `jass` | Call a JASS native by name (1291 of them): the name and string arguments go in the slot's extra area, the other arguments go into `args` according to the signature; the return value is in `value[0]`. Only for local-tool lanes; anything that takes a function argument or would suspend the script thread is rejected. See [JASS channel](https://war3ai.com/en/docs/jass/) | | 71 / 72 | `jass_handle_of` / `jass_unit_of` | Convert between snapshot units and JASS handles (the handle pairs in the snapshot are not JASS handles) | | 73 | `canvas_enable` | Create the canvas shared memory and install the draw hook; any lane can send it (the canvas only draws on the local screen). The first call has to install the hook, so give it a timeout of 2 seconds or more | | 74 | `input_enable` | `extra` = 1 takes over the game window's input (canvas item clicks / hover, hotkeys, ground clicks), 0 = hands it back. Input block `Local\War3Input_`: 128-byte header + 32 hotkeys × 16 bytes; you write the hotkey table and mouse switches, and the runtime writes back the mouse position, the ground point under the cursor and the hovered item. Any lane can send it (it only affects local input). See [UI & input](https://war3ai.com/en/docs/ui-input/) | ## 5. Receipts A receipt is 52 bytes (+8 bytes of timing): `status`, `engineReturn`, `verdict` (rejection reason code), `orderBefore / orderAfter` (the unit's order read back in the same frame), `value[8]` (query results), `execUs` (microseconds this command took to execute on the game thread), `engineUs` (the portion spent in the engine's own order function). For all status codes and reason codes, see [Receipts and reason codes](https://war3ai.com/en/docs/reason-codes/). ## 6. Lane roles | Role | What it can do | |---|---| | `dev` | Local tools: semantic commands (commanding the local player's units) + the JASS channel | | `player` | Semantic commands only, and only for units owned by the lane's player (anyone else's = `not_owner`) | | `observer` | Queries, camera, reading HUD panel state, and enabling the canvas and local input only; everything else is `forbidden` | Two AIs playing each other = two `player` lanes in the same game (player 0 / player 1). > **Warning** > > In local mode, the role is declared by the client itself (a convention, not a security boundary). On the [Arena](https://war3ai.com/en/arena/), the referee process creates the lanes and hands only the `player` lane to each contestant. ## 7. Semantics verified in live games - Right-click (smart) on an enemy = attack **this one** (both the order target and the task target are that unit); a raw attack order sent as a target command only switches to the attack order without recording the target, so the unit goes off and attacks something else nearby; - The engine won't accept target commands on units you can't see: after nightfall, distant camps fall into the fog of war and every right-click is rejected (1001); - A build being "accepted" only means the worker took the order: a spot inside a forest is also accepted on the spot, and the worker only fails once it gets there; obviously occupied spots are rejected immediately; - A hero can only be revived about 3 game seconds after dying; it's also rejected if you don't have enough food (heroes cost food); - Items in an inventory don't count as ground items; picking one up emits `item.removed`; - While paused, the engine clock stops, but commands can still be issued; - A game started minimized has its simulation stopped (the clock doesn't move). --- # Receipts and reason codes > Every command's receipt carries a status code and a reason code. They're how bots and agents correct themselves, turning “why didn't it work?” into a machine-readable number. ```python r = g.train(barracks, "hfoo") bool(r) # False r.status # 1 -> rejected r.verdict # 3 -> not enough food r.reason # 'rejected(人口不够)' (SDK output: "rejected (not enough food)") r.exec_us # microseconds this command took on the game thread ``` `if r:` is equivalent to `r.status == 0` (the engine accepted it). ## Status codes: `status` | Code | Name | Meaning | Common causes | |---|---|---|---| | 0 | `accepted` | The engine accepted it | — (but accepted ≠ done; see below) | | 1 | `rejected` | Rejected by the engine | See `verdict` | | 2 | `bad_unit` | The unit doesn't exist or its handle doesn't match | The unit is already dead; you used a stale unit object | | 3 | `not_owner` | Not your unit | Commanding someone else's unit as a `player` | | 4 | `fault` | Exception during execution (caught by the runtime; it won't take down the game) | Please report it with steps to reproduce | | 5 | `bad_args` | Bad arguments | Wrong coordinates, slot index or four-character code | | 6 | `unsupported` | Not supported | This runtime version doesn't have that capability | | 7 | `bad_target` | Invalid target | The target is gone; wrong target type | | 8 | `forbidden` | Not allowed for the lane's role | Issuing commands as an `observer` | | 97 | `cancelled` | An exception was raised inside a batch block, so nothing in the batch was sent | Code inside a `with g.batch():` block raised an error | | 98 | `held` | The unit is held by a higher-priority layer, so the command wasn't sent | The reference brain's reflex layer or a manual command from the console is holding the unit | | 99 | `timeout` | Timed out | The deadline passed while the game was paused or lagging (expired commands are never executed) | ## Reason codes: `verdict` When a command is rejected, the runtime explains why using the engine's own feasibility check. You can also ask before issuing the command: `g.can_do(unit, code)` returns the same codes. | Code | Meaning | What to do | |---|---|---| | 0 / 220 | OK | — | | 3 | Not enough food | Build food structures; watch `g.production(b).blocked` to catch it early | | 8 | Not enough gold | Wait for gold; check `g.can_afford(code)` before issuing | | 9 | Not enough lumber | Put more workers on lumber | | 32 | Training queue full (7 slots) | Queue only 1 at a time: queue the next when `g.queue(b)` is empty | | 183 | Missing prerequisite tech / building | Build the prerequisite first, or tier up | | 185 | Building is busy | The altar is reviving a hero; the town hall can't upgrade while its queue isn't empty | | 221 | Not available / under construction / upgrading / already exists | The hero already exists (use `revive` if it's dead); this shop doesn't sell that | | 89 | Shop not stocked yet | At game start, items become available at the stock time in the item table; a newly built shop starts counting from the moment it's finished | | 1001 | Target not visible | The target is in fog of war or the black mask; `attack_move` to its position | ## Accepted ≠ done A receipt only tells you that the engine accepted the command, and it's read back in the same frame. It can't account for what happens afterward: | Command | Can still fail after being accepted | How to confirm | |---|---|---| | Build | A spot in a forest is accepted immediately too; it fails when the worker gets there | Use `build_near` (it tracks whether the foundation appears), or wait for `production.done` | | Cast | Interrupted, out of mana | On the next tick, check whether `g.cooldown(u, ability)` has started | | Train | Queued, but never starts because there's not enough food | `g.production(b).blocked` | | Move / attack | Overridden by other logic (or a higher-priority layer) | `g.current_target(u)`, `g.order_of(u)` | ## Query APIs These APIs don't issue orders; they only ask the engine. The result is also placed in the receipt's `value` (the SDK returns the value directly): | API | Returns | |---|---| | `g.can_do(u, code)` / `g.can_do_many([(u, code), ...])` | The reason codes in the table above | | `g.tech(code, player=None)` / `g.tech_many([...])` | Research level / number of completed buildings (upgrade chain included) | | `g.visible(x, y)` | Whether this point is visible to us | | `g.gold_left(mine)` | How much gold is left in a mine | | `g.enemy_ai_plan(enemy_unit)` | Where the computer opponent's captain plans to lead its troops (computer AI only) | --- # Data sources > Where each kind of data comes from and how precise it is. When something looks wrong, check this page first. | Data | Source | Precision | |---|---|---| | Units, resources, orders, abilities, buffs, inventory | World block pushed by the runtime every 50 ms | Publish interval (adjustable down to 16 ms) | | Damage and kill events | Recorded by the runtime on the game thread, every hit | Immediate | | Other events (appear, die, order change, level up…) | Diff between two consecutive publishes | Publish interval | | Production table (train / research / build / upgrade) | Timer fields of the engine's production abilities + elapsed time accumulated by the runtime | About ±0.2 game seconds | | Combat stats, counter table | The game's own data tables (extracted from the game on your machine) | Excludes item, aura and buff modifiers | | Pathfinding | The engine's terrain walkability (128 per cell) + trees + building footprints, with A* on the SDK side | One cell; gaps narrower than a cell count as blocked | | In-game time | JASS `GetFloatGameState(GAME_STATE_TIME_OF_DAY)` | Publish interval | | Visibility | Per-unit, per-player visibility mask computed by the runtime | Publish interval | | Tech counts, feasibility, gold remaining | Fast lane queries straight to the engine | Immediate | ## Game data isn't distributed with the code Unit tables, abilities, items, heroes, buffs, the damage/counter table and so on come from Blizzard's game files and **are not in the repository**. Once you set the game folder in Farsight's **Control Center**, they're extracted from your own game automatically; you can also run it by hand: ```bash python data/tools/extract_game_data.py ``` The output goes to `data/game/` (not in git): the raw `.slk` / `.txt` files, plus the processed `units.json`, `names.json`, `skills.json`, `items.json`, `heroes.json` and `buffs.json`. ## Some specific numbers | Quantity | Value | |---|---| | One day | 480 game seconds (240 each for day and night); one hour = 20 game seconds; games start at 8 AM | | Daytime | 6:00–18:00 | | Armor coefficient | 0.06 (from the game data tables) | | World block capacity | 16 players, 1024 units, 256 unit details, 256 ground items, 128 production entries | | Trees | Up to 4096 destructibles, refreshed every 2 seconds | | Event ring | 8192 entries; reading too slowly drops events (the SDK can detect this) | | Map grid | 128 game units per cell, up to 256 × 256 | ## Examples calibrated against measurements - Production times: peasant 14.9, farm 34.9, Iron Forged Swords 59.9 game seconds, matching what the runtime pushes (error under 0.2 seconds); - Combat stats checked against the in-game panel: Paladin 650 HP, 255 mana, 3.9 armor, attack 24–34; footman with level-1 attack upgrade 13–15; - Pre-armor damage from engine damage events (14 / 15 / 15) falls within the range computed by `stats()`; - Pathfinding: on Echo Isles (116 × 88 cells), the ground distance to the opposing town hall is 10642 (9856 in a straight line); building the grid takes 18 ms, and one A* search takes about 1 ms. --- # FAQ > Is this a cheat? Which versions are supported? What can the AI see and do? Can I use it without programming?… ## Is this a cheat? No. It's a development interface for AI research and entertainment, used only with **a client you legally own**, playing against the computer or other AIs locally, over LAN, or in self-hosted games. It **must not be used on Battle.net or any server with anti-cheat**, and it offers no features aimed at games against real people. See [Terms of use](https://war3ai.com/en/docs/legal/). ## Which game versions are supported? Only **Warcraft III 1.27** (The Frozen Throne) for now. Versions 1.24–1.28 share the same engine structure; multi-version support (per-version symbol tables, a signature-scan fallback, and a capability list from a startup self-check) is phase P4 of the [roadmap](https://war3ai.com/en/roadmap/). Versions 1.29 and later, as well as Reforged, use a different engine that would need separate work, and there's no commitment for them yet. ## Does it modify my game files? No. The runtime is injected while the game is running and **does not modify Game.dll on disk** or any other game file. To run multiple instances, it simply copies the original `War3.exe` launcher as-is under a new name. Game data (unit tables and so on) is extracted from your own game and isn't distributed with the code. ## What can the AI see? Basically everything a pro player would want to know, updated every 50 ms: - Every player's gold, lumber and food; every unit's position, HP and mana, current order, **what it's attacking**, level and XP; - Heroes' and units' ability levels and remaining cooldowns, their buffs and inventory; - What each building is training / researching / constructing / upgrading, its progress, and whether it's blocked on food; - Items on the ground, trees, the map's walkable / buildable grid, start locations, and in-game time (day/night); - The event stream: units appearing and dying, **every single hit of damage** (who dealt it, attack type, pre-armor damage), kills, production completing, hero level-ups… - You can also ask the engine directly: can this be done right now, and if not, why; what level a tech is at; whether a point is visible; how much gold a mine has left; where the computer opponent plans to send its army. On top of that, the SDK computes combat stats (counters, armor, attack/armor upgrades), "how many seconds to kill it," and ground pathfinding. See the [API catalog](https://war3ai.com/en/api/) for every API. ## What can the AI do? Pretty much everything a player can: move, attack-move, attack a specific target, stop, hold position, patrol, attack ground, gather, repair, build (with automatic placement), train / research / upgrade, cancel, learn skills, cast (on a unit / on a point / with no target), set rally points, revive heroes, pick up / use / drop / give / sell items, buy, and Call to Arms; Shift-queueing, marching along waypoints, and one worker building several structures in a row; plus game speed, pause and speech bubbles. Every command returns a receipt. Beyond player actions, it can also draw your own panels and markers on the game screen ([canvas](https://war3ai.com/en/docs/canvas/)), and in single-player games call the 1291 JASS functions available to map makers ([JASS channel](https://war3ai.com/en/docs/jass/)). ## Can I use it in RPG / custom maps? Yes. Pick an RPG map and choose the "Companion example" scheme for the instance. Once the game starts you play yourself, with an AI partner at your side that fights with you, heals you and chats with you — see [RPG companion](https://war3ai.com/en/docs/companion/). `g.map_data` reads the names of the map's custom units; the [JASS channel](https://war3ai.com/en/docs/jass/) can spawn units, set alliances, pop up panels… how you play is up to you. Actions that change the world only work in single-player games (they would desync in multiplayer), while the canvas is safe in multiplayer too. ## Can I use it without programming? Yes. Set up your environment with the [Quickstart](https://war3ai.com/en/docs/quickstart/), then read [Write a bot with an LLM](https://war3ai.com/en/docs/ai-bot/): you describe your strategy in plain language and the LLM writes the code. If something goes wrong when it runs, tell it the error or what you saw in the game and have it fix the code. ## Is it Python only? The SDK is Python. Between the runtime and external programs there's nothing but a shared-memory protocol ([W3P](https://war3ai.com/en/docs/protocol/)), so any language that can read and write Windows shared memory can connect. The easier route is the [gateway](https://war3ai.com/en/docs/gateway/) (WebSocket / JSON): JS, C#, Go, Rust, browser pages and programs on another machine can all call the same APIs; LLM agents can hook up [MCP](https://war3ai.com/en/docs/mcp/) directly. ## Which LLM works best? Any mainstream model that can write code will do. What matters isn't the model but **giving it the right material** (the manual + `api.json` + an example) and requiring it to use only methods that exist in the API catalog. Real-time in-game decisions (coaching, unit dialogue) are latency-sensitive, and local MoE models do very well there; see [LLM strategy coach](https://war3ai.com/en/docs/llm-coach/) and [Speech bubbles and local models](https://war3ai.com/en/docs/speech/). ## Will it slow the game down? One world-state capture takes a median of 0.5–0.9 ms on the game thread (100–120 units), once every 50 ms. Commands take a few microseconds each on the game thread; each drain has a 4 ms time budget, and anything left over waits for the next drain, so the game is never held up. Every call into the game is exception-guarded: if a bot crashes, only that side stops, and the game keeps running. ## Can I run multiple games at once? Yes. `runtime/farm.py` handles multi-instance orchestration, with one number per instance; start and stop them in the [Farsight console](https://war3ai.com/en/docs/console/). Your bot connects to a specific instance with `--inst N`. ## Can two AIs play each other? Open two `player` channels in the same game (`--player 0` / `--player 1`) and you have AI vs. AI. In local mode, fairness is by convention; official matches with a referee, vision filtering and ownership checks happen on the [Arena](https://war3ai.com/en/arena/) (phase P6). ## Does it support Mac / Linux? Only Windows 10 / 11 for now. ## What license does it use? The license will be published with the official release. Third-party components keep their own licenses (for example, MinHook is BSD-2); AMAI has a custom license, so data derived from it isn't distributed with the project. Instead, it's pulled from AMAI's public repository and generated at install time. ## Where do I report problems? An issue-reporting channel will open after the official release. When you report a problem, include the instance number, the output of `python -m openwar3 status`, and steps to reproduce. First, see whether [Debugging and performance](https://war3ai.com/en/docs/debugging/) solves it. --- # Terms of use > What you can and can't do, analytics on this website, and trademark and third-party license notices. By using this project, you agree to stay within these terms. ## You may - Use it on a Warcraft III 1.27 client **that you legally own**; - Have AIs play against computer opponents or other AIs locally, offline, over LAN or in self-hosted games; - Research, teach, have fun, and stream your own AI games; - Build on the SDK, reference brain, examples and tools, in compliance with their licenses. ## You may not - **Use it on Battle.net or on any server or platform with anti-cheat**, or run it at the same time as an active anti-cheat session; - Use it to gain an unfair advantage in games against real people; - Distribute Blizzard's game files or data extracted from them (this project doesn't either: users extract game data from their own game); - Violate the runtime's license terms. ## Our technical commitments - We don't modify `Game.dll` on disk or any game file; all changes happen at runtime; - Running multiple instances just copies the original `War3.exe` launcher as-is under a new name; - The project contains no Blizzard code or game files. ## Your responsibility Laws on reverse engineering and game modification vary by region. **You are responsible for determining whether using this project is legal where you live, and you bear the consequences of using it.** This project is provided "as is," without warranty of any kind, express or implied. ## Analytics on this website This website (war3ai.com) uses Microsoft Clarity to measure visits: which pages are viewed, where visitors come from, how long they stay, where they click and how far they scroll, plus anonymous session replays and heatmaps. We use it only to improve the docs and pages. - No sign-up is required, and no identifying information such as names or email addresses is collected; text in input fields is masked by default and never recorded; - Clarity stores cookies in your browser to tell repeat visits by the same visitor apart; the data is processed by Microsoft, see the [Microsoft Privacy Statement](https://privacy.microsoft.com/privacystatement); - To opt out: open any URL on this site once with `?stats=off` added to the end, and this browser won't be tracked from then on (`?stats=on` turns it back on). Blocking `clarity.ms` with your browser's tracking protection also works, and the site keeps working as usual. Farsight, the SDK and the runtime on your machine include no analytics of this kind. Farsight connects to war3ai.com in only two cases: at startup and every 6 hours after that, it reads the version manifest to check for a new version; and when you click submit on the **Feedback** page, it sends the feedback you wrote plus a machine identifier (a salted hash of the system ID that can't be reversed to the original value, used to prevent spam). Diagnostic information is attached only if you tick the box, and you can preview it before sending. When these two kinds of requests reach war3ai.com, the server logs the IP address, the country or region Cloudflare determines, and the client version (User-Agent), to prevent abuse and count how many machines are running Farsight. Records from version checks are deleted automatically after 90 days; feedback, together with this information, is kept until a maintainer has processed it and deleted it. This data is visible only to project maintainers in the backend, and is never shared with anyone else. ## Trademarks Warcraft® is a trademark or registered trademark of Blizzard Entertainment, Inc. War3AI / OpenWar3 is an independent community project; it is not affiliated with, endorsed by or sponsored by Blizzard Entertainment. Other product names mentioned here (Claude, GPT, Gemini, Qwen, etc.) belong to their respective owners and are used only to describe compatibility. ## Third-party components and data | Component / data | License | Handling | |---|---|---| | MinHook | BSD-2-Clause | Used with the runtime; its license notice is retained | | AMAI | Custom license | Not distributed with the project; when `start.bat` deploys, it pulls it from AMAI's public repository, and a tool then generates the data the reference brain needs | | Game data (units, abilities, items, etc.) | Blizzard | Not distributed with the project; users extract it from their own game | | Factual data extracted from public match replays (building placements, opening build orders) | — | Factual data only, used by the reference brain | --- # API catalog (api.json) Status: verified = underlying path verified in live games; experimental = new API that already works and is still being verified item by item in live games; inferred = inferred / not fully tested. Latency:Pushed snapshot(Reads shared memory without waiting on the game thread (~0.05 ms)); Fast lane(~1 frame: executed in batches on the game thread); Control channel(20~40 ms (legacy path for UI-type operations)); Direct write(No game-thread queue: writes shared memory (canvas) or posts a message to the game window); Local compute(Pure computation or file reads; doesn't touch the game) ## Observe Read state without changing the game. Nearly all of it reads the pushed snapshot directly, with zero wait. - `snapshot(max_age: 'float' = 0.05)` [verified] [Pushed snapshot] Full state of the whole map (WorldState): .units .players .items .clock .me; repeated calls within max_age seconds return the same copy. ⚠ Workers inside a gold mine are not in the list; by default the whole map is visible (in the lockstep model everything exists locally), and only Game(fair=True) filters by vision. (Mechanism: W3P world block Local\War3World_ (pushed by the runtime every 50 ms, seqlock)) - `last_seen(owner: 'str | int' = 'enemy', max_age: 'float | None' = None) -> 'list'` [verified] [Pushed snapshot] Enemy units (or 'creep' for creeps, or a given player number) as last seen: [(the unit as it was then, the game clock at that time, seconds elapsed since)], newest first. Units seen dying are removed from the list. Both fair mode and normal mode record based on "what we can see right now" — this is the map in a player's head: scouted army, where the enemy hero was last seen, when the enemy expansion went up. max_age keeps only entries from within that many game seconds. (Mechanism: visibleTo of the pushed snapshot (every snapshot refresh records the visible enemy/creep units)) - `map()` [verified] [Pushed snapshot] This game's terrain table MapInfo: .walkable(x,y) .buildable(x,y) .at(x,y) .bounds (playable area) .starts (start locations) .cells (bit0 unwalkable, bit1 unbuildable). It takes a few seconds after the game starts to compute; returns None until it's ready. Trees aren't included (use trees()). (Mechanism: W3P map block Local\War3Map_ (computed in batches by the runtime after the game starts; IsTerrainPathable walk/build)) - `me() -> 'int | None'` [verified] [Pushed snapshot] Which player number I am (0~11). (Mechanism: World block header) - `resources(player: 'int | None' = None) -> 'dict | None'` [verified] [Pushed snapshot] {'gold','lumber','food_used','food_cap','gold_gathered','lumber_gathered'}; player defaults to us, and any player can be read. Returns None when it can't be read — don't treat that as 0. (Mechanism: World block players[16]) - `players() -> 'list'` [verified] [Pushed snapshot] All 16 player slots: Player(id, gold, lumber, food_used, food_cap, gold_gathered, lumber_gathered, race, known). (Mechanism: World block players[16]) - `units(owner: 'str | int' = 'all', types=None, alive: 'bool' = True) -> 'list'` [verified] [Pushed snapshot] Filter units by owner/type. owner: 'me' / 'enemy' / 'creep' / 'all' / player number. types: a set of four-character codes. (Mechanism: World block units[]) - `unit(handle) -> 'object | None'` [verified] [Pushed snapshot] Find a unit by handle pair (lo, hi) (order targets, task targets and events all give handle pairs). (Mechanism: World block by_handle) - `is_building(u) -> 'bool'` [verified] [Pushed snapshot] Whether it's a building (including towers). Determined by movement speed 0 in the unit table; the Undead hall has a footprint of 0, so don't go by footprint. (Mechanism: Snapshot + units.json (spd==0 = building)) - `my_workers() -> 'list'` [verified] [Pushed snapshot] Our workers (Peasants/Peons/Acolytes/Wisps). (Mechanism: Pushed snapshot) - `idle_workers() -> 'list'` [verified] [Pushed snapshot] Workers with nothing to do: no order and no task (workers you just gave a job this tick don't count). ⚠ Re-issuing a gather order to a worker that has a task interrupts its harvest cycle (income drops to zero). (Mechanism: Pushed snapshot (order slot + task slot)) - `my_heroes() -> 'list'` [verified] [Pushed snapshot] Our living heroes (dead ones are in the altar's revive list; see revive). (Mechanism: Pushed snapshot) - `my_army() -> 'list'` [verified] [Pushed snapshot] Our combat units: not workers, not buildings. (Mechanism: Pushed snapshot + units.json) - `my_buildings(types=None) -> 'list'` [verified] [Pushed snapshot] Our buildings (including towers and foundations under construction); types can restrict it to certain kinds, e.g. {'hbar'}. (Mechanism: Pushed snapshot) - `is_constructing(worker) -> 'bool'` [verified] [Pushed snapshot] Whether this worker is building something (or walking over to build / helping repair; includes jobs assigned this tick). Skip it when picking a builder, otherwise the previous foundation stops. (Mechanism: Pushed snapshot (order = building four-character code, or construction/repair order)) - `under_construction(building) -> 'bool'` [verified] [Pushed snapshot] This building isn't finished yet (HP not full). ⚠ Damaged buildings aren't at full HP either — good enough for the opening, but once fighting starts, combine it with timing. (Mechanism: Pushed snapshot (a foundation's HP climbs from very low all the way to full)) - `gold_mines() -> 'list'` [verified] [Pushed snapshot] Gold mines on the map. ⚠ A Night Elf Entangled Gold Mine and the neutral gold mine are two separate units at the same coordinates; send harvesters to your own one. (Mechanism: Pushed snapshot (ngol/egol/ugol)) - `enemies(fighters_only: 'bool' = False) -> 'list'` [verified] [Pushed snapshot] Units belonging to enemy players (not creeps). fighters_only: excludes workers and buildings. (Mechanism: Pushed snapshot) - `creeps() -> 'list'` [verified] [Pushed snapshot] Creeps (neutral hostile). ⚠ Vision shrinks at night; once distant camps fall into the fog of war, target commands on them are rejected (reason code 1001). (Mechanism: Pushed snapshot (owner 12 = neutral hostile)) - `life_mana(u) -> 'dict | None'` [verified] [Pushed snapshot] {'hp','hp_max','mana','mana_max'} (floats, raw engine values). Passing the unit you got from a snapshot is fine (it's swapped for the latest copy). (Mechanism: World block unit hp/hpMax/mana/manaMax) - `hero_info(hero) -> 'dict | None'` [verified] [Pushed snapshot] {'level','xp','skill_points'}. (Mechanism: World block unit level/xp/skillPoints) - `abilities(u) -> 'list'` [verified] [Pushed snapshot] [{code, level, cooldown, flags}]; buffs are in buffs(u). Only available for units that "have details" (heroes > player units > creeps, up to 256). (Mechanism: World block details: abilities (code/level/flags/cooldown remaining)) - `buffs(u) -> 'list'` [verified] [Pushed snapshot] Buff codes on the unit (e.g. 'BHds' Divine Shield, 'Bslo' Slow). See data/game/buffs.json for what each code does. (Mechanism: World block details: ability objects starting with B) - `cooldown(u, ability: 'str') -> 'float | None'` [verified] [Pushed snapshot] Seconds of cooldown left on this ability (game seconds); 0 = ready to cast; returns None if the unit doesn't have this ability (or has no details). (Mechanism: World block details: ability cooldown remaining (ability timer)) - `inventory(hero) -> 'list | None'` [verified] [Pushed snapshot] Four-character codes of the 6 item slots (None for empty slots); returns None if there's no inventory. (Mechanism: World block details: 6 inventory slots) - `current_order(u) -> 'dict | None'` [verified] [Pushed snapshot] {'order','target','x','y'}: the order the unit is currently carrying out (order is 0x000D00xx or a building four-character code; 0 = idle). target is a handle pair; use g.unit(target) to turn it into a unit. (Mechanism: World block unit order / order target / order target point) - `current_target(u)` [verified] [Pushed snapshot] The unit it is **actually attacking/chasing** (None if there isn't one). ⚠ After an attack order, the order slot quickly empties and the attack hangs on the task — to tell "what it's attacking", use this, not current_order. (Mechanism: World block unit task target) - `clock() -> 'float | None'` [verified] [Pushed snapshot] Engine game clock (game seconds; 0 while loading). At higher game speeds it runs faster than the wall clock. (Mechanism: World block header clockMs (engine game clock)) - `production(building)` [verified] [Pushed snapshot] What this building is producing: Production(kind, queue, duration, elapsed, blocked, progress, remaining…); returns None if it's idle. kind 'queue' (training/research/hero; queue has up to 7 slots, [0] is the one in progress) / 'construction' (being built) / 'upgrade' (upgrading a hall/tower); blocked = something is queued but hasn't started (usually not enough food — time to build a Farm); progress 0..1. Opponents' buildings can be inspected too (in fair mode, only buildings you can see). (Mechanism: World block production table (Aque/ABnP/AUnP ability objects + elapsed time tracked by the runtime; measured error < 0.2 game seconds)) - `queue(building) -> 'list'` [verified] [Pushed snapshot] Four-character codes in the training/research queue ([0] is in progress); idle or not a production building = []. (Mechanism: World block production table) - `all_production(owner: 'str | int' = 'me') -> 'list'` [verified] [Pushed snapshot] All production in progress [(building, Production)]. owner works like units(): 'me' / 'enemy' / player number / 'all'. Pro use: see what units the opponent is training, what tech it's researching, and when it tiers up (once you've scouted the buildings). (Mechanism: World block production table) - `path_distance(a, b) -> 'float | None'` [verified] [Pushed snapshot] Walking distance for a ground unit from a to b (a and b can be units or (x,y)); None if unreachable. On island maps, use this to decide "can ground units reach this creep camp/expansion" — it's more reliable than straight-line distance (it goes around forests, cliffs and buildings). Precision is one 128 cell; gaps narrower than a cell count as blocked. (Mechanism: Map block (engine IsTerrainPathable) + tree block + building footprints, A* on the SDK side (128 per cell)) - `reachable(a, b) -> 'bool | None'` [verified] [Pushed snapshot] Whether it can be reached on the ground (map block not ready = None). (Mechanism: Same as above) - `walk_path(a, b) -> 'list | None'` [verified] [Pushed snapshot] Path corner points [(x,y)...] (the last point is b); combine with path(units, points) to move the army along this route (around towers, via side paths). (Mechanism: Same as above) - `upkeep(player: 'int | None' = None) -> 'dict | None'` [inferred] [Pushed snapshot] Upkeep level: {'level': 'none'/'low'/'high', 'income': 1.0/0.7/0.4, 'next_at': food count at which the next level starts (None if there isn't one)}. Pro common knowledge: stay at 50 food while teching to tier 3 / getting attack/armor upgrades, and only go up to 80 right before the decisive fight. (Mechanism: Fixed 1.27 rule: 0~50 food no upkeep, 51~80 income ×0.7, 81~100 ×0.4) - `xp_to_next(hero) -> 'int | None'` [verified] [Pushed snapshot] How much XP the hero still needs for the next level (level 10 = 0). (Mechanism: World block level/xp + MiscGame NeedHeroXP formula) - `creep_camps(link: 'float' = 600.0) -> 'list'` [verified] [Pushed snapshot] Groups the (visible) creeps on the map into camps: [{'x','y','units','level','hp','max_level'}], sorted from nearest to farthest from our main base. level = total camp level (the usual measure of creeping difficulty), hp = total HP. Combine with time_to_kill / path_distance to pick camps. (Mechanism: Pushed snapshot (creeps within 600 of each other are grouped together) + levels from units.json) - `buff_info(code: 'str') -> 'dict | None'` [verified] [Local compute] What a buff code is: {'ability','effect','dur','hero_dur','targets'} (e.g. 'Bslo' -> Slow). When a code has multiple rows, the first row is returned. (Mechanism: data/game/buffs.json (BuffID -> ability/effect/duration from AbilityData.slk)) - `stats(u, player: 'int | None' = None)` [verified] [Pushed snapshot] The unit's combat stats combat.UnitStats: max HP/mana, armor (including attack/armor upgrades and hero agility), armor type, movement speed, day/night sight range, weapons (what it can hit, range, attack cooldown, damage range, attack type, splash). u is a unit (automatically uses its owner's tech and its hero level) or a four-character code (player defaults to us). Then use .dps_vs(other) / .hits_to_kill(other) / combat.time_to_kill(group, other). ⚠ Doesn't include items, auras or buffs. (Mechanism: Data tables (UnitBalance/UnitWeapons/UpgradeData/MiscGame) + live tech levels + hero level) - `time_to_kill(attackers, target) -> 'float | None'` [verified] [Pushed snapshot] How many game seconds this group of units needs to kill target together (uses target's current HP; accounts for counters, armor and attack/armor upgrades; not for movement, splash or healing). Pro use: focus fire the one that "dies fastest" (smallest time_to_kill) first, not the closest one. Can't hit it = None. (Mechanism: stats() + live HP) - `time_of_day() -> 'float | None'` [verified] [Pushed snapshot] In-game time of day (hours, 0~24). The game starts at 8 AM; a full day = 480 game seconds (240 seconds each for day and night, scaled by the day/night speed). Returns None when it can't be read (old runtime / not in a game). (Mechanism: World block extension area: GetFloatGameState(GAME_STATE_TIME_OF_DAY)) - `is_night() -> 'bool | None'` [verified] [Pushed snapshot] Whether it's night now (18:00~6:00). Pro play: at night creeps are asleep (you get the first hit when creeping, without being surrounded), and every unit's sight range shrinks (a good time for surprise attacks); Night Elf Sentinels/units are invisible next to trees at night. Returns None when it can't be read. (Mechanism: World block extension area (daytime is 6~18)) - `seconds_until(hour: 'float') -> 'float | None'` [verified] [Pushed snapshot] Game seconds until the in-game time reaches hour o'clock (e.g. seconds_until(18) = time until nightfall, for planning night creeping). (Mechanism: World block extension area + a 480-second day (measured 20 game seconds per hour)) - `items_on_ground() -> 'list'` [verified] [Pushed snapshot] Items on the ground [Item(addr, handle_lo, handle_hi, type, x, y, life)]. Picking one up or using it emits an item.removed event. (Mechanism: World block items[] (ground items only: holder handle is all FF)) - `trees(x: 'float | None' = None, y: 'float | None' = None, limit: 'int' = 60) -> 'list'` [verified] [Pushed snapshot] Living trees (those whose targType in DestructableData includes tree); given (x,y), sorted from nearest to farthest, up to limit trees. Each is Tree(addr, handle_lo, handle_hi, type, x, y, life), and can be passed straight to gather to harvest lumber. (Mechanism: Tree block Local\War3Trees_ (refreshed every 2 seconds)) - `events() -> 'list'` [verified] [Pushed snapshot] What happened since the last call: unit.appeared / unit.died / unit.removed / unit.damaged / order.changed / hero.levelup / owner.changed / item.appeared / item.removed / game.started (these come from diffing publishes; precision = publish period 50 ms), plus the engine-level damage / killed (the runtime records them on the game thread as they happen, so there's one for **every single hit**): damage: handle = the unit being hit, .source_addr = who hit it (snapshot().unit_by_addr turns it into a unit), .value = actual HP lost, .raw_damage = pre-armor damage, .attack_type (normal/pierce/siege/magic/chaos/hero/spell), .damage_type killed: this hit killed it, .source_addr = the killer plus production.done, derived by the runtime tracking the production table (precision = publish period): unit = the building, .done_code = four-character code of what finished, .done_kind = 'training' (units/heroes/revives) / 'research' / 'construction' (building completed) / 'upgrade' (tier-up/tower upgrade), .value = game seconds taken Added 09-25: spell.cast: unit = the caster, .spell spell four-character code, b level, value cooldown seconds, x,y cast point (recognized when the spell's cooldown starts; precision = publish period) player.left: .player the number of the player who left / was removed after being defeated; game.ended: left the game selection.changed: the local player's selection changed (get the units with g.selection()) message: a line in an on-screen message frame (game hints, chat, system): .text full text, .frame message frame number, .chat = {'channel', 'sender', 'text'} (when it's chat; this is where you read what the player typed in the chat box) ui.click / ui.hover / hotkey / mouse.world: UI & input (g.ui); .key is the canvas key / hotkey string Each one is Event(seq, kind, clock, addr, handle, type, owner, a, b, x, y, value, extra). Fair mode (fair=True) only gives: events for your own units, events for units visible right now (or still visible within the last 1 second), damage dealt to us or by us, and local UI / message / game events. (Mechanism: Event ring Local\War3Events_ (publish diffing + damage events captured by the runtime)) - `selection() -> 'list'` [verified] [Pushed snapshot] The units the local player has selected right now (the main unit comes first; up to 12). Any change to the selection fires a selection.changed event. (Mechanism: W3P world block extension area selAddrs (the runtime includes the local player's selection in every publish)) - `messages() -> 'list'` [verified] [Pushed snapshot] Messages newly shown in the on-screen message frames since the last call: [{'text', 'frame', 'repeat', 'seq', 'game_ms'}]. Game hints ("You need more farms", "Can't build there"), chat and system messages are all here; frame tells you which message frame. Same batch as the message events in the event stream (each has its own cursor). (Mechanism: Shared memory Local\War3Msgs_ (screen messages captured by the runtime)) - `tech(code: 'str', player: 'int | None' = None) -> 'int | None'` [verified] [Fast lane] Research level / number of completed buildings (upgrade chains count: a Castle also counts as htow). player defaults to us; any player can be queried. (Mechanism: W3P query q_tech (the engine's player tech count)) - `can_do(u, code: 'str') -> 'int | None'` [verified] [Fast lane] The engine's feasibility verdict: 0/220 OK; 3 not enough food, 8 not enough gold, 9 not enough lumber, 32 queue full, 183 missing prerequisite, 185 altar is reviving, 221 no such item/under construction. ⚠ Always 221 for a worker constructing a building, so it can't be used to check placement (use build_near). (Mechanism: W3P query q_feasible (engine feasibility check)) - `can_do_many(pairs) -> 'list'` [verified] [Fast lane] Ask many can_do at once: pairs = [(unit, four-character code), ...]; returns a list of verdict codes in the same order (None where it couldn't be asked). When planning what to build/train in a tick, ask about everything at once first — N times faster than one can_do at a time (reference brain 09-23: build planning 76 -> 25 ms). (Mechanism: W3P query q_feasible × N, submitted as one batch) - `tech_many(codes, player: 'int | None' = None) -> 'dict'` [verified] [Fast lane] Look up many tech/building counts at once: {four-character code: count or None}. (Mechanism: W3P query q_tech × N, submitted as one batch) - `visible(x: 'float', y: 'float') -> 'bool | None'` [verified] [Fast lane] Whether we can see this point right now (not in the fog of war/black mask). Bots in fair mode should only use visible enemies. (Mechanism: W3P query q_visible (visible / fog of war / black mask)) - `gold_left(mine) -> 'int | None'` [inferred] [Fast lane] How much gold is left in a gold mine. (Mechanism: W3P query q_mine_gold (the engine's remaining gold in the mine)) - `enemy_ai_plan(enemy_unit) -> 'dict | None'` [verified] [Fast lane] The computer AI's captain: where it's taking its army (you know which part of your base it will attack before it leaves home). Only works against computer opponents; returns None if the unit isn't following a captain. (Mechanism: W3P query q_captain (the computer captain that enemy units follow)) - `order_of(u) -> 'int | None'` [verified] [Pushed snapshot] The unit's current order, **including ones you just issued this tick** (uses the new order from the receipt until the snapshot catches up). ⚠ 09-23 live game: hello_bot had just sent a peasant to build a Farm, and in the same tick rush_bot saw it as "idle" in the snapshot and sent it to build Barracks, so the Farm was abandoned halfway over and over. When picking "idle/not building" units, use this instead of u.order. (Mechanism: Snapshot order + commands just accepted in this process (receipts)) - `can_afford(code: 'str') -> 'bool'` [verified] [Pushed snapshot] Whether current gold/lumber is enough to buy code (units, buildings; by the prices in units.json). Anything missing from the price table is treated as affordable. ⚠ Tier-up four-character codes have cumulative prices in the table, so this errs on the conservative side; the engine's receipt is the final word. (Mechanism: Our resources from the pushed snapshot + prices from units.json) - `map_data()` [verified] [Local compute] Data for the map being played (openwar3.mapdata.MapData): name_of('HC07') for the names of custom units/items/abilities, hero_names, tooltip. Most units in RPG maps are created by the map itself and aren't in the built-in name table. Returns None for games not started by the launcher (the map file can't be found). (Mechanism: Map file (the launcher's --map path): w3u/w3t/w3a + wts; for protected maps, reads the TXT files inside the map) ## Command Make units do things. Lands in about one frame, with a receipt for every command. - `batch() -> 'Batch'` [verified] [Fast lane] Combine a tick's commands into one batch: with g.batch() as b: g.attack(archers, target) # returns Pending; becomes a receipt only after the block ends g.move(wounded, *home) g.cast(hero, "thunderclap") print(b.sent, b.wait_ms, [r.reason for r in b.receipts]) Sent one by one, every command waits for the game thread once (about 10 ms); a batch waits only once — this is how the reference brain cut a round from 48 -> 26 ms on 09-23. * Claims are still checked per command (a unit held by someone else gets a held receipt immediately and isn't added to the batch); * Commands inside the block return Pending: reading its .ok before the block ends raises an error (the receipt doesn't exist yet); after the block ends, use it like a Receipt; * An exception inside the block = the whole batch is discarded (status 97 cancelled), and the units it held are released; * Queries (can_do / tech / visible …), build_near and buy aren't batched and are still asked immediately — their results are needed on the spot; to ask about many things at once, use can_do_many / tech_many; * A nested with g.batch() merges into the outermost batch; beyond 16 commands the runtime automatically splits it into several segments (one wait per segment). (Mechanism: Commands in the block are gathered into one batch and submitted once when the block ends (executed in the same frame, waiting on the game thread only once)) - `move(units, x: 'float', y: 'float', queue: 'str | None' = None)` [verified] [Fast lane] Move to (x,y) without fighting on the way (use this for retreating). Accepts a single unit or a list (ordered together in the same frame). queue='after': go after finishing the current task (inserted after the current order). Receipt values[0] = how many orders this unit has queued after the command (including the current one). (Mechanism: W3P point: move (extra bits = queue mode)) - `attack_move(units, x: 'float', y: 'float', queue: 'str | None' = None)` [verified] [Fast lane] Attack-move (A-click on the ground): attacks any enemy met on the way. queue works like move. (Mechanism: W3P point: attack at a point) - `attack(units, target, force: 'bool' = False, queue: 'str | None' = None)` [verified] [Fast lane] Attack target. Uses right-click by default (on an enemy = attack this one; measured 09-23: both the order target and the task target are it). ⚠ The target must be in vision; targets you can't see are rejected (reason code 1001). force=True uses the attack order 0x0F (needed to attack your own units/neutral critters) — measured: it only switches to the attack order and doesn't remember the target, so the unit goes off to attack other enemies nearby. Don't use it to attack a specific target. (Mechanism: W3P target: target command (right-click smart)) - `stop(units)` [verified] [Fast lane] Stop everything it's doing (order ID 0x000D0004), and clear queued orders too. (Mechanism: W3P immediate: stop) - `hold(units, queue: 'str | None' = None)` [verified] [Fast lane] Hold position (doesn't chase; only attacks what's in range). (Mechanism: W3P immediate: holdposition) - `patrol(units, x: 'float', y: 'float', queue: 'str | None' = None)` [inferred] [Fast lane] Patrol between the current position and (x,y). (Mechanism: W3P point: patrol) - `attack_ground(units, x: 'float', y: 'float', queue: 'str | None' = None)` [verified] [Fast lane] Attack ground: artillery fires at an area (to hit invisible units, units behind trees, or to block a choke point). Only units that can attack ground accept it. (Mechanism: W3P point: attackground (siege units / Mortar Teams / Demolishers)) - `cancel(building)` [verified] [Fast lane] Cancel: the last slot of the training/research queue (refunded), a building under construction (75% refunded), or a hall that's upgrading. (Mechanism: W3P immediate: cancel) - `path(units, points, attack: 'bool' = False)` [verified] [Fast lane] Move through a series of points in order (Shift-click waypoints: patrol routes, routing around towers, scouting routes). attack=True makes every leg an attack-move. Submitted once; one receipt per point (in points order). (Mechanism: One batch: the first leg runs immediately, the rest are inserted in reverse order with queue='after' (the engine can only insert right after the current order)) - `gather(workers, target, queue: 'str | None' = None)` [verified] [Fast lane] Harvest gold/lumber (target is a gold mine or a tree from trees()). ⚠ Only assign idle workers (idle_workers): re-issuing to a worker that has a task interrupts its harvest cycle. Pro use: go back to mining after building = gather(worker, mine, queue='after') after build(...). (Mechanism: W3P target: harvest (gold mine or tree)) - `repair(workers, building, queue: 'str | None' = None)` [verified] [Fast lane] Repair / help build (Human and Orc construction sites stop when nobody is building them). (Mechanism: W3P target: repair) - `build(worker, code: 'str', x: 'float', y: 'float', queue: 'str | None' = None)` [verified] [Fast lane] Have a worker build code at (x,y) (coordinates aligned to a 32 grid). Receipt accepted = the worker's order is now this building (or the start-construction order); with queue='after' = added to the worker's order queue (receipt values[0] is the queue count). ⚠ Accepted ≠ built: the engine also accepts a spot inside a forest on the spot, and the worker only fails once it gets there (measured 09-23); gold being spent elsewhere can also keep the foundation from appearing. If you don't know where it fits, use build_near (it tracks the result and blacklists failed spots). To build several in a row, use build_queue. (Mechanism: W3P build: build order, with the worker's order read back in the same frame to confirm) - `build_queue(worker, plan)` [verified] [Fast lane] One worker builds several in order (Shift-queued building): plan = [(four-character code, x, y), ...]. Submitted once; receipts in plan order. ⚠ Gold is deducted only when construction starts (not when queued) — if you queue 3 but can only afford 1, the other two fail when the worker gets there. (Mechanism: One batch: the first immediately, the rest in reverse order with queue='after') - `build_near(worker, code: 'str', x: 'float', y: 'float', min_r: 'float' = 450, max_r: 'float' = 1500, max_tries: 'int' = 24)` [verified] [Fast lane] Find a spot that fits around (x,y), searching from near to far, and build code there. **Non-blocking** — fine to call every tick: * An attempt for this building type is still in progress (the worker is on its way) -> returns that spot without re-issuing the order; * The last attempt succeeded (the foundation appeared) -> finds a new spot this time if needed; * The last attempt failed (the worker found on arrival that it didn't fit, the engine dropped the order, no foundation) -> that spot is blacklisted for 45 seconds and the next one is tried; * Not enough gold -> returns None right away (no attempt, no blacklisting); returns None once every spot has been tried. ⚠ Why tracking is needed: in 09-23 live games, the engine **accepted on the spot** a point inside a forest, and the worker only failed once it got there (the same-frame receipt can't tell); and the engine's placement check always returns 221 for a worker constructing a building, so you can't "check" before building either. Only obviously occupied spots (the middle of the hall) are rejected on the spot. (Mechanism: Per-point build + tracking (foundation appears = success; worker drops the order with no foundation = that spot is blacklisted)) - `train(building, code: 'str')` [verified] [Fast lane] Train units / research tech / upgrade the hall (tier up = give the hall itself the target hall's four-character code, e.g. 'hkee'). When rejected, the receipt's reason says why (not enough food, not enough gold, not enough lumber, queue full, missing prerequisite…). (Mechanism: W3P immediate: four-character code, with the feasibility reason code when rejected) - `learn(hero, ability: 'str')` [verified] [Fast lane] The hero learns an ability (four-character code, e.g. 'AHbz' Blizzard). (Mechanism: W3P learn: only counts as learned once the skill points decrease) - `cast(u, spell, target=None, x: 'float | None' = None, y: 'float | None' = None)` [verified] [Fast lane] Cast a spell. spell is an order string ('thunderbolt' Storm Bolt, 'blizzard', 'holybolt' Holy Light…; see data/order-ids.txt) or an order ID. Give target = on a unit; give x,y = on the ground; neither = no target (Thunder Clap, Divine Shield, Summon Water Elemental). An accepted receipt only means the engine accepted it; to see whether it actually went off, check whether cooldown() shows a cooldown or buffs() shows the buff. (Mechanism: W3P target / point / immediate (chosen by arguments)) - `rally(building, x: 'float | None' = None, y: 'float | None' = None, target=None)` [verified] [Fast lane] Set the rally point (on a point, or on a unit/gold mine). (Mechanism: W3P rally) - `revive(altar, hero=None)` [verified] [Fast lane] Revive a dead hero at the altar (if hero isn't given, revives the first one in the list). Common rejection reasons (written in the receipt's reason): not enough food (heroes cost food too), not enough gold, died too recently (a hero can only be revived about 3 game seconds after dying), revive already in progress (the engine clears that slot on the spot once it's accepted). (Mechanism: W3P revive: dead hero list -> the altar casts revive on the dead hero) - `pick_up(hero, item)` [verified] [Fast lane] The hero goes to pick up an item from the ground (item comes from items_on_ground). Once picked up, it appears in the inventory and an item.removed event is emitted for the ground item. (Mechanism: W3P target: right-click the item) - `use_item(hero, slot: 'int', target=None, x: 'float | None' = None, y: 'float | None' = None)` [verified] [Fast lane] Use the item in inventory slot slot (0~5); can take a target unit or a target point. ⚠ For point-targeted item use (e.g. Ivory Tower), the engine returns 0 even on success, so the receipt always counts as accepted — check whether that slot has emptied. (Mechanism: W3P use_item (by slot number)) - `drop_item(hero, slot: 'int', x: 'float', y: 'float')` [verified] [Fast lane] Drop the item in inventory slot slot at (x,y) (the hero walks over and puts it down). (Mechanism: W3P item_drop (mirrors JASS UnitDropItemPoint: dropitem 0xD0021 on a point + the item as the instant target)) - `give_item(hero, slot: 'int', to)` [verified] [Fast lane] Give the item in inventory slot slot to to (another hero / unit; the hero walks over and hands it over). Giving it to a shop = selling it (see sell_item). (Mechanism: W3P item_drop (mirrors JASS UnitDropItemTarget: dropitem on a unit)) - `sell_item(hero, slot: 'int', shop)` [verified] [Fast lane] Sell the item in inventory slot slot to a shop (the hero must walk next to the shop; only sellable items are accepted, for half the price). (Mechanism: Same as give_item, with the shop as the target (measured: Staff of Sanctuary sells for 125 gold)) - `move_item(hero, slot: 'int', to_slot: 'int')` [verified] [Fast lane] Move an item within the inventory (from slot slot to slot to_slot; if both slots hold items, they swap). Useful for arranging hotkey positions. (Mechanism: W3P target: order 0xD0022 + slot number, target = the item (mirrors JASS UnitDropItemSlot)) - `buy(shop, item_code: 'str')` [inferred] [Fast lane] Buy an item at a shop (for a hero standing next to the shop). If a tech prerequisite is missing, the engine returns 0 and no gold is spent. (Mechanism: W3P buy: the shop sells to a nearby hero) - `call_to_arms(hall, on: 'bool' = True)` [verified] [Fast lane] Human Call to Arms: Peasants become Militia (a tier-1 Town Hall doesn't have this ability; only works on a Keep/Castle). (Mechanism: W3P immediate: townbellon/off) ## Game control Game speed, pause, publish interval, speech bubbles, canvas, UI & input, messages. - `ui()` [verified] [Direct write] UI & input (openwar3.ui.UI): clickable buttons and choice cards, hotkeys, picking a position by clicking the ground, where the mouse is pointing. The game never receives the click that lands on a button; pure local input + local drawing, so it's safe in multiplayer. (Mechanism: W3P 74 input_enable + shared memory Local\War3Input_ (the runtime receives window input)) - `set_speed(percent: 'int') -> 'bool'` [verified] [Control channel] Game speed (100 = normal speed). (Mechanism: Action 47 (25~800%)) - `pause(on: 'bool' = True)` [verified] [Fast lane] Pause / resume the game. While paused, the engine clock stops, but the fast lane can still issue orders (event dispatch keeps running). (Mechanism: W3P pause) - `set_publish_period(ms: 'int') -> 'None'` [verified] [Pushed snapshot] Publish period of the world state (16~1000 ms, default 50). One sample takes about 0.5 ms, so even 33 ms is fine; there's a single value shared machine-wide, and the last write wins. (Mechanism: World block requestedPeriodMs) - `say(u, text: 'str', seconds: 'float' = 4.0) -> 'bool'` [verified] [Control channel] Show a chat bubble over a unit's head (for streaming/debugging; doesn't affect the game). Returns False if the bubble didn't appear; the reason is in g.last_say_error. (Mechanism: Action 56) - `message(text: 'str') -> 'bool'` [inferred] [Control channel] Print a line in the message area at the bottom left of the game (visible only on this machine). The game must have shown a notice on its own first (the DLL grabs the message box from that one). (Mechanism: Action 45) - `end_game() -> 'bool'` [verified] [Control channel] End this game process (farm.py --keep automatically starts the next game according to next_game.json). (Mechanism: Action 22) - `canvas()` [verified] [Direct write] Canvas: draw text boxes, panels, progress bars, images, circles on the ground and routes on the game screen (openwar3.canvas.Canvas). Drawn by the runtime itself — no game handles created, no game state changed — so it's safe in multiplayer; style it however you like (CJK text, rounded corners, translucency). (Mechanism: W3P 73 canvas_enable + shared memory Local\War3Canvas_ (drawn by the runtime every frame just before the game draws the mouse cursor; the cursor covers it)) - `press_to_continue() -> 'bool'` [verified] [Direct write] Press Space once on the "Press any key to continue" loading screen. Many RPG / story maps need a key press after loading before they start (measured on WarChasers, 09-24: without it the game sits on the loading screen, the game clock stays at 0 and the fast lane never drains). openwar3.run presses it itself while waiting to enter the game, so you rarely need to call it by hand. (Mechanism: PostMessage WM_KEYDOWN/UP Space to the game window (doesn't steal focus)) ## Sandbox JASS channel: spawn units, set alliances, rename players, show text… for RPG helpers and companions. It can change the world only in single-player games and local tools. - `jass()` [verified] [Fast lane] Call any JASS native by name: g.jass.CreateUnit(g.jass.Player(1), "Hpal", x, y, 270.0). Arguments I/R/B/S/H are converted automatically (pass unit/item objects directly); in multiplayer only read-only natives can be called. See openwar3/jass.py and docs/COMPANION_ZH.md for details. (Mechanism: W3P 70 jass (the runtime looks the name up in the native table, 1291 entries)) - `player_slots() -> 'list[dict]'` [verified] [Fast lane] The 16 player slots: controller (user = human / computer / neutral…), state (empty / playing / left), human, me, ally (whether it's allied with me). Use it in RPG maps to find an empty slot for a companion, or to tell whether it's a single-player game. (Mechanism: JASS GetPlayerController / GetPlayerSlotState / IsPlayerAlly) - `spawn(code: 'str', x: 'float', y: 'float', player: 'int | None' = None, facing: 'float' = 270.0)` [verified] [Fast lane] Create a unit at (x,y) (player defaults to the local player) and return it as a snapshot unit (after the next world publish, ~50 ms); returns None if it can't be created. The returned unit has an extra attribute, jass_handle. ⚠ Single-player only (it desyncs in multiplayer). (Mechanism: JASS CreateUnit + W3P 72 handle -> unit) - `set_alliance(a: 'int', b: 'int', allied: 'bool' = True, vision: 'bool' = True, control: 'bool' = False, xp: 'bool' = False, both: 'bool' = True) -> 'None'` [verified] [Fast lane] Set player a's alliance toward b: allied = don't attack each other + request help from each other; vision = shared vision; control = shared unit control (b can command a's units); xp = shared experience. both=True sets both directions at once (control is only set a -> b). (Mechanism: JASS SetPlayerAlliance) - `set_player_name(player: 'int', name: 'str') -> 'None'` [verified] [Fast lane] Change a player's name (the one shown on the scoreboard, in chat and in the allies panel). Used to give a companion a name. (Mechanism: JASS SetPlayerName) - `show_text(text: 'str', seconds: 'float' = 6.0, player: 'int | None' = None) -> 'None'` [verified] [Fast lane] Show a line of text at the bottom left of the screen (the kind map triggers use), shown to the local player by default. Supports |cffRRGGBB color codes. (Mechanism: JASS DisplayTimedTextToPlayer) ## Connection and utilities Connection status and pure computation helpers. - `status() -> 'dict'` [verified] [Local compute] Connection status: pid, world publishing (period, sampling time), fast lane counters. (Mechanism: World block + fast lane + claim table) - `nearest(candidates, to)` [verified] [Local compute] The one closest to to (a unit or (x,y)); returns None if there are no candidates. (Mechanism: Pure computation)