@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.
@@ -0,0 +1,15 @@
1
+ # Versioning
2
+
3
+ Why online play needs the right SDK version, and how to upgrade.
4
+
5
+ - 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.
6
+ - Every engine release is breaking: unit stats, ids and rules can change, and two clients on different engines would disagree about the game.
7
+ - **Offline** (local games, custom games, replays recorded with the same version) works with any SDK version, forever.
8
+ - **Online**, `Warpass.connect` compares versions with the server and throws `OutdatedEngine { have, need }` on a mismatch. Install the version in `need`:
9
+
10
+ ```
11
+ npm i @pikehub/warpass-bot@<need>
12
+ ```
13
+
14
+ - A replay records the engine it was played on; loading it on another version fails verification with reason `engine`.
15
+ - The rules pages under [../rules/index.md](../rules/index.md) are generated for the version you have installed.
@@ -0,0 +1,25 @@
1
+ # Seeing the game
2
+
3
+ What a bot can know, how entities are named, and the two coordinate systems.
4
+
5
+ ## Fog of war
6
+
7
+ The view is built from your player's vision, exactly what a person sees on screen:
8
+
9
+ - `units`, `buildings`: everything you own.
10
+ - `enemyUnits`: enemy units your units or buildings see right now.
11
+ - `enemyBuildings`: enemy buildings you see now (`visible: true`) and the ones you have seen before (`visible: false`, last known state).
12
+ - `neutrals`: neutral creatures in sight.
13
+
14
+ There is no way to see more; the server verifies every match by replaying it.
15
+
16
+ ## Entity references
17
+
18
+ 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 }`.
19
+
20
+ ## Coordinates
21
+
22
+ - **Positions** (`x`, `y` on units and buildings, and the `x`, `y` of move orders) are in 1/256 tile.
23
+ - **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]`.
24
+ - A building's `tile` is its top-left tile; `w`, `h` its size in tiles.
25
+ - The map is `mapSize × mapSize` tiles (see `rules().mapSize` and `game.map()`).
package/docs/errors.md ADDED
@@ -0,0 +1,28 @@
1
+ # Errors
2
+
3
+ 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`.
4
+
5
+ | `kind` | Fields | Meaning | What to do |
6
+ |---|---|---|---|
7
+ | `outdatedEngine` | `have`, `need` | the server runs another engine | install the SDK version `need` |
8
+ | `unauthenticated` | | the key is wrong, rotated, or the bot was deleted | create or rotate the key on the Developer page |
9
+ | `refused` | `about`, `reason`, `message` | the server refused a request | see reasons below |
10
+ | `cancelled` | `reason` | the offer fell through (`declined`, `timeout`, the rival left) or the host closed the lobby (`closed`) | queue again |
11
+ | `closed` | `reason` | the socket closed and could not reconnect, or you called `close()` (`stopped`) | connect again; `Warpass.join` resumes a running match |
12
+ | `timeout` | | no answer in time (`result` waits 120 s by default) | retry `result()`; the match may still be sealing |
13
+ | `network` | `message` | DNS, TLS or HTTP failure | check `url` and connectivity |
14
+
15
+ Reasons for `refused`:
16
+
17
+ | `reason` | |
18
+ |---|---|
19
+ | `version` | the queue or lobby is for another engine (thrown as `outdatedEngine`) |
20
+ | `inMatch` | this bot is already in a match; finish it or `Warpass.join` it |
21
+ | `penalty` | declined or left recently; wait the time in `message` |
22
+ | `invalid` | a malformed request (unknown race, bad arguments) |
23
+ | `missing` | `lobby` with a code that doesn't exist (or expired) |
24
+ | `full` | `lobby` when another player already took the guest seat |
25
+ | `started` | `lobby` when that lobby's battle already started |
26
+ | `matchNotFound` | `join` with a match id that doesn't exist or isn't yours |
27
+
28
+ 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.
@@ -0,0 +1,28 @@
1
+ # Drills with custom games
2
+
3
+ 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.
4
+
5
+ ```js
6
+ import { CustomGame, rules } from "@pikehub/warpass-bot";
7
+
8
+ const r = rules();
9
+ const [a, b] = r.races;
10
+ const fighter = (race) => r.units.filter((u) => u.race === race && !u.worker && u.attack && u.attack.range < 256)[0];
11
+
12
+ function duel(seed, n) {
13
+ const g = new CustomGame({ seed, races: [a, b] });
14
+ const [x0, y0] = g.flattenArea(24, 12);
15
+ const left = Array.from({ length: n }, (_, k) => g.spawn(0, fighter(a).id, (x0 + 4) * 256, (y0 + 2 + k) * 256));
16
+ const right = Array.from({ length: n }, (_, k) => g.spawn(1, fighter(b).id, (x0 + 18) * 256, (y0 + 2 + k) * 256));
17
+ g.order(0, (act) => act.attackMove(left, (x0 + 18) * 256, (y0 + 6) * 256));
18
+ g.order(1, (act) => act.attackMove(right, (x0 + 4) * 256, (y0 + 6) * 256));
19
+ const alive = (obs, owner) => obs.units.concat(obs.enemyUnits).filter((u) => u.owner === owner && !u.worker && u.hp > 0).length;
20
+ g.until(20 * 60, (obs) => alive(obs, 0) === 0 || alive(obs, 1) === 0);
21
+ const end = g.observer();
22
+ return [alive(end, 0), alive(end, 1)];
23
+ }
24
+
25
+ console.log([1, 2, 3].map((seed) => duel(seed, 5)));
26
+ ```
27
+
28
+ `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.
@@ -0,0 +1,36 @@
1
+ # An economy bot
2
+
3
+ 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.
4
+
5
+ ```js
6
+ import { Ai, Bot, Match, play, rules } from "@pikehub/warpass-bot";
7
+
8
+ const r = rules();
9
+ const race = r.races[0];
10
+ const worker = r.units.find((u) => u.race === race && u.worker);
11
+ const hall = r.buildings.find((b) => b.race === race && b.trains.includes(worker.id));
12
+ 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];
13
+
14
+ function think({ view, game, act }) {
15
+ const home = view.buildings.find((b) => b.type === hall.id && b.done);
16
+ if (!home) return;
17
+ const workers = view.units.filter((u) => u.worker);
18
+ const idle = workers.filter((u) => u.order === "idle");
19
+ if (workers.length < 20 && home.queue.length < 2 && game.canTrain(home, worker.id)) act.train(home, worker.id, 1);
20
+ const building = view.buildings.some((b) => b.type === house.id && !b.done);
21
+ if (house && !building && view.supply.cap - view.supply.used <= 3 && game.canAfford(house.id) && workers.length) {
22
+ const spot = game.findPlacement(house.id, home.tile, 12);
23
+ if (spot) act.build(workers[0], house.id, spot);
24
+ }
25
+ for (const [i, kind] of ["food", "wood", "gold"].entries()) {
26
+ const crew = idle.filter((_, k) => k % 3 === i);
27
+ const tile = game.nearestResource(home.tile, kind, 30);
28
+ if (crew.length && tile) act.gather(crew, tile);
29
+ }
30
+ }
31
+
32
+ const g = await play(new Match({ seed: 5, maxTicks: 6000, players: [Bot({ race, think }), Ai({ race: r.races[1], level: 1 })] }));
33
+ console.log(g.stats[0].unitsProduced, g.stats[0].buildingsBuilt);
34
+ ```
35
+
36
+ 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.
@@ -0,0 +1,33 @@
1
+ # LLM strategists
2
+
3
+ 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.
4
+
5
+ ```js online
6
+ import { Warpass } from "@pikehub/warpass-bot";
7
+
8
+ let plan = { focus: "economy", attackAt: null };
9
+ let planning = false;
10
+
11
+ async function replan(view) {
12
+ planning = true;
13
+ try {
14
+ const summary = { tick: view.tick, resources: view.resources, supply: view.supply, army: view.units.filter((u) => !u.worker).length, seen: view.enemyBuildings.length };
15
+ plan = await askModel(summary);
16
+ } finally {
17
+ planning = false;
18
+ }
19
+ }
20
+
21
+ const wp = await Warpass.connect({ token: process.env.WARPASS_BOT_TOKEN });
22
+ const match = await (await wp.queue({ race: "templars" })).accept();
23
+ match.onThink(({ view, game, act }) => {
24
+ if (!planning && view.tick % 200 === 0) replan(view);
25
+ runPlan(plan, view, game, act);
26
+ });
27
+ console.log(await match.result());
28
+ ```
29
+
30
+ - `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.
31
+ - Don't `await` the model inside `think`: online, a think that takes seconds makes your bot skip seconds of play.
32
+ - Put [AGENTS.md](../AGENTS.md) and [rules/index.md](../rules/index.md) in the model's context; `docs()` returns every page as one text.
33
+ - Test the planner offline with `play` before going online; replace `askModel` with a stub that returns fixed plans to keep games reproducible.
@@ -0,0 +1,22 @@
1
+ # Testing a bot
2
+
3
+ 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.
4
+
5
+ ```js
6
+ import { Ai, Bot, Match, playMany, rules } from "@pikehub/warpass-bot";
7
+
8
+ const [mine, theirs] = rules().races;
9
+ const passive = () => {};
10
+ const seeds = [1, 2, 3, 4];
11
+ const games = await playMany(
12
+ seeds.map((seed) => new Match({ seed, maxTicks: 1200, players: [Bot({ race: mine, think: passive }), Ai({ race: theirs, level: 2 })] })),
13
+ { concurrency: 4 },
14
+ );
15
+ const wins = games.filter((g) => g.won(0)).length;
16
+ console.log(`${wins}/${games.length} wins, ${games.filter((g) => !g.finished).length} unfinished`);
17
+ ```
18
+
19
+ - Compare two versions of a bot on the same seeds; a difference is the change, not luck.
20
+ - Climb the AI levels: when a bot wins most games at one level, try the next.
21
+ - Keep the replays of losses (`g.replay.save(...)`) and read `analyse()` to see where fights went wrong.
22
+ - 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.