@clunkyard/sdk 0.0.0-stage → 2.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/LICENSE +21 -0
- package/README.md +76 -2
- package/THIRD_PARTY_NOTICES.txt +31 -0
- package/api.js +85 -0
- package/clunkyard-core.wasm +0 -0
- package/core.js +229 -0
- package/index.d.ts +254 -0
- package/index.js +55 -0
- package/licenses/itoa/LICENSE-APACHE +176 -0
- package/licenses/itoa/LICENSE-MIT +23 -0
- package/licenses/libm/LICENSE.txt +258 -0
- package/licenses/memchr/COPYING +3 -0
- package/licenses/memchr/LICENSE-MIT +21 -0
- package/licenses/memchr/UNLICENSE +24 -0
- package/licenses/rust-Apache-2.0.txt +73 -0
- package/licenses/rust-MIT.txt +9 -0
- package/licenses/rust-stdlib-1.96.0.html +8266 -0
- package/licenses/serde_core/LICENSE-APACHE +176 -0
- package/licenses/serde_core/LICENSE-MIT +23 -0
- package/licenses/serde_json/LICENSE-APACHE +176 -0
- package/licenses/serde_json/LICENSE-MIT +23 -0
- package/licenses/zmij/LICENSE-MIT +23 -0
- package/package.json +50 -4
- package/words.js +265 -0
package/core.js
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// The robot core's wasm32 build (sdk/core/dist/clunkyard-core.wasm) in
|
|
3
|
+
// process, for the DSL interpreters and later the JavaScript binding
|
|
4
|
+
// (docs/design/robot-core.md sections 1.4 and 4.2).
|
|
5
|
+
//
|
|
6
|
+
// Each robot gets its own instance and a memory-port session in it: lines go
|
|
7
|
+
// in with feed() and replies come out with reply(). A panic in the core
|
|
8
|
+
// traps the whole instance (wasm32 cannot unwind), so every call goes
|
|
9
|
+
// through invoke(), which on a WebAssembly.RuntimeError discards the
|
|
10
|
+
// instance, instantiates the module afresh, feeds it the kept hello and hands
|
|
11
|
+
// it the panic count and the tick that faulted. It then throws a CoreFault.
|
|
12
|
+
// The turn is answered as usual with send(), and the core writes an empty
|
|
13
|
+
// act for it; the next state plays on a fresh session, as on native. After
|
|
14
|
+
// three faults in a match the core answers every state with an empty act.
|
|
15
|
+
//
|
|
16
|
+
// The module is passed in as bytes or a compiled WebAssembly.Module, so a
|
|
17
|
+
// bundle can carry it; loadDefault() reads the committed file in Node.
|
|
18
|
+
|
|
19
|
+
const { API_VERSION, SIZE: WORDS_SIZE } = require('./words');
|
|
20
|
+
|
|
21
|
+
const OK = 0, NONE = 1, SHORT = 2;
|
|
22
|
+
const EVENTS = [null, 'hello', 'state', 'end'];
|
|
23
|
+
const [API_MAJOR, API_MINOR] = API_VERSION.split('.').map(Number);
|
|
24
|
+
const encoder = new TextEncoder(), decoder = new TextDecoder();
|
|
25
|
+
|
|
26
|
+
// A negative status from the core, with its message.
|
|
27
|
+
class CoreError extends Error {
|
|
28
|
+
constructor(symbol, status, message) {
|
|
29
|
+
super(`${symbol}: ${message || `status ${status}`}`);
|
|
30
|
+
this.status = status;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// The core trapped; the robot now runs on a fresh instance.
|
|
35
|
+
class CoreFault extends Error {
|
|
36
|
+
constructor(symbol, cause, panics) {
|
|
37
|
+
super(`a fault in the core (${symbol}); it is not the robot's: ${cause.message}`);
|
|
38
|
+
this.cause = cause;
|
|
39
|
+
this.panics = panics;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function compile(source) {
|
|
44
|
+
const module = source instanceof WebAssembly.Module ? source : new WebAssembly.Module(source);
|
|
45
|
+
const imports = WebAssembly.Module.imports(module);
|
|
46
|
+
if (imports.length) throw new Error(`the core imports nothing, but this module imports ${imports.map(item => `${item.module}.${item.name}`).join(', ')}`);
|
|
47
|
+
return module;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
class CoreRobot {
|
|
51
|
+
constructor(module, { binding = 'clunkyard-dsl', version = API_VERSION } = {}) {
|
|
52
|
+
this.module = module;
|
|
53
|
+
this.binding = binding;
|
|
54
|
+
this.version = version;
|
|
55
|
+
this.panics = 0;
|
|
56
|
+
this.hello = null;
|
|
57
|
+
// The last state line read, for the tick of a fault.
|
|
58
|
+
this.state = null;
|
|
59
|
+
this.fed = null;
|
|
60
|
+
this.instantiate();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
instantiate() {
|
|
64
|
+
this.exports = new WebAssembly.Instance(this.module, {}).exports;
|
|
65
|
+
this.scratchSize = 0;
|
|
66
|
+
this.scratch = 0;
|
|
67
|
+
this.reserve(1 << 16);
|
|
68
|
+
const name = encoder.encode(this.binding), version = encoder.encode(this.version);
|
|
69
|
+
const at = this.write(new Uint8Array([...name, ...version]), 8);
|
|
70
|
+
const status = this.exports.cy_open_memory(API_MAJOR, API_MINOR, at, name.length, at + name.length, version.length, this.scratch);
|
|
71
|
+
if (status !== OK) throw new CoreError('cy_open_memory', status);
|
|
72
|
+
this.handle = this.view().getUint32(this.scratch, true);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Memory moves when it grows, so every read takes a fresh view.
|
|
76
|
+
view() { return new DataView(this.exports.memory.buffer); }
|
|
77
|
+
bytes() { return new Uint8Array(this.exports.memory.buffer); }
|
|
78
|
+
|
|
79
|
+
// At least size bytes of scratch memory, 8-byte aligned.
|
|
80
|
+
reserve(size) {
|
|
81
|
+
if (size <= this.scratchSize) return;
|
|
82
|
+
if (this.scratch) this.exports.cy_free(this.scratch, this.scratchSize);
|
|
83
|
+
this.scratchSize = Math.max(size, this.scratchSize * 2);
|
|
84
|
+
this.scratch = this.exports.cy_alloc(this.scratchSize);
|
|
85
|
+
if (!this.scratch) throw new Error('the core is out of memory');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Copies bytes into scratch memory at offset, giving their address.
|
|
89
|
+
write(bytes, offset = 0) {
|
|
90
|
+
this.reserve(offset + bytes.length);
|
|
91
|
+
this.bytes().set(bytes, this.scratch + offset);
|
|
92
|
+
return this.scratch + offset;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Calls an export, recovering from a trap.
|
|
96
|
+
invoke(symbol, ...args) {
|
|
97
|
+
try {
|
|
98
|
+
return this.exports[symbol](...args);
|
|
99
|
+
} catch (error) {
|
|
100
|
+
if (!(error instanceof WebAssembly.RuntimeError)) throw error;
|
|
101
|
+
this.recover(symbol);
|
|
102
|
+
throw new CoreFault(symbol, error, this.panics);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// A trap in next() came while reading the line just fed, so that line is
|
|
107
|
+
// the hello to keep or the state whose tick to answer.
|
|
108
|
+
recover(symbol) {
|
|
109
|
+
if (symbol === 'cy_session_next' && this.fed !== null) {
|
|
110
|
+
let type = null;
|
|
111
|
+
try { type = JSON.parse(this.fed).type; } catch { /* not JSON: the state stays as it was */ }
|
|
112
|
+
if (type === 'hello') Object.assign(this, { hello: this.fed, state: null, panics: 0 });
|
|
113
|
+
else if (type === 'state') this.state = this.fed;
|
|
114
|
+
}
|
|
115
|
+
this.panics += 1;
|
|
116
|
+
this.instantiate();
|
|
117
|
+
const tick = this.state === null ? -1 : JSON.parse(this.state).tick;
|
|
118
|
+
if (this.hello !== null) {
|
|
119
|
+
try {
|
|
120
|
+
this.feedLine(this.hello);
|
|
121
|
+
this.check('cy_session_next', this.exports.cy_session_next(this.handle, this.scratch));
|
|
122
|
+
} catch (error) {
|
|
123
|
+
// The hello itself traps: carry on without it.
|
|
124
|
+
if (!(error instanceof WebAssembly.RuntimeError)) throw error;
|
|
125
|
+
this.instantiate();
|
|
126
|
+
this.hello = null;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
this.exports.cy_wasm_recover(this.handle, this.panics, tick);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
check(symbol, status) {
|
|
133
|
+
if (status < 0) throw new CoreError(symbol, status, this.lastError());
|
|
134
|
+
return status;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
lastError() {
|
|
138
|
+
const status = this.exports.cy_last_error(this.handle, this.scratch + 8, this.scratchSize - 8, this.scratch);
|
|
139
|
+
return status === OK ? decoder.decode(this.bytes().subarray(this.scratch + 8, this.scratch + 8 + this.view().getUint32(this.scratch, true))) : '';
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
feedLine(line) {
|
|
143
|
+
const bytes = encoder.encode(line);
|
|
144
|
+
return this.exports.cy_session_feed(this.handle, this.write(bytes), bytes.length);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// ---- the session ----
|
|
148
|
+
|
|
149
|
+
// Hands the core one line; next() then reads it.
|
|
150
|
+
feed(line) {
|
|
151
|
+
this.fed = line;
|
|
152
|
+
const bytes = encoder.encode(line);
|
|
153
|
+
const at = this.write(bytes);
|
|
154
|
+
this.check('cy_session_feed', this.invoke('cy_session_feed', this.handle, at, bytes.length));
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// 'hello', 'state' or 'end'. The hello is kept for a recovery, and a new
|
|
158
|
+
// one starts the panic count again.
|
|
159
|
+
next() {
|
|
160
|
+
this.check('cy_session_next', this.invoke('cy_session_next', this.handle, this.scratch));
|
|
161
|
+
const event = EVENTS[this.view().getInt32(this.scratch, true)];
|
|
162
|
+
if (event === 'hello') Object.assign(this, { hello: this.fed, state: null, panics: 0 });
|
|
163
|
+
else if (event === 'state') this.state = this.fed;
|
|
164
|
+
return event;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
send() { this.check('cy_session_send', this.invoke('cy_session_send', this.handle)); }
|
|
168
|
+
|
|
169
|
+
forfeit(code, message = '') {
|
|
170
|
+
const a = encoder.encode(code), b = encoder.encode(message);
|
|
171
|
+
const at = this.write(new Uint8Array([...a, ...b]));
|
|
172
|
+
this.check('cy_session_forfeit', this.invoke('cy_session_forfeit', this.handle, at, a.length, at + a.length, b.length));
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// The next line the core wrote, or null.
|
|
176
|
+
reply() {
|
|
177
|
+
for (;;) {
|
|
178
|
+
const status = this.check('cy_session_reply', this.invoke('cy_session_reply', this.handle, this.scratch + 8, this.scratchSize - 8, this.scratch));
|
|
179
|
+
if (status === NONE) return null;
|
|
180
|
+
const length = this.view().getUint32(this.scratch, true);
|
|
181
|
+
if (status === SHORT) { this.reserve(length + 8); continue; }
|
|
182
|
+
return decoder.decode(this.bytes().subarray(this.scratch + 8, this.scratch + 8 + length));
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// ---- the DSL ----
|
|
187
|
+
|
|
188
|
+
// The words record (cy_dsl_words_v1) for this state, one f64 a word; see
|
|
189
|
+
// src/dsl/words.js read().
|
|
190
|
+
words() {
|
|
191
|
+
this.reserve(WORDS_SIZE);
|
|
192
|
+
this.check('cy_dsl_fill_v1', this.invoke('cy_dsl_fill_v1', this.handle, this.scratch));
|
|
193
|
+
return new Float64Array(this.exports.memory.buffer.slice(this.scratch, this.scratch + WORDS_SIZE));
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// A control's per-field setter (words.js CONTROLS): a number, text, or
|
|
197
|
+
// nothing for a flag. The core clamps and writes the act.
|
|
198
|
+
set(symbol, value) {
|
|
199
|
+
let status;
|
|
200
|
+
if (value === undefined) status = this.invoke(symbol, this.handle);
|
|
201
|
+
else if (typeof value === 'string') {
|
|
202
|
+
const bytes = encoder.encode(value);
|
|
203
|
+
status = this.invoke(symbol, this.handle, this.write(bytes), bytes.length);
|
|
204
|
+
} else status = this.invoke(symbol, this.handle, value);
|
|
205
|
+
return this.check(symbol, status);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
close() {
|
|
209
|
+
this.exports.cy_close(this.handle);
|
|
210
|
+
this.handle = 0;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// The core, compiled once; robot() opens one robot on its own instance.
|
|
215
|
+
function load(source) {
|
|
216
|
+
const module = compile(source);
|
|
217
|
+
return { module, robot: options => new CoreRobot(module, options) };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
let committed = null;
|
|
221
|
+
function loadDefault() {
|
|
222
|
+
if (!committed) {
|
|
223
|
+
const fs = require('node:fs'), path = require('node:path');
|
|
224
|
+
committed = load(fs.readFileSync(path.join(__dirname, '../../sdk/core/dist/clunkyard-core.wasm')));
|
|
225
|
+
}
|
|
226
|
+
return committed;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
module.exports = { load, loadDefault, compile, CoreRobot, CoreError, CoreFault, OK, NONE, SHORT };
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
// Generated from protocol/v2/api.json; do not edit. Run make api.
|
|
2
|
+
|
|
3
|
+
export interface Vec3 {
|
|
4
|
+
x: number;
|
|
5
|
+
y: number;
|
|
6
|
+
z: number;
|
|
7
|
+
}
|
|
8
|
+
export interface Rotation {
|
|
9
|
+
yaw: number;
|
|
10
|
+
pitch: number;
|
|
11
|
+
roll: number;
|
|
12
|
+
}
|
|
13
|
+
export interface Direction {
|
|
14
|
+
heading: number;
|
|
15
|
+
pitch: number;
|
|
16
|
+
}
|
|
17
|
+
export interface Attitude {
|
|
18
|
+
forward: Vec3;
|
|
19
|
+
right: Vec3;
|
|
20
|
+
up: Vec3;
|
|
21
|
+
}
|
|
22
|
+
export interface Applied {
|
|
23
|
+
force: Vec3;
|
|
24
|
+
torque: Rotation;
|
|
25
|
+
brake: number;
|
|
26
|
+
}
|
|
27
|
+
export interface Scan {
|
|
28
|
+
heading: number;
|
|
29
|
+
pitch: number;
|
|
30
|
+
arc: number;
|
|
31
|
+
range: number;
|
|
32
|
+
}
|
|
33
|
+
export interface Contact {
|
|
34
|
+
slot: number;
|
|
35
|
+
source: "scan" | "proximity" | "faint" | "beam" | "muzzle" | "motion" | "ping";
|
|
36
|
+
range: number;
|
|
37
|
+
bearing: number;
|
|
38
|
+
elevation: number;
|
|
39
|
+
bearingError: number;
|
|
40
|
+
rangeError: number;
|
|
41
|
+
position: Vec3;
|
|
42
|
+
}
|
|
43
|
+
export interface Cue {
|
|
44
|
+
source: "scan" | "proximity" | "faint" | "beam" | "muzzle" | "motion" | "ping";
|
|
45
|
+
tick: number;
|
|
46
|
+
bearingError: number;
|
|
47
|
+
position: Vec3;
|
|
48
|
+
}
|
|
49
|
+
export interface Solution {
|
|
50
|
+
heading: number;
|
|
51
|
+
pitch: number;
|
|
52
|
+
time: number;
|
|
53
|
+
}
|
|
54
|
+
export interface Zone {
|
|
55
|
+
shape: "circle" | "sphere" | "column";
|
|
56
|
+
radius: number;
|
|
57
|
+
center: Vec3;
|
|
58
|
+
margin(point: Vec3): number | null;
|
|
59
|
+
contains(point: Vec3, margin?: number): boolean | null;
|
|
60
|
+
distanceFrom(point: Vec3): number | null;
|
|
61
|
+
}
|
|
62
|
+
export interface Storm {
|
|
63
|
+
shape: "circle" | "sphere" | "column";
|
|
64
|
+
radius: number;
|
|
65
|
+
dps: number;
|
|
66
|
+
center: Vec3;
|
|
67
|
+
velocity: Vec3;
|
|
68
|
+
margin(point: Vec3): number | null;
|
|
69
|
+
contains(point: Vec3, margin?: number): boolean | null;
|
|
70
|
+
distanceFrom(point: Vec3): number | null;
|
|
71
|
+
}
|
|
72
|
+
export interface Hill {
|
|
73
|
+
shape: "circle" | "sphere" | "column";
|
|
74
|
+
holder: number;
|
|
75
|
+
contested: boolean;
|
|
76
|
+
movesTick: number;
|
|
77
|
+
heldTicks: number;
|
|
78
|
+
bestHeldTicks: number;
|
|
79
|
+
radius: number;
|
|
80
|
+
center: Vec3;
|
|
81
|
+
next: Vec3;
|
|
82
|
+
margin(point: Vec3): number | null;
|
|
83
|
+
contains(point: Vec3, margin?: number): boolean | null;
|
|
84
|
+
distanceFrom(point: Vec3): number | null;
|
|
85
|
+
}
|
|
86
|
+
export interface Pickup {
|
|
87
|
+
kind: "energy" | "health" | "armour";
|
|
88
|
+
expiresTick: number;
|
|
89
|
+
amount: number;
|
|
90
|
+
position: Vec3;
|
|
91
|
+
}
|
|
92
|
+
export interface Compute {
|
|
93
|
+
k: number;
|
|
94
|
+
quanta: number;
|
|
95
|
+
unit: "operations" | "fuel" | "cpu-ms";
|
|
96
|
+
}
|
|
97
|
+
export interface Match {
|
|
98
|
+
readonly mode: "2d" | "3d";
|
|
99
|
+
readonly ruleset: "classic" | "inertial-2d" | "gravity-flat" | "gravity-hills" | "zero-g";
|
|
100
|
+
readonly control: "direct" | "forces";
|
|
101
|
+
readonly robots: number;
|
|
102
|
+
readonly is3d: boolean;
|
|
103
|
+
readonly tickRate: number;
|
|
104
|
+
readonly maxTicks: number;
|
|
105
|
+
robotId(slot: number): string;
|
|
106
|
+
}
|
|
107
|
+
export interface Clock {
|
|
108
|
+
readonly tick: number;
|
|
109
|
+
readonly seconds: number;
|
|
110
|
+
readonly remainingTicks: number;
|
|
111
|
+
}
|
|
112
|
+
export interface Arena {
|
|
113
|
+
readonly size: Vec3;
|
|
114
|
+
readonly gravity: Vec3;
|
|
115
|
+
readonly terrainKind: "none" | "flat" | "hills";
|
|
116
|
+
groundHeight(x: number, y: number): number;
|
|
117
|
+
}
|
|
118
|
+
export interface Me {
|
|
119
|
+
readonly health: number;
|
|
120
|
+
readonly position: Vec3;
|
|
121
|
+
readonly velocity: Vec3;
|
|
122
|
+
readonly heading: number;
|
|
123
|
+
readonly pitch: number;
|
|
124
|
+
readonly roll: number;
|
|
125
|
+
readonly angularVelocity: Rotation;
|
|
126
|
+
readonly attitude: Attitude;
|
|
127
|
+
readonly grounded: boolean;
|
|
128
|
+
readonly altitude: number;
|
|
129
|
+
readonly applied: Applied;
|
|
130
|
+
}
|
|
131
|
+
export interface Battery {
|
|
132
|
+
readonly charge: number;
|
|
133
|
+
readonly capacity: number;
|
|
134
|
+
readonly regen: number;
|
|
135
|
+
readonly sustainableDrive: number;
|
|
136
|
+
readonly compute: Compute | null;
|
|
137
|
+
driveCost(fraction: number): number;
|
|
138
|
+
}
|
|
139
|
+
export interface Drive {
|
|
140
|
+
readonly hoverLift: number;
|
|
141
|
+
set(fields?: { throttle?: number; turn?: number; pitchTurn?: number }): void;
|
|
142
|
+
forces(fields?: { thrust?: number; strafe?: number; lift?: number; yawTorque?: number; pitchTorque?: number; rollTorque?: number; brake?: number }): void;
|
|
143
|
+
get(field: "throttle" | "turn" | "pitchTurn" | "thrust" | "strafe" | "lift" | "yawTorque" | "pitchTorque" | "rollTorque" | "brake"): number | string | null;
|
|
144
|
+
}
|
|
145
|
+
export interface Gun {
|
|
146
|
+
readonly aimNow: Direction;
|
|
147
|
+
readonly cooldownTicks: number;
|
|
148
|
+
readonly spread: number;
|
|
149
|
+
readonly maxPower: number;
|
|
150
|
+
readonly damageScale: number;
|
|
151
|
+
canFire(power: number, reserve: number): boolean;
|
|
152
|
+
shotSpeed(power: number): number;
|
|
153
|
+
shotCost(power: number): number;
|
|
154
|
+
reloadTicks(power: number): number;
|
|
155
|
+
shotDamage(power: number): number;
|
|
156
|
+
powerForSpeed(speed: number): number;
|
|
157
|
+
powerForFlightTime(range: number, seconds: number): number;
|
|
158
|
+
lead(contact: Contact, power: number): Solution | null;
|
|
159
|
+
leadPoint(position: Vec3, velocity: Vec3, power: number): Solution | null;
|
|
160
|
+
aim(fields?: { heading?: number; pitch?: number }): void;
|
|
161
|
+
fire(fields?: { power?: number }): void;
|
|
162
|
+
get(field: "heading" | "pitch" | "fire" | "power"): number | string | null;
|
|
163
|
+
}
|
|
164
|
+
export interface Scanner {
|
|
165
|
+
readonly last: Scan;
|
|
166
|
+
readonly proximityRange: number;
|
|
167
|
+
reach(arc: number): number;
|
|
168
|
+
arcForReach(range: number): number;
|
|
169
|
+
set(fields?: { heading?: number; arc?: number; pitch?: number }): void;
|
|
170
|
+
get(field: "heading" | "arc" | "pitch"): number | string | null;
|
|
171
|
+
}
|
|
172
|
+
export interface Sensors {
|
|
173
|
+
readonly contacts: Array<Contact>;
|
|
174
|
+
readonly nearest: Contact | null;
|
|
175
|
+
readonly cues: Array<Contact>;
|
|
176
|
+
contact(slot: number): Contact | null;
|
|
177
|
+
positionOf(contact: Contact): Vec3;
|
|
178
|
+
}
|
|
179
|
+
export interface Tracker {
|
|
180
|
+
readonly cue: Cue | null;
|
|
181
|
+
readonly nearestBearingRate: number;
|
|
182
|
+
velocity(slot: number): Vec3;
|
|
183
|
+
position(slot: number): Vec3 | null;
|
|
184
|
+
lastSeen(slot: number): number | null;
|
|
185
|
+
configure(smoothing?: number, forgetTicks?: number, cueTicks?: number): void;
|
|
186
|
+
}
|
|
187
|
+
export interface Nav {
|
|
188
|
+
readonly zone: Zone | null;
|
|
189
|
+
readonly storm: Storm | null;
|
|
190
|
+
readonly hill: Hill | null;
|
|
191
|
+
readonly pickups: Array<Pickup>;
|
|
192
|
+
nearestPickup(kind?: "energy" | "health" | "armour"): Pickup | null;
|
|
193
|
+
fieldDistance(a: Vec3, b: Vec3): number;
|
|
194
|
+
}
|
|
195
|
+
export interface Score {
|
|
196
|
+
readonly me: number | null;
|
|
197
|
+
readonly best: number | null;
|
|
198
|
+
readonly armour: number | null;
|
|
199
|
+
}
|
|
200
|
+
export interface Angles {
|
|
201
|
+
normalize(degrees: number): number;
|
|
202
|
+
angleDiff(target: number, current: number): number;
|
|
203
|
+
bearingTo(from: Vec3, to: Vec3): number;
|
|
204
|
+
elevationTo(from: Vec3, to: Vec3): number;
|
|
205
|
+
direction(heading: number, pitch: number): Vec3;
|
|
206
|
+
distance(a: Vec3, b: Vec3): number;
|
|
207
|
+
clamp(value: number, low?: number, high?: number): number;
|
|
208
|
+
}
|
|
209
|
+
export interface Status {
|
|
210
|
+
set(fields?: { text?: string; value?: number }): void;
|
|
211
|
+
get(field: "text" | "value"): number | string | null;
|
|
212
|
+
}
|
|
213
|
+
export interface Raw {
|
|
214
|
+
number(path: string): number | null;
|
|
215
|
+
text(path: string): string | null;
|
|
216
|
+
has(path: string): boolean;
|
|
217
|
+
flag(path: string): boolean | null;
|
|
218
|
+
count(path: string): number | null;
|
|
219
|
+
line(): string;
|
|
220
|
+
extra(json: string): void;
|
|
221
|
+
}
|
|
222
|
+
export interface Session {
|
|
223
|
+
readonly event: "hello" | "state" | "end";
|
|
224
|
+
next(): "hello" | "state" | "end";
|
|
225
|
+
send(): void;
|
|
226
|
+
forfeit(code: string, message: string): void;
|
|
227
|
+
log(line: string): void;
|
|
228
|
+
feed(line: string): void;
|
|
229
|
+
reply(): string | null;
|
|
230
|
+
}
|
|
231
|
+
export class Robot {
|
|
232
|
+
match: Match;
|
|
233
|
+
clock: Clock;
|
|
234
|
+
arena: Arena;
|
|
235
|
+
me: Me;
|
|
236
|
+
battery: Battery;
|
|
237
|
+
drive: Drive;
|
|
238
|
+
gun: Gun;
|
|
239
|
+
scanner: Scanner;
|
|
240
|
+
sensors: Sensors;
|
|
241
|
+
tracker: Tracker;
|
|
242
|
+
nav: Nav;
|
|
243
|
+
score: Score;
|
|
244
|
+
angles: Angles;
|
|
245
|
+
status: Status;
|
|
246
|
+
raw: Raw;
|
|
247
|
+
session: Session;
|
|
248
|
+
onStart(): void;
|
|
249
|
+
onTurn(): void;
|
|
250
|
+
}
|
|
251
|
+
export class Forfeit extends Error { constructor(code: string, message?: string); }
|
|
252
|
+
export const angles: Angles;
|
|
253
|
+
export function run(robot: Robot, options?: { bindingName?: string }): Promise<void>;
|
|
254
|
+
export function play(robot: Robot, lines: Iterable<string>, options?: { bindingName?: string }): string[];
|
package/index.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// Node's run loop. The generated API marshals calls to the core; the shared
|
|
3
|
+
// wasm32 loader owns session parsing, act serialization and fault recovery.
|
|
4
|
+
const {attach,subsystem,core,CoreError,CoreFault}=require('./api');
|
|
5
|
+
|
|
6
|
+
class Forfeit extends Error {
|
|
7
|
+
constructor(code,message=''){super(message);this.code=code;}
|
|
8
|
+
}
|
|
9
|
+
class Robot { onStart(){} onTurn(){} }
|
|
10
|
+
const angles=subsystem(core.robot({binding:'clunkyard-javascript-angles'}),'angles');
|
|
11
|
+
function turn(robot,event){
|
|
12
|
+
try{event==='hello'?robot.onStart():robot.onTurn();}
|
|
13
|
+
catch(error){
|
|
14
|
+
if(error instanceof Forfeit){robot.session.forfeit(error.code,error.message);return;}
|
|
15
|
+
console.error(error.stack||error);
|
|
16
|
+
}
|
|
17
|
+
robot.session.send();
|
|
18
|
+
}
|
|
19
|
+
function next(robot){
|
|
20
|
+
try{return robot.session.next();}
|
|
21
|
+
catch(error){
|
|
22
|
+
console.error(error.stack||error);
|
|
23
|
+
if(error instanceof CoreFault||(error instanceof CoreError&&error.status===-6)){
|
|
24
|
+
try{robot.session.send();}catch(sendError){console.error(sendError.stack||sendError);return 'end';}
|
|
25
|
+
}
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function play(robot,lines,options={}){
|
|
30
|
+
const handle=attach(robot,options),out=[];
|
|
31
|
+
try{
|
|
32
|
+
for(const line of lines){
|
|
33
|
+
robot.session.feed(line);const event=next(robot);if(event==='end')break;
|
|
34
|
+
if(event)turn(robot,event);let reply;while((reply=robot.session.reply())!==null)out.push(reply);
|
|
35
|
+
}
|
|
36
|
+
return out;
|
|
37
|
+
}finally{handle.close();}
|
|
38
|
+
}
|
|
39
|
+
async function run(robot,options={}){
|
|
40
|
+
const handle=attach(robot,options),stdoutWrite=process.stdout.write.bind(process.stdout);
|
|
41
|
+
const write=process.env.CLUNKYARD_PROTOCOL_FD==='3'
|
|
42
|
+
? line=>{require('node:fs').writeSync(3,line);return true;}:stdoutWrite;
|
|
43
|
+
// Robot diagnostics use stderr. Only core replies use the captured writer.
|
|
44
|
+
const stdout=process.stdout.write;process.stdout.write=process.stderr.write.bind(process.stderr);
|
|
45
|
+
const lines=require('node:readline').createInterface({input:process.stdin,crlfDelay:Infinity});
|
|
46
|
+
try{
|
|
47
|
+
for await(const line of lines){
|
|
48
|
+
robot.session.feed(line);const event=next(robot);
|
|
49
|
+
if(event==='end')break;
|
|
50
|
+
if(event)turn(robot,event);
|
|
51
|
+
let reply;while((reply=robot.session.reply())!==null)if(!write(reply+'\n'))await new Promise(resolve=>process.stdout.once('drain',resolve));
|
|
52
|
+
}
|
|
53
|
+
}finally{lines.close();process.stdout.write=stdout;handle.close();}
|
|
54
|
+
}
|
|
55
|
+
module.exports={Robot,Forfeit,angles,run,play};
|