@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.
package/index.js ADDED
@@ -0,0 +1,483 @@
1
+ import { createRequire } from "node:module";
2
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
3
+ import { dirname, join } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+
6
+ const require = createRequire(import.meta.url);
7
+ const here = dirname(fileURLToPath(import.meta.url));
8
+
9
+ function triple() {
10
+ const abi = process.platform === "linux" ? "-gnu" : process.platform === "win32" ? "-msvc" : "";
11
+ return `${process.platform}-${process.arch}${abi}`;
12
+ }
13
+
14
+ function loadNative() {
15
+ const local = join(here, `warpass-bot.${triple()}.node`);
16
+ if (existsSync(local)) return require(local);
17
+ return require(`@pikehub/warpass-bot-${triple()}`);
18
+ }
19
+
20
+ const native = loadNative();
21
+
22
+ export const ENGINE_VERSION = native.ENGINE_VERSION;
23
+ export const DEFAULT_URL = "https://warpass.pike.com.br";
24
+ export const TILE = 256;
25
+ export const TICKS_PER_SECOND = 20;
26
+
27
+ const parse = (text) => (text === null || text === undefined ? null : JSON.parse(text));
28
+
29
+ export class WarpassError extends Error {
30
+ constructor(raw) {
31
+ let info = null;
32
+ try {
33
+ info = JSON.parse(raw);
34
+ } catch {
35
+ info = null;
36
+ }
37
+ super(info && info.kind ? describe(info) : String(raw));
38
+ this.name = "WarpassError";
39
+ this.kind = info?.kind ?? "error";
40
+ Object.assign(this, info || {});
41
+ }
42
+ }
43
+
44
+ function describe(e) {
45
+ switch (e.kind) {
46
+ case "outdatedEngine":
47
+ return `This SDK runs engine ${e.have} but the server runs engine ${e.need}. Install @pikehub/warpass-bot@${e.need}.`;
48
+ case "unauthenticated":
49
+ return "The bot key was refused.";
50
+ case "refused":
51
+ return `${e.about} refused (${e.reason}): ${e.message}`;
52
+ case "cancelled":
53
+ return `The match offer was cancelled (${e.reason}).`;
54
+ default:
55
+ return e.message || e.reason || e.kind;
56
+ }
57
+ }
58
+
59
+ function attempt(call) {
60
+ try {
61
+ return call();
62
+ } catch (err) {
63
+ throw new WarpassError(err && err.message ? err.message : err);
64
+ }
65
+ }
66
+
67
+ async function guard(promise) {
68
+ try {
69
+ return await promise;
70
+ } catch (err) {
71
+ throw new WarpassError(err && err.message ? err.message : err);
72
+ }
73
+ }
74
+
75
+ function units(list) {
76
+ return (Array.isArray(list) ? list : [list]).map((u) => (typeof u === "number" ? u : u.eid));
77
+ }
78
+
79
+ function point(p) {
80
+ return Array.isArray(p) ? p : [p.x, p.y];
81
+ }
82
+
83
+ export class Orders {
84
+ constructor() {
85
+ this.list = [];
86
+ }
87
+
88
+ push(order, args) {
89
+ this.list.push(attempt(() => native.encode(order, args)));
90
+ return this;
91
+ }
92
+
93
+ move(who, x, y, { queued = false } = {}) {
94
+ return this.push("move", { units: units(who), x, y, queued });
95
+ }
96
+
97
+ attackMove(who, x, y, { queued = false } = {}) {
98
+ return this.push("attackMove", { units: units(who), x, y, queued });
99
+ }
100
+
101
+ formMove(who, x, y, { face = [0, 256], queued = false, attack = false } = {}) {
102
+ return this.push("formMove", { units: units(who), x, y, face, queued, attack });
103
+ }
104
+
105
+ target(who, target, { queued = false } = {}) {
106
+ return this.push("target", { units: units(who), target, queued });
107
+ }
108
+
109
+ attack(who, target, { queued = false } = {}) {
110
+ return this.push("attack", { units: units(who), target, queued });
111
+ }
112
+
113
+ repair(who, target, { queued = false } = {}) {
114
+ return this.push("repair", { units: units(who), target, queued });
115
+ }
116
+
117
+ gather(who, tile, { queued = false } = {}) {
118
+ return this.push("gather", { units: units(who), tile: point(tile), queued });
119
+ }
120
+
121
+ build(who, building, at, { queued = false } = {}) {
122
+ return this.push("build", { units: units(who), building, at: point(at), queued });
123
+ }
124
+
125
+ buildLine(who, building, from, to, { queued = false } = {}) {
126
+ return this.push("buildLine", { units: units(who), building, from: point(from), to: point(to), queued });
127
+ }
128
+
129
+ train(building, unit, count = 1) {
130
+ return this.push("train", { building, unit, count });
131
+ }
132
+
133
+ research(building, upgrade) {
134
+ return this.push("research", { building, upgrade });
135
+ }
136
+
137
+ cancel(building, index) {
138
+ return this.push("cancel", { building, index });
139
+ }
140
+
141
+ rally(buildings, x, y) {
142
+ return this.push("rally", { buildings: units(buildings), x, y });
143
+ }
144
+
145
+ stop(who) {
146
+ return this.push("stop", { units: units(who) });
147
+ }
148
+
149
+ hold(who) {
150
+ return this.push("hold", { units: units(who) });
151
+ }
152
+
153
+ formUp(who) {
154
+ return this.push("formUp", { units: units(who) });
155
+ }
156
+
157
+ remove(who) {
158
+ return this.push("remove", { units: units(who) });
159
+ }
160
+
161
+ spawnToggle(buildings, on) {
162
+ return this.push("spawnToggle", { buildings: units(buildings), on });
163
+ }
164
+
165
+ surrender() {
166
+ return this.push("surrender", {});
167
+ }
168
+ }
169
+
170
+ function queries(call) {
171
+ const refusal = (q, args) => call(q, args);
172
+ return {
173
+ canAfford: (id) => call("canAfford", { id }),
174
+ cost: (id) => call("cost", { id }),
175
+ trainRefusal: (building, unit) => refusal("trainRefusal", { building, unit }),
176
+ canTrain: (building, unit) => refusal("trainRefusal", { building, unit }) === null,
177
+ researchRefusal: (building, upgrade) => refusal("researchRefusal", { building, upgrade }),
178
+ canResearch: (building, upgrade) => refusal("researchRefusal", { building, upgrade }) === null,
179
+ placeRefusal: (building, at) => refusal("placeRefusal", { building, at: point(at) }),
180
+ canPlace: (building, at) => refusal("placeRefusal", { building, at: point(at) }) === null,
181
+ findPlacement: (building, near, radius = 16) => call("findPlacement", { building, near: point(near), radius }),
182
+ nearestResource: (from, resource, radius = 40) => call("nearestResource", { from: point(from), resource, radius }),
183
+ reachable: (from, to) => call("reachable", { from: point(from), to: point(to) }),
184
+ front: (toward) => call("front", toward ? { toward: point(toward) } : {}),
185
+ tile: (at) => call("tile", { at: point(at) }),
186
+ map: () => call("map", {}),
187
+ view: () => call("view", {}),
188
+ };
189
+ }
190
+
191
+ async function runThink(think, req, game) {
192
+ const act = new Orders();
193
+ await think({ view: req.view, events: req.events, tick: req.tick, slot: req.slot, game, act });
194
+ return act.list;
195
+ }
196
+
197
+ export class Warpass {
198
+ constructor(session, url, token, hello) {
199
+ this.session = session;
200
+ this.url = url;
201
+ this.token = token;
202
+ this.name = hello.name;
203
+ this.engineVersion = hello.engineVersion;
204
+ }
205
+
206
+ static async connect({ token, url = DEFAULT_URL } = {}) {
207
+ if (!token) throw new WarpassError(JSON.stringify({ kind: "unauthenticated" }));
208
+ const session = new native.OnlineSession();
209
+ const hello = parse(await guard(session.connect(url, token)));
210
+ return new Warpass(session, url, token, hello);
211
+ }
212
+
213
+ async queue({ race, avoidBots = false } = {}) {
214
+ const offer = parse(await guard(this.session.queue(race, avoidBots)));
215
+ return new Offer(this.session, offer);
216
+ }
217
+
218
+ async lobby({ code, race, everyTicks = 10 } = {}) {
219
+ const info = parse(await guard(this.session.lobby(String(code ?? ""), race, everyTicks)));
220
+ return new OnlineMatch(this.session, info);
221
+ }
222
+
223
+ static async join({ token, url = DEFAULT_URL, matchId, everyTicks = 10 } = {}) {
224
+ const session = new native.OnlineSession();
225
+ const info = parse(await guard(session.join(url, token, matchId, everyTicks)));
226
+ return new OnlineMatch(session, info);
227
+ }
228
+ }
229
+
230
+ export class Offer {
231
+ constructor(session, offer) {
232
+ this.session = session;
233
+ this.offerId = offer.offerId;
234
+ this.opponent = offer.opponent;
235
+ }
236
+
237
+ async accept({ everyTicks = 10 } = {}) {
238
+ const info = parse(await guard(this.session.accept(everyTicks)));
239
+ return new OnlineMatch(this.session, info);
240
+ }
241
+
242
+ async decline() {
243
+ await guard(this.session.decline());
244
+ }
245
+ }
246
+
247
+ export class OnlineMatch {
248
+ constructor(session, info) {
249
+ this.session = session;
250
+ this.id = info.matchId;
251
+ this.slot = info.slot;
252
+ this.loop = null;
253
+ this.game = queries((q, args) => session.query(q, args));
254
+ }
255
+
256
+ onThink(think, { everyTicks } = {}) {
257
+ if (everyTicks) this.session.setEvery(everyTicks);
258
+ this.loop = (async () => {
259
+ for (;;) {
260
+ const req = parse(await guard(this.session.nextThink()));
261
+ if (!req) return;
262
+ let orders = [];
263
+ try {
264
+ orders = await runThink(think, req, this.game);
265
+ } finally {
266
+ this.session.submit(orders);
267
+ }
268
+ }
269
+ })();
270
+ return this;
271
+ }
272
+
273
+ async ended() {
274
+ if (!this.loop) this.onThink(() => {});
275
+ await this.loop;
276
+ return { reason: this.session.endReason() };
277
+ }
278
+
279
+ async result({ timeoutMs = 120000 } = {}) {
280
+ await this.ended();
281
+ return parse(await guard(this.session.result(timeoutMs)));
282
+ }
283
+ }
284
+
285
+ export function Bot({ race, think, everyTicks = 10, name = "" }) {
286
+ return { kind: "bot", race, everyTicks, name, think };
287
+ }
288
+
289
+ export function Ai({ race, level = 5 }) {
290
+ return { kind: "ai", race, level };
291
+ }
292
+
293
+ export function Idle({ race }) {
294
+ return { kind: "idle", race };
295
+ }
296
+
297
+ export class Match {
298
+ constructor({ seed, players, mapSize = null, maxTicks = null }) {
299
+ this.seed = seed;
300
+ this.players = players;
301
+ this.mapSize = mapSize;
302
+ this.maxTicks = maxTicks;
303
+ }
304
+
305
+ toNative() {
306
+ return {
307
+ seed: this.seed,
308
+ mapSize: this.mapSize ?? undefined,
309
+ maxTicks: this.maxTicks ?? undefined,
310
+ players: this.players.map(({ think, ...p }) => p),
311
+ };
312
+ }
313
+ }
314
+
315
+ export class Replay {
316
+ constructor(data) {
317
+ this.data = data;
318
+ }
319
+
320
+ static load(pathOrText) {
321
+ const text = existsSync(pathOrText) ? readFileSync(pathOrText, "utf8") : pathOrText;
322
+ return new Replay(JSON.parse(text));
323
+ }
324
+
325
+ save(path) {
326
+ writeFileSync(path, JSON.stringify(this.data));
327
+ return path;
328
+ }
329
+
330
+ verify() {
331
+ return attempt(() => native.verifyReplay(JSON.stringify(this.data)));
332
+ }
333
+
334
+ analyse() {
335
+ return this.verify().analysis;
336
+ }
337
+ }
338
+
339
+ export class GameResult {
340
+ constructor(r) {
341
+ Object.assign(this, r);
342
+ this.replay = new Replay(r.replay);
343
+ }
344
+
345
+ won(slot) {
346
+ return this.finished && this.winnerSlot === slot;
347
+ }
348
+ }
349
+
350
+ export async function play(match) {
351
+ const m = match instanceof Match ? match : new Match(match);
352
+ const session = attempt(() => new native.LocalSession(m.toNative()));
353
+ for (let req = session.nextThink(); req; req = session.nextThink()) {
354
+ const player = m.players[req.slot];
355
+ let orders = [];
356
+ if (player && player.think) {
357
+ const game = queries((q, args) => session.query(req.slot, q, args));
358
+ orders = await runThink(player.think, req, game);
359
+ }
360
+ session.submit(req.slot, orders);
361
+ }
362
+ return new GameResult(session.result());
363
+ }
364
+
365
+ export async function playMany(matches, { concurrency = 4 } = {}) {
366
+ const list = matches.map((m) => (m instanceof Match ? m : new Match(m)));
367
+ if (list.every((m) => m.players.every((p) => p.kind !== "bot"))) {
368
+ const raw = parse(await guard(native.playMany(list.map((m) => m.toNative()))));
369
+ return raw.map((r) => new GameResult(r));
370
+ }
371
+ const out = new Array(list.length);
372
+ let next = 0;
373
+ const worker = async () => {
374
+ while (next < list.length) {
375
+ const k = next++;
376
+ out[k] = await play(list[k]);
377
+ }
378
+ };
379
+ await Promise.all(Array.from({ length: Math.min(concurrency, list.length) }, worker));
380
+ return out;
381
+ }
382
+
383
+ export class CustomGame {
384
+ constructor({ seed = 1, races = [], mapType = null, aiLevel = 0 } = {}) {
385
+ this.native = new native.CustomSession(seed, races, mapType, aiLevel);
386
+ this.game = (slot) => queries((q, args) => this.native.query(slot, q, args));
387
+ }
388
+
389
+ setup(op) {
390
+ return attempt(() => this.native.setup(op));
391
+ }
392
+
393
+ spawn(owner, unit, x, y) {
394
+ return this.setup({ op: "spawn", owner, unit, x, y });
395
+ }
396
+
397
+ building(owner, building, tx, ty) {
398
+ return this.setup({ op: "building", owner, building, tx, ty });
399
+ }
400
+
401
+ terrain(tx, ty, terrain) {
402
+ const id = typeof terrain === "number" ? terrain : rules().terrains[terrain];
403
+ return this.setup({ op: "terrain", tile: ty * this.map().width + tx, terrain: id });
404
+ }
405
+
406
+ flattenArea(w, h) {
407
+ return attempt(() => this.native.flattenArea(w, h));
408
+ }
409
+
410
+ research(owner, upgrade) {
411
+ return this.setup({ op: "research", owner, upgrade });
412
+ }
413
+
414
+ resources(owner, { food = 0, wood = 0, stone = 0, gold = 0 } = {}) {
415
+ return this.setup({ op: "resources", owner, food, wood, stone, gold });
416
+ }
417
+
418
+ weather(key, duration, ramp = 0) {
419
+ return this.setup({ op: "weather", key, duration, ramp });
420
+ }
421
+
422
+ reveal(owner, eid = -1) {
423
+ return this.setup({ op: "reveal", owner, eid });
424
+ }
425
+
426
+ remove(eid) {
427
+ return this.setup({ op: "remove", eid });
428
+ }
429
+
430
+ order(slot, build) {
431
+ const act = new Orders();
432
+ build(act);
433
+ this.native.order(slot, act.list);
434
+ }
435
+
436
+ step(n = 1) {
437
+ return this.native.step(n);
438
+ }
439
+
440
+ until(maxTicks, done) {
441
+ for (let k = 0; k < maxTicks; k++) {
442
+ this.native.step(1);
443
+ if (done(this.observer(), this.tick)) return k + 1;
444
+ }
445
+ return -1;
446
+ }
447
+
448
+ view(slot) {
449
+ return this.native.view(slot);
450
+ }
451
+
452
+ observer() {
453
+ return this.native.view(null);
454
+ }
455
+
456
+ map() {
457
+ return this.native.query(0, "map", {});
458
+ }
459
+
460
+ replay() {
461
+ return new Replay(this.native.replay());
462
+ }
463
+
464
+ get tick() {
465
+ return this.native.tick;
466
+ }
467
+
468
+ get winner() {
469
+ return this.native.winner;
470
+ }
471
+ }
472
+
473
+ export function rules(race) {
474
+ return native.rules(race ?? null);
475
+ }
476
+
477
+ export function docs(page) {
478
+ return native.docs(page ?? null);
479
+ }
480
+
481
+ export function docsIndex() {
482
+ return native.docsIndex();
483
+ }