@kradle/challenges-sdk 0.11.0 → 0.13.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.
Files changed (95) hide show
  1. package/CHANGELOG.md +71 -1
  2. package/LLM_README.md +286 -193
  3. package/MIGRATION-0.13.md +109 -0
  4. package/README.md +98 -90
  5. package/dist/engine.d.ts +8 -0
  6. package/dist/engine.d.ts.map +1 -0
  7. package/dist/engine.js +17 -0
  8. package/dist/engine.js.map +1 -0
  9. package/dist/index.d.ts +19 -1
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +42 -16
  12. package/dist/index.js.map +1 -1
  13. package/dist/runtime/builder.d.ts +33 -0
  14. package/dist/runtime/builder.d.ts.map +1 -0
  15. package/dist/runtime/builder.js +357 -0
  16. package/dist/runtime/builder.js.map +1 -0
  17. package/dist/runtime/errors.d.ts +61 -0
  18. package/dist/runtime/errors.d.ts.map +1 -0
  19. package/dist/runtime/errors.js +71 -0
  20. package/dist/runtime/errors.js.map +1 -0
  21. package/dist/runtime/reducer.d.ts +205 -0
  22. package/dist/runtime/reducer.d.ts.map +1 -0
  23. package/dist/runtime/reducer.js +2025 -0
  24. package/dist/runtime/reducer.js.map +1 -0
  25. package/dist/runtime/rules.d.ts +29 -0
  26. package/dist/runtime/rules.d.ts.map +1 -0
  27. package/dist/runtime/rules.js +23 -0
  28. package/dist/runtime/rules.js.map +1 -0
  29. package/dist/runtime/sandbox.d.ts +2 -1
  30. package/dist/runtime/sandbox.d.ts.map +1 -1
  31. package/dist/runtime/sandbox.js +7 -4
  32. package/dist/runtime/sandbox.js.map +1 -1
  33. package/dist/runtime/time.d.ts +33 -0
  34. package/dist/runtime/time.d.ts.map +1 -0
  35. package/dist/runtime/time.js +53 -0
  36. package/dist/runtime/time.js.map +1 -0
  37. package/dist/runtime/types.d.ts +963 -0
  38. package/dist/runtime/types.d.ts.map +1 -0
  39. package/dist/runtime/types.js +25 -0
  40. package/dist/runtime/types.js.map +1 -0
  41. package/dist/runtime/wire.d.ts +858 -0
  42. package/dist/runtime/wire.d.ts.map +1 -0
  43. package/dist/runtime/wire.js +28 -0
  44. package/dist/runtime/wire.js.map +1 -0
  45. package/dist/runtime/world.d.ts +58 -3
  46. package/dist/runtime/world.d.ts.map +1 -1
  47. package/dist/runtime/world.js +12 -0
  48. package/dist/runtime/world.js.map +1 -1
  49. package/dist/structures.d.ts +119 -0
  50. package/dist/structures.d.ts.map +1 -0
  51. package/dist/structures.js +279 -0
  52. package/dist/structures.js.map +1 -0
  53. package/dist/test/harness.d.ts +280 -0
  54. package/dist/test/harness.d.ts.map +1 -0
  55. package/dist/{runtime → test}/harness.js +193 -68
  56. package/dist/test/harness.js.map +1 -0
  57. package/dist/test/helpers.d.ts +8 -0
  58. package/dist/test/helpers.d.ts.map +1 -0
  59. package/dist/test/helpers.js +12 -0
  60. package/dist/test/helpers.js.map +1 -0
  61. package/dist/test/index.d.ts +3 -0
  62. package/dist/test/index.d.ts.map +1 -0
  63. package/dist/{runtime → test}/index.js +3 -1
  64. package/dist/test/index.js.map +1 -0
  65. package/package.json +31 -16
  66. package/dist/runtime/examples/capture-the-flag.d.ts +0 -33
  67. package/dist/runtime/examples/capture-the-flag.d.ts.map +0 -1
  68. package/dist/runtime/examples/capture-the-flag.js +0 -79
  69. package/dist/runtime/examples/capture-the-flag.js.map +0 -1
  70. package/dist/runtime/examples/climb.d.ts +0 -34
  71. package/dist/runtime/examples/climb.d.ts.map +0 -1
  72. package/dist/runtime/examples/climb.js +0 -54
  73. package/dist/runtime/examples/climb.js.map +0 -1
  74. package/dist/runtime/examples/four-bridges.d.ts +0 -107
  75. package/dist/runtime/examples/four-bridges.d.ts.map +0 -1
  76. package/dist/runtime/examples/four-bridges.js +0 -294
  77. package/dist/runtime/examples/four-bridges.js.map +0 -1
  78. package/dist/runtime/examples/herding.d.ts +0 -59
  79. package/dist/runtime/examples/herding.d.ts.map +0 -1
  80. package/dist/runtime/examples/herding.js +0 -181
  81. package/dist/runtime/examples/herding.js.map +0 -1
  82. package/dist/runtime/examples/skywars.d.ts +0 -116
  83. package/dist/runtime/examples/skywars.d.ts.map +0 -1
  84. package/dist/runtime/examples/skywars.js +0 -253
  85. package/dist/runtime/examples/skywars.js.map +0 -1
  86. package/dist/runtime/harness.d.ts +0 -176
  87. package/dist/runtime/harness.d.ts.map +0 -1
  88. package/dist/runtime/harness.js.map +0 -1
  89. package/dist/runtime/index.d.ts +0 -2
  90. package/dist/runtime/index.d.ts.map +0 -1
  91. package/dist/runtime/index.js.map +0 -1
  92. package/dist/runtime/sdk.d.ts +0 -1038
  93. package/dist/runtime/sdk.d.ts.map +0 -1
  94. package/dist/runtime/sdk.js +0 -1271
  95. package/dist/runtime/sdk.js.map +0 -1
@@ -1,1271 +0,0 @@
1
- "use strict";
2
- // Runtime SDK. A challenge is one pure function: reduce(state, event) -> {state, actions}.
3
- // The arena turns engine facts into events and turns actions into engine effects;
4
- // the event log is the record of the run, so a run replays from its log.
5
- //
6
- // This file is the engine contract (Event / Effect / Action) plus the authoring
7
- // layer, defineChallenge(config, spec), on top of the reducer. Nothing here
8
- // touches Minecraft: the code runs in a sandboxed worker in the arena and in the
9
- // author's tests, unchanged.
10
- Object.defineProperty(exports, "__esModule", { value: true });
11
- exports.Win = exports.End = exports.WORLD_DEFAULTS = exports.ALL_CAPABILITIES = exports.asMinutes = exports.asSeconds = exports.subtractDuration = exports.addDuration = exports.minutes = exports.seconds = exports.ticks = exports.TICKS_PER_SECOND = exports.PLAYER_VOTE = exports.inBox = exports.bounds = exports.blockIdOf = exports.applyEquipment = void 0;
12
- exports.toTicks = toTicks;
13
- exports.defineChallenge = defineChallenge;
14
- const world_js_1 = require("./world.js");
15
- var world_js_2 = require("./world.js");
16
- Object.defineProperty(exports, "applyEquipment", { enumerable: true, get: function () { return world_js_2.applyEquipment; } });
17
- Object.defineProperty(exports, "blockIdOf", { enumerable: true, get: function () { return world_js_2.blockIdOf; } });
18
- Object.defineProperty(exports, "bounds", { enumerable: true, get: function () { return world_js_2.bounds; } });
19
- Object.defineProperty(exports, "inBox", { enumerable: true, get: function () { return world_js_2.inBox; } });
20
- /**
21
- * The vote `skills.voteForPlayer` casts in. Its options are the other participants' names, so it
22
- * needs no `votingOptions` entry: a challenge opens it with `votes: { player: {...} }`.
23
- */
24
- exports.PLAYER_VOTE = "player";
25
- exports.TICKS_PER_SECOND = 20;
26
- /** A whole, finite, non-negative number of ticks; a fraction rounds to the nearest tick. */
27
- function wholeTicks(n, what) {
28
- if (!Number.isFinite(n))
29
- throw new Error(`${what} is not a finite number`);
30
- if (n < 0)
31
- throw new Error(`${what} is negative; a duration is zero or more`);
32
- return Math.round(n);
33
- }
34
- const ticks = (n) => wholeTicks(n, `ticks(${n})`);
35
- exports.ticks = ticks;
36
- const seconds = (n) => wholeTicks(n * exports.TICKS_PER_SECOND, `seconds(${n})`);
37
- exports.seconds = seconds;
38
- const minutes = (n) => wholeTicks(n * 60 * exports.TICKS_PER_SECOND, `minutes(${n})`);
39
- exports.minutes = minutes;
40
- /** The ticks of a duration: "35s", "2m", "40t" or a Ticks value. Throws on a negative, non-finite or malformed one. */
41
- function toTicks(d) {
42
- if (typeof d === "number")
43
- return wholeTicks(d, `${d}`);
44
- const m = /^(-?\d+(?:\.\d+)?)([smt])$/.exec(d);
45
- if (!m)
46
- throw new Error(`"${d}" is not a duration; use "35s", "2m", "40t", seconds(), minutes() or ticks()`);
47
- const n = Number(m[1]);
48
- return wholeTicks(m[2] === "s" ? n * exports.TICKS_PER_SECOND : m[2] === "m" ? n * 60 * exports.TICKS_PER_SECOND : n, `"${d}"`);
49
- }
50
- /** `a + b`, as ticks. */
51
- const addDuration = (a, b) => (toTicks(a) + toTicks(b));
52
- exports.addDuration = addDuration;
53
- /** `a - b`, as ticks, clamped at zero: a countdown that passed its deadline reads 0, not an error. */
54
- const subtractDuration = (a, b) => Math.max(0, toTicks(a) - toTicks(b));
55
- exports.subtractDuration = subtractDuration;
56
- /** A duration in seconds (for messages): `asSeconds("90s")` is 90, `asSeconds(ticks(30))` is 1.5. */
57
- const asSeconds = (d) => toTicks(d) / exports.TICKS_PER_SECOND;
58
- exports.asSeconds = asSeconds;
59
- /** A duration in minutes (for messages): `asMinutes("90s")` is 1.5. */
60
- const asMinutes = (d) => toTicks(d) / (60 * exports.TICKS_PER_SECOND);
61
- exports.asMinutes = asMinutes;
62
- exports.ALL_CAPABILITIES = [
63
- "move",
64
- "damage",
65
- "death",
66
- "pickup",
67
- "inventory",
68
- "blocks",
69
- "zones",
70
- "entities",
71
- "custom",
72
- "batch",
73
- "query",
74
- "walk",
75
- "mirror",
76
- "chat",
77
- "lifecycle",
78
- "requests",
79
- "containers",
80
- ];
81
- exports.WORLD_DEFAULTS = {
82
- time: "day",
83
- daylightCycle: false,
84
- mobSpawning: false,
85
- naturalRegeneration: true,
86
- maxHealth: 20,
87
- };
88
- exports.End = {
89
- allEliminated: (g) => g.active.length === 0,
90
- };
91
- exports.Win = {
92
- /** Highest score among the eligible players (default: still in the game), if it reaches `min` (default 1). Ties win together. */
93
- topScore: (opts = {}) => (g) => {
94
- const pool = g.players.filter(opts.eligible ?? ((p) => !p.eliminated));
95
- if (pool.length === 0)
96
- return [];
97
- const best = Math.max(...pool.map((p) => p.score));
98
- if (best < (opts.min ?? 1))
99
- return [];
100
- return pool.filter((p) => p.score === best);
101
- },
102
- scoreAtLeast: (min) => (g) => g.players.filter((p) => p.score >= min),
103
- };
104
- // ---------------------------------------------------------------- the reducer
105
- const SETUP_TIMEOUT_TICKS = 30 * exports.TICKS_PER_SECOND;
106
- /** Drops undefined-valued keys: wire objects carry only what JSON would carry. */
107
- function compact(o) {
108
- for (const k of Object.keys(o))
109
- if (o[k] === undefined)
110
- delete o[k];
111
- return o;
112
- }
113
- function mulberry(rng) {
114
- const next = (rng + 0x6d2b79f5) >>> 0;
115
- let t = next;
116
- t = Math.imul(t ^ (t >>> 15), t | 1);
117
- t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
118
- return { rng: next, u: ((t ^ (t >>> 14)) >>> 0) / 4294967296 };
119
- }
120
- /** JSON-only state: no functions, class instances, cycles, shared objects or non-finite numbers. */
121
- function assertJson(value, what) {
122
- const seen = new Map();
123
- const walk = (v, path) => {
124
- if (v === null || v === undefined || typeof v === "boolean" || typeof v === "string")
125
- return;
126
- if (typeof v === "number") {
127
- if (!Number.isFinite(v))
128
- throw new Error(`${path} is ${v}; state must be JSON`);
129
- return;
130
- }
131
- if (typeof v !== "object")
132
- throw new Error(`${path} is a ${typeof v}; state must be JSON`);
133
- const prior = seen.get(v);
134
- if (prior !== undefined)
135
- throw new Error(`${path} is shared with ${prior}; return a fresh object`);
136
- seen.set(v, path);
137
- const proto = Object.getPrototypeOf(v);
138
- if (!Array.isArray(v) && proto !== Object.prototype && proto !== null) {
139
- throw new Error(`${path} is a ${v.constructor?.name ?? "class instance"}; use plain objects and arrays`);
140
- }
141
- for (const [k, x] of Object.entries(v))
142
- walk(x, `${path}.${k}`);
143
- };
144
- walk(value, what);
145
- }
146
- const missingCapabilities = (have) => ["move", "damage", "death", "mirror"].filter((c) => !have.includes(c));
147
- const containerKey = (at) => `${Math.floor(at.x)},${Math.floor(at.y)},${Math.floor(at.z)}`;
148
- function containerCounts(s, at) {
149
- const c = s.containers[containerKey(at)];
150
- if (!c)
151
- return undefined;
152
- const counts = {};
153
- for (const st of Object.values(c.slots))
154
- counts[st.item] = (counts[st.item] ?? 0) + st.count;
155
- return counts;
156
- }
157
- function validateSpec(spec) {
158
- if (spec.life !== "single" && spec.life !== "respawn") {
159
- throw new Error(`life must be "single" or "respawn" (got ${JSON.stringify(spec.life)})`);
160
- }
161
- if (spec.duration !== undefined)
162
- toTicks(spec.duration);
163
- const phases = spec.phases ?? {};
164
- const names = Object.keys(phases);
165
- for (const [name, ph] of Object.entries(phases)) {
166
- if (ph.duration !== undefined)
167
- toTicks(ph.duration);
168
- for (const st of ph.at ?? []) {
169
- if ("remaining" in st && ph.duration === undefined) {
170
- throw new Error(`phases.${name}.at: 'remaining' needs a 'duration'`);
171
- }
172
- if (!("remaining" in st) && !("elapsed" in st)) {
173
- throw new Error(`phases.${name}.at: each step needs 'elapsed' or 'remaining'`);
174
- }
175
- toTicks("remaining" in st ? st.remaining : st.elapsed);
176
- }
177
- for (const ev of ph.every ?? [])
178
- toTicks(ev.interval);
179
- if (typeof ph.next === "string" && ph.next !== "END" && !names.includes(ph.next)) {
180
- throw new Error(`phases.${name}.next: "${ph.next}" is not a phase (declared: ${names.join(", ") || "none"})`);
181
- }
182
- }
183
- for (const st of spec.at ?? [])
184
- toTicks(st.at);
185
- for (const ev of spec.every ?? [])
186
- toTicks(ev.interval);
187
- for (const [voteId, rule] of Object.entries((spec.votes ?? {}))) {
188
- for (const ph of rule?.phases ?? []) {
189
- if (!names.includes(ph))
190
- throw new Error(`votes.${voteId}.phases: "${ph}" is not a phase`);
191
- }
192
- }
193
- }
194
- function defineChallenge(config, spec) {
195
- validateSpec(spec);
196
- // The vote named player is the one skills.voteForPlayer casts in; its options are the other participants.
197
- if (config.challengeConfig?.votingOptions && exports.PLAYER_VOTE in config.challengeConfig.votingOptions) {
198
- throw new Error(`votingOptions.${exports.PLAYER_VOTE} is reserved: open it with votes: { ${exports.PLAYER_VOTE}: {...} }, without options`);
199
- }
200
- const phaseNames = Object.keys(spec.phases ?? {});
201
- const firstPhase = (phaseNames[0] ?? "MAIN");
202
- const phaseOf = (s) => s.phase === "END" ? undefined : spec.phases?.[s.phase];
203
- const paramDefaults = () => Object.fromEntries(Object.entries(config.params ?? {}).map(([k, p]) => [k, p.default]));
204
- const duration = spec.duration === undefined ? undefined : toTicks(spec.duration);
205
- const world = spec.world === false ? null : { ...exports.WORLD_DEFAULTS, ...spec.world };
206
- const declaredZones = Object.keys(spec.zones ?? {});
207
- const zoneBoxes = (spec.zones ?? {});
208
- const declaredTimers = spec.timers;
209
- const notDeclared = (kind, name, declared) => new Error(`"${name}" is not declared in ${kind} (declared: ${declared.join(", ") || "none"})`);
210
- const manifest = {
211
- runtime: 1,
212
- name: config.name,
213
- life: spec.life,
214
- roles: Object.keys(config.roles),
215
- };
216
- const declaredLocations = Object.keys(config.challengeConfig?.locations ?? {});
217
- if (declaredLocations.length > 0)
218
- manifest.locations = declaredLocations;
219
- if (spec.mirror)
220
- manifest.mirror = spec.mirror;
221
- if (spec.markers)
222
- manifest.markers = spec.markers;
223
- if (spec.containers)
224
- manifest.containers = Object.values(spec.containers).map((at) => ({ ...at }));
225
- // The engine samples the declared positions only; a report for another block is out of contract and ignored.
226
- const declaredContainers = new Set(Object.values(spec.containers ?? {}).map(containerKey));
227
- const init = () => ({
228
- seed: 0,
229
- rng: 0,
230
- params: paramDefaults(),
231
- capabilities: [],
232
- locations: {},
233
- markers: [],
234
- status: "LOBBY",
235
- tick: 0,
236
- dt: 0,
237
- startTick: 0,
238
- setupTick: 0,
239
- phase: firstPhase,
240
- phaseStartTick: 0,
241
- phaseEntries: 0,
242
- fired: {},
243
- every: {},
244
- timers: {},
245
- players: {},
246
- game: {},
247
- votes: {},
248
- walks: {},
249
- containers: {},
250
- world: (0, world_js_1.emptyWorld)(),
251
- nextId: 0,
252
- winners: [],
253
- endState: "",
254
- });
255
- // One reduction: the context writes into `actions`, or into `batch` during setup.
256
- function makeCtx(s, actions, batch) {
257
- const push = (effect) => {
258
- compact(effect);
259
- if (batch)
260
- batch.push(effect);
261
- else
262
- actions.push({ type: "effect", effect });
263
- };
264
- const targets = (to) => to === "all" ? [undefined] : (Array.isArray(to) ? to : [to]).map((p) => p.name);
265
- const g = {
266
- get game() {
267
- return s.game;
268
- },
269
- get params() {
270
- return s.params;
271
- },
272
- get players() {
273
- return Object.values(s.players).sort((a, b) => a.number - b.number);
274
- },
275
- get active() {
276
- return g.players.filter((p) => !p.eliminated);
277
- },
278
- player: (name) => s.players[name],
279
- role: (role) => g.players.filter((p) => p.role === role),
280
- one: (role) => g.players.find((p) => p.role === role),
281
- get tick() {
282
- return s.tick;
283
- },
284
- get dt() {
285
- return s.dt;
286
- },
287
- get elapsed() {
288
- return s.tick - s.startTick;
289
- },
290
- get phase() {
291
- return s.phase;
292
- },
293
- get phaseElapsed() {
294
- return s.tick - s.phaseStartTick;
295
- },
296
- get phaseRemaining() {
297
- const d = phaseOf(s)?.duration;
298
- return d === undefined ? Number.MAX_SAFE_INTEGER : Math.max(0, toTicks(d) - (s.tick - s.phaseStartTick));
299
- },
300
- location(name) {
301
- const at = s.locations[name];
302
- if (!at)
303
- throw notDeclared("challengeConfig.locations", name, Object.keys(s.locations));
304
- return { ...at };
305
- },
306
- markers: (tag) => s.markers
307
- .filter((m) => tag === undefined || m.tags.includes(tag))
308
- .map((m) => ({ ...m, at: { ...m.at }, tags: [...m.tags] })),
309
- contents: (target) => {
310
- if (typeof target !== "string") {
311
- // A position that is not declared reads as nothing forever: refuse it, like an unknown name.
312
- if (!declaredContainers.has(containerKey(target))) {
313
- throw notDeclared("containers", containerKey(target), Object.keys(spec.containers ?? {}));
314
- }
315
- return containerCounts(s, target);
316
- }
317
- const at = spec.containers?.[target];
318
- if (!at)
319
- throw notDeclared("containers", target, Object.keys(spec.containers ?? {}));
320
- return containerCounts(s, at);
321
- },
322
- inZone(p, zone) {
323
- if (typeof zone === "string") {
324
- if (!declaredZones.includes(zone))
325
- throw notDeclared("zones", zone, declaredZones);
326
- return p.zones.includes(zone);
327
- }
328
- return p.pos !== null && (0, world_js_1.inBox)(p.pos, zone);
329
- },
330
- count: (p, item) => p.inventory[item] ?? 0,
331
- // Frozen at the boundary too: a state that went through JSON (a checkpoint,
332
- // the harness) holds unfrozen copies.
333
- get others() {
334
- return Object.values(s.world.others).map(world_js_1.freezeObservation);
335
- },
336
- blockAt: (at) => (0, world_js_1.blockAt)(s.world, at),
337
- blockStateAt: (at) => (0, world_js_1.blockStateAt)(s.world, at),
338
- get worldTime() {
339
- return s.world.time;
340
- },
341
- blockCount: (box, block) => (0, world_js_1.blockCount)(s.world, box, block),
342
- findBlocks: (box, block, limit) => (0, world_js_1.findBlocks)(s.world, box, block, limit),
343
- entities: (filter) => (0, world_js_1.entitiesIn)(s.world, filter).map(world_js_1.freezeObservation),
344
- entity: (id) => {
345
- const e = s.world.entities[id];
346
- return e ? (0, world_js_1.freezeObservation)(e) : undefined;
347
- },
348
- votes: (voteId) => ({ ...(s.votes[voteId] ?? {}) }),
349
- vote: (p, voteId) => s.votes[voteId]?.[p.name],
350
- openVote(voteId) {
351
- delete s.votes[voteId];
352
- },
353
- goTo(phase) {
354
- if (s.status !== "PLAYING")
355
- return;
356
- if (phase !== "END" && !(spec.phases && Object.hasOwn(spec.phases, phase))) {
357
- throw new Error(`goTo("${phase}"): not a declared phase (${Object.keys(spec.phases ?? {}).join(", ") || "none"})`);
358
- }
359
- const leaving = s.phase;
360
- const entry = s.phaseEntries;
361
- phaseOf(s)?.exit?.(g);
362
- for (const key of Object.keys(s.fired))
363
- if (key.startsWith(`phase:${entry}:${leaving}:`))
364
- delete s.fired[key];
365
- for (const key of Object.keys(s.every))
366
- if (key.startsWith(`phase:${entry}:${leaving}:`))
367
- delete s.every[key];
368
- s.phase = phase;
369
- s.phaseStartTick = s.tick;
370
- s.phaseEntries += 1;
371
- if (phase === "END") {
372
- finish(s, g, actions);
373
- return;
374
- }
375
- const ph = phaseOf(s);
376
- (ph?.every ?? []).forEach((_e, i) => {
377
- s.every[`phase:${s.phaseEntries}:${phase}:every:${i}`] = s.tick;
378
- });
379
- ph?.enter?.(g);
380
- },
381
- end(endState) {
382
- if (s.status !== "PLAYING")
383
- return;
384
- finish(s, g, actions, endState);
385
- },
386
- eliminate(p) {
387
- if (p.eliminated)
388
- return;
389
- (s.players[p.name] ?? p).eliminated = true;
390
- push({ kind: "eliminate", player: p.name });
391
- },
392
- schedule(name, after) {
393
- if (declaredTimers && !declaredTimers.includes(name))
394
- throw notDeclared("timers", name, declaredTimers);
395
- s.timers[name] = s.tick + toTicks(after);
396
- },
397
- cancel(name) {
398
- delete s.timers[name];
399
- },
400
- rand(lo, hi) {
401
- const r = mulberry(s.rng);
402
- s.rng = r.rng;
403
- return lo + Math.floor(r.u * (hi - lo + 1));
404
- },
405
- pick: (items) => items[g.rand(0, items.length - 1)],
406
- shuffle(items) {
407
- const out = [...items];
408
- for (let i = out.length - 1; i > 0; i--) {
409
- const j = g.rand(0, i);
410
- [out[i], out[j]] = [out[j], out[i]];
411
- }
412
- return out;
413
- },
414
- say: (message) => push({ kind: "announce", message }),
415
- tell(to, message) {
416
- for (const name of targets(to))
417
- push({ kind: "announce", message, to: name });
418
- },
419
- title(to, title, subtitle, times) {
420
- for (const name of targets(to))
421
- push({ kind: "title", player: name, title, subtitle, times });
422
- },
423
- log: (player, message, data) => actions.push(compact({ type: "log", player: player?.name, message, data })),
424
- store: (name, value, player) => actions.push(compact({ type: "store", name, value, player: player?.name })),
425
- achievement: (player, id) => actions.push({ type: "log", player: player.name, message: `ACHIEVEMENT:${id}`, data: { achievement: id } }),
426
- give: (player, item, count = 1, nbt) => push({ kind: "give", player: player.name, item, count, nbt }),
427
- clear: (player, item, count) => push({ kind: "clear", player: player.name, item, count }),
428
- effect: (player, effect, seconds, amplifier = 0) => push({ kind: "effect", player: player.name, effect, seconds, amplifier }),
429
- teleport: (player, to) => push({ kind: "teleport", player: player.name, to: { ...to } }),
430
- walk(player, to, opts) {
431
- // One pending walk per player: a new walk supersedes the old one.
432
- for (const [id, w] of Object.entries(s.walks))
433
- if (w.player === player.name)
434
- delete s.walks[id];
435
- const id = `walk:${s.nextId++}`;
436
- const fallback = opts?.fallback ?? "teleport";
437
- s.walks[id] = { player: player.name, fallback, ...to };
438
- actions.push({ type: "walk", id, player: player.name, to: { ...to }, fallback });
439
- },
440
- kill: (player) => push({ kind: "kill", player: player.name }),
441
- gamemode: (player, mode) => push({ kind: "gamemode", player: player.name, mode }),
442
- attribute: (player, attribute, value) => push({ kind: "attribute", player: player.name, attribute, value }),
443
- fill: (from, to, block) => push({ kind: "fill", from: { ...from }, to: { ...to }, block }),
444
- setblock: (at, block, nbt) => push({ kind: "setblock", at: { ...at }, block, nbt }),
445
- summon: (entity, at, nbt) => push({ kind: "summon", entity, at: { ...at }, nbt }),
446
- killEntities: (entities) => push({ kind: "kill_entities", entities }),
447
- time: (value) => push({ kind: "time", value }),
448
- gamerule: (rule, value) => push({ kind: "gamerule", rule, value }),
449
- setSpawn: (at) => push({ kind: "spawn", at: { ...at } }),
450
- worldBorder: (opts) => push({ kind: "worldborder", center: opts.center, size: opts.size, seconds: opts.seconds }),
451
- container: (at, items) => push({ kind: "container", at: { ...at }, items: items.map((i) => ({ ...i })) }),
452
- command(cmd) {
453
- actions.push({ type: "log", message: `raw command: ${cmd}` });
454
- push({ kind: "command", cmd });
455
- },
456
- };
457
- return g;
458
- }
459
- /** Global handlers first, then the current phase's. */
460
- function fire(s, g, key, e) {
461
- const global = spec.on?.[key];
462
- if (global)
463
- global(g, e);
464
- if (s.status !== "PLAYING")
465
- return;
466
- const local = phaseOf(s)?.on?.[key];
467
- if (local)
468
- local(g, e);
469
- }
470
- function guard(actions, what, fallback, f) {
471
- try {
472
- return { value: f(), failed: false };
473
- }
474
- catch (err) {
475
- actions.push({ type: "log", message: `challenge error in ${what}: ${err.message}` });
476
- return { value: fallback, failed: true };
477
- }
478
- }
479
- function over(s, g, actions, endState, winners) {
480
- s.status = "OVER";
481
- s.endState = endState;
482
- s.winners = winners;
483
- if (spec.onOver) {
484
- const won = new Set(winners);
485
- const result = {
486
- winners: [...winners],
487
- winnerPlayers: g.players.filter((p) => won.has(p.name)),
488
- isWinner: (p) => won.has(p.name),
489
- endState: endState,
490
- };
491
- guard(actions, "onOver", null, () => spec.onOver?.(g, result));
492
- }
493
- const judgeContext = spec.judgeContext;
494
- const judge = judgeContext
495
- ? guard(actions, "judgeContext", null, () => judgeContext(g))
496
- : null;
497
- if (spec.announce?.gameOver !== false) {
498
- g.say("=== GAME OVER ===");
499
- for (const w of winners)
500
- g.say(`${w} has won the challenge!`);
501
- }
502
- const gameOver = { type: "game_over", winners, endState: s.endState };
503
- if (judge && judge.value !== null)
504
- gameOver.judgeContext = judge.value;
505
- actions.push(gameOver);
506
- }
507
- // Author code runs only inside guards here, so a throw in winners/endState never loses game_over.
508
- function finish(s, g, actions, endState) {
509
- if (s.status !== "PLAYING")
510
- return;
511
- const winners = guard(actions, "winners", [], () => (spec.winners?.(g) ?? []).map((p) => p.name));
512
- const state = endState !== undefined
513
- ? { value: endState, failed: false }
514
- : guard(actions, "endState", "", () => spec.endState?.(g) ?? "");
515
- over(s, g, actions, winners.failed || state.failed ? "error_challenge" : state.value, winners.value);
516
- }
517
- function fail(s, g, actions, endState, message, data) {
518
- actions.push(compact({ type: "log", message, data }));
519
- if (s.status !== "OVER")
520
- over(s, g, actions, endState, []);
521
- }
522
- function worldGlobals() {
523
- if (!world)
524
- return [];
525
- const out = [];
526
- if (world.spawn)
527
- out.push({ kind: "spawn", at: { ...world.spawn } });
528
- if (world.time !== false)
529
- out.push({ kind: "time", value: world.time });
530
- out.push({ kind: "gamerule", rule: "doDaylightCycle", value: world.daylightCycle });
531
- out.push({ kind: "gamerule", rule: "doMobSpawning", value: world.mobSpawning });
532
- out.push({ kind: "gamerule", rule: "naturalRegeneration", value: world.naturalRegeneration });
533
- return out;
534
- }
535
- function worldPlayers(s) {
536
- if (!world)
537
- return [];
538
- const out = [];
539
- for (const p of Object.values(s.players)) {
540
- if (world.maxHealth !== false) {
541
- out.push({
542
- kind: "attribute",
543
- player: p.name,
544
- attribute: "minecraft:generic.max_health",
545
- value: world.maxHealth,
546
- });
547
- }
548
- }
549
- return out;
550
- }
551
- function play(s, g, actions) {
552
- s.status = "PLAYING";
553
- s.startTick = s.tick;
554
- s.phaseStartTick = s.tick;
555
- s.phaseEntries = 1;
556
- for (const p of Object.values(s.players)) {
557
- p.alive = true;
558
- p.deaths = 0;
559
- p.kills = 0;
560
- }
561
- const head = spec.setup ? worldPlayers(s) : [...worldGlobals(), ...worldPlayers(s)];
562
- for (const e of head)
563
- actions.push({ type: "effect", effect: e });
564
- (spec.every ?? []).forEach((_e, i) => {
565
- s.every[`every:${i}`] = s.tick;
566
- });
567
- spec.start?.(g);
568
- const ph = phaseOf(s);
569
- (ph?.every ?? []).forEach((_e, i) => {
570
- s.every[`phase:1:${s.phase}:every:${i}`] = s.tick;
571
- });
572
- ph?.enter?.(g);
573
- }
574
- function onTick(s, g, actions) {
575
- if (s.status === "LOBBY") {
576
- const roster = Object.values(s.players);
577
- if (roster.length === 0 || !roster.every((p) => p.connected))
578
- return;
579
- if (!spec.setup) {
580
- play(s, g, actions);
581
- return;
582
- }
583
- const batch = [...worldGlobals()];
584
- spec.setup(makeCtx(s, actions, batch));
585
- s.status = "SETUP";
586
- s.setupTick = s.tick;
587
- actions.push({ type: "batch", id: "setup", effects: batch });
588
- return;
589
- }
590
- if (s.status === "SETUP") {
591
- if (s.tick - s.setupTick > SETUP_TIMEOUT_TICKS) {
592
- fail(s, g, actions, "error_setup", "the engine did not confirm the setup batch in 30 s");
593
- }
594
- return;
595
- }
596
- if (s.status !== "PLAYING")
597
- return;
598
- // Everything due on this tick, in due-tick order, one shot each.
599
- const work = [];
600
- let order = 0;
601
- (spec.at ?? []).forEach((st, i) => {
602
- const key = `at:${i}`;
603
- const due = s.startTick + toTicks(st.at);
604
- if (s.tick >= due && !s.fired[key])
605
- work.push({
606
- due,
607
- order: order++,
608
- run: () => {
609
- s.fired[key] = true;
610
- st.run(g);
611
- },
612
- });
613
- });
614
- (spec.every ?? []).forEach((st, i) => {
615
- const key = `every:${i}`;
616
- const interval = toTicks(st.interval);
617
- const due = (s.every[key] ?? s.startTick) + interval;
618
- if (s.tick >= due)
619
- work.push({
620
- due,
621
- order: order++,
622
- run: () => {
623
- s.every[key] = s.tick;
624
- st.run(g);
625
- },
626
- });
627
- });
628
- const ph = phaseOf(s);
629
- if (ph) {
630
- const entry = s.phaseEntries;
631
- const name = s.phase;
632
- const phaseDuration = ph.duration === undefined ? undefined : toTicks(ph.duration);
633
- (ph.at ?? []).forEach((st, i) => {
634
- const key = `phase:${entry}:${name}:${i}`;
635
- const offset = "elapsed" in st ? toTicks(st.elapsed) : (phaseDuration ?? 0) - toTicks(st.remaining);
636
- const due = s.phaseStartTick + offset;
637
- if (s.tick >= due && !s.fired[key])
638
- work.push({
639
- due,
640
- order: order++,
641
- run: () => {
642
- s.fired[key] = true;
643
- st.run(g);
644
- },
645
- });
646
- });
647
- (ph.every ?? []).forEach((st, i) => {
648
- const key = `phase:${entry}:${name}:every:${i}`;
649
- const interval = toTicks(st.interval);
650
- const due = (s.every[key] ?? s.phaseStartTick) + interval;
651
- if (s.tick >= due)
652
- work.push({
653
- due,
654
- order: order++,
655
- run: () => {
656
- s.every[key] = s.tick;
657
- st.run(g);
658
- },
659
- });
660
- });
661
- }
662
- for (const [name, due] of Object.entries(s.timers)) {
663
- if (s.tick >= due) {
664
- work.push({
665
- due,
666
- order: order++,
667
- run: () => {
668
- // A sibling handler may have cancelled or rescheduled it on this same tick.
669
- if (s.timers[name] !== due)
670
- return;
671
- delete s.timers[name];
672
- fire(s, g, "timer", { name: name });
673
- },
674
- });
675
- }
676
- }
677
- work.sort((a, b) => a.due - b.due || a.order - b.order);
678
- const entryAtStart = s.phaseEntries;
679
- for (const w of work) {
680
- if (s.status !== "PLAYING")
681
- return;
682
- // A phase change drops the rest of the old phase's work; the new phase gets a clean tick.
683
- if (s.phaseEntries !== entryAtStart)
684
- break;
685
- w.run();
686
- }
687
- if (s.status !== "PLAYING")
688
- return;
689
- // One transition per tick event: a late phase still gets its full duration.
690
- const cur = phaseOf(s);
691
- if (cur &&
692
- s.phaseEntries === entryAtStart &&
693
- cur.duration !== undefined &&
694
- g.phaseElapsed >= toTicks(cur.duration)) {
695
- g.goTo(typeof cur.next === "function" ? cur.next(g) : (cur.next ?? "END"));
696
- }
697
- if (s.status === "PLAYING") {
698
- spec.on?.tick?.(g);
699
- if (s.status === "PLAYING")
700
- phaseOf(s)?.on?.tick?.(g);
701
- }
702
- }
703
- /** The declared zones a position is in, in declaration order. */
704
- function zonesAt(pos) {
705
- if (!pos)
706
- return [];
707
- return declaredZones.filter((zone) => {
708
- const box = zoneBoxes[zone];
709
- return box !== undefined && (0, world_js_1.inBox)(pos, box);
710
- });
711
- }
712
- /** The zone edges between two memberships, as events; nothing outside PLAYING. */
713
- function fireZoneEdges(s, g, p, before, after) {
714
- if (s.status !== "PLAYING")
715
- return;
716
- for (const zone of after)
717
- if (!before.includes(zone))
718
- fire(s, g, "zoneEntered", { player: p, zone: zone });
719
- for (const zone of before)
720
- if (!after.includes(zone))
721
- fire(s, g, "zoneLeft", { player: p, zone: zone });
722
- }
723
- /** Re-evaluates the declared zones for one participant after a move. */
724
- function updateZones(s, g, p) {
725
- if (!p.pos)
726
- return;
727
- const before = p.zones;
728
- const after = zonesAt(p.pos);
729
- p.zones = after;
730
- fireZoneEdges(s, g, p, before, after);
731
- }
732
- /** Applies one sample's fields; returns the move, for the events that fire once the whole tick applied. */
733
- function applySample(p, d) {
734
- let move = null;
735
- if (d.pos) {
736
- const from = p.pos;
737
- p.pos = { ...d.pos };
738
- move = { p, from, to: { ...d.pos }, maxY: d.maxY ?? d.pos.y, minY: d.minY ?? d.pos.y };
739
- }
740
- if (d.yaw !== undefined)
741
- p.yaw = d.yaw;
742
- if (d.pitch !== undefined)
743
- p.pitch = d.pitch;
744
- if (d.headYaw !== undefined)
745
- p.headYaw = d.headYaw;
746
- if (d.id !== undefined)
747
- p.entityId = d.id;
748
- if (d.uuid !== undefined)
749
- p.uuid = d.uuid;
750
- if (d.equipment)
751
- p.equipment = (0, world_js_1.applyEquipment)(p.equipment, d.equipment);
752
- if (d.invisible !== undefined)
753
- p.invisible = d.invisible;
754
- if (d.health !== undefined)
755
- p.health = d.health;
756
- if (d.food !== undefined)
757
- p.food = d.food;
758
- if (d.gamemode !== undefined)
759
- p.gamemode = d.gamemode;
760
- if (d.held !== undefined)
761
- p.held = d.held;
762
- return move;
763
- }
764
- function fireMove(s, g, m) {
765
- updateZones(s, g, m.p);
766
- if (s.status === "PLAYING") {
767
- fire(s, g, "move", { player: m.p, from: m.from, to: { ...m.to }, maxY: m.maxY, minY: m.minY });
768
- }
769
- }
770
- function samplePlayer(s, g, p, d) {
771
- const move = applySample(p, d);
772
- if (move)
773
- fireMove(s, g, move);
774
- }
775
- function sampleOther(s, d) {
776
- const prev = s.world.others[d.name];
777
- s.world = (0, world_js_1.putOther)(s.world, compact({
778
- name: d.name,
779
- pos: d.pos ? { ...d.pos } : (prev?.pos ?? null),
780
- health: d.health ?? prev?.health ?? 20,
781
- food: d.food ?? prev?.food ?? 20,
782
- gamemode: d.gamemode ?? prev?.gamemode ?? "survival",
783
- held: d.held !== undefined ? d.held : (prev?.held ?? null),
784
- uuid: d.uuid ?? prev?.uuid,
785
- entityId: d.id ?? prev?.entityId,
786
- yaw: d.yaw ?? prev?.yaw,
787
- pitch: d.pitch ?? prev?.pitch,
788
- headYaw: d.headYaw ?? prev?.headYaw,
789
- equipment: d.equipment ? (0, world_js_1.applyEquipment)(prev?.equipment, d.equipment) : prev?.equipment,
790
- invisible: d.invisible ?? prev?.invisible,
791
- }));
792
- }
793
- function step(s, g, actions, e) {
794
- const playing = () => s.status === "PLAYING";
795
- const resolve = (name) => (name === undefined ? undefined : s.players[name]);
796
- switch (e.type) {
797
- case "run_started": {
798
- s.seed = e.seed;
799
- s.rng = e.seed >>> 0;
800
- s.params = { ...paramDefaults(), ...e.params };
801
- s.capabilities = e.capabilities;
802
- s.locations = e.locations ?? {};
803
- s.markers = (e.markers ?? []).map((m) => ({ ...m, at: { ...m.at }, tags: [...m.tags] }));
804
- s.game = spec.game ? spec.game() : {};
805
- e.participants.forEach((p, i) => {
806
- const roster = { name: p.name, role: p.role, number: i + 1, connected: false };
807
- const built = {
808
- alive: true,
809
- eliminated: false,
810
- deaths: 0,
811
- kills: 0,
812
- score: 0,
813
- pos: null,
814
- yaw: 0,
815
- pitch: 0,
816
- headYaw: 0,
817
- entityId: null,
818
- uuid: null,
819
- equipment: {},
820
- invisible: false,
821
- health: 20,
822
- food: 20,
823
- gamemode: "survival",
824
- held: null,
825
- slots: {},
826
- inventory: {},
827
- zones: [],
828
- human: p.human === true,
829
- };
830
- const forRole = spec.players?.[p.role];
831
- const own = forRole ? forRole(roster) : (spec.player?.(roster) ?? {});
832
- s.players[p.name] = { ...roster, ...built, ...own };
833
- });
834
- const unknownRoles = e.participants.filter((p) => !(p.role in config.roles));
835
- if (unknownRoles.length > 0) {
836
- fail(s, g, actions, "error_challenge", `unknown role: ${unknownRoles.map((p) => `${p.role} (${p.name})`).join(", ")}; the roles are ${Object.keys(config.roles).join(", ")}`);
837
- return;
838
- }
839
- const missing = missingCapabilities(e.capabilities);
840
- if (spec.setup && !e.capabilities.includes("batch"))
841
- missing.push("batch");
842
- if (spec.containers && Object.keys(spec.containers).length > 0 && !e.capabilities.includes("containers")) {
843
- missing.push("containers");
844
- }
845
- if (missing.length > 0) {
846
- fail(s, g, actions, "error_unsupported_facts", `this engine cannot supply: ${missing.join(", ")}`, {
847
- missing,
848
- });
849
- }
850
- return;
851
- }
852
- case "player_joined": {
853
- const p = resolve(e.name);
854
- if (!p)
855
- return;
856
- p.connected = true;
857
- if (e.uuid !== undefined)
858
- p.uuid = e.uuid;
859
- if (e.gamemode !== undefined)
860
- p.gamemode = e.gamemode;
861
- if (playing())
862
- fire(s, g, "joined", { player: p });
863
- break;
864
- }
865
- case "gamemode": {
866
- const p = resolve(e.name);
867
- if (!p) {
868
- const other = s.world.others[e.name];
869
- if (other)
870
- s.world = (0, world_js_1.putOther)(s.world, { ...other, gamemode: e.gamemode });
871
- return;
872
- }
873
- p.gamemode = e.gamemode;
874
- if (playing())
875
- fire(s, g, "gamemode", { player: p, gamemode: e.gamemode });
876
- break;
877
- }
878
- case "player_left": {
879
- const p = resolve(e.name);
880
- if (!p) {
881
- s.world = (0, world_js_1.removeOther)(s.world, e.name);
882
- return;
883
- }
884
- p.connected = false;
885
- if (playing())
886
- fire(s, g, "left", { player: p });
887
- break;
888
- }
889
- case "tick": {
890
- // The whole tick applies first (the clock, the entities, every
891
- // player's sample), then the derived events fire: a move or zone
892
- // handler sees one consistent tick, whatever the order of the players.
893
- s.dt = e.serverTick - s.tick;
894
- s.tick = e.serverTick;
895
- if (e.entities)
896
- s.world = (0, world_js_1.moveEntities)(s.world, e.entities);
897
- if (e.time !== undefined && e.time !== s.world.time)
898
- s.world = { ...s.world, time: e.time };
899
- const moves = [];
900
- for (const d of e.players ?? []) {
901
- const p = resolve(d.name);
902
- if (p) {
903
- const move = applySample(p, d);
904
- if (move)
905
- moves.push(move);
906
- }
907
- else if (!d.participant)
908
- sampleOther(s, d);
909
- }
910
- // Every player's zone membership is complete before any handler runs:
911
- // a move or zone handler that reads another player's zones sees this
912
- // tick, not the one before, whatever the order of the samples.
913
- const edges = moves.map((m) => {
914
- const before = m.p.zones;
915
- const after = zonesAt(m.p.pos);
916
- m.p.zones = after;
917
- return { m, before, after };
918
- });
919
- for (const { m, before, after } of edges) {
920
- fireZoneEdges(s, g, m.p, before, after);
921
- if (playing())
922
- fire(s, g, "move", { player: m.p, from: m.from, to: { ...m.to }, maxY: m.maxY, minY: m.minY });
923
- }
924
- onTick(s, g, actions);
925
- break;
926
- }
927
- case "effect_result": {
928
- if (e.id === "setup" && s.status === "SETUP") {
929
- if (e.ok)
930
- play(s, g, actions);
931
- else
932
- fail(s, g, actions, "error_setup", `setup batch failed: ${e.detail ?? "unknown"}`);
933
- return;
934
- }
935
- const w = s.walks[e.id];
936
- if (w) {
937
- delete s.walks[e.id];
938
- if (!e.ok && w.fallback === "teleport") {
939
- actions.push({
940
- type: "effect",
941
- effect: { kind: "teleport", player: w.player, to: { x: w.x, y: w.y, z: w.z } },
942
- });
943
- }
944
- const p = resolve(w.player);
945
- if (p && playing())
946
- fire(s, g, "walkDone", { player: p, ok: e.ok });
947
- }
948
- break;
949
- }
950
- case "effect_failed":
951
- actions.push({ type: "log", message: `effect failed: ${e.kind}: ${e.detail}` });
952
- if (playing())
953
- fire(s, g, "effectFailed", { kind: e.kind, detail: e.detail });
954
- break;
955
- case "player_moved": {
956
- const p = resolve(e.name);
957
- if (!p)
958
- return;
959
- samplePlayer(s, g, p, { pos: e.to, maxY: e.maxY, minY: e.minY });
960
- break;
961
- }
962
- case "player_damaged": {
963
- const p = resolve(e.name);
964
- if (!p || !playing())
965
- return;
966
- fire(s, g, "damaged", { player: p, amount: e.amount, cause: e.cause, attacker: resolve(e.attacker) });
967
- break;
968
- }
969
- case "player_died": {
970
- const p = resolve(e.name);
971
- if (!p)
972
- return;
973
- p.alive = false;
974
- const killer = resolve(e.killer);
975
- if (!playing())
976
- return;
977
- p.deaths += 1;
978
- if (killer && killer !== p)
979
- killer.kills += 1;
980
- if (spec.life === "single")
981
- g.eliminate(p);
982
- if (spec.announce?.deaths !== false)
983
- g.say(`${p.name} has died!`);
984
- fire(s, g, "died", { player: p, cause: e.cause, killer });
985
- break;
986
- }
987
- case "player_respawned": {
988
- const p = resolve(e.name);
989
- if (!p)
990
- return;
991
- if (!p.eliminated)
992
- p.alive = true;
993
- if (playing())
994
- fire(s, g, "respawned", { player: p });
995
- break;
996
- }
997
- case "item_picked_up": {
998
- const p = resolve(e.name);
999
- if (!p || !playing())
1000
- return;
1001
- fire(s, g, "pickup", { player: p, item: e.item, count: e.count, at: e.at });
1002
- break;
1003
- }
1004
- case "inventory": {
1005
- const p = resolve(e.name);
1006
- if (!p)
1007
- return;
1008
- for (const sl of e.slots) {
1009
- if (sl.item && sl.count)
1010
- p.slots[sl.slot] = { item: sl.item, count: sl.count };
1011
- else
1012
- delete p.slots[sl.slot];
1013
- }
1014
- const before = p.inventory;
1015
- const after = {};
1016
- for (const st of Object.values(p.slots))
1017
- after[st.item] = (after[st.item] ?? 0) + st.count;
1018
- p.inventory = after;
1019
- if (!playing())
1020
- return;
1021
- for (const item of new Set([...Object.keys(before), ...Object.keys(after)])) {
1022
- if ((before[item] ?? 0) !== (after[item] ?? 0)) {
1023
- fire(s, g, "inventory", { player: p, item: item, count: after[item] ?? 0 });
1024
- }
1025
- }
1026
- break;
1027
- }
1028
- case "container": {
1029
- const key = containerKey(e.at);
1030
- if (!declaredContainers.has(key))
1031
- return;
1032
- const before = containerCounts(s, e.at) ?? {};
1033
- const slots = { ...(s.containers[key]?.slots ?? {}) };
1034
- for (const sl of e.slots) {
1035
- if (sl.item && sl.count)
1036
- slots[sl.slot] = { item: sl.item, count: sl.count };
1037
- else
1038
- delete slots[sl.slot];
1039
- }
1040
- s.containers[key] = { block: e.block, slots };
1041
- if (!playing() || e.initial)
1042
- return;
1043
- const after = containerCounts(s, e.at) ?? {};
1044
- const changes = [];
1045
- for (const item of new Set([...Object.keys(before), ...Object.keys(after)])) {
1046
- const delta = (after[item] ?? 0) - (before[item] ?? 0);
1047
- if (delta !== 0)
1048
- changes.push({ item: item, delta });
1049
- }
1050
- if (changes.length === 0)
1051
- return;
1052
- fire(s, g, "container", { at: { ...e.at }, block: e.block, items: after, changes, by: resolve(e.by) });
1053
- break;
1054
- }
1055
- case "entity_spawned": {
1056
- const entity = compact({
1057
- id: e.id,
1058
- type: e.entity,
1059
- pos: { ...e.at },
1060
- tags: [...(e.tags ?? [])],
1061
- name: e.name,
1062
- health: e.health,
1063
- item: e.item,
1064
- count: e.count,
1065
- uuid: e.uuid,
1066
- yaw: e.yaw,
1067
- pitch: e.pitch,
1068
- headYaw: e.headYaw,
1069
- equipment: e.equipment ? (0, world_js_1.applyEquipment)(undefined, e.equipment) : undefined,
1070
- invisible: e.invisible,
1071
- villager: e.villager ? { ...e.villager } : undefined,
1072
- });
1073
- s.world = (0, world_js_1.putEntity)(s.world, entity);
1074
- if (playing())
1075
- fire(s, g, "entitySpawned", { entity });
1076
- break;
1077
- }
1078
- case "entity_changed": {
1079
- const prev = s.world.entities[e.id];
1080
- if (!prev)
1081
- return;
1082
- const next = { ...prev };
1083
- if (e.tags)
1084
- next.tags = [...e.tags];
1085
- if (e.name !== undefined)
1086
- next.name = e.name ?? undefined;
1087
- if (e.health !== undefined)
1088
- next.health = e.health;
1089
- if (e.item !== undefined)
1090
- next.item = e.item;
1091
- if (e.count !== undefined)
1092
- next.count = e.count;
1093
- if (e.equipment)
1094
- next.equipment = (0, world_js_1.applyEquipment)(prev.equipment, e.equipment);
1095
- if (e.invisible !== undefined)
1096
- next.invisible = e.invisible;
1097
- if (e.villager)
1098
- next.villager = { ...e.villager };
1099
- s.world = (0, world_js_1.putEntity)(s.world, next);
1100
- break;
1101
- }
1102
- case "entity_removed": {
1103
- const prev = s.world.entities[e.id];
1104
- if (!prev)
1105
- return;
1106
- s.world = (0, world_js_1.removeEntity)(s.world, e.id);
1107
- // Frozen like every other observation: the harness's state is a JSON copy, the arena's is frozen already.
1108
- if (playing())
1109
- fire(s, g, "entityRemoved", { entity: (0, world_js_1.freezeObservation)(prev), cause: e.cause, killer: resolve(e.killer) });
1110
- break;
1111
- }
1112
- case "chunk":
1113
- s.world = (0, world_js_1.applyChunk)(s.world, e);
1114
- break;
1115
- case "chunk_unloaded":
1116
- s.world = (0, world_js_1.removeChunk)(s.world, e.x, e.z);
1117
- break;
1118
- case "blocks_changed": {
1119
- s.world = (0, world_js_1.setBlocks)(s.world, e.changes);
1120
- if (playing()) {
1121
- fire(s, g, "blocksChanged", {
1122
- changes: e.changes.map((c) => compact({ x: c.x, y: c.y, z: c.z, block: c.block, was: c.was, cause: c.cause, by: resolve(c.by) })),
1123
- });
1124
- }
1125
- break;
1126
- }
1127
- case "blocks_filled":
1128
- s.world = (0, world_js_1.fillBlocks)(s.world, { from: e.from, to: e.to }, e.block);
1129
- break;
1130
- case "block_broken": {
1131
- const p = resolve(e.name);
1132
- if (p && playing())
1133
- fire(s, g, "blockBroken", { player: p, at: e.at, block: e.block });
1134
- break;
1135
- }
1136
- case "block_placed": {
1137
- const p = resolve(e.name);
1138
- if (p && playing())
1139
- fire(s, g, "blockPlaced", { player: p, at: e.at, block: e.block });
1140
- break;
1141
- }
1142
- case "block_used": {
1143
- const p = resolve(e.name);
1144
- if (p && playing())
1145
- fire(s, g, "blockUsed", { player: p, at: e.at, block: e.block, item: e.item });
1146
- break;
1147
- }
1148
- case "chat": {
1149
- const p = resolve(e.from);
1150
- if (p && playing())
1151
- fire(s, g, "chat", { player: p, text: e.text });
1152
- break;
1153
- }
1154
- case "animation": {
1155
- const p = resolve(e.name);
1156
- if (p && playing())
1157
- fire(s, g, "animation", { player: p, animation: e.animation });
1158
- break;
1159
- }
1160
- case "vote_attempt": {
1161
- const p = resolve(e.name);
1162
- const reject = (feedback) => {
1163
- actions.push({ type: "vote_decision", attemptId: e.attemptId, accepted: false, feedback });
1164
- };
1165
- if (!p) {
1166
- reject("You are not a participant in this run.");
1167
- return;
1168
- }
1169
- if (!playing()) {
1170
- reject("The game is not running.");
1171
- return;
1172
- }
1173
- const rule = spec.votes?.[e.voteId];
1174
- // The player vote has no configured options: the other participants are its options.
1175
- const options = e.voteId === exports.PLAYER_VOTE
1176
- ? rule
1177
- ? Object.keys(s.players).filter((name) => name !== p.name)
1178
- : undefined
1179
- : config.challengeConfig.votingOptions?.[e.voteId];
1180
- if (!options) {
1181
- reject(`"${e.voteId}" is not a vote in this challenge.`);
1182
- return;
1183
- }
1184
- if (!options.includes(e.option)) {
1185
- reject(e.voteId === exports.PLAYER_VOTE
1186
- ? `"${e.option}" is not another participant. Players: ${options.join(", ")}.`
1187
- : `"${e.option}" is not an option for "${e.voteId}". Options: ${options.join(", ")}.`);
1188
- return;
1189
- }
1190
- if (rule?.phases && !rule.phases.includes(s.phase)) {
1191
- reject(`Votes for "${e.voteId}" are not open yet.`);
1192
- return;
1193
- }
1194
- const prior = s.votes[e.voteId]?.[p.name];
1195
- if (prior !== undefined && rule?.final !== false) {
1196
- reject(`You already voted "${prior}" for "${e.voteId}". Votes are final.`);
1197
- return;
1198
- }
1199
- if (rule?.open) {
1200
- const verdict = rule.open(g, p, e.option);
1201
- if (verdict !== true) {
1202
- reject(verdict);
1203
- return;
1204
- }
1205
- }
1206
- const round = s.votes[e.voteId] ?? {};
1207
- round[p.name] = e.option;
1208
- s.votes[e.voteId] = round;
1209
- actions.push({ type: "vote_decision", attemptId: e.attemptId, accepted: true });
1210
- fire(s, g, "vote", { player: p, voteId: e.voteId, option: e.option });
1211
- break;
1212
- }
1213
- case "custom": {
1214
- if (!playing())
1215
- return;
1216
- fire(s, g, "custom", { name: e.name, player: resolve(e.player), data: e.data });
1217
- break;
1218
- }
1219
- }
1220
- if (s.status === "PLAYING" && ((duration !== undefined && g.elapsed >= duration) || spec.end?.(g))) {
1221
- finish(s, g, actions);
1222
- }
1223
- }
1224
- // JSON semantics, no shared references, except the world mirror: it is never
1225
- // changed in place, so the clone keeps the reference and an update copies what it touches.
1226
- const clone = (input) => {
1227
- const { world, ...rest } = input;
1228
- return { ...JSON.parse(JSON.stringify(rest)), world };
1229
- };
1230
- const reduce = (input, event) => {
1231
- let s = clone(input);
1232
- let actions = [];
1233
- let g = makeCtx(s, actions, null);
1234
- try {
1235
- step(s, g, actions, event);
1236
- assertJson({ game: s.game, players: s.players }, "state");
1237
- }
1238
- catch (err) {
1239
- // A challenge bug must neither kill the arena nor pass as a result. The crashed
1240
- // reduction's state and effects are dropped; only the SDK's own values remain.
1241
- s = clone(input);
1242
- actions = [];
1243
- g = makeCtx(s, actions, null);
1244
- if (event.type === "tick")
1245
- s.tick = event.serverTick;
1246
- fail(s, g, actions, "error_challenge", `challenge error on ${event.type}: ${err.message}`, {
1247
- event: event,
1248
- });
1249
- }
1250
- // The SDK reports `score`; the author never emits it.
1251
- const scores = [];
1252
- for (const p of Object.values(s.players)) {
1253
- if (input.players[p.name]?.score !== p.score)
1254
- scores.push({ type: "score", player: p.name, score: p.score });
1255
- }
1256
- const tail = actions.filter((a) => a.type === "game_over");
1257
- const head = actions.filter((a) => a.type !== "game_over");
1258
- return { state: s, actions: [...head, ...scores, ...tail] };
1259
- };
1260
- return {
1261
- config,
1262
- manifest,
1263
- zones: zoneBoxes,
1264
- init,
1265
- reduce,
1266
- get ctx() {
1267
- throw new Error("ctx is a type: write `typeof x.ctx`, and take `g` from a handler");
1268
- },
1269
- };
1270
- }
1271
- //# sourceMappingURL=sdk.js.map