@pikehub/warpass-bot 0.0.0-stage → 63.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/AGENTS.md ADDED
@@ -0,0 +1,37 @@
1
+ # AGENTS.md: writing a Warpass bot
2
+
3
+ You are writing code that plays a real-time strategy game through the Warpass SDK. Read this page first, then the page for what you need from [llms.txt](llms.txt). Every fact about units, costs and ids is in [rules/index.md](rules/index.md) and `rules()`; never guess an id.
4
+
5
+ ## The loop
6
+
7
+ A bot is one function, `think({ view, events, tick, slot, game, act })`, called every `everyTicks` ticks (default 10, minimum 2; 20 ticks = 1 second).
8
+
9
+ 1. Read `view` (what your player can see right now) and `events` (what happened to you since the last think).
10
+ 2. Ask `game` questions (`canTrain`, `findPlacement`, `nearestResource`, …). Queries never change the game.
11
+ 3. Give orders on `act` (`train`, `gather`, `build`, `attackMove`, …).
12
+ 4. Return. The orders are sent together and land in the next turn.
13
+
14
+ ## Invariants
15
+
16
+ - **Orders land later.** An order given at tick `t` takes effect a couple of ticks later. The next view may not show it yet; don't re-issue the same order every think.
17
+ - **Entities are `(eid, uid)`.** `eid` slots are reused after death; a stored reference is valid only while its `uid` matches. Pass the whole view object (`{ eid, uid }`) as an attack target.
18
+ - **Fog.** `view` only has what your units and buildings see; `enemyBuildings` also keeps buildings you saw before (`visible: false`).
19
+ - **Coordinates.** `x`, `y` are in 1/256 tile; `tile`, `at`, `near` are whole tiles `[tx, ty]`. Orders that move take positions (`x`, `y` in 1/256 tile).
20
+ - **Rate limit online.** At most 20 orders per second (burst 40). Consecutive orders with the same opcode and arguments are merged into one; give one order for many units rather than one order per unit.
21
+ - **Never block think.** Online, the game keeps running while you think. A slow think skips thinks, it doesn't pause the match. Async thinks are fine.
22
+ - **Version.** Online play needs the SDK built for the server's engine; otherwise `connect` throws `OutdatedEngine`. Upgrade the package to the version in the error.
23
+ - **Determinism.** Local games are exact for a given seed and deterministic bot. Use `playMany` over seeds to measure a change, not one game.
24
+
25
+ ## Where things are
26
+
27
+ - Orders: [api/act.md](api/act.md). View fields: [api/view.md](api/view.md). Queries: [api/game.md](api/game.md). Events: [api/events.md](api/events.md).
28
+ - Online: [api/online.md](api/online.md). Local and batch play: [api/local.md](api/local.md). Custom games: [api/custom.md](api/custom.md). Replays: [api/replay.md](api/replay.md).
29
+ - Errors and what to do: [errors.md](errors.md).
30
+
31
+ ## Pitfalls
32
+
33
+ - A building trains only when finished (`done: true`) and has room in its `queue` (5 entries).
34
+ - Supply: training stops at `supply.cap`; build the race's housing building (see rules) before you hit it.
35
+ - Workers gather from a resource tile (`nearestResource`), food comes from farms and bushes, and every worker needs a drop-off building nearby.
36
+ - `findPlacement` returns `null` when nothing fits within the radius; try a larger radius or another spot.
37
+ - Surrender (`act.surrender()`) ends the match as a loss at once.
package/CHANGELOG.md ADDED
@@ -0,0 +1,3 @@
1
+ # Changes in 63.0.0
2
+
3
+ Compared with engine 62.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Pikehub
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,60 @@
1
- # Temporary Holding Version
1
+ # Warpass bot SDK
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Play Warpass from code. The SDK is the game engine itself (simulation, AI, map generation, replays) compiled to native code, with bindings for Node.js and Python. It plays online matches against people and other bots, local games at full CPU speed, staged custom games, and replays.
4
+
5
+ - **Online:** a bot account queues, accepts, plays a real ranked match in lockstep like the browser does, and gets the server-verified result.
6
+ - **Local:** any mix of your bot, the built-in AI (levels 1–10) and idle players, seeded and reproducible, one game or hundreds in parallel.
7
+ - **Custom games:** stage terrain, units, buildings, tech and resources, then play it out. Custom games replay exactly.
8
+ - **Replays:** save, load, verify against recorded hashes, and analyse per-minute combat.
9
+
10
+ The SDK version is `<ENGINE_VERSION>.0.0`. Offline play works with any version; online play needs the version the server runs (see [concepts/versioning.md](concepts/versioning.md)).
11
+
12
+ ## Install
13
+
14
+ Node 20+:
15
+
16
+ ```
17
+ npm i @pikehub/warpass-bot
18
+ ```
19
+
20
+ ## A first bot
21
+
22
+ ```js
23
+ import { Ai, Bot, Match, play, rules } from "@pikehub/warpass-bot";
24
+
25
+ const r = rules();
26
+ const race = r.races[0];
27
+ const worker = r.units.find((u) => u.race === race && u.worker);
28
+ const hall = r.buildings.find((b) => b.race === race && b.trains.includes(worker.id));
29
+
30
+ export function think({ view, game, act }) {
31
+ const home = view.buildings.find((b) => b.type === hall.id);
32
+ if (!home) return;
33
+ if (game.canTrain(home, worker.id) && home.queue.length < 2) act.train(home, worker.id, 1);
34
+ const idle = view.units.filter((u) => u.worker && u.order === "idle");
35
+ const gold = game.nearestResource(home.tile, "gold", 30);
36
+ if (idle.length && gold) act.gather(idle, gold);
37
+ }
38
+
39
+ const result = await play(new Match({ seed: 42, maxTicks: 6000, players: [Bot({ race, think }), Ai({ race: r.races[1], level: 3 })] }));
40
+ console.log(result.winnerSlot, result.endTick, result.stats[0].unitsProduced);
41
+ ```
42
+
43
+ The same `think` plays online (code blocks marked `js online` need a server and a bot key):
44
+
45
+ ```js online
46
+ import { Warpass } from "@pikehub/warpass-bot";
47
+
48
+ const wp = await Warpass.connect({ token: process.env.WARPASS_BOT_TOKEN });
49
+ const offer = await wp.queue({ race: "templars" });
50
+ const match = await offer.accept();
51
+ match.onThink(think, { everyTicks: 10 });
52
+ console.log(await match.result());
53
+ ```
54
+
55
+ ## Next
56
+
57
+ - [quickstart.md](quickstart.md): bot account, key, first local game, first online match
58
+ - [AGENTS.md](AGENTS.md): the rules of the road for coding agents
59
+ - [llms.txt](llms.txt): every page with a one-line summary; `llms-full.txt` is all of them in one file
60
+ - [rules/index.md](rules/index.md): every unit, building and upgrade of the bundled engine
package/docs/AGENTS.md ADDED
@@ -0,0 +1,37 @@
1
+ # AGENTS.md: writing a Warpass bot
2
+
3
+ You are writing code that plays a real-time strategy game through the Warpass SDK. Read this page first, then the page for what you need from [llms.txt](llms.txt). Every fact about units, costs and ids is in [rules/index.md](rules/index.md) and `rules()`; never guess an id.
4
+
5
+ ## The loop
6
+
7
+ A bot is one function, `think({ view, events, tick, slot, game, act })`, called every `everyTicks` ticks (default 10, minimum 2; 20 ticks = 1 second).
8
+
9
+ 1. Read `view` (what your player can see right now) and `events` (what happened to you since the last think).
10
+ 2. Ask `game` questions (`canTrain`, `findPlacement`, `nearestResource`, …). Queries never change the game.
11
+ 3. Give orders on `act` (`train`, `gather`, `build`, `attackMove`, …).
12
+ 4. Return. The orders are sent together and land in the next turn.
13
+
14
+ ## Invariants
15
+
16
+ - **Orders land later.** An order given at tick `t` takes effect a couple of ticks later. The next view may not show it yet; don't re-issue the same order every think.
17
+ - **Entities are `(eid, uid)`.** `eid` slots are reused after death; a stored reference is valid only while its `uid` matches. Pass the whole view object (`{ eid, uid }`) as an attack target.
18
+ - **Fog.** `view` only has what your units and buildings see; `enemyBuildings` also keeps buildings you saw before (`visible: false`).
19
+ - **Coordinates.** `x`, `y` are in 1/256 tile; `tile`, `at`, `near` are whole tiles `[tx, ty]`. Orders that move take positions (`x`, `y` in 1/256 tile).
20
+ - **Rate limit online.** At most 20 orders per second (burst 40). Consecutive orders with the same opcode and arguments are merged into one; give one order for many units rather than one order per unit.
21
+ - **Never block think.** Online, the game keeps running while you think. A slow think skips thinks, it doesn't pause the match. Async thinks are fine.
22
+ - **Version.** Online play needs the SDK built for the server's engine; otherwise `connect` throws `OutdatedEngine`. Upgrade the package to the version in the error.
23
+ - **Determinism.** Local games are exact for a given seed and deterministic bot. Use `playMany` over seeds to measure a change, not one game.
24
+
25
+ ## Where things are
26
+
27
+ - Orders: [api/act.md](api/act.md). View fields: [api/view.md](api/view.md). Queries: [api/game.md](api/game.md). Events: [api/events.md](api/events.md).
28
+ - Online: [api/online.md](api/online.md). Local and batch play: [api/local.md](api/local.md). Custom games: [api/custom.md](api/custom.md). Replays: [api/replay.md](api/replay.md).
29
+ - Errors and what to do: [errors.md](errors.md).
30
+
31
+ ## Pitfalls
32
+
33
+ - A building trains only when finished (`done: true`) and has room in its `queue` (5 entries).
34
+ - Supply: training stops at `supply.cap`; build the race's housing building (see rules) before you hit it.
35
+ - Workers gather from a resource tile (`nearestResource`), food comes from farms and bushes, and every worker needs a drop-off building nearby.
36
+ - `findPlacement` returns `null` when nothing fits within the radius; try a larger radius or another spot.
37
+ - Surrender (`act.surrender()`) ends the match as a loss at once.
package/docs/README.md ADDED
@@ -0,0 +1,60 @@
1
+ # Warpass bot SDK
2
+
3
+ Play Warpass from code. The SDK is the game engine itself (simulation, AI, map generation, replays) compiled to native code, with bindings for Node.js and Python. It plays online matches against people and other bots, local games at full CPU speed, staged custom games, and replays.
4
+
5
+ - **Online:** a bot account queues, accepts, plays a real ranked match in lockstep like the browser does, and gets the server-verified result.
6
+ - **Local:** any mix of your bot, the built-in AI (levels 1–10) and idle players, seeded and reproducible, one game or hundreds in parallel.
7
+ - **Custom games:** stage terrain, units, buildings, tech and resources, then play it out. Custom games replay exactly.
8
+ - **Replays:** save, load, verify against recorded hashes, and analyse per-minute combat.
9
+
10
+ The SDK version is `<ENGINE_VERSION>.0.0`. Offline play works with any version; online play needs the version the server runs (see [concepts/versioning.md](concepts/versioning.md)).
11
+
12
+ ## Install
13
+
14
+ Node 20+:
15
+
16
+ ```
17
+ npm i @pikehub/warpass-bot
18
+ ```
19
+
20
+ ## A first bot
21
+
22
+ ```js
23
+ import { Ai, Bot, Match, play, rules } from "@pikehub/warpass-bot";
24
+
25
+ const r = rules();
26
+ const race = r.races[0];
27
+ const worker = r.units.find((u) => u.race === race && u.worker);
28
+ const hall = r.buildings.find((b) => b.race === race && b.trains.includes(worker.id));
29
+
30
+ export function think({ view, game, act }) {
31
+ const home = view.buildings.find((b) => b.type === hall.id);
32
+ if (!home) return;
33
+ if (game.canTrain(home, worker.id) && home.queue.length < 2) act.train(home, worker.id, 1);
34
+ const idle = view.units.filter((u) => u.worker && u.order === "idle");
35
+ const gold = game.nearestResource(home.tile, "gold", 30);
36
+ if (idle.length && gold) act.gather(idle, gold);
37
+ }
38
+
39
+ const result = await play(new Match({ seed: 42, maxTicks: 6000, players: [Bot({ race, think }), Ai({ race: r.races[1], level: 3 })] }));
40
+ console.log(result.winnerSlot, result.endTick, result.stats[0].unitsProduced);
41
+ ```
42
+
43
+ The same `think` plays online (code blocks marked `js online` need a server and a bot key):
44
+
45
+ ```js online
46
+ import { Warpass } from "@pikehub/warpass-bot";
47
+
48
+ const wp = await Warpass.connect({ token: process.env.WARPASS_BOT_TOKEN });
49
+ const offer = await wp.queue({ race: "templars" });
50
+ const match = await offer.accept();
51
+ match.onThink(think, { everyTicks: 10 });
52
+ console.log(await match.result());
53
+ ```
54
+
55
+ ## Next
56
+
57
+ - [quickstart.md](quickstart.md): bot account, key, first local game, first online match
58
+ - [AGENTS.md](AGENTS.md): the rules of the road for coding agents
59
+ - [llms.txt](llms.txt): every page with a one-line summary; `llms-full.txt` is all of them in one file
60
+ - [rules/index.md](rules/index.md): every unit, building and upgrade of the bundled engine
@@ -0,0 +1,30 @@
1
+ # Orders
2
+
3
+ Every order a bot can give, on `act` during a think (and on the builder passed to `CustomGame.order`).
4
+
5
+ `who` is a unit view, an eid, or a list of them. Positions `x`, `y` are in 1/256 tile; `at`, `tile`, `from`, `to` are tiles `[tx, ty]`. Every order returns `act`, so orders chain. `queued: true` adds the order after the units' current ones instead of replacing them.
6
+
7
+ | JS | Python | |
8
+ |---|---|---|
9
+ | `move(who, x, y, { queued })` | `move(who, x, y, queued=False)` | walk there, ignoring enemies |
10
+ | `attackMove(who, x, y, { queued })` | `attack_move(who, x, y, queued=False)` | walk there, fighting what they meet |
11
+ | `formMove(who, x, y, { face, queued, attack })` | `form_move(who, x, y, face=(0, 256), queued=False, attack=False)` | move as a formation facing the direction `face` |
12
+ | `formUp(who)` | `form_up(who)` | gather into formation where they stand |
13
+ | `attack(who, target, { queued })` | `attack(who, target, queued=False)` | attack one entity (`target` = a view object or `{ eid, uid }`) |
14
+ | `target(who, target, { queued })` | `target(who, target, queued=False)` | the right-click order: attack an enemy, gather a resource, repair your building |
15
+ | `repair(who, target, { queued })` | `repair(who, target, queued=False)` | workers repair one of your buildings |
16
+ | `gather(who, tile, { queued })` | `gather(who, tile, queued=False)` | workers gather the resource on a tile (from `nearestResource`) |
17
+ | `build(who, building, at, { queued })` | `build(who, building, at, queued=False)` | workers place and build a building with its top-left at `at` (the footprint must be in view); pays on placement |
18
+ | `buildLine(who, building, from, to, { queued })` | `build_line(who, building, start, end, queued=False)` | a line of wall segments |
19
+ | `train(building, unit, count = 1)` | `train(building, unit, count=1)` | queue units (pays now) |
20
+ | `research(building, upgrade)` | `research(building, upgrade)` | queue an upgrade (pays now) |
21
+ | `cancel(building, index)` | `cancel(building, index)` | cancel queue entry `index` (0 = in progress) and get the cost back |
22
+ | `rally(buildings, x, y)` | `rally(buildings, x, y)` | where new units from those buildings walk |
23
+ | `stop(who)` | `stop(who)` | drop all orders |
24
+ | `hold(who)` | `hold(who)` | stand still and fight only what comes in range |
25
+ | `remove(who)` | `remove(who)` | delete your own units or buildings |
26
+ | `spawnToggle(buildings, on)` | `spawn_toggle(buildings, on)` | pause or resume buildings that produce units by themselves |
27
+ | `surrender()` | `surrender()` | concede the match |
28
+
29
+ - Orders that the game refuses (not enough resources, wrong building, dead unit) are ignored when they land; check with [queries](game.md) first.
30
+ - Low level: `act.push(name, args)` takes the order name and an argument object (`{ units, x, y, queued, … }`); the methods above call it.
@@ -0,0 +1,81 @@
1
+ # Custom games
2
+
3
+ Stage a situation, then play it out step by step. Custom games are local only and replay exactly: the setup is recorded with the match.
4
+
5
+ ```js
6
+ import { CustomGame, rules } from "@pikehub/warpass-bot";
7
+
8
+ const r = rules();
9
+ const [a, b] = r.races;
10
+ const g = new CustomGame({ seed: 11, races: [a, b] });
11
+ const [x0, y0] = g.flattenArea(30, 16);
12
+ const soldier = r.units.find((u) => u.race === a && !u.worker && u.attack);
13
+ const target = r.buildings.filter((x) => x.race === b && !x.site).sort((p, q) => p.hp - q.hp)[0];
14
+ const hall = g.building(1, target.id, x0 + 20, y0 + 6);
15
+ g.reveal(0, hall);
16
+ const army = [0, 1, 2, 3].map((k) => g.spawn(0, soldier.id, (x0 + 3) * 256, (y0 + 4 + k) * 256));
17
+ const ref = g.observer().enemyBuildings.find((x) => x.eid === hall) ?? g.observer().buildings.find((x) => x.eid === hall);
18
+ g.order(0, (act) => act.attack(army, ref));
19
+ const ticks = g.until(20 * 120, (obs) => !obs.buildings.concat(obs.enemyBuildings).some((x) => x.eid === hall && x.uid === ref.uid));
20
+ console.log(ticks > 0 ? `razed in ${ticks / 20} s` : "still standing");
21
+ console.log(g.replay().verify().ok);
22
+ ```
23
+
24
+ ## Creating
25
+
26
+ `new CustomGame({ seed, races?, mapType?, aiLevel? })` / `CustomGame(seed=1, races=(), map_type=None, ai_level=0)`. Two players; slot 1 is the built-in AI when `aiLevel > 0`, otherwise idle. The map is a standard map of `mapType` (default `riverVale`).
27
+
28
+ ## Setup ops (before the first step)
29
+
30
+ | JS | Python | Returns |
31
+ |---|---|---|
32
+ | `flattenArea(w, h)` | `flatten_area(w, h)` | `[tx, ty]`: an empty flat grass rectangle away from bases |
33
+ | `spawn(owner, unit, x, y)` | `spawn(owner, unit, x, y)` | eid of a new unit at position `x`, `y` (1/256 tile) |
34
+ | `building(owner, building, tx, ty)` | `building(owner, building, tx, ty)` | eid of a finished building at tile `tx`, `ty` |
35
+ | `terrain(tx, ty, terrain)` | `terrain(tx, ty, terrain)` | set one tile (`terrain` = name or id from `rules().terrains`) |
36
+ | `research(owner, upgrade)` | `research(owner, upgrade)` | complete an upgrade |
37
+ | `resources(owner, { food, wood, stone, gold })` | `resources(owner, food=0, wood=0, stone=0, gold=0)` | set a bank |
38
+ | `weather(key, duration, ramp?)` | `weather(key, duration, ramp=0)` | force weather for `duration` ticks |
39
+ | `reveal(owner, eid = -1)` | `reveal(owner, eid=-1)` | make `owner` know an entity (`-1`: every building) |
40
+ | `remove(eid)` | `remove(eid)` | remove an entity |
41
+ | `setup(op)` | `setup(op)` | any op as data, e.g. `{ op: "spawn", owner, unit, x, y }` |
42
+
43
+ Ops after the first step still apply but make the game unreplayable.
44
+
45
+ ## Playing
46
+
47
+ | JS | Python | |
48
+ |---|---|---|
49
+ | `order(slot, (act) => …)` | `order(slot, lambda act: …)` | give [orders](act.md) for any slot |
50
+ | `step(n = 1)` | `step(n=1)` | advance `n` ticks; returns the tick |
51
+ | `until(maxTicks, (observer, tick) => done)` | `until(max_ticks, lambda observer, tick: done)` | step until `done`; returns ticks taken or `-1` |
52
+ | `view(slot)` | `view(slot)` | the fogged [view](view.md) of a slot |
53
+ | `observer()` | `observer()` | the full view of everything, no fog |
54
+ | `events(slot)` | `events(slot)` | the [events](events.md) that slot got since the last call, fog-filtered exactly as in a match |
55
+ | `observerEvents()` | `observer_events()` | every event since the last call, unfiltered |
56
+ | `game(slot)` | `game(slot)` | [queries](game.md) as that slot |
57
+ | `map()` | `map()` | map size, starts, chokes, sites |
58
+ | `replay()` | `replay()` | a [Replay](replay.md) of setup and orders so far |
59
+ | `tick`, `winner` | `tick`, `winner` | current tick; winning slot once over |
60
+
61
+ The first `events` call for a slot (or `observerEvents`) starts recording for it and returns `[]`, so call it once before stepping. After that, each call returns what that player would have received in its `think` during a [local match](local.md):
62
+
63
+ ```js
64
+ import { CustomGame, rules } from "@pikehub/warpass-bot";
65
+
66
+ const r = rules();
67
+ const [a, b] = r.races;
68
+ const g = new CustomGame({ seed: 5, races: [a, b] });
69
+ const [x0, y0] = g.flattenArea(24, 12);
70
+ const fighter = (race) => r.units.find((u) => u.race === race && !u.worker && u.attack);
71
+ const ours = [0, 1, 2].map((k) => g.spawn(0, fighter(a).id, (x0 + 4) * 256, (y0 + 3 + k) * 256));
72
+ [0, 1, 2].forEach((k) => g.spawn(1, fighter(b).id, (x0 + 10) * 256, (y0 + 3 + k) * 256));
73
+ g.events(0);
74
+ g.events(1);
75
+ g.order(0, (act) => act.attackMove(ours, (x0 + 12) * 256, (y0 + 4) * 256));
76
+ g.step(20 * 30);
77
+ const mine = g.events(0);
78
+ const theirs = g.events(1);
79
+ console.log(`slot 0: ${mine.filter((e) => e.t === "hit").length} hits, ${mine.filter((e) => e.t === "death").length} deaths`);
80
+ console.log(`slot 1: ${theirs.filter((e) => e.t === "hit").length} hits`);
81
+ ```
@@ -0,0 +1,36 @@
1
+ # Events
2
+
3
+ What happened to your player since the last think, in `events` (oldest first). Each event is an object with a type tag `t` and its own fields; ids are numbers that index the rules (unit, building and upgrade lists in `rules()` order).
4
+
5
+ In a [custom game](custom.md), `events(slot)` returns the same events for any slot.
6
+
7
+ | `t` | When |
8
+ |---|---|
9
+ | `trained` | one of your units finished training |
10
+ | `researched` | an upgrade finished |
11
+ | `built` | a building finished |
12
+ | `placed` | a foundation was placed |
13
+ | `death` | an entity died (`owner`, `killer` slot, `kind`, position): yours, one you killed, or one where you have vision |
14
+ | `hit` | an entity took damage (`eid` attacker, `tgt` target, position): yours dealt or took it, or it happened where you have vision |
15
+ | `defeated` | a player was defeated |
16
+ | `over` | the match ended (`winnerSlot`) |
17
+ | `deplete` | a resource tile ran out |
18
+ | `deposit` | a worker dropped off resources |
19
+ | `site` | a site (an objective on the map) changed hands |
20
+ | `weather`, `forecast` | weather started or is coming |
21
+ | `msg` | a game message for the player (`key`, `params`) |
22
+ | `glimpse`, `armyPing` | an enemy was spotted |
23
+
24
+ Other tags (`shoot`, `impact`, `beam`, `charge`, `whiff`, `snared`, `heal`, `fire`, `fireOut`, `meteor`, `meteorWarn`, `lightning`, `lightningWarn`, `regrow`, `reseed`, `spawned`, `trap`) describe visual effects and rarer happenings.
25
+
26
+ ## Fog of war
27
+
28
+ You get exactly the events a person playing your slot gets. Every event falls in one of four groups:
29
+
30
+ | Group | Tags | You get it |
31
+ |---|---|---|
32
+ | public | `defeated`, `over`, `site`, `weather`, `forecast`, `meteor`, `meteorWarn`, `lightning`, `lightningWarn`, `fire`, `fireOut`, `msg` without an owner (site and weather news) | always |
33
+ | private | `trained`, `researched`, `built`, `placed`, `deposit`, `reseed`, `glimpse`, `armyPing`, `aiOpportunity`, `aiRaid`, `aiRetreat`, `msg` with an owner | only your own |
34
+ | seen | `hit`, `death`, `shoot`, `impact`, `beam`, `charge`, `whiff`, `snared`, `heal`, `spawned`, `trap`, `deplete`, `regrow` | when it involves you (your slot is its `owner` or `killer`, or its `eid` or `tgt` is your entity) or its position is in your current vision (a `beam` counts if either end is) |
35
+ | hidden | `aiWave`, `aiCounter` | never |
36
+
@@ -0,0 +1,26 @@
1
+ # Game queries
2
+
3
+ Questions a bot can ask during a think (`game`, or `match.game` online). Queries read the game and never change it.
4
+
5
+ | JS | Python | Returns |
6
+ |---|---|---|
7
+ | `canAfford(id)` | `can_afford(id)` | `true` if you have the resources for a unit, building or upgrade id |
8
+ | `cost(id)` | `cost(id)` | `{ food, wood, stone, gold }` with your current discounts, or `null` |
9
+ | `trainRefusal(building, unit)` | `train_refusal(building, unit)` | `null` if the building can train it now, else a reason |
10
+ | `canTrain(building, unit)` | `can_train(building, unit)` | `trainRefusal(...) === null` |
11
+ | `researchRefusal(building, upgrade)` | `research_refusal(building, upgrade)` | `null` or a reason |
12
+ | `canResearch(building, upgrade)` | `can_research(building, upgrade)` | |
13
+ | `placeRefusal(building, at)` | `place_refusal(building, at)` | `null` if a building id fits with its top-left at tile `at`, else a reason |
14
+ | `canPlace(building, at)` | `can_place(building, at)` | |
15
+ | `findPlacement(building, near, radius = 16)` | `find_placement(building, near, radius=16)` | `[tx, ty]` where it fits closest to `near` and is in view now, or `null` |
16
+ | `nearestResource(from, resource, radius = 40)` | `nearest_resource(start, resource, radius=40)` | `[tx, ty]` of the nearest tile with `food`, `wood`, `stone` or `gold` left |
17
+ | `reachable(from, to)` | `reachable(start, end)` | `true` if your units can walk between the two tiles |
18
+ | `front(toward?)` | `front(toward=None)` | `[tx, ty]`: the tile where your army should gather to face tile `toward` (default: the map centre), usually a choke near your base |
19
+ | `tile(at)` | `tile(at)` | `{ tile, x, y, terrain, height, passable, buildable, resource, amount }` |
20
+ | `map()` | `map()` | `{ width, height, starts, chokes, sites }` (tiles); each site is `{ id, kind, tile, size, centre, owner }`: `kind` `passive` (claim by standing near it) or `active` (build the site's building, same id, on it), `tile` its top-left, `owner` a slot or -1 |
21
+ | `view()` | `view()` | the current [view](view.md) |
22
+
23
+ - `building` arguments take a view object or an eid; `unit`, `upgrade` and building ids are strings from the rules.
24
+ - Tile arguments take `[tx, ty]` or an object with `x`, `y` in tiles.
25
+ - Training and research refusals: `notEnough` (resources), `supplyLimit`, `needHousing`, `queueFull`, `requirementsNotMet`, `limitReached`, `invalid` (wrong building, not finished, not yours).
26
+ - Placement refusals: `outOfBounds`, `notVisible` (every footprint tile must be in your current vision; send a unit to look first), `somethingInWay`, `cannotBuildThere`, `unevenGround`, `rampBlocked`, `buildOnSite` / `siteGround` (site buildings must stand on their site), `alreadyBuilt`, `limitReached`, `requirementsNotMet`, `unknownBuilding`.
@@ -0,0 +1,45 @@
1
+ # Local play
2
+
3
+ Play matches on your machine: bots, the built-in AI and idle players, one at a time or many in parallel.
4
+
5
+ ```js
6
+ import { Ai, Idle, Match, play, playMany, rules } from "@pikehub/warpass-bot";
7
+
8
+ const [a, b] = rules().races;
9
+ const one = await play(new Match({ seed: 3, maxTicks: 1200, players: [Ai({ race: a, level: 4 }), Idle({ race: b })] }));
10
+ const many = await playMany([1, 2, 3].map((seed) => new Match({ seed, maxTicks: 1200, players: [Ai({ race: a, level: 3 }), Ai({ race: b, level: 3 })] })));
11
+ console.log(one.endTick, many.map((g) => g.finalHash));
12
+ ```
13
+
14
+ ## Match
15
+
16
+ | JS | Python | |
17
+ |---|---|---|
18
+ | `new Match({ seed, players, maxTicks?, mapSize? })` | `Match(seed, players, max_ticks=None, map_size=None)` | `seed` is a 32-bit integer; the same seed makes the same map |
19
+ | `Bot({ race, think, everyTicks?, name? })` | `Bot(race, think, every_ticks=10, name="")` | your code |
20
+ | `Ai({ race, level? })` | `Ai(race, level=5)` | built-in AI, level 1–10 |
21
+ | `Idle({ race })` | `Idle(race)` | never acts |
22
+
23
+ Slot numbers follow the order of `players`.
24
+
25
+ ## Playing
26
+
27
+ | JS | Python | |
28
+ |---|---|---|
29
+ | `await play(match)` | `play(match)` | one game; thinks run in order, so it is reproducible |
30
+ | `await playMany(matches, { concurrency? })` | `play_many(matches, workers=4)` | many games; AI-only lists run on every core inside the engine |
31
+
32
+ ## `GameResult`
33
+
34
+ | Field | |
35
+ |---|---|
36
+ | `winnerSlot` / `winner_slot` | winning slot, `-1` if none |
37
+ | `finished` | `false` when `maxTicks` stopped the game first |
38
+ | `reason` | why it ended (`unfinished` when stopped by `maxTicks`) |
39
+ | `endTick` / `end_tick`, `finalHash` / `final_hash` | where it stopped and the state hash there |
40
+ | `stats` | per slot: `unitsProduced`, `unitsLost`, `unitsKilled`, `buildingsBuilt`, `buildingsLost`, `buildingsRazed`, `gathered*`, `upgradesResearched`, `peakSupply`, `commandCount` |
41
+ | `timeline` | per slot every 10 s: resources, supply, workers, army, buildings |
42
+ | `replay` | a [Replay](replay.md) |
43
+ | `won(slot)` | `finished && winnerSlot === slot` |
44
+
45
+ In Python, results and views are dictionaries that also read as attributes in snake_case: `result.end_tick == result["endTick"]`.
@@ -0,0 +1,39 @@
1
+ # Online
2
+
3
+ Connect a bot account, queue or join a lobby by code, play a match and read the verified result.
4
+
5
+ | JS | Python | Returns |
6
+ |---|---|---|
7
+ | `await Warpass.connect({ token, url? })` | `Warpass.connect(token=, url=)` | `Warpass` (`name`, `engineVersion` / `engine_version`) |
8
+ | `await wp.queue({ race, avoidBots? })` | `wp.queue(race, avoid_bots=False)` | `Offer` once a rival is found |
9
+ | `await offer.accept({ everyTicks? })` | `offer.accept(every_ticks=10)` | `OnlineMatch` once both accepted |
10
+ | `await wp.lobby({ code, race, everyTicks? })` | `wp.lobby(code, race, every_ticks=10)` | `OnlineMatch` once the host starts: joins a player's lobby by its code, marks ready (again after the host changes the map or race) and waits |
11
+ | `await offer.decline()` | `offer.decline()` | declines; the queue may give you a short penalty |
12
+ | `wp.close()` | `wp.close()` | leaves the queue or lobby and closes the socket; a pending `connect`, `queue`, `accept` or `lobby` fails with `closed` (`stopped`). Call it before exiting on Ctrl-C |
13
+ | `await Warpass.join({ token, matchId, url?, everyTicks? })` | `Warpass.join(match_id, token=, url=, every_ticks=10)` | `OnlineMatch`: rejoin a match you are in after a crash |
14
+ | `match.onThink(think, { everyTicks? })` | `match.on_think(think, every_ticks=None)` | registers your think; returns the match |
15
+ | `await match.ended()` | `match.ended()` | `{ reason }` when the match ends |
16
+ | `await match.result({ timeoutMs? })` | `match.result(timeout_ms=120000)` | the verified result (below) |
17
+ | `match.game` | `match.game` | [queries](game.md) on the live game |
18
+ | `match.id`, `match.slot` | `match.id`, `match.slot` | the match id and your slot |
19
+
20
+ - `url` defaults to `DEFAULT_URL` (`https://warpass.pike.com.br`).
21
+ - `Offer.opponent` is `{ name, bot, rating, race }`.
22
+ - `lobby` fails with `refused` (`missing`, `full`, `started`) when the code is wrong or taken, and with `cancelled` (`closed`) when the host closes the lobby.
23
+ - In Python, Ctrl-C during `connect`, `queue`, `accept` or `lobby` closes the session and raises `KeyboardInterrupt`.
24
+ - In Python, `ended()` and `result()` run your think loop on the calling thread until the match ends; an `async def` think is awaited.
25
+ - In JS, the think loop runs in the background after `onThink`; `ended()` and `result()` wait for it.
26
+
27
+ ## Result
28
+
29
+ ```
30
+ { matchId, outcome, won, winnerSlot, endTick, verified, ratingDelta }
31
+ ```
32
+
33
+ - `outcome`: `victory`, `defeat`, `draw`, `surrender`, `abandoned` (your side).
34
+ - `verified`: `ok` when the server's replay of the match matched every client hash, `failed` otherwise.
35
+ - `ratingDelta`: points won or lost, `null` when the match was unrated (too short or idle).
36
+
37
+ ## Reconnects
38
+
39
+ The SDK reconnects by itself when the socket drops and catches up from the server's turn log; your think keeps running. If the process dies, `Warpass.join` resumes the same match while the grace period lasts.
@@ -0,0 +1,28 @@
1
+ # Replays
2
+
3
+ A replay is the seed, the setup, every order and the state hashes. Replaying it gives the same game, tick for tick.
4
+
5
+ | JS | Python | |
6
+ |---|---|---|
7
+ | `result.replay` / `customGame.replay()` | same | a `Replay` |
8
+ | `replay.save(path)` | `replay.save(path)` | write JSON |
9
+ | `Replay.load(pathOrText)` | `Replay.load(path_or_text)` | read JSON |
10
+ | `replay.verify()` | `replay.verify()` | replay it and compare hashes |
11
+ | `replay.analyse()` | `replay.analyse()` | the combat analysis from `verify()` |
12
+ | `replay.data` | `replay.data` | `{ engineVersion, config, commands, hashes, endTick }` |
13
+
14
+ `verify()` returns `{ ok, reason, tick, stats, timeline, events, analysis }`:
15
+
16
+ - `ok`: every recorded hash matched; `reason` is `mismatch` (with the first bad `tick`) or `engine` (recorded on another engine version).
17
+ - `analysis.minutes`: per slot and minute, kills, losses, buildings razed and lost, units trained, damage dealt and taken.
18
+ - `analysis.units`: per unit and building type, trained, lost, kills, damage dealt and taken.
19
+ - `analysis.army`: army composition per minute.
20
+
21
+ ```js
22
+ import { Ai, Match, Replay, play, rules } from "@pikehub/warpass-bot";
23
+
24
+ const [a, b] = rules().races;
25
+ const game = await play(new Match({ seed: 9, maxTicks: 1200, players: [Ai({ race: a, level: 3 }), Ai({ race: b, level: 3 })] }));
26
+ const again = Replay.load(JSON.stringify(game.replay.data));
27
+ console.log(again.verify().ok, again.analyse().minutes.length);
28
+ ```
@@ -0,0 +1,31 @@
1
+ # Rules and docs
2
+
3
+ Static facts about the bundled engine, and these docs, from code.
4
+
5
+ | JS | Python | |
6
+ |---|---|---|
7
+ | `rules(race?)` | `rules(race=None)` | every unit, building and upgrade (or one race's) |
8
+ | `docs(page?)` | `docs(page=None)` | a docs page as Markdown; no argument: `llms-full.txt`, every page in one text |
9
+ | `docsIndex()` | `docs_index()` | every page path |
10
+ | `ENGINE_VERSION` | `ENGINE_VERSION` | the engine this SDK runs |
11
+ | `TILE`, `TICKS_PER_SECOND` | same | 256 and 20 |
12
+ | `DEFAULT_URL` | same | the public server |
13
+ | `WarpassError` | `WarpassError` | see [../errors.md](../errors.md) |
14
+ | `Orders` | `Orders` | the order builder behind `act` |
15
+
16
+ `rules()` returns:
17
+
18
+ ```
19
+ { engineVersion, races, resources, terrains: { name: id }, tile, ticksPerSecond, mapSize,
20
+ units: [{ id, race, cost, supply, hp, speed, sight, trainTicks, trainedAt, requires, abilities, worker, lane, armor: { melee, pierce }, attack }],
21
+ buildings: [{ id, race, size, cost, hp, buildTicks, supply, trains, researches, requires, site }],
22
+ upgrades: [{ id, race, cost, ticks, requires, group }] }
23
+ ```
24
+
25
+ `requires` is `{ buildings, anyBuildings, upgrades, anyUpgrades }` (ids). `attack` is `{ damage, range, minRange, cooldown, splash, onlyBuildings }` or `null`. Units of measure are in [../rules/index.md](../rules/index.md); the same data as tables is under `rules/`.
26
+
27
+ ```js
28
+ import { docs, docsIndex } from "@pikehub/warpass-bot";
29
+
30
+ console.log(docsIndex().length, docs("AGENTS.md").split("\n")[0]);
31
+ ```
@@ -0,0 +1,44 @@
1
+ # View
2
+
3
+ Everything a bot sees in one think: `view` (and `game.view()` / `match.game.view()`).
4
+
5
+ ```
6
+ {
7
+ tick, slot, race,
8
+ resources: { food, wood, stone, gold },
9
+ supply: { used, cap },
10
+ researched: [upgradeId], researching: [upgradeId],
11
+ units: [Unit], buildings: [Building],
12
+ enemyUnits: [Unit], enemyBuildings: [Building],
13
+ neutrals: [Unit],
14
+ players: [{ slot, race, defeated }],
15
+ over: winnerSlot | null
16
+ }
17
+ ```
18
+
19
+ ## Unit
20
+
21
+ | Field | |
22
+ |---|---|
23
+ | `eid`, `uid` | reference (see [../concepts/view.md](../concepts/view.md)) |
24
+ | `owner` | slot, or `-1` for neutral creatures |
25
+ | `type` | unit id (see [../rules/index.md](../rules/index.md)) |
26
+ | `worker` | can gather, build and repair |
27
+ | `x`, `y` | position in 1/256 tile |
28
+ | `tile` | `[tx, ty]` |
29
+ | `hp`, `maxHp` | hit points |
30
+ | `order` | `idle`, `move`, `attackMove`, `attack`, `gather`, `returnCargo`, `build`, `repair`, `hold`, `farm`, `follow`, `flee`, `guard` |
31
+ | `moving` | walking this tick |
32
+ | `target` | eid it is working on or attacking, `-1` if none |
33
+ | `carrying` | resource index it carries (0 food, 1 wood, 2 stone, 3 gold), `-1` if none |
34
+
35
+ ## Building
36
+
37
+ | Field | |
38
+ |---|---|
39
+ | `eid`, `uid`, `owner`, `type`, `x`, `y`, `hp`, `maxHp` | as for units |
40
+ | `tile`, `w`, `h` | top-left tile and size in tiles |
41
+ | `done` | construction finished |
42
+ | `progress` | construction progress |
43
+ | `queue` | your own buildings only: unit and upgrade ids in production, first is in progress |
44
+ | `visible` | `false` for an enemy building you saw before and can't see now (last known state) |
@@ -0,0 +1,10 @@
1
+ # Limits
2
+
3
+ The few limits that apply to bots online.
4
+
5
+ - **Order rate:** the server accepts 20 orders per second per player, with a burst of 40, and drops the rest (answering once with a `rate` error). The SDK paces your orders below that (18 per second, burst 32): orders above the pace wait in a queue and go out in order, a little later. Consecutive orders with the same opcode and arguments are merged, and one order can carry many units, so a busy bot rarely queues.
6
+ - **Order size:** an order carries at most 256 values (its arguments and unit ids); the SDK stops merging before that. Split huge selections yourself.
7
+ - **Think interval:** at least 2 ticks.
8
+ - **One match at a time** per bot account; queueing while in a match is refused (`inMatch`).
9
+ - **Leaving:** a bot that disconnects has a grace period to reconnect, then forfeits, exactly like a person.
10
+ - Rating, titles and matchmaking rules are the same as for people. A very short match (under 3 minutes of play, or with too little activity) is unrated for both sides.
@@ -0,0 +1,14 @@
1
+ # Players
2
+
3
+ Who fills each slot of a match. They differ only in where orders come from.
4
+
5
+ | Kind | Where its orders come from | Where |
6
+ |---|---|---|
7
+ | `Bot({ race, think, everyTicks })` | your `think`, from the fogged view | local, custom, and you online |
8
+ | `Ai({ race, level })` | the built-in AI inside the simulation, levels 1–10 | local and custom |
9
+ | `Idle({ race })` | nobody; it never gives an order | local and custom |
10
+ | remote opponent | the server, from the other player (person or bot) | online |
11
+
12
+ - Online, you never build the player list: `accept()` gives you a match where you are one slot and the opponent is whoever the queue found.
13
+ - The built-in AI sees only its own vision, like you.
14
+ - Bots play by the same rules as people online. Their names show as `<name> (AI)`; people can choose to avoid bots in matchmaking.
@@ -0,0 +1,12 @@
1
+ # Time and turns
2
+
3
+ Ticks, turns, when your think runs and when your orders land.
4
+
5
+ - The game advances in **ticks**, 20 per second. Every number of time in the SDK is ticks unless it says seconds.
6
+ - Online, the match is **lockstep**: every client runs the same simulation and the server only relays orders. A turn is 1 tick (50 ms); an order is scheduled a couple of ticks ahead so every client applies it on the same tick.
7
+ - Your **think** runs every `everyTicks` ticks (default 10 = 0.5 s, minimum 2).
8
+ - Online, the game does not wait for you. If a think is still running when the next one is due, the next one is skipped; you always get the newest tick, never a backlog.
9
+ - Locally, the game waits for each think, so a local game is reproducible for a given seed and deterministic bot.
10
+ - Orders from one think are sent together. They land in a later tick, so the view you get next may still show the old state.
11
+ - `events` holds what happened to your player since your last think (see [../api/events.md](../api/events.md)).
12
+ - A local `Match` stops at `maxTicks` if nobody has won; `result.finished` is then `false`.