@pikehub/warpass-bot 0.0.0-stage → 62.0.1

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.
@@ -0,0 +1,842 @@
1
+ <!-- README.md -->
2
+
3
+ # Warpass bot SDK
4
+
5
+ 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.
6
+
7
+ - **Online:** a bot account queues, accepts, plays a real ranked match in lockstep like the browser does, and gets the server-verified result.
8
+ - **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.
9
+ - **Custom games:** stage terrain, units, buildings, tech and resources, then play it out. Custom games replay exactly.
10
+ - **Replays:** save, load, verify against recorded hashes, and analyse per-minute combat.
11
+
12
+ 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)).
13
+
14
+ ## Install
15
+
16
+ Node 20+:
17
+
18
+ ```
19
+ npm i @pikehub/warpass-bot
20
+ ```
21
+
22
+ ## A first bot
23
+
24
+ ```js
25
+ import { Ai, Bot, Match, play, rules } from "@pikehub/warpass-bot";
26
+
27
+ const r = rules();
28
+ const race = r.races[0];
29
+ const worker = r.units.find((u) => u.race === race && u.worker);
30
+ const hall = r.buildings.find((b) => b.race === race && b.trains.includes(worker.id));
31
+
32
+ export function think({ view, game, act }) {
33
+ const home = view.buildings.find((b) => b.type === hall.id);
34
+ if (!home) return;
35
+ if (game.canTrain(home, worker.id) && home.queue.length < 2) act.train(home, worker.id, 1);
36
+ const idle = view.units.filter((u) => u.worker && u.order === "idle");
37
+ const gold = game.nearestResource(home.tile, "gold", 30);
38
+ if (idle.length && gold) act.gather(idle, gold);
39
+ }
40
+
41
+ const result = await play(new Match({ seed: 42, maxTicks: 6000, players: [Bot({ race, think }), Ai({ race: r.races[1], level: 3 })] }));
42
+ console.log(result.winnerSlot, result.endTick, result.stats[0].unitsProduced);
43
+ ```
44
+
45
+ The same `think` plays online (code blocks marked `js online` need a server and a bot key):
46
+
47
+ ```js online
48
+ import { Warpass } from "@pikehub/warpass-bot";
49
+
50
+ const wp = await Warpass.connect({ token: process.env.WARPASS_BOT_TOKEN });
51
+ const offer = await wp.queue({ race: "templars" });
52
+ const match = await offer.accept();
53
+ match.onThink(think, { everyTicks: 10 });
54
+ console.log(await match.result());
55
+ ```
56
+
57
+ ## Next
58
+
59
+ - [quickstart.md](quickstart.md): bot account, key, first local game, first online match
60
+ - [AGENTS.md](AGENTS.md): the rules of the road for coding agents
61
+ - [llms.txt](llms.txt): every page with a one-line summary; `llms-full.txt` is all of them in one file
62
+ - [rules/index.md](rules/index.md): every unit, building and upgrade of the bundled engine
63
+
64
+ <!-- AGENTS.md -->
65
+
66
+ # AGENTS.md: writing a Warpass bot
67
+
68
+ 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.
69
+
70
+ ## The loop
71
+
72
+ A bot is one function, `think({ view, events, tick, slot, game, act })`, called every `everyTicks` ticks (default 10, minimum 2; 20 ticks = 1 second).
73
+
74
+ 1. Read `view` (what your player can see right now) and `events` (what happened to you since the last think).
75
+ 2. Ask `game` questions (`canTrain`, `findPlacement`, `nearestResource`, …). Queries never change the game.
76
+ 3. Give orders on `act` (`train`, `gather`, `build`, `attackMove`, …).
77
+ 4. Return. The orders are sent together and land in the next turn.
78
+
79
+ ## Invariants
80
+
81
+ - **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.
82
+ - **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.
83
+ - **Fog.** `view` only has what your units and buildings see; `enemyBuildings` also keeps buildings you saw before (`visible: false`).
84
+ - **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).
85
+ - **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.
86
+ - **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.
87
+ - **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.
88
+ - **Determinism.** Local games are exact for a given seed and deterministic bot. Use `playMany` over seeds to measure a change, not one game.
89
+
90
+ ## Where things are
91
+
92
+ - 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).
93
+ - 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).
94
+ - Errors and what to do: [errors.md](errors.md).
95
+
96
+ ## Pitfalls
97
+
98
+ - A building trains only when finished (`done: true`) and has room in its `queue` (5 entries).
99
+ - Supply: training stops at `supply.cap`; build the race's housing building (see rules) before you hit it.
100
+ - Workers gather from a resource tile (`nearestResource`), food comes from farms and bushes, and every worker needs a drop-off building nearby.
101
+ - `findPlacement` returns `null` when nothing fits within the radius; try a larger radius or another spot.
102
+ - Surrender (`act.surrender()`) ends the match as a loss at once.
103
+
104
+ <!-- quickstart.md -->
105
+
106
+ # Quickstart
107
+
108
+ From nothing to a bot that has played a local game and an online match.
109
+
110
+ ## 1. Create a bot account
111
+
112
+ Sign in on the site, open **Developer** in the menu and create a bot. It gets its own name (shown as `<name> (AI)` to other players), rating and match history. The key (`wpb_…`) is shown once; keep it secret and pass it to the SDK. Rotating the key kills the old one at once.
113
+
114
+ ## 2. Install
115
+
116
+ Node.js 20 or newer:
117
+
118
+ ```
119
+ npm i @pikehub/warpass-bot
120
+ ```
121
+
122
+ The package carries the engine for Linux (x64 and arm64, glibc 2.17 or newer). On macOS or Windows, run your bot in a Linux container or WSL.
123
+
124
+ ## 3. Play locally
125
+
126
+ Local games need no key and no network. They run as fast as the CPU allows.
127
+
128
+ ```js
129
+ import { Ai, Bot, Match, play, rules } from "@pikehub/warpass-bot";
130
+
131
+ const r = rules();
132
+ const [mine, theirs] = r.races;
133
+
134
+ function think({ view, act }) {
135
+ const army = view.units.filter((u) => !u.worker);
136
+ const seen = view.enemyBuildings[0];
137
+ if (army.length >= 4 && seen) act.attackMove(army, seen.x, seen.y);
138
+ }
139
+
140
+ const result = await play(new Match({ seed: 7, maxTicks: 2400, players: [Bot({ race: mine, think }), Ai({ race: theirs, level: 2 })] }));
141
+ console.log({ winner: result.winnerSlot, ticks: result.endTick, finished: result.finished });
142
+ ```
143
+
144
+ ## 4. Play online
145
+
146
+ ```js online
147
+ import { Warpass } from "@pikehub/warpass-bot";
148
+
149
+ const wp = await Warpass.connect({ token: process.env.WARPASS_BOT_TOKEN });
150
+ const offer = await wp.queue({ race: "templars" });
151
+ console.log(`matched with ${offer.opponent.name} (${offer.opponent.rating})`);
152
+ const match = await offer.accept({ everyTicks: 10 });
153
+ match.onThink(({ view, act }) => {
154
+ const idle = view.units.filter((u) => !u.worker && u.order === "idle");
155
+ const enemy = view.enemyBuildings[0];
156
+ if (idle.length >= 6 && enemy) act.attackMove(idle, enemy.x, enemy.y);
157
+ });
158
+ const result = await match.result();
159
+ console.log(result.outcome, result.verified, result.ratingDelta);
160
+ ```
161
+
162
+ `result()` waits until the match is over and the server has replayed it (`verified: "ok"`). A bot plays under the same rules as a person: same queue, same rating, same match ledger.
163
+
164
+ ## Python
165
+
166
+ The same API in snake_case (`pip install warpass-bot` once published):
167
+
168
+ ```python
169
+ from warpass_bot import Ai, Bot, Match, play, rules
170
+
171
+ r = rules()
172
+
173
+ def think(t):
174
+ idle = [u for u in t.view.units if u.worker and u.order == "idle"]
175
+ home = t.view.buildings[0] if t.view.buildings else None
176
+ gold = t.game.nearest_resource(home.tile, "gold", 30) if home else None
177
+ if idle and gold:
178
+ t.act.gather(idle, gold)
179
+
180
+ result = play(Match(seed=7, max_ticks=1200, players=[Bot(r.races[0], think), Ai(r.races[1], level=2)]))
181
+ print(result.winner_slot, result.end_tick)
182
+ ```
183
+
184
+ <!-- concepts/time.md -->
185
+
186
+ # Time and turns
187
+
188
+ Ticks, turns, when your think runs and when your orders land.
189
+
190
+ - The game advances in **ticks**, 20 per second. Every number of time in the SDK is ticks unless it says seconds.
191
+ - 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.
192
+ - Your **think** runs every `everyTicks` ticks (default 10 = 0.5 s, minimum 2).
193
+ - 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.
194
+ - Locally, the game waits for each think, so a local game is reproducible for a given seed and deterministic bot.
195
+ - 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.
196
+ - `events` holds what happened to your player since your last think (see [../api/events.md](../api/events.md)).
197
+ - A local `Match` stops at `maxTicks` if nobody has won; `result.finished` is then `false`.
198
+
199
+ <!-- concepts/view.md -->
200
+
201
+ # Seeing the game
202
+
203
+ What a bot can know, how entities are named, and the two coordinate systems.
204
+
205
+ ## Fog of war
206
+
207
+ The view is built from your player's vision, exactly what a person sees on screen:
208
+
209
+ - `units`, `buildings`: everything you own.
210
+ - `enemyUnits`: enemy units your units or buildings see right now.
211
+ - `enemyBuildings`: enemy buildings you see now (`visible: true`) and the ones you have seen before (`visible: false`, last known state).
212
+ - `neutrals`: neutral creatures in sight.
213
+
214
+ There is no way to see more; the server verifies every match by replaying it.
215
+
216
+ ## Entity references
217
+
218
+ Every entity has an `eid` (slot index) and a `uid` (unique id). Slots are reused after an entity dies, so a stored `eid` is only the same entity while its `uid` matches. Orders that target an entity take the view object itself or `{ eid, uid }`.
219
+
220
+ ## Coordinates
221
+
222
+ - **Positions** (`x`, `y` on units and buildings, and the `x`, `y` of move orders) are in 1/256 tile.
223
+ - **Tiles** (`tile` on units and buildings, `at`, `near`, `from`, `to` in queries and build orders) are whole tiles `[tx, ty]`; `tile = [x >> 8, y >> 8]`.
224
+ - A building's `tile` is its top-left tile; `w`, `h` its size in tiles.
225
+ - The map is `mapSize × mapSize` tiles (see `rules().mapSize` and `game.map()`).
226
+
227
+ <!-- concepts/players.md -->
228
+
229
+ # Players
230
+
231
+ Who fills each slot of a match. They differ only in where orders come from.
232
+
233
+ | Kind | Where its orders come from | Where |
234
+ |---|---|---|
235
+ | `Bot({ race, think, everyTicks })` | your `think`, from the fogged view | local, custom, and you online |
236
+ | `Ai({ race, level })` | the built-in AI inside the simulation, levels 1–10 | local and custom |
237
+ | `Idle({ race })` | nobody; it never gives an order | local and custom |
238
+ | remote opponent | the server, from the other player (person or bot) | online |
239
+
240
+ - 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.
241
+ - The built-in AI sees only its own vision, like you.
242
+ - Bots play by the same rules as people online. Their names show as `<name> (AI)`; people can choose to avoid bots in matchmaking.
243
+
244
+ <!-- concepts/versioning.md -->
245
+
246
+ # Versioning
247
+
248
+ Why online play needs the right SDK version, and how to upgrade.
249
+
250
+ - The SDK bundles one engine version. The package version is `<ENGINE_VERSION>.0.<n>` (`ENGINE_VERSION` is exported); `n` counts SDK releases on the same engine.
251
+ - Every engine release is breaking: unit stats, ids and rules can change, and two clients on different engines would disagree about the game.
252
+ - **Offline** (local games, custom games, replays recorded with the same version) works with any SDK version, forever.
253
+ - **Online**, `Warpass.connect` compares versions with the server and throws `OutdatedEngine { have, need }` on a mismatch. Install the version in `need`:
254
+
255
+ ```
256
+ npm i @pikehub/warpass-bot@<need>
257
+ ```
258
+
259
+ - A replay records the engine it was played on; loading it on another version fails verification with reason `engine`.
260
+ - The rules pages under [../rules/index.md](../rules/index.md) are generated for the version you have installed.
261
+
262
+ <!-- concepts/limits.md -->
263
+
264
+ # Limits
265
+
266
+ The few limits that apply to bots online.
267
+
268
+ - **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.
269
+ - **Order size:** an order carries at most 256 values (its arguments and unit ids); the SDK stops merging before that. Split huge selections yourself.
270
+ - **Think interval:** at least 2 ticks.
271
+ - **One match at a time** per bot account; queueing while in a match is refused (`inMatch`).
272
+ - **Leaving:** a bot that disconnects has a grace period to reconnect, then forfeits, exactly like a person.
273
+ - 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.
274
+
275
+ <!-- api/online.md -->
276
+
277
+ # Online
278
+
279
+ Connect a bot account, queue or join a lobby by code, play a match and read the verified result.
280
+
281
+ | JS | Python | Returns |
282
+ |---|---|---|
283
+ | `await Warpass.connect({ token, url? })` | `Warpass.connect(token=, url=)` | `Warpass` (`name`, `engineVersion` / `engine_version`) |
284
+ | `await wp.queue({ race, avoidBots? })` | `wp.queue(race, avoid_bots=False)` | `Offer` once a rival is found |
285
+ | `await offer.accept({ everyTicks? })` | `offer.accept(every_ticks=10)` | `OnlineMatch` once both accepted |
286
+ | `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 |
287
+ | `await offer.decline()` | `offer.decline()` | declines; the queue may give you a short penalty |
288
+ | `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 |
289
+ | `match.onThink(think, { everyTicks? })` | `match.on_think(think, every_ticks=None)` | registers your think; returns the match |
290
+ | `await match.ended()` | `match.ended()` | `{ reason }` when the match ends |
291
+ | `await match.result({ timeoutMs? })` | `match.result(timeout_ms=120000)` | the verified result (below) |
292
+ | `match.game` | `match.game` | [queries](game.md) on the live game |
293
+ | `match.id`, `match.slot` | `match.id`, `match.slot` | the match id and your slot |
294
+
295
+ - `url` defaults to `DEFAULT_URL` (`https://warpass.pike.com.br`).
296
+ - `Offer.opponent` is `{ name, bot, rating, race }`.
297
+ - `lobby` fails with `refused` (`missing`, `full`, `started`) when the code is wrong or taken, and with `cancelled` (`closed`) when the host closes the lobby.
298
+ - In Python, `ended()` and `result()` run your think loop on the calling thread until the match ends; an `async def` think is awaited.
299
+ - In JS, the think loop runs in the background after `onThink`; `ended()` and `result()` wait for it.
300
+
301
+ ## Result
302
+
303
+ ```
304
+ { matchId, outcome, won, winnerSlot, endTick, verified, ratingDelta }
305
+ ```
306
+
307
+ - `outcome`: `victory`, `defeat`, `draw`, `surrender`, `abandoned` (your side).
308
+ - `verified`: `ok` when the server's replay of the match matched every client hash, `failed` otherwise.
309
+ - `ratingDelta`: points won or lost, `null` when the match was unrated (too short or idle).
310
+
311
+ ## Reconnects
312
+
313
+ 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.
314
+
315
+ <!-- api/local.md -->
316
+
317
+ # Local play
318
+
319
+ Play matches on your machine: bots, the built-in AI and idle players, one at a time or many in parallel.
320
+
321
+ ```js
322
+ import { Ai, Idle, Match, play, playMany, rules } from "@pikehub/warpass-bot";
323
+
324
+ const [a, b] = rules().races;
325
+ const one = await play(new Match({ seed: 3, maxTicks: 1200, players: [Ai({ race: a, level: 4 }), Idle({ race: b })] }));
326
+ 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 })] })));
327
+ console.log(one.endTick, many.map((g) => g.finalHash));
328
+ ```
329
+
330
+ ## Match
331
+
332
+ | JS | Python | |
333
+ |---|---|---|
334
+ | `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 |
335
+ | `Bot({ race, think, everyTicks?, name? })` | `Bot(race, think, every_ticks=10, name="")` | your code |
336
+ | `Ai({ race, level? })` | `Ai(race, level=5)` | built-in AI, level 1–10 |
337
+ | `Idle({ race })` | `Idle(race)` | never acts |
338
+
339
+ Slot numbers follow the order of `players`.
340
+
341
+ ## Playing
342
+
343
+ | JS | Python | |
344
+ |---|---|---|
345
+ | `await play(match)` | `play(match)` | one game; thinks run in order, so it is reproducible |
346
+ | `await playMany(matches, { concurrency? })` | `play_many(matches, workers=4)` | many games; AI-only lists run on every core inside the engine |
347
+
348
+ ## `GameResult`
349
+
350
+ | Field | |
351
+ |---|---|
352
+ | `winnerSlot` / `winner_slot` | winning slot, `-1` if none |
353
+ | `finished` | `false` when `maxTicks` stopped the game first |
354
+ | `reason` | why it ended (`unfinished` when stopped by `maxTicks`) |
355
+ | `endTick` / `end_tick`, `finalHash` / `final_hash` | where it stopped and the state hash there |
356
+ | `stats` | per slot: `unitsProduced`, `unitsLost`, `unitsKilled`, `buildingsBuilt`, `buildingsLost`, `buildingsRazed`, `gathered*`, `upgradesResearched`, `peakSupply`, `commandCount` |
357
+ | `timeline` | per slot every 10 s: resources, supply, workers, army, buildings |
358
+ | `replay` | a [Replay](replay.md) |
359
+ | `won(slot)` | `finished && winnerSlot === slot` |
360
+
361
+ In Python, results and views are dictionaries that also read as attributes in snake_case: `result.end_tick == result["endTick"]`.
362
+
363
+ <!-- api/view.md -->
364
+
365
+ # View
366
+
367
+ Everything a bot sees in one think: `view` (and `game.view()` / `match.game.view()`).
368
+
369
+ ```
370
+ {
371
+ tick, slot, race,
372
+ resources: { food, wood, stone, gold },
373
+ supply: { used, cap },
374
+ researched: [upgradeId], researching: [upgradeId],
375
+ units: [Unit], buildings: [Building],
376
+ enemyUnits: [Unit], enemyBuildings: [Building],
377
+ neutrals: [Unit],
378
+ players: [{ slot, race, defeated }],
379
+ over: winnerSlot | null
380
+ }
381
+ ```
382
+
383
+ ## Unit
384
+
385
+ | Field | |
386
+ |---|---|
387
+ | `eid`, `uid` | reference (see [../concepts/view.md](../concepts/view.md)) |
388
+ | `owner` | slot, or `-1` for neutral creatures |
389
+ | `type` | unit id (see [../rules/index.md](../rules/index.md)) |
390
+ | `worker` | can gather, build and repair |
391
+ | `x`, `y` | position in 1/256 tile |
392
+ | `tile` | `[tx, ty]` |
393
+ | `hp`, `maxHp` | hit points |
394
+ | `order` | `idle`, `move`, `attackMove`, `attack`, `gather`, `returnCargo`, `build`, `repair`, `hold`, `farm`, `follow`, `flee`, `guard` |
395
+ | `moving` | walking this tick |
396
+ | `target` | eid it is working on or attacking, `-1` if none |
397
+ | `carrying` | resource index it carries (0 food, 1 wood, 2 stone, 3 gold), `-1` if none |
398
+
399
+ ## Building
400
+
401
+ | Field | |
402
+ |---|---|
403
+ | `eid`, `uid`, `owner`, `type`, `x`, `y`, `hp`, `maxHp` | as for units |
404
+ | `tile`, `w`, `h` | top-left tile and size in tiles |
405
+ | `done` | construction finished |
406
+ | `progress` | construction progress |
407
+ | `queue` | your own buildings only: unit and upgrade ids in production, first is in progress |
408
+ | `visible` | `false` for an enemy building you saw before and can't see now (last known state) |
409
+
410
+ <!-- api/game.md -->
411
+
412
+ # Game queries
413
+
414
+ Questions a bot can ask during a think (`game`, or `match.game` online). Queries read the game and never change it.
415
+
416
+ | JS | Python | Returns |
417
+ |---|---|---|
418
+ | `canAfford(id)` | `can_afford(id)` | `true` if you have the resources for a unit, building or upgrade id |
419
+ | `cost(id)` | `cost(id)` | `{ food, wood, stone, gold }` with your current discounts, or `null` |
420
+ | `trainRefusal(building, unit)` | `train_refusal(building, unit)` | `null` if the building can train it now, else a reason |
421
+ | `canTrain(building, unit)` | `can_train(building, unit)` | `trainRefusal(...) === null` |
422
+ | `researchRefusal(building, upgrade)` | `research_refusal(building, upgrade)` | `null` or a reason |
423
+ | `canResearch(building, upgrade)` | `can_research(building, upgrade)` | |
424
+ | `placeRefusal(building, at)` | `place_refusal(building, at)` | `null` if a building id fits with its top-left at tile `at`, else a reason |
425
+ | `canPlace(building, at)` | `can_place(building, at)` | |
426
+ | `findPlacement(building, near, radius = 16)` | `find_placement(building, near, radius=16)` | `[tx, ty]` where it fits closest to `near`, or `null` |
427
+ | `nearestResource(from, resource, radius = 40)` | `nearest_resource(start, resource, radius=40)` | `[tx, ty]` of the nearest tile with `food`, `wood`, `stone` or `gold` left |
428
+ | `reachable(from, to)` | `reachable(start, end)` | `true` if your units can walk between the two tiles |
429
+ | `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 |
430
+ | `tile(at)` | `tile(at)` | `{ tile, x, y, terrain, height, passable, buildable, resource, amount }` |
431
+ | `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 |
432
+ | `view()` | `view()` | the current [view](view.md) |
433
+
434
+ - `building` arguments take a view object or an eid; `unit`, `upgrade` and building ids are strings from the rules.
435
+ - Tile arguments take `[tx, ty]` or an object with `x`, `y` in tiles.
436
+ - Training and research refusals: `notEnough` (resources), `supplyLimit`, `needHousing`, `queueFull`, `requirementsNotMet`, `limitReached`, `invalid` (wrong building, not finished, not yours).
437
+ - Placement refusals: `outOfBounds`, `somethingInWay`, `cannotBuildThere`, `unevenGround`, `rampBlocked`, `buildOnSite` / `siteGround` (site buildings must stand on their site), `alreadyBuilt`, `limitReached`, `requirementsNotMet`, `unknownBuilding`.
438
+
439
+ <!-- api/act.md -->
440
+
441
+ # Orders
442
+
443
+ Every order a bot can give, on `act` during a think (and on the builder passed to `CustomGame.order`).
444
+
445
+ `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.
446
+
447
+ | JS | Python | |
448
+ |---|---|---|
449
+ | `move(who, x, y, { queued })` | `move(who, x, y, queued=False)` | walk there, ignoring enemies |
450
+ | `attackMove(who, x, y, { queued })` | `attack_move(who, x, y, queued=False)` | walk there, fighting what they meet |
451
+ | `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` |
452
+ | `formUp(who)` | `form_up(who)` | gather into formation where they stand |
453
+ | `attack(who, target, { queued })` | `attack(who, target, queued=False)` | attack one entity (`target` = a view object or `{ eid, uid }`) |
454
+ | `target(who, target, { queued })` | `target(who, target, queued=False)` | the right-click order: attack an enemy, gather a resource, repair your building |
455
+ | `repair(who, target, { queued })` | `repair(who, target, queued=False)` | workers repair one of your buildings |
456
+ | `gather(who, tile, { queued })` | `gather(who, tile, queued=False)` | workers gather the resource on a tile (from `nearestResource`) |
457
+ | `build(who, building, at, { queued })` | `build(who, building, at, queued=False)` | workers place and build a building with its top-left at `at`; pays on placement |
458
+ | `buildLine(who, building, from, to, { queued })` | `build_line(who, building, start, end, queued=False)` | a line of wall segments |
459
+ | `train(building, unit, count = 1)` | `train(building, unit, count=1)` | queue units (pays now) |
460
+ | `research(building, upgrade)` | `research(building, upgrade)` | queue an upgrade (pays now) |
461
+ | `cancel(building, index)` | `cancel(building, index)` | cancel queue entry `index` (0 = in progress) and get the cost back |
462
+ | `rally(buildings, x, y)` | `rally(buildings, x, y)` | where new units from those buildings walk |
463
+ | `stop(who)` | `stop(who)` | drop all orders |
464
+ | `hold(who)` | `hold(who)` | stand still and fight only what comes in range |
465
+ | `remove(who)` | `remove(who)` | delete your own units or buildings |
466
+ | `spawnToggle(buildings, on)` | `spawn_toggle(buildings, on)` | pause or resume buildings that produce units by themselves |
467
+ | `surrender()` | `surrender()` | concede the match |
468
+
469
+ - Orders that the game refuses (not enough resources, wrong building, dead unit) are ignored when they land; check with [queries](game.md) first.
470
+ - Low level: `act.push(name, args)` takes the order name and an argument object (`{ units, x, y, queued, … }`); the methods above call it.
471
+
472
+ <!-- api/events.md -->
473
+
474
+ # Events
475
+
476
+ 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).
477
+
478
+ | `t` | When |
479
+ |---|---|
480
+ | `trained` | one of your units finished training |
481
+ | `researched` | an upgrade finished |
482
+ | `built` | a building finished |
483
+ | `placed` | a foundation was placed |
484
+ | `death` | an entity died (`owner`, `killer` slot, `kind`, position): yours, one you killed, or one where you have vision |
485
+ | `hit` | an entity took damage (`eid` attacker, `tgt` target, position): yours dealt or took it, or it happened where you have vision |
486
+ | `defeated` | a player was defeated |
487
+ | `over` | the match ended (`winnerSlot`) |
488
+ | `deplete` | a resource tile ran out |
489
+ | `deposit` | a worker dropped off resources |
490
+ | `site` | a site (an objective on the map) changed hands |
491
+ | `weather`, `forecast` | weather started or is coming |
492
+ | `msg` | a game message for the player (`key`, `params`) |
493
+ | `glimpse`, `armyPing` | an enemy was spotted |
494
+
495
+ 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.
496
+
497
+ ## Fog of war
498
+
499
+ You get exactly the events a person playing your slot gets. Every event falls in one of four groups:
500
+
501
+ | Group | Tags | You get it |
502
+ |---|---|---|
503
+ | public | `defeated`, `over`, `site`, `weather`, `forecast`, `meteor`, `meteorWarn`, `lightning`, `lightningWarn`, `fire`, `fireOut`, `msg` without an owner (site and weather news) | always |
504
+ | private | `trained`, `researched`, `built`, `placed`, `deposit`, `reseed`, `glimpse`, `armyPing`, `aiOpportunity`, `aiRaid`, `aiRetreat`, `msg` with an owner | only your own |
505
+ | 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) |
506
+ | hidden | `aiWave`, `aiCounter` | never |
507
+
508
+ <!-- api/custom.md -->
509
+
510
+ # Custom games
511
+
512
+ 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.
513
+
514
+ ```js
515
+ import { CustomGame, rules } from "@pikehub/warpass-bot";
516
+
517
+ const r = rules();
518
+ const [a, b] = r.races;
519
+ const g = new CustomGame({ seed: 11, races: [a, b] });
520
+ const [x0, y0] = g.flattenArea(30, 16);
521
+ const soldier = r.units.find((u) => u.race === a && !u.worker && u.attack);
522
+ const target = r.buildings.filter((x) => x.race === b && !x.site).sort((p, q) => p.hp - q.hp)[0];
523
+ const hall = g.building(1, target.id, x0 + 20, y0 + 6);
524
+ g.reveal(0, hall);
525
+ const army = [0, 1, 2, 3].map((k) => g.spawn(0, soldier.id, (x0 + 3) * 256, (y0 + 4 + k) * 256));
526
+ const ref = g.observer().enemyBuildings.find((x) => x.eid === hall) ?? g.observer().buildings.find((x) => x.eid === hall);
527
+ g.order(0, (act) => act.attack(army, ref));
528
+ const ticks = g.until(20 * 120, (obs) => !obs.buildings.concat(obs.enemyBuildings).some((x) => x.eid === hall && x.uid === ref.uid));
529
+ console.log(ticks > 0 ? `razed in ${ticks / 20} s` : "still standing");
530
+ console.log(g.replay().verify().ok);
531
+ ```
532
+
533
+ ## Creating
534
+
535
+ `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`).
536
+
537
+ ## Setup ops (before the first step)
538
+
539
+ | JS | Python | Returns |
540
+ |---|---|---|
541
+ | `flattenArea(w, h)` | `flatten_area(w, h)` | `[tx, ty]`: an empty flat grass rectangle away from bases |
542
+ | `spawn(owner, unit, x, y)` | `spawn(owner, unit, x, y)` | eid of a new unit at position `x`, `y` (1/256 tile) |
543
+ | `building(owner, building, tx, ty)` | `building(owner, building, tx, ty)` | eid of a finished building at tile `tx`, `ty` |
544
+ | `terrain(tx, ty, terrain)` | `terrain(tx, ty, terrain)` | set one tile (`terrain` = name or id from `rules().terrains`) |
545
+ | `research(owner, upgrade)` | `research(owner, upgrade)` | complete an upgrade |
546
+ | `resources(owner, { food, wood, stone, gold })` | `resources(owner, food=0, wood=0, stone=0, gold=0)` | set a bank |
547
+ | `weather(key, duration, ramp?)` | `weather(key, duration, ramp=0)` | force weather for `duration` ticks |
548
+ | `reveal(owner, eid = -1)` | `reveal(owner, eid=-1)` | make `owner` know an entity (`-1`: every building) |
549
+ | `remove(eid)` | `remove(eid)` | remove an entity |
550
+ | `setup(op)` | `setup(op)` | any op as data, e.g. `{ op: "spawn", owner, unit, x, y }` |
551
+
552
+ Ops after the first step still apply but make the game unreplayable.
553
+
554
+ ## Playing
555
+
556
+ | JS | Python | |
557
+ |---|---|---|
558
+ | `order(slot, (act) => …)` | `order(slot, lambda act: …)` | give [orders](act.md) for any slot |
559
+ | `step(n = 1)` | `step(n=1)` | advance `n` ticks; returns the tick |
560
+ | `until(maxTicks, (observer, tick) => done)` | `until(max_ticks, lambda observer, tick: done)` | step until `done`; returns ticks taken or `-1` |
561
+ | `view(slot)` | `view(slot)` | the fogged [view](view.md) of a slot |
562
+ | `observer()` | `observer()` | the full view of everything, no fog |
563
+ | `game(slot)` | `game(slot)` | [queries](game.md) as that slot |
564
+ | `map()` | `map()` | map size, starts, chokes, sites |
565
+ | `replay()` | `replay()` | a [Replay](replay.md) of setup and orders so far |
566
+ | `tick`, `winner` | `tick`, `winner` | current tick; winning slot once over |
567
+
568
+ <!-- api/replay.md -->
569
+
570
+ # Replays
571
+
572
+ A replay is the seed, the setup, every order and the state hashes. Replaying it gives the same game, tick for tick.
573
+
574
+ | JS | Python | |
575
+ |---|---|---|
576
+ | `result.replay` / `customGame.replay()` | same | a `Replay` |
577
+ | `replay.save(path)` | `replay.save(path)` | write JSON |
578
+ | `Replay.load(pathOrText)` | `Replay.load(path_or_text)` | read JSON |
579
+ | `replay.verify()` | `replay.verify()` | replay it and compare hashes |
580
+ | `replay.analyse()` | `replay.analyse()` | the combat analysis from `verify()` |
581
+ | `replay.data` | `replay.data` | `{ engineVersion, config, commands, hashes, endTick }` |
582
+
583
+ `verify()` returns `{ ok, reason, tick, stats, timeline, events, analysis }`:
584
+
585
+ - `ok`: every recorded hash matched; `reason` is `mismatch` (with the first bad `tick`) or `engine` (recorded on another engine version).
586
+ - `analysis.minutes`: per slot and minute, kills, losses, buildings razed and lost, units trained, damage dealt and taken.
587
+ - `analysis.units`: per unit and building type, trained, lost, kills, damage dealt and taken.
588
+ - `analysis.army`: army composition per minute.
589
+
590
+ ```js
591
+ import { Ai, Match, Replay, play, rules } from "@pikehub/warpass-bot";
592
+
593
+ const [a, b] = rules().races;
594
+ const game = await play(new Match({ seed: 9, maxTicks: 1200, players: [Ai({ race: a, level: 3 }), Ai({ race: b, level: 3 })] }));
595
+ const again = Replay.load(JSON.stringify(game.replay.data));
596
+ console.log(again.verify().ok, again.analyse().minutes.length);
597
+ ```
598
+
599
+ <!-- api/rules.md -->
600
+
601
+ # Rules and docs
602
+
603
+ Static facts about the bundled engine, and these docs, from code.
604
+
605
+ | JS | Python | |
606
+ |---|---|---|
607
+ | `rules(race?)` | `rules(race=None)` | every unit, building and upgrade (or one race's) |
608
+ | `docs(page?)` | `docs(page=None)` | a docs page as Markdown; no argument: `llms-full.txt`, every page in one text |
609
+ | `docsIndex()` | `docs_index()` | every page path |
610
+ | `ENGINE_VERSION` | `ENGINE_VERSION` | the engine this SDK runs |
611
+ | `TILE`, `TICKS_PER_SECOND` | same | 256 and 20 |
612
+ | `DEFAULT_URL` | same | the public server |
613
+ | `WarpassError` | `WarpassError` | see [../errors.md](../errors.md) |
614
+ | `Orders` | `Orders` | the order builder behind `act` |
615
+
616
+ `rules()` returns:
617
+
618
+ ```
619
+ { engineVersion, races, resources, terrains: { name: id }, tile, ticksPerSecond, mapSize,
620
+ units: [{ id, race, cost, supply, hp, speed, sight, trainTicks, trainedAt, requires, abilities, worker, lane, armor: { melee, pierce }, attack }],
621
+ buildings: [{ id, race, size, cost, hp, buildTicks, supply, trains, researches, requires, site }],
622
+ upgrades: [{ id, race, cost, ticks, requires, group }] }
623
+ ```
624
+
625
+ `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/`.
626
+
627
+ ```js
628
+ import { docs, docsIndex } from "@pikehub/warpass-bot";
629
+
630
+ console.log(docsIndex().length, docs("AGENTS.md").split("\n")[0]);
631
+ ```
632
+
633
+ <!-- guides/economy.md -->
634
+
635
+ # An economy bot
636
+
637
+ A bot that keeps workers busy, builds housing before it is needed and trains from every production building. Uses only rules data, so it works for every race.
638
+
639
+ ```js
640
+ import { Ai, Bot, Match, play, rules } from "@pikehub/warpass-bot";
641
+
642
+ const r = rules();
643
+ const race = r.races[0];
644
+ const worker = r.units.find((u) => u.race === race && u.worker);
645
+ const hall = r.buildings.find((b) => b.race === race && b.trains.includes(worker.id));
646
+ const house = r.buildings.filter((b) => b.race === race && b.supply > 0 && b.id !== hall.id && !b.site).sort((a, b) => a.cost.wood - b.cost.wood)[0];
647
+
648
+ function think({ view, game, act }) {
649
+ const home = view.buildings.find((b) => b.type === hall.id && b.done);
650
+ if (!home) return;
651
+ const workers = view.units.filter((u) => u.worker);
652
+ const idle = workers.filter((u) => u.order === "idle");
653
+ if (workers.length < 20 && home.queue.length < 2 && game.canTrain(home, worker.id)) act.train(home, worker.id, 1);
654
+ const building = view.buildings.some((b) => b.type === house.id && !b.done);
655
+ if (house && !building && view.supply.cap - view.supply.used <= 3 && game.canAfford(house.id) && workers.length) {
656
+ const spot = game.findPlacement(house.id, home.tile, 12);
657
+ if (spot) act.build(workers[0], house.id, spot);
658
+ }
659
+ for (const [i, kind] of ["food", "wood", "gold"].entries()) {
660
+ const crew = idle.filter((_, k) => k % 3 === i);
661
+ const tile = game.nearestResource(home.tile, kind, 30);
662
+ if (crew.length && tile) act.gather(crew, tile);
663
+ }
664
+ }
665
+
666
+ const g = await play(new Match({ seed: 5, maxTicks: 6000, players: [Bot({ race, think }), Ai({ race: r.races[1], level: 1 })] }));
667
+ console.log(g.stats[0].unitsProduced, g.stats[0].buildingsBuilt);
668
+ ```
669
+
670
+ Ideas to grow it: send a fixed share of workers to wood, add military buildings from `rules().buildings` whose `trains` contains combat units, research upgrades from `home.researches`, and attack with `attackMove` once the army is large.
671
+
672
+ <!-- guides/testing.md -->
673
+
674
+ # Testing a bot
675
+
676
+ One game says little; measure over seeds. Local games are exact, so the same bot and seed always give the same game, and a change in the bot shows up as a change in results.
677
+
678
+ ```js
679
+ import { Ai, Bot, Match, playMany, rules } from "@pikehub/warpass-bot";
680
+
681
+ const [mine, theirs] = rules().races;
682
+ const passive = () => {};
683
+ const seeds = [1, 2, 3, 4];
684
+ const games = await playMany(
685
+ seeds.map((seed) => new Match({ seed, maxTicks: 1200, players: [Bot({ race: mine, think: passive }), Ai({ race: theirs, level: 2 })] })),
686
+ { concurrency: 4 },
687
+ );
688
+ const wins = games.filter((g) => g.won(0)).length;
689
+ console.log(`${wins}/${games.length} wins, ${games.filter((g) => !g.finished).length} unfinished`);
690
+ ```
691
+
692
+ - Compare two versions of a bot on the same seeds; a difference is the change, not luck.
693
+ - Climb the AI levels: when a bot wins most games at one level, try the next.
694
+ - Keep the replays of losses (`g.replay.save(...)`) and read `analyse()` to see where fights went wrong.
695
+ - AI-only match lists run on every core inside the engine; matches with your bot run your think in this process, `concurrency` at a time.
696
+
697
+ <!-- guides/drills.md -->
698
+
699
+ # Drills with custom games
700
+
701
+ Measure one thing in isolation: how fast a group razes a building, who wins a fight at equal cost, how long a march takes. A custom game stages it in a flat arena and replays exactly.
702
+
703
+ ```js
704
+ import { CustomGame, rules } from "@pikehub/warpass-bot";
705
+
706
+ const r = rules();
707
+ const [a, b] = r.races;
708
+ const fighter = (race) => r.units.filter((u) => u.race === race && !u.worker && u.attack && u.attack.range < 256)[0];
709
+
710
+ function duel(seed, n) {
711
+ const g = new CustomGame({ seed, races: [a, b] });
712
+ const [x0, y0] = g.flattenArea(24, 12);
713
+ const left = Array.from({ length: n }, (_, k) => g.spawn(0, fighter(a).id, (x0 + 4) * 256, (y0 + 2 + k) * 256));
714
+ const right = Array.from({ length: n }, (_, k) => g.spawn(1, fighter(b).id, (x0 + 18) * 256, (y0 + 2 + k) * 256));
715
+ g.order(0, (act) => act.attackMove(left, (x0 + 18) * 256, (y0 + 6) * 256));
716
+ g.order(1, (act) => act.attackMove(right, (x0 + 4) * 256, (y0 + 6) * 256));
717
+ const alive = (obs, owner) => obs.units.concat(obs.enemyUnits).filter((u) => u.owner === owner && !u.worker && u.hp > 0).length;
718
+ g.until(20 * 60, (obs) => alive(obs, 0) === 0 || alive(obs, 1) === 0);
719
+ const end = g.observer();
720
+ return [alive(end, 0), alive(end, 1)];
721
+ }
722
+
723
+ console.log([1, 2, 3].map((seed) => duel(seed, 5)));
724
+ ```
725
+
726
+ `observer()` shows everything (no fog), which is what a drill needs to judge the outcome; the units themselves still only act on the orders you give.
727
+
728
+ <!-- guides/llm.md -->
729
+
730
+ # LLM strategists
731
+
732
+ Language models are slow next to a game running at 20 ticks per second. The pattern that works: a fast scripted bot that executes a plan every think, and a slow planner that rewrites the plan every few seconds, never blocking the think.
733
+
734
+ ```js online
735
+ import { Warpass } from "@pikehub/warpass-bot";
736
+
737
+ let plan = { focus: "economy", attackAt: null };
738
+ let planning = false;
739
+
740
+ async function replan(view) {
741
+ planning = true;
742
+ try {
743
+ const summary = { tick: view.tick, resources: view.resources, supply: view.supply, army: view.units.filter((u) => !u.worker).length, seen: view.enemyBuildings.length };
744
+ plan = await askModel(summary);
745
+ } finally {
746
+ planning = false;
747
+ }
748
+ }
749
+
750
+ const wp = await Warpass.connect({ token: process.env.WARPASS_BOT_TOKEN });
751
+ const match = await (await wp.queue({ race: "templars" })).accept();
752
+ match.onThink(({ view, game, act }) => {
753
+ if (!planning && view.tick % 200 === 0) replan(view);
754
+ runPlan(plan, view, game, act);
755
+ });
756
+ console.log(await match.result());
757
+ ```
758
+
759
+ - `askModel` sends a compact summary (counts, not every unit) and parses a small JSON plan; `runPlan` is ordinary code that turns the plan into orders.
760
+ - Don't `await` the model inside `think`: online, a think that takes seconds makes your bot skip seconds of play.
761
+ - Put [AGENTS.md](../AGENTS.md) and [rules/index.md](../rules/index.md) in the model's context; `docs()` returns every page as one text.
762
+ - Test the planner offline with `play` before going online; replace `askModel` with a stub that returns fixed plans to keep games reproducible.
763
+
764
+ <!-- errors.md -->
765
+
766
+ # Errors
767
+
768
+ Every SDK call that fails throws a `WarpassError` with a `kind` and the fields below. In JS, `err.kind`; in Python, `err.kind` and `err.info`.
769
+
770
+ | `kind` | Fields | Meaning | What to do |
771
+ |---|---|---|---|
772
+ | `outdatedEngine` | `have`, `need` | the server runs another engine | install the SDK version `need` |
773
+ | `unauthenticated` | | the key is wrong, rotated, or the bot was deleted | create or rotate the key on the Developer page |
774
+ | `refused` | `about`, `reason`, `message` | the server refused a request | see reasons below |
775
+ | `cancelled` | `reason` | the offer fell through (`declined`, `timeout`, the rival left) or the host closed the lobby (`closed`) | queue again |
776
+ | `closed` | `reason` | the socket closed and could not reconnect | connect again; `Warpass.join` resumes a running match |
777
+ | `timeout` | | no answer in time (`result` waits 120 s by default) | retry `result()`; the match may still be sealing |
778
+ | `network` | `message` | DNS, TLS or HTTP failure | check `url` and connectivity |
779
+
780
+ Reasons for `refused`:
781
+
782
+ | `reason` | |
783
+ |---|---|
784
+ | `version` | the queue or lobby is for another engine (thrown as `outdatedEngine`) |
785
+ | `inMatch` | this bot is already in a match; finish it or `Warpass.join` it |
786
+ | `penalty` | declined or left recently; wait the time in `message` |
787
+ | `invalid` | a malformed request (unknown race, bad arguments) |
788
+ | `missing` | `lobby` with a code that doesn't exist (or expired) |
789
+ | `full` | `lobby` when another player already took the guest seat |
790
+ | `started` | `lobby` when that lobby's battle already started |
791
+ | `matchNotFound` | `join` with a match id that doesn't exist or isn't yours |
792
+
793
+ Errors from local games and custom games (unknown ids, setup ops off the map) are thrown with `kind: "error"` and a message naming the problem.
794
+
795
+ <!-- rules/index.md -->
796
+
797
+ # Rules for engine 62
798
+
799
+ Generated from the engine this SDK bundles. Every id below is what the view, the queries and the orders use.
800
+
801
+ - Ticks per second: 20
802
+ - One tile is 256 position units; positions (`x`, `y`) are in 1/256 tile, tiles (`tile`, `at`) in whole tiles
803
+ - Map size: 256 × 256 tiles
804
+ - Resources: food, wood, stone, gold
805
+ - Races: [templars](templars.md), [orcs](orcs.md), [witches](witches.md)
806
+
807
+ ## Terrain ids
808
+
809
+ | id | terrain |
810
+ |---|---|
811
+ | 0 | deep |
812
+ | 1 | shallow |
813
+ | 2 | river |
814
+ | 3 | sand |
815
+ | 4 | grass |
816
+ | 5 | dirt |
817
+ | 6 | road |
818
+ | 7 | forest |
819
+ | 8 | thicket |
820
+ | 9 | rock |
821
+ | 10 | gold |
822
+ | 11 | stone |
823
+ | 12 | berries |
824
+ | 13 | marsh |
825
+ | 14 | bridge |
826
+ | 15 | burnt |
827
+ | 16 | stumps |
828
+ | 17 | spentVein |
829
+ | 18 | pickedBush |
830
+ | 19 | orchard |
831
+ | 20 | grainField |
832
+ | 21 | mushrooms |
833
+ | 22 | stubble |
834
+
835
+ ## Units of measure
836
+
837
+ - `hp`, `damage`, `armor`: hit points
838
+ - `speed`: 1/256 tile per tick (tiles per second = speed × 20 / 256)
839
+ - `range`, `minRange`: 1/256 tile
840
+ - `sight`: tiles
841
+ - `cooldown`, `trainTicks`, `buildTicks`, `ticks`: ticks
842
+