@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.
- package/CHANGELOG.md +71 -1
- package/LLM_README.md +286 -193
- package/MIGRATION-0.13.md +109 -0
- package/README.md +98 -90
- package/dist/engine.d.ts +8 -0
- package/dist/engine.d.ts.map +1 -0
- package/dist/engine.js +17 -0
- package/dist/engine.js.map +1 -0
- package/dist/index.d.ts +19 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +42 -16
- package/dist/index.js.map +1 -1
- package/dist/runtime/builder.d.ts +33 -0
- package/dist/runtime/builder.d.ts.map +1 -0
- package/dist/runtime/builder.js +357 -0
- package/dist/runtime/builder.js.map +1 -0
- package/dist/runtime/errors.d.ts +61 -0
- package/dist/runtime/errors.d.ts.map +1 -0
- package/dist/runtime/errors.js +71 -0
- package/dist/runtime/errors.js.map +1 -0
- package/dist/runtime/reducer.d.ts +205 -0
- package/dist/runtime/reducer.d.ts.map +1 -0
- package/dist/runtime/reducer.js +2025 -0
- package/dist/runtime/reducer.js.map +1 -0
- package/dist/runtime/rules.d.ts +29 -0
- package/dist/runtime/rules.d.ts.map +1 -0
- package/dist/runtime/rules.js +23 -0
- package/dist/runtime/rules.js.map +1 -0
- package/dist/runtime/sandbox.d.ts +2 -1
- package/dist/runtime/sandbox.d.ts.map +1 -1
- package/dist/runtime/sandbox.js +7 -4
- package/dist/runtime/sandbox.js.map +1 -1
- package/dist/runtime/time.d.ts +33 -0
- package/dist/runtime/time.d.ts.map +1 -0
- package/dist/runtime/time.js +53 -0
- package/dist/runtime/time.js.map +1 -0
- package/dist/runtime/types.d.ts +963 -0
- package/dist/runtime/types.d.ts.map +1 -0
- package/dist/runtime/types.js +25 -0
- package/dist/runtime/types.js.map +1 -0
- package/dist/runtime/wire.d.ts +858 -0
- package/dist/runtime/wire.d.ts.map +1 -0
- package/dist/runtime/wire.js +28 -0
- package/dist/runtime/wire.js.map +1 -0
- package/dist/runtime/world.d.ts +58 -3
- package/dist/runtime/world.d.ts.map +1 -1
- package/dist/runtime/world.js +12 -0
- package/dist/runtime/world.js.map +1 -1
- package/dist/structures.d.ts +119 -0
- package/dist/structures.d.ts.map +1 -0
- package/dist/structures.js +279 -0
- package/dist/structures.js.map +1 -0
- package/dist/test/harness.d.ts +280 -0
- package/dist/test/harness.d.ts.map +1 -0
- package/dist/{runtime → test}/harness.js +193 -68
- package/dist/test/harness.js.map +1 -0
- package/dist/test/helpers.d.ts +8 -0
- package/dist/test/helpers.d.ts.map +1 -0
- package/dist/test/helpers.js +12 -0
- package/dist/test/helpers.js.map +1 -0
- package/dist/test/index.d.ts +3 -0
- package/dist/test/index.d.ts.map +1 -0
- package/dist/{runtime → test}/index.js +3 -1
- package/dist/test/index.js.map +1 -0
- package/package.json +31 -16
- package/dist/runtime/examples/capture-the-flag.d.ts +0 -33
- package/dist/runtime/examples/capture-the-flag.d.ts.map +0 -1
- package/dist/runtime/examples/capture-the-flag.js +0 -79
- package/dist/runtime/examples/capture-the-flag.js.map +0 -1
- package/dist/runtime/examples/climb.d.ts +0 -34
- package/dist/runtime/examples/climb.d.ts.map +0 -1
- package/dist/runtime/examples/climb.js +0 -54
- package/dist/runtime/examples/climb.js.map +0 -1
- package/dist/runtime/examples/four-bridges.d.ts +0 -107
- package/dist/runtime/examples/four-bridges.d.ts.map +0 -1
- package/dist/runtime/examples/four-bridges.js +0 -294
- package/dist/runtime/examples/four-bridges.js.map +0 -1
- package/dist/runtime/examples/herding.d.ts +0 -59
- package/dist/runtime/examples/herding.d.ts.map +0 -1
- package/dist/runtime/examples/herding.js +0 -181
- package/dist/runtime/examples/herding.js.map +0 -1
- package/dist/runtime/examples/skywars.d.ts +0 -116
- package/dist/runtime/examples/skywars.d.ts.map +0 -1
- package/dist/runtime/examples/skywars.js +0 -253
- package/dist/runtime/examples/skywars.js.map +0 -1
- package/dist/runtime/harness.d.ts +0 -176
- package/dist/runtime/harness.d.ts.map +0 -1
- package/dist/runtime/harness.js.map +0 -1
- package/dist/runtime/index.d.ts +0 -2
- package/dist/runtime/index.d.ts.map +0 -1
- package/dist/runtime/index.js.map +0 -1
- package/dist/runtime/sdk.d.ts +0 -1038
- package/dist/runtime/sdk.d.ts.map +0 -1
- package/dist/runtime/sdk.js +0 -1271
- package/dist/runtime/sdk.js.map +0 -1
|
@@ -0,0 +1,2025 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// The reducer: one locked definition, one `init()` per run, one `reduce(state,
|
|
3
|
+
// event)` per engine event. Everything a rule sees comes through a context
|
|
4
|
+
// built for the reduction; everything a rule asks for leaves as actions.
|
|
5
|
+
//
|
|
6
|
+
// Lifecycle (see the design notes in the repository's docs):
|
|
7
|
+
// - CONNECTING until every participant is connected and has a position; then
|
|
8
|
+
// the state initializers run, then `setup` (batches) or the game starts.
|
|
9
|
+
// - A phase or end request from a callback is recorded on the reduction and
|
|
10
|
+
// applied after the callback returns: exit once, enter once.
|
|
11
|
+
// - After every event's handlers, one checkpoint: derived scores, the phase's
|
|
12
|
+
// `until`, the phase timeout, the game duration and `endWhen`. A game end
|
|
13
|
+
// drops a staged phase change.
|
|
14
|
+
// - Finalization: exit, final scores, winners and end state, a frozen result,
|
|
15
|
+
// the game-over hook, the judge data, `game_over`.
|
|
16
|
+
// - An author error rolls the reduction back and ends the run as
|
|
17
|
+
// `error_challenge` without another author callback.
|
|
18
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
19
|
+
exports.MAX_TRANSITIONS_PER_EVENT = exports.MAX_FILL_VOLUME = exports.MAX_BATCH_EFFECTS = void 0;
|
|
20
|
+
exports.assertJson = assertJson;
|
|
21
|
+
exports.parseBlockState = parseBlockState;
|
|
22
|
+
exports.blockSpecString = blockSpecString;
|
|
23
|
+
exports.splitBatches = splitBatches;
|
|
24
|
+
exports.createReducer = createReducer;
|
|
25
|
+
const errors_js_1 = require("./errors.js");
|
|
26
|
+
const time_js_1 = require("./time.js");
|
|
27
|
+
const types_js_1 = require("./types.js");
|
|
28
|
+
const world_js_1 = require("./world.js");
|
|
29
|
+
// ---------------------------------------------------------------- limits
|
|
30
|
+
/** Effects per batch the engine accepts. The setup is split under it. */
|
|
31
|
+
exports.MAX_BATCH_EFFECTS = 4096;
|
|
32
|
+
/** Blocks one batch may fill in total. The setup is split under it. */
|
|
33
|
+
exports.MAX_FILL_VOLUME = 32768;
|
|
34
|
+
/** Phase transitions one event may chain (enter requests included). */
|
|
35
|
+
exports.MAX_TRANSITIONS_PER_EVENT = 8;
|
|
36
|
+
const SETUP_BATCH_TIMEOUT_TICKS = 30 * 20;
|
|
37
|
+
// ---------------------------------------------------------------- small helpers
|
|
38
|
+
/** Drops undefined-valued keys: wire objects carry only what JSON would carry. */
|
|
39
|
+
function compact(o) {
|
|
40
|
+
for (const k of Object.keys(o))
|
|
41
|
+
if (o[k] === undefined)
|
|
42
|
+
delete o[k];
|
|
43
|
+
return o;
|
|
44
|
+
}
|
|
45
|
+
function mulberry(rng) {
|
|
46
|
+
const next = (rng + 0x6d2b79f5) >>> 0;
|
|
47
|
+
let t = next;
|
|
48
|
+
t = Math.imul(t ^ (t >>> 15), t | 1);
|
|
49
|
+
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
|
|
50
|
+
return { rng: next, u: ((t ^ (t >>> 14)) >>> 0) / 4294967296 };
|
|
51
|
+
}
|
|
52
|
+
/** JSON-only state: no functions, class instances, cycles, shared objects or non-finite numbers. */
|
|
53
|
+
function assertJson(value, what) {
|
|
54
|
+
const seen = new Map();
|
|
55
|
+
const walk = (v, path) => {
|
|
56
|
+
if (v === null || v === undefined || typeof v === "boolean" || typeof v === "string")
|
|
57
|
+
return;
|
|
58
|
+
if (typeof v === "number") {
|
|
59
|
+
if (!Number.isFinite(v))
|
|
60
|
+
throw (0, errors_js_1.apiError)("state_not_json", `${path} is ${v}; state must be JSON`, { path });
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
if (typeof v !== "object")
|
|
64
|
+
throw (0, errors_js_1.apiError)("state_not_json", `${path} is a ${typeof v}; state must be JSON`, { path });
|
|
65
|
+
if (VIEWS.has(v)) {
|
|
66
|
+
throw (0, errors_js_1.apiError)("state_not_json", `${path} holds a read-only view (a player, an observation, g.params); store its data (\`p.name\`, \`{ ...p.pos }\`)`, { path });
|
|
67
|
+
}
|
|
68
|
+
const prior = seen.get(v);
|
|
69
|
+
if (prior !== undefined)
|
|
70
|
+
throw (0, errors_js_1.apiError)("state_shared", `${path} is shared with ${prior}; return a fresh object`, { path });
|
|
71
|
+
seen.set(v, path);
|
|
72
|
+
const proto = Object.getPrototypeOf(v);
|
|
73
|
+
if (!Array.isArray(v) && proto !== Object.prototype && proto !== null) {
|
|
74
|
+
throw (0, errors_js_1.apiError)("state_not_json", `${path} is a ${v.constructor?.name ?? "class instance"}; use plain objects and arrays`, { path });
|
|
75
|
+
}
|
|
76
|
+
for (const [k, x] of Object.entries(v))
|
|
77
|
+
walk(x, `${path}.${k}`);
|
|
78
|
+
};
|
|
79
|
+
walk(value, what);
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Freezes every object and array under `value`, in place. A frozen parent does not stop the walk: an
|
|
83
|
+
* author's own shallow `Object.freeze` must not shelter its children. `seen` guards against cycles.
|
|
84
|
+
*/
|
|
85
|
+
function deepFreeze(value, seen = new WeakSet()) {
|
|
86
|
+
if (value === null || typeof value !== "object" || seen.has(value))
|
|
87
|
+
return;
|
|
88
|
+
seen.add(value);
|
|
89
|
+
Object.freeze(value);
|
|
90
|
+
for (const x of Object.values(value))
|
|
91
|
+
deepFreeze(x, seen);
|
|
92
|
+
}
|
|
93
|
+
const posKey = (at) => `${Math.floor(at.x)},${Math.floor(at.y)},${Math.floor(at.z)}`;
|
|
94
|
+
/** `minecraft:oak_stairs[facing=east,half=bottom]` -> `{ id, props }`. */
|
|
95
|
+
function parseBlockState(state) {
|
|
96
|
+
const i = state.indexOf("[");
|
|
97
|
+
if (i < 0)
|
|
98
|
+
return { id: state, props: {} };
|
|
99
|
+
const props = {};
|
|
100
|
+
for (const pair of state.slice(i + 1, -1).split(",")) {
|
|
101
|
+
const eq = pair.indexOf("=");
|
|
102
|
+
if (eq > 0)
|
|
103
|
+
props[pair.slice(0, eq)] = pair.slice(eq + 1);
|
|
104
|
+
}
|
|
105
|
+
return { id: state.slice(0, i), props };
|
|
106
|
+
}
|
|
107
|
+
/** `{ id, props }` -> `minecraft:oak_stairs[facing=east]`. */
|
|
108
|
+
function blockSpecString(spec) {
|
|
109
|
+
if (typeof spec === "string")
|
|
110
|
+
return spec;
|
|
111
|
+
const entries = Object.entries(spec.props ?? {});
|
|
112
|
+
if (entries.length === 0)
|
|
113
|
+
return spec.id;
|
|
114
|
+
return `${spec.id}[${entries.map(([k, v]) => `${k}=${v}`).join(",")}]`;
|
|
115
|
+
}
|
|
116
|
+
/** An author's block predicate as a predicate over the mirror's state strings. */
|
|
117
|
+
function blockPredicate(match) {
|
|
118
|
+
if (typeof match === "function")
|
|
119
|
+
return (state) => match(parseBlockState(state));
|
|
120
|
+
if (typeof match === "string") {
|
|
121
|
+
if (match.includes("["))
|
|
122
|
+
return (state) => state === match;
|
|
123
|
+
return (state) => (0, world_js_1.blockIdOf)(state) === match;
|
|
124
|
+
}
|
|
125
|
+
const want = Object.entries(match.props ?? {});
|
|
126
|
+
return (state) => {
|
|
127
|
+
if ((0, world_js_1.blockIdOf)(state) !== match.id)
|
|
128
|
+
return false;
|
|
129
|
+
if (want.length === 0)
|
|
130
|
+
return true;
|
|
131
|
+
const have = parseBlockState(state).props;
|
|
132
|
+
return want.every(([k, v]) => have[k] === v);
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
/** The SNBT of an item request's metadata, merged with its raw `snbt`. Empty string when there is none. */
|
|
136
|
+
function itemNbt(req) {
|
|
137
|
+
if (typeof req === "string")
|
|
138
|
+
return undefined;
|
|
139
|
+
const parts = [];
|
|
140
|
+
if (req.enchantments && Object.keys(req.enchantments).length > 0) {
|
|
141
|
+
const list = Object.entries(req.enchantments).map(([id, lvl]) => `{id:"${id}",lvl:${lvl}s}`);
|
|
142
|
+
parts.push(`Enchantments:[${list.join(",")}]`);
|
|
143
|
+
}
|
|
144
|
+
const display = [];
|
|
145
|
+
// A JSON text in a single-quoted SNBT string: `\` and `'` escaped.
|
|
146
|
+
const quoted = (text) => `'${JSON.stringify({ text }).replace(/\\/g, "\\\\").replace(/'/g, "\\'")}'`;
|
|
147
|
+
if (req.name !== undefined)
|
|
148
|
+
display.push(`Name:${quoted(req.name)}`);
|
|
149
|
+
if (req.lore && req.lore.length > 0)
|
|
150
|
+
display.push(`Lore:[${req.lore.map(quoted).join(",")}]`);
|
|
151
|
+
if (display.length > 0)
|
|
152
|
+
parts.push(`display:{${display.join(",")}}`);
|
|
153
|
+
if (req.damage !== undefined)
|
|
154
|
+
parts.push(`Damage:${req.damage}`);
|
|
155
|
+
if (parts.length === 0)
|
|
156
|
+
return req.snbt;
|
|
157
|
+
const own = `{${parts.join(",")}}`;
|
|
158
|
+
if (!req.snbt)
|
|
159
|
+
return own;
|
|
160
|
+
// Two SNBT compounds: splice the raw one's body into ours.
|
|
161
|
+
const body = req.snbt.trim().replace(/^\{/, "").replace(/\}$/, "");
|
|
162
|
+
return body ? `{${parts.join(",")},${body}}` : own;
|
|
163
|
+
}
|
|
164
|
+
function itemMatches(stack, match) {
|
|
165
|
+
if (typeof match === "string")
|
|
166
|
+
return stack.id === match;
|
|
167
|
+
if (stack.id !== match.id)
|
|
168
|
+
return false;
|
|
169
|
+
if (match.name !== undefined && stack.name !== match.name)
|
|
170
|
+
return false;
|
|
171
|
+
if (match.damage !== undefined && stack.damage !== match.damage)
|
|
172
|
+
return false;
|
|
173
|
+
if (match.lore !== undefined && JSON.stringify(stack.lore ?? []) !== JSON.stringify(match.lore))
|
|
174
|
+
return false;
|
|
175
|
+
if (match.enchantments !== undefined) {
|
|
176
|
+
for (const [id, lvl] of Object.entries(match.enchantments))
|
|
177
|
+
if (stack.enchantments?.[id] !== lvl)
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
180
|
+
return true;
|
|
181
|
+
}
|
|
182
|
+
function stackOf(sl) {
|
|
183
|
+
if (!sl.item || !sl.count)
|
|
184
|
+
return null;
|
|
185
|
+
return compact({
|
|
186
|
+
id: sl.item,
|
|
187
|
+
count: sl.count,
|
|
188
|
+
name: sl.name,
|
|
189
|
+
lore: sl.lore ? [...sl.lore] : undefined,
|
|
190
|
+
enchantments: sl.enchantments ? { ...sl.enchantments } : undefined,
|
|
191
|
+
damage: sl.damage,
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
function countsOf(slots) {
|
|
195
|
+
const counts = {};
|
|
196
|
+
for (const st of Object.values(slots))
|
|
197
|
+
counts[st.id] = (counts[st.id] ?? 0) + st.count;
|
|
198
|
+
return counts;
|
|
199
|
+
}
|
|
200
|
+
function fillVolume(e) {
|
|
201
|
+
if (e.kind !== "fill")
|
|
202
|
+
return 0;
|
|
203
|
+
const { lo, hi } = (0, world_js_1.bounds)({ from: e.from, to: e.to });
|
|
204
|
+
return (hi.x - lo.x + 1) * (hi.y - lo.y + 1) * (hi.z - lo.z + 1);
|
|
205
|
+
}
|
|
206
|
+
/** Splits the setup effects into batches under the engine's limits, in order. */
|
|
207
|
+
function splitBatches(effects) {
|
|
208
|
+
const out = [];
|
|
209
|
+
let current = [];
|
|
210
|
+
let volume = 0;
|
|
211
|
+
for (const e of effects) {
|
|
212
|
+
const v = fillVolume(e);
|
|
213
|
+
if (v > exports.MAX_FILL_VOLUME) {
|
|
214
|
+
throw (0, errors_js_1.apiError)("fill_too_large", `a fill of ${v} blocks exceeds ${exports.MAX_FILL_VOLUME}; split it`, { path: "setup" });
|
|
215
|
+
}
|
|
216
|
+
if (current.length >= exports.MAX_BATCH_EFFECTS || volume + v > exports.MAX_FILL_VOLUME) {
|
|
217
|
+
out.push(current);
|
|
218
|
+
current = [];
|
|
219
|
+
volume = 0;
|
|
220
|
+
}
|
|
221
|
+
current.push(e);
|
|
222
|
+
volume += v;
|
|
223
|
+
}
|
|
224
|
+
if (current.length > 0 || out.length === 0)
|
|
225
|
+
out.push(current);
|
|
226
|
+
return out;
|
|
227
|
+
}
|
|
228
|
+
const missingCapabilities = (have) => ["move", "damage", "death", "mirror"].filter((c) => !have.includes(c));
|
|
229
|
+
/** The non-enumerable derived fields of a player object: `isActive`. Survives a JSON round trip by re-attachment. */
|
|
230
|
+
function attachDerived(p) {
|
|
231
|
+
Object.defineProperty(p, "isActive", {
|
|
232
|
+
get() {
|
|
233
|
+
return !this.eliminated;
|
|
234
|
+
},
|
|
235
|
+
enumerable: false,
|
|
236
|
+
configurable: true,
|
|
237
|
+
});
|
|
238
|
+
return p;
|
|
239
|
+
}
|
|
240
|
+
// ---------------------------------------------------------------- views
|
|
241
|
+
/** Every view handed to an author callback: the state must never hold one. */
|
|
242
|
+
const VIEWS = new WeakSet();
|
|
243
|
+
function refuseWrite(what) {
|
|
244
|
+
throw (0, errors_js_1.apiError)("read_only", `${what} is read-only in this callback`, { path: what });
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* The traps of a view over `target`. The Proxy's own target is an empty shadow (an array for an
|
|
248
|
+
* array), never the live object: the invariants of a frozen property then never force a raw child
|
|
249
|
+
* out, so a parent the author froze themself still hands out views. `read` maps a nested value.
|
|
250
|
+
*/
|
|
251
|
+
function viewTraps(target, what, read) {
|
|
252
|
+
return {
|
|
253
|
+
get: (_t, key, receiver) => read(key, Reflect.get(target, key, receiver), receiver),
|
|
254
|
+
has: (_t, key) => key in target,
|
|
255
|
+
ownKeys: () => Reflect.ownKeys(target),
|
|
256
|
+
getOwnPropertyDescriptor(shadow, key) {
|
|
257
|
+
const d = Reflect.getOwnPropertyDescriptor(target, key);
|
|
258
|
+
if (d === undefined)
|
|
259
|
+
return undefined;
|
|
260
|
+
// The shadow's own non-configurable property (an array's `length`, always writable there) keeps
|
|
261
|
+
// its shape; every other property is reported configurable, so a frozen live object binds nothing.
|
|
262
|
+
const own = Reflect.getOwnPropertyDescriptor(shadow, key);
|
|
263
|
+
if (own !== undefined && own.configurable === false) {
|
|
264
|
+
return { ...d, configurable: false, writable: own.writable ?? true, value: read(key, d.value, undefined) };
|
|
265
|
+
}
|
|
266
|
+
if ("value" in d)
|
|
267
|
+
return { ...d, configurable: true, value: read(key, d.value, undefined) };
|
|
268
|
+
return { ...d, configurable: true };
|
|
269
|
+
},
|
|
270
|
+
getPrototypeOf: () => Object.getPrototypeOf(target),
|
|
271
|
+
set: (_t, key) => refuseWrite(`${what}.${String(key)}`),
|
|
272
|
+
deleteProperty: (_t, key) => refuseWrite(`${what}.${String(key)}`),
|
|
273
|
+
defineProperty: (_t, key) => refuseWrite(`${what}.${String(key)}`),
|
|
274
|
+
preventExtensions: () => refuseWrite(what),
|
|
275
|
+
setPrototypeOf: () => refuseWrite(what),
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
const shadowOf = (target) => (Array.isArray(target) ? [] : {});
|
|
279
|
+
/**
|
|
280
|
+
* A deep read-only view of an object: every nested object is a view too, and a write throws
|
|
281
|
+
* `read_only`. One view per target and reduction, so identities hold inside a callback. A view kept
|
|
282
|
+
* after the reduction reads the published state, which is frozen.
|
|
283
|
+
*/
|
|
284
|
+
function readView(r, target, what) {
|
|
285
|
+
const cached = r.views.read.get(target);
|
|
286
|
+
if (cached)
|
|
287
|
+
return cached;
|
|
288
|
+
const view = new Proxy(shadowOf(target), viewTraps(target, what, (_key, v) => (v !== null && typeof v === "object" ? readView(r, v, what) : v)));
|
|
289
|
+
VIEWS.add(view);
|
|
290
|
+
r.views.read.set(target, view);
|
|
291
|
+
return view;
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* A participant as a callback sees it. The observations (`pos`, `slots`, `score`, ...) are read-only
|
|
295
|
+
* in every mode; `state` is writable in a handler and read-only in a pure callback. While a derived
|
|
296
|
+
* score pass runs, `score` reads the vector under evaluation (`r.scoreOverlay`).
|
|
297
|
+
*/
|
|
298
|
+
function playerView(r, p, writable) {
|
|
299
|
+
const cache = writable ? r.views.player : r.views.read;
|
|
300
|
+
const cached = cache.get(p);
|
|
301
|
+
if (cached)
|
|
302
|
+
return cached;
|
|
303
|
+
const view = new Proxy(shadowOf(p), viewTraps(p, "p", (key, v) => {
|
|
304
|
+
if (key === "score" && r.scoreOverlay !== null)
|
|
305
|
+
return r.scoreOverlay[p.name] ?? p.score;
|
|
306
|
+
if (v === null || typeof v !== "object")
|
|
307
|
+
return v;
|
|
308
|
+
if (key === "state" && writable)
|
|
309
|
+
return v;
|
|
310
|
+
return readView(r, v, `p.${String(key)}`);
|
|
311
|
+
}));
|
|
312
|
+
VIEWS.add(view);
|
|
313
|
+
cache.set(p, view);
|
|
314
|
+
return view;
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* A plain deep copy of a value an author handed back: views become plain objects, so the copy can
|
|
318
|
+
* be validated as JSON, stored on an action or sent across the worker boundary.
|
|
319
|
+
*/
|
|
320
|
+
function snapshot(value, seen = new WeakMap()) {
|
|
321
|
+
if (value === null || typeof value !== "object")
|
|
322
|
+
return value;
|
|
323
|
+
const prior = seen.get(value);
|
|
324
|
+
if (prior !== undefined)
|
|
325
|
+
return prior;
|
|
326
|
+
if (Array.isArray(value)) {
|
|
327
|
+
const out = [];
|
|
328
|
+
seen.set(value, out);
|
|
329
|
+
for (const x of value)
|
|
330
|
+
out.push(snapshot(x, seen));
|
|
331
|
+
return out;
|
|
332
|
+
}
|
|
333
|
+
const proto = Object.getPrototypeOf(value);
|
|
334
|
+
if (proto !== Object.prototype && proto !== null)
|
|
335
|
+
return value; // a class instance: assertJson names it
|
|
336
|
+
const out = {};
|
|
337
|
+
seen.set(value, out);
|
|
338
|
+
for (const [k, v] of Object.entries(value))
|
|
339
|
+
out[k] = snapshot(v, seen);
|
|
340
|
+
return out;
|
|
341
|
+
}
|
|
342
|
+
/** An event payload with every player replaced by its handler view. Frozen observations pass through. */
|
|
343
|
+
function viewPayload(r, value) {
|
|
344
|
+
if (value === null || typeof value !== "object" || Object.isFrozen(value))
|
|
345
|
+
return value;
|
|
346
|
+
const asPlayer = value;
|
|
347
|
+
if (typeof asPlayer.name === "string" && r.s.players[asPlayer.name] === value)
|
|
348
|
+
return playerView(r, asPlayer, true);
|
|
349
|
+
if (Array.isArray(value))
|
|
350
|
+
return value.map((x) => viewPayload(r, x));
|
|
351
|
+
const out = {};
|
|
352
|
+
for (const [k, v] of Object.entries(value))
|
|
353
|
+
out[k] = viewPayload(r, v);
|
|
354
|
+
return out;
|
|
355
|
+
}
|
|
356
|
+
function createReducer(def) {
|
|
357
|
+
const config = def.config;
|
|
358
|
+
const roleNames = Object.keys(config.roles);
|
|
359
|
+
const declaredZones = Object.keys(def.zones);
|
|
360
|
+
const derivedScores = def.outcome.scores !== undefined;
|
|
361
|
+
const paramDefaults = () => Object.fromEntries(Object.entries(config.params ?? {}).map(([k, p]) => [k, p.default]));
|
|
362
|
+
const endStateNames = () => Array.isArray(config.endStates) ? [...config.endStates] : Object.keys(config.endStates ?? {});
|
|
363
|
+
const voteIds = Object.keys(def.votes);
|
|
364
|
+
const init = () => ({
|
|
365
|
+
seed: 0,
|
|
366
|
+
rng: 0,
|
|
367
|
+
params: paramDefaults(),
|
|
368
|
+
capabilities: [],
|
|
369
|
+
locations: {},
|
|
370
|
+
markers: [],
|
|
371
|
+
status: "CONNECTING",
|
|
372
|
+
initialized: false,
|
|
373
|
+
tick: 0,
|
|
374
|
+
dt: 0,
|
|
375
|
+
startTick: 0,
|
|
376
|
+
setup: null,
|
|
377
|
+
phase: null,
|
|
378
|
+
phaseStartTick: 0,
|
|
379
|
+
phaseEntries: 0,
|
|
380
|
+
phaseDuration: null,
|
|
381
|
+
fired: {},
|
|
382
|
+
every: {},
|
|
383
|
+
timers: {},
|
|
384
|
+
nextGen: 0,
|
|
385
|
+
players: {},
|
|
386
|
+
game: {},
|
|
387
|
+
votes: Object.fromEntries(voteIds.map((id) => [id, { open: false, round: 0, cast: {} }])),
|
|
388
|
+
walks: {},
|
|
389
|
+
batches: {},
|
|
390
|
+
containers: {},
|
|
391
|
+
world: (0, world_js_1.emptyWorld)(),
|
|
392
|
+
nextId: 0,
|
|
393
|
+
winners: [],
|
|
394
|
+
endState: "",
|
|
395
|
+
reason: null,
|
|
396
|
+
frozen: null,
|
|
397
|
+
});
|
|
398
|
+
// JSON semantics, no shared references, except the world mirror: it is never
|
|
399
|
+
// changed in place, so the clone keeps the reference and an update copies what it touches.
|
|
400
|
+
const clone = (input) => {
|
|
401
|
+
const { world, ...rest } = input;
|
|
402
|
+
const s = { ...JSON.parse(JSON.stringify(rest)), world };
|
|
403
|
+
for (const p of Object.values(s.players))
|
|
404
|
+
attachDerived(p);
|
|
405
|
+
return s;
|
|
406
|
+
};
|
|
407
|
+
// ------------------------------------------------------------ callbacks
|
|
408
|
+
/** A Promise-like return is an author error: callbacks are synchronous. */
|
|
409
|
+
function assertSync(out, what) {
|
|
410
|
+
if (out !== null && typeof out === "object" && typeof out.then === "function") {
|
|
411
|
+
out.catch(() => undefined);
|
|
412
|
+
throw (0, errors_js_1.apiError)("async_callback", `${what} returned a Promise; callbacks are synchronous`);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
/** Runs an author callback under a frame; a Promise-like return is an author error. */
|
|
416
|
+
function run(r, kind, fn, ...args) {
|
|
417
|
+
if (typeof fn !== "function")
|
|
418
|
+
return undefined;
|
|
419
|
+
const prev = r.frame;
|
|
420
|
+
r.frame = { kind };
|
|
421
|
+
try {
|
|
422
|
+
const out = fn(...args);
|
|
423
|
+
assertSync(out, "a challenge callback");
|
|
424
|
+
return out;
|
|
425
|
+
}
|
|
426
|
+
finally {
|
|
427
|
+
r.frame = prev;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
const phaseSpec = (s) => (s.phase === null ? undefined : def.phaseSpecs[s.phase]);
|
|
431
|
+
// ------------------------------------------------------------ contexts
|
|
432
|
+
/** A seeded generator over the reduction's state. `close()` shuts this instance (the init one, after the initializers). */
|
|
433
|
+
function makeRandom(r, whenClosed) {
|
|
434
|
+
const s = r.s;
|
|
435
|
+
let closed = false;
|
|
436
|
+
const draw = () => {
|
|
437
|
+
if (r.closed || closed)
|
|
438
|
+
throw (0, errors_js_1.apiError)("ctx_closed", `${whenClosed}: this random source is closed`);
|
|
439
|
+
const x = mulberry(s.rng);
|
|
440
|
+
s.rng = x.rng;
|
|
441
|
+
return x.u;
|
|
442
|
+
};
|
|
443
|
+
const random = {
|
|
444
|
+
int(lo, hi) {
|
|
445
|
+
if (!Number.isFinite(lo) || !Number.isFinite(hi) || hi < lo)
|
|
446
|
+
throw (0, errors_js_1.apiError)("bad_range", `random.int(${lo}, ${hi}): lo must be <= hi`);
|
|
447
|
+
return lo + Math.floor(draw() * (hi - lo + 1));
|
|
448
|
+
},
|
|
449
|
+
pick(items) {
|
|
450
|
+
if (items.length === 0)
|
|
451
|
+
return undefined;
|
|
452
|
+
return items[Math.floor(draw() * items.length)];
|
|
453
|
+
},
|
|
454
|
+
shuffle(items) {
|
|
455
|
+
const out = [...items];
|
|
456
|
+
for (let i = out.length - 1; i > 0; i--) {
|
|
457
|
+
const j = Math.floor(draw() * (i + 1));
|
|
458
|
+
[out[i], out[j]] = [out[j], out[i]];
|
|
459
|
+
}
|
|
460
|
+
return out;
|
|
461
|
+
},
|
|
462
|
+
weighted(items) {
|
|
463
|
+
const total = items.reduce((n, it) => n + Math.max(0, it.weight), 0);
|
|
464
|
+
if (total <= 0)
|
|
465
|
+
return undefined;
|
|
466
|
+
let x = draw() * total;
|
|
467
|
+
for (const it of items) {
|
|
468
|
+
x -= Math.max(0, it.weight);
|
|
469
|
+
if (x < 0)
|
|
470
|
+
return it.value;
|
|
471
|
+
}
|
|
472
|
+
return items[items.length - 1]?.value;
|
|
473
|
+
},
|
|
474
|
+
};
|
|
475
|
+
return {
|
|
476
|
+
random,
|
|
477
|
+
close: () => {
|
|
478
|
+
closed = true;
|
|
479
|
+
},
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
function makeCtx(r, mode) {
|
|
483
|
+
const s = r.s;
|
|
484
|
+
const writable = mode === "setup" || mode === "full";
|
|
485
|
+
const guard = (op, allowed) => {
|
|
486
|
+
if (r.closed)
|
|
487
|
+
throw (0, errors_js_1.apiError)("ctx_closed", `${op}: the reduction ended; a context does not survive its callback`);
|
|
488
|
+
if (!allowed.includes(mode)) {
|
|
489
|
+
throw (0, errors_js_1.apiError)("read_only", `${op} is not available in a ${mode} callback`, { path: op });
|
|
490
|
+
}
|
|
491
|
+
};
|
|
492
|
+
const write = (op) => guard(op, ["setup", "full"]);
|
|
493
|
+
const notify = (op) => guard(op, ["notify", "setup", "full"]);
|
|
494
|
+
const flow = (op) => guard(op, ["full"]);
|
|
495
|
+
const push = (raw) => {
|
|
496
|
+
// A plain copy: an author's argument may be a view (`p.pos`, `g.params.box`).
|
|
497
|
+
const effect = compact(snapshot(raw));
|
|
498
|
+
if (r.batch)
|
|
499
|
+
r.batch.push(effect);
|
|
500
|
+
else
|
|
501
|
+
r.actions.push({ type: "effect", effect });
|
|
502
|
+
};
|
|
503
|
+
const targets = (to) => to === "all" ? [undefined] : (Array.isArray(to) ? to : [to]).map((p) => p.name);
|
|
504
|
+
const players = () => Object.values(s.players)
|
|
505
|
+
.sort((a, b) => a.seatNumber - b.seatNumber)
|
|
506
|
+
.map((p) => playerView(r, p, writable));
|
|
507
|
+
const requestGuard = (op) => {
|
|
508
|
+
flow(op);
|
|
509
|
+
if (r.frame.kind === "exit")
|
|
510
|
+
throw (0, errors_js_1.apiError)("request_in_exit", `${op}: no phase or end request inside exit`);
|
|
511
|
+
if (r.frame.kind !== "handler")
|
|
512
|
+
throw (0, errors_js_1.apiError)("read_only", `${op} is not available in a ${r.frame.kind} callback`);
|
|
513
|
+
if (s.status !== "PLAYING")
|
|
514
|
+
throw (0, errors_js_1.apiError)("lifecycle", `${op}: the game is not running (status ${s.status})`);
|
|
515
|
+
};
|
|
516
|
+
const voteState = (voteId) => {
|
|
517
|
+
const v = s.votes[voteId];
|
|
518
|
+
if (!v)
|
|
519
|
+
throw (0, errors_js_1.notDeclared)("votes", voteId, voteIds);
|
|
520
|
+
return v;
|
|
521
|
+
};
|
|
522
|
+
const { random } = makeRandom(r, "g.random");
|
|
523
|
+
const g = {
|
|
524
|
+
get state() {
|
|
525
|
+
return writable ? s.game : readView(r, s.game, "g.state");
|
|
526
|
+
},
|
|
527
|
+
get params() {
|
|
528
|
+
return readView(r, s.params, "g.params");
|
|
529
|
+
},
|
|
530
|
+
get players() {
|
|
531
|
+
return players();
|
|
532
|
+
},
|
|
533
|
+
get activePlayers() {
|
|
534
|
+
return players().filter((p) => !p.eliminated);
|
|
535
|
+
},
|
|
536
|
+
player(name) {
|
|
537
|
+
const p = s.players[name];
|
|
538
|
+
return p ? playerView(r, p, writable) : undefined;
|
|
539
|
+
},
|
|
540
|
+
playersWithRole: (role) => players().filter((p) => p.role === role),
|
|
541
|
+
playerWithRole(role) {
|
|
542
|
+
if (!roleNames.includes(role))
|
|
543
|
+
throw (0, errors_js_1.notDeclared)("roles", role, roleNames);
|
|
544
|
+
const found = players().filter((p) => p.role === role);
|
|
545
|
+
if (found.length !== 1) {
|
|
546
|
+
throw (0, errors_js_1.apiError)("role_count", `playerWithRole("${role}"): ${found.length} participants have that role, not 1`, {
|
|
547
|
+
path: "roles",
|
|
548
|
+
value: role,
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
return found[0];
|
|
552
|
+
},
|
|
553
|
+
get tick() {
|
|
554
|
+
return s.tick;
|
|
555
|
+
},
|
|
556
|
+
get elapsed() {
|
|
557
|
+
return (s.frozen ? s.frozen.elapsed : s.tick - s.startTick);
|
|
558
|
+
},
|
|
559
|
+
get status() {
|
|
560
|
+
return s.status;
|
|
561
|
+
},
|
|
562
|
+
get phase() {
|
|
563
|
+
return s.phase;
|
|
564
|
+
},
|
|
565
|
+
get phaseEntry() {
|
|
566
|
+
return s.phase === null ? 0 : s.phaseEntries;
|
|
567
|
+
},
|
|
568
|
+
get phaseElapsed() {
|
|
569
|
+
return (s.frozen ? s.frozen.phaseElapsed : s.phase === null ? 0 : s.tick - s.phaseStartTick);
|
|
570
|
+
},
|
|
571
|
+
get phaseRemaining() {
|
|
572
|
+
if (s.phaseDuration === null)
|
|
573
|
+
return null;
|
|
574
|
+
return Math.max(0, s.phaseDuration - (s.tick - s.phaseStartTick));
|
|
575
|
+
},
|
|
576
|
+
location(name) {
|
|
577
|
+
const at = s.locations[name];
|
|
578
|
+
if (!at)
|
|
579
|
+
throw (0, errors_js_1.notDeclared)("challengeConfig.locations", name, Object.keys(s.locations));
|
|
580
|
+
return { ...at };
|
|
581
|
+
},
|
|
582
|
+
markers: (tag) => s.markers
|
|
583
|
+
.filter((m) => tag === undefined || m.tags.includes(tag))
|
|
584
|
+
.map((m) => ({ ...m, at: { ...m.at }, tags: [...m.tags] })),
|
|
585
|
+
inZone(p, zone) {
|
|
586
|
+
if (typeof zone === "string") {
|
|
587
|
+
if (!declaredZones.includes(zone))
|
|
588
|
+
throw (0, errors_js_1.notDeclared)("zones", zone, declaredZones);
|
|
589
|
+
return p.zones.includes(zone);
|
|
590
|
+
}
|
|
591
|
+
return p.pos !== null && (0, world_js_1.inBox)(p.pos, zone);
|
|
592
|
+
},
|
|
593
|
+
countItems(p, match) {
|
|
594
|
+
let n = 0;
|
|
595
|
+
for (const st of Object.values(p.slots))
|
|
596
|
+
if (itemMatches(st, match))
|
|
597
|
+
n += st.count;
|
|
598
|
+
return n;
|
|
599
|
+
},
|
|
600
|
+
blockAt: (at) => (0, world_js_1.blockAt)(s.world, at),
|
|
601
|
+
blockStateAt(at) {
|
|
602
|
+
const state = (0, world_js_1.blockStateAt)(s.world, at);
|
|
603
|
+
return state === undefined ? undefined : parseBlockState(state);
|
|
604
|
+
},
|
|
605
|
+
blockMatches(at, match) {
|
|
606
|
+
const state = (0, world_js_1.blockStateAt)(s.world, at);
|
|
607
|
+
if (state === undefined) {
|
|
608
|
+
throw (0, errors_js_1.unavailableError)("block_unknown", `blockMatches: the chunk of ${posKey(at)} is not loaded`);
|
|
609
|
+
}
|
|
610
|
+
return blockPredicate(match)(state);
|
|
611
|
+
},
|
|
612
|
+
blockCount(box, match, opts) {
|
|
613
|
+
if (!opts?.partial && !(0, world_js_1.regionLoaded)(s.world, box)) {
|
|
614
|
+
throw (0, errors_js_1.unavailableError)("block_unknown", "blockCount: a chunk of the box is not loaded (pass { partial: true } to count what is)");
|
|
615
|
+
}
|
|
616
|
+
return (0, world_js_1.blockCount)(s.world, box, blockPredicate(match));
|
|
617
|
+
},
|
|
618
|
+
findBlocks(box, match, opts) {
|
|
619
|
+
if (!opts?.partial && !(0, world_js_1.regionLoaded)(s.world, box)) {
|
|
620
|
+
throw (0, errors_js_1.unavailableError)("block_unknown", "findBlocks: a chunk of the box is not loaded (pass { partial: true } to search what is)");
|
|
621
|
+
}
|
|
622
|
+
return (0, world_js_1.findBlocks)(s.world, box, blockPredicate(match), opts?.limit);
|
|
623
|
+
},
|
|
624
|
+
regionLoaded: (box) => (0, world_js_1.regionLoaded)(s.world, box),
|
|
625
|
+
get worldTime() {
|
|
626
|
+
return s.world.time;
|
|
627
|
+
},
|
|
628
|
+
entities: (filter) => (0, world_js_1.entitiesIn)(s.world, filter).map(world_js_1.freezeObservation),
|
|
629
|
+
entity(id) {
|
|
630
|
+
const e = s.world.entities[id];
|
|
631
|
+
return e ? (0, world_js_1.freezeObservation)(e) : undefined;
|
|
632
|
+
},
|
|
633
|
+
containerContents(at) {
|
|
634
|
+
const c = s.containers[posKey(at)];
|
|
635
|
+
return c ? countsOf(c.slots) : undefined;
|
|
636
|
+
},
|
|
637
|
+
votes: (voteId) => ({ ...voteState(voteId).cast }),
|
|
638
|
+
vote: (p, voteId) => voteState(voteId).cast[p.name],
|
|
639
|
+
voteOpen: (voteId) => voteState(voteId).open,
|
|
640
|
+
tally(voteId) {
|
|
641
|
+
const v = voteState(voteId);
|
|
642
|
+
const counts = {};
|
|
643
|
+
for (const option of Object.values(v.cast))
|
|
644
|
+
counts[option] = (counts[option] ?? 0) + 1;
|
|
645
|
+
const best = Math.max(0, ...Object.values(counts));
|
|
646
|
+
const leaders = best === 0 ? [] : Object.keys(counts).filter((o) => counts[o] === best);
|
|
647
|
+
const abstaining = players().filter((p) => !p.eliminated && v.cast[p.name] === undefined);
|
|
648
|
+
return { counts, leaders, abstaining };
|
|
649
|
+
},
|
|
650
|
+
// ---- notifications
|
|
651
|
+
say(message) {
|
|
652
|
+
notify("say");
|
|
653
|
+
push({ kind: "announce", message });
|
|
654
|
+
},
|
|
655
|
+
tell(to, message) {
|
|
656
|
+
notify("tell");
|
|
657
|
+
for (const name of targets(to))
|
|
658
|
+
push({ kind: "announce", message, to: name });
|
|
659
|
+
},
|
|
660
|
+
title(to, options) {
|
|
661
|
+
notify("title");
|
|
662
|
+
const times = options.fadeIn !== undefined || options.stay !== undefined || options.fadeOut !== undefined
|
|
663
|
+
? {
|
|
664
|
+
fadeIn: options.fadeIn === undefined ? 10 : (0, time_js_1.toTicks)(options.fadeIn),
|
|
665
|
+
stay: options.stay === undefined ? 70 : (0, time_js_1.toTicks)(options.stay),
|
|
666
|
+
fadeOut: options.fadeOut === undefined ? 20 : (0, time_js_1.toTicks)(options.fadeOut),
|
|
667
|
+
}
|
|
668
|
+
: undefined;
|
|
669
|
+
for (const name of targets(to))
|
|
670
|
+
push({ kind: "title", player: name, title: options.title, subtitle: options.subtitle, times });
|
|
671
|
+
},
|
|
672
|
+
log(message, opts) {
|
|
673
|
+
notify("log");
|
|
674
|
+
r.actions.push(compact({ type: "log", player: opts?.player?.name, message, data: snapshot(opts?.data) }));
|
|
675
|
+
},
|
|
676
|
+
setMetric(name, value, opts) {
|
|
677
|
+
notify("setMetric");
|
|
678
|
+
if (!Number.isFinite(value))
|
|
679
|
+
throw (0, errors_js_1.apiError)("bad_metric", `setMetric("${name}"): ${value} is not finite`);
|
|
680
|
+
r.actions.push(compact({ type: "metric", name, value, player: opts?.player?.name }));
|
|
681
|
+
},
|
|
682
|
+
achievement(player, id) {
|
|
683
|
+
notify("achievement");
|
|
684
|
+
r.actions.push({ type: "log", player: player.name, message: `ACHIEVEMENT:${id}`, data: { achievement: id } });
|
|
685
|
+
},
|
|
686
|
+
// ---- state writes
|
|
687
|
+
setScore(p, score) {
|
|
688
|
+
write("setScore");
|
|
689
|
+
if (derivedScores) {
|
|
690
|
+
throw (0, errors_js_1.apiError)("score_derived", "setScore: outcome.scores computes the scores; a manual write is refused");
|
|
691
|
+
}
|
|
692
|
+
if (!Number.isFinite(score))
|
|
693
|
+
throw (0, errors_js_1.apiError)("bad_score", `setScore: ${score} is not finite`);
|
|
694
|
+
(s.players[p.name] ?? p).score = score;
|
|
695
|
+
},
|
|
696
|
+
addScore(p, delta) {
|
|
697
|
+
write("addScore");
|
|
698
|
+
if (derivedScores) {
|
|
699
|
+
throw (0, errors_js_1.apiError)("score_derived", "addScore: outcome.scores computes the scores; a manual write is refused");
|
|
700
|
+
}
|
|
701
|
+
if (!Number.isFinite(delta))
|
|
702
|
+
throw (0, errors_js_1.apiError)("bad_score", `addScore: ${delta} is not finite`);
|
|
703
|
+
const target = s.players[p.name] ?? p;
|
|
704
|
+
target.score += delta;
|
|
705
|
+
},
|
|
706
|
+
eliminate(p) {
|
|
707
|
+
write("eliminate");
|
|
708
|
+
const target = s.players[p.name] ?? p;
|
|
709
|
+
if (target.eliminated)
|
|
710
|
+
return;
|
|
711
|
+
target.eliminated = true;
|
|
712
|
+
push({ kind: "eliminate", player: p.name });
|
|
713
|
+
},
|
|
714
|
+
get random() {
|
|
715
|
+
write("random");
|
|
716
|
+
return random;
|
|
717
|
+
},
|
|
718
|
+
// ---- effects
|
|
719
|
+
give(p, item, count) {
|
|
720
|
+
write("give");
|
|
721
|
+
const id = typeof item === "string" ? item : item.id;
|
|
722
|
+
const n = count ?? (typeof item === "string" ? 1 : (item.count ?? 1));
|
|
723
|
+
push({ kind: "give", player: p.name, item: id, count: n, nbt: itemNbt(item) });
|
|
724
|
+
},
|
|
725
|
+
clear(p, item, count) {
|
|
726
|
+
write("clear");
|
|
727
|
+
push({ kind: "clear", player: p.name, item, count });
|
|
728
|
+
},
|
|
729
|
+
effect(p, options) {
|
|
730
|
+
write("effect");
|
|
731
|
+
push({
|
|
732
|
+
kind: "effect",
|
|
733
|
+
player: p.name,
|
|
734
|
+
effect: options.id,
|
|
735
|
+
seconds: Math.max(1, Math.round((0, time_js_1.toTicks)(options.duration) / 20)),
|
|
736
|
+
amplifier: options.amplifier ?? 0,
|
|
737
|
+
});
|
|
738
|
+
},
|
|
739
|
+
teleport(p, to) {
|
|
740
|
+
write("teleport");
|
|
741
|
+
push({ kind: "teleport", player: p.name, to: { ...to } });
|
|
742
|
+
},
|
|
743
|
+
kill(p) {
|
|
744
|
+
write("kill");
|
|
745
|
+
push({ kind: "kill", player: p.name });
|
|
746
|
+
},
|
|
747
|
+
gamemode(p, mode) {
|
|
748
|
+
write("gamemode");
|
|
749
|
+
push({ kind: "gamemode", player: p.name, mode });
|
|
750
|
+
},
|
|
751
|
+
attribute(p, attribute, value) {
|
|
752
|
+
write("attribute");
|
|
753
|
+
push({ kind: "attribute", player: p.name, attribute, value });
|
|
754
|
+
},
|
|
755
|
+
fill(from, to, block) {
|
|
756
|
+
write("fill");
|
|
757
|
+
push({ kind: "fill", from: { ...from }, to: { ...to }, block: blockSpecString(block) });
|
|
758
|
+
},
|
|
759
|
+
setBlock(at, block) {
|
|
760
|
+
write("setBlock");
|
|
761
|
+
const nbt = typeof block === "string" ? undefined : block.blockEntity;
|
|
762
|
+
push({ kind: "setblock", at: { ...at }, block: blockSpecString(block), nbt });
|
|
763
|
+
},
|
|
764
|
+
summon(entity, at, opts) {
|
|
765
|
+
write("summon");
|
|
766
|
+
push({ kind: "summon", entity, at: { ...at }, nbt: opts?.nbt });
|
|
767
|
+
},
|
|
768
|
+
killEntities(entities) {
|
|
769
|
+
write("killEntities");
|
|
770
|
+
push({ kind: "kill_entities", entities });
|
|
771
|
+
},
|
|
772
|
+
time(value) {
|
|
773
|
+
write("time");
|
|
774
|
+
push({ kind: "time", value });
|
|
775
|
+
},
|
|
776
|
+
gamerule(rule, value) {
|
|
777
|
+
write("gamerule");
|
|
778
|
+
push({ kind: "gamerule", rule, value });
|
|
779
|
+
},
|
|
780
|
+
setSpawn(at) {
|
|
781
|
+
write("setSpawn");
|
|
782
|
+
push({ kind: "spawn", at: { ...at } });
|
|
783
|
+
},
|
|
784
|
+
worldBorder(opts) {
|
|
785
|
+
write("worldBorder");
|
|
786
|
+
push({
|
|
787
|
+
kind: "worldborder",
|
|
788
|
+
center: opts.center,
|
|
789
|
+
size: opts.size,
|
|
790
|
+
seconds: opts.over === undefined ? undefined : (0, time_js_1.toTicks)(opts.over) / 20,
|
|
791
|
+
});
|
|
792
|
+
},
|
|
793
|
+
container(at, items) {
|
|
794
|
+
write("container");
|
|
795
|
+
const out = items.map((it) => typeof it === "string"
|
|
796
|
+
? { item: it, count: 1 }
|
|
797
|
+
: compact({
|
|
798
|
+
item: it.id,
|
|
799
|
+
count: it.count ?? 1,
|
|
800
|
+
nbt: itemNbt(it),
|
|
801
|
+
name: it.name,
|
|
802
|
+
lore: it.lore,
|
|
803
|
+
enchantments: it.enchantments,
|
|
804
|
+
damage: it.damage,
|
|
805
|
+
}));
|
|
806
|
+
push({ kind: "container", at: { ...at }, items: out });
|
|
807
|
+
},
|
|
808
|
+
command(cmd) {
|
|
809
|
+
write("command");
|
|
810
|
+
r.actions.push({ type: "log", message: `raw command: ${cmd}` });
|
|
811
|
+
push({ kind: "command", cmd });
|
|
812
|
+
},
|
|
813
|
+
// ---- flow
|
|
814
|
+
startPhase(name) {
|
|
815
|
+
requestGuard("startPhase");
|
|
816
|
+
if (!def.phases.includes(name))
|
|
817
|
+
throw (0, errors_js_1.notDeclared)("phases", name, def.phases);
|
|
818
|
+
if (r.req.end)
|
|
819
|
+
throw (0, errors_js_1.apiError)("end_requested", "startPhase: the game end is already requested");
|
|
820
|
+
// One transition per event group: a second request, from this callback or another handler of the same event, is an error.
|
|
821
|
+
if (r.req.phase !== null)
|
|
822
|
+
throw (0, errors_js_1.apiError)("phase_twice", "startPhase: one phase transition per callback");
|
|
823
|
+
r.req.phase = { target: name };
|
|
824
|
+
},
|
|
825
|
+
endPhase() {
|
|
826
|
+
requestGuard("endPhase");
|
|
827
|
+
if (s.phase === null)
|
|
828
|
+
throw (0, errors_js_1.apiError)("phase_none", "endPhase: no phase is active");
|
|
829
|
+
if (!def.phaseSpecs[s.phase]) {
|
|
830
|
+
throw (0, errors_js_1.apiError)("phase_no_next", `endPhase: the phase "${s.phase}" has no .phase() step, so no next`, {
|
|
831
|
+
path: `phases.${s.phase}`,
|
|
832
|
+
});
|
|
833
|
+
}
|
|
834
|
+
if (r.req.end)
|
|
835
|
+
throw (0, errors_js_1.apiError)("end_requested", "endPhase: the game end is already requested");
|
|
836
|
+
if (r.req.phase !== null)
|
|
837
|
+
throw (0, errors_js_1.apiError)("phase_twice", "endPhase: one phase transition per callback");
|
|
838
|
+
r.req.phase = { next: true };
|
|
839
|
+
},
|
|
840
|
+
endGame(endState) {
|
|
841
|
+
requestGuard("endGame");
|
|
842
|
+
if (endState !== undefined && !endStateNames().includes(endState)) {
|
|
843
|
+
throw (0, errors_js_1.notDeclared)("endStates", endState, endStateNames());
|
|
844
|
+
}
|
|
845
|
+
if (r.req.end)
|
|
846
|
+
return;
|
|
847
|
+
r.req.end = { reason: "call", endState };
|
|
848
|
+
r.req.phase = null;
|
|
849
|
+
},
|
|
850
|
+
schedule(name, after) {
|
|
851
|
+
flow("schedule");
|
|
852
|
+
if (def.timers && !def.timers.includes(name))
|
|
853
|
+
throw (0, errors_js_1.notDeclared)("timers", name, def.timers);
|
|
854
|
+
s.timers[name] = { due: s.tick + (0, time_js_1.toTicks)(after), gen: ++s.nextGen };
|
|
855
|
+
},
|
|
856
|
+
cancel(name) {
|
|
857
|
+
flow("cancel");
|
|
858
|
+
delete s.timers[name];
|
|
859
|
+
},
|
|
860
|
+
openVote(voteId) {
|
|
861
|
+
flow("openVote");
|
|
862
|
+
const v = voteState(voteId);
|
|
863
|
+
if (v.open)
|
|
864
|
+
throw (0, errors_js_1.apiError)("vote_open", `openVote("${voteId}"): a round is open; close it or reset it`);
|
|
865
|
+
v.open = true;
|
|
866
|
+
v.round += 1;
|
|
867
|
+
v.cast = {};
|
|
868
|
+
r.actions.push({ type: "vote_round", voteId, open: true, round: v.round });
|
|
869
|
+
},
|
|
870
|
+
closeVote(voteId) {
|
|
871
|
+
flow("closeVote");
|
|
872
|
+
const v = voteState(voteId);
|
|
873
|
+
if (!v.open)
|
|
874
|
+
return;
|
|
875
|
+
v.open = false;
|
|
876
|
+
r.actions.push({ type: "vote_round", voteId, open: false, round: v.round });
|
|
877
|
+
},
|
|
878
|
+
resetVote(voteId) {
|
|
879
|
+
flow("resetVote");
|
|
880
|
+
const v = voteState(voteId);
|
|
881
|
+
v.cast = {};
|
|
882
|
+
// A reset is a new round for the arena: a request stamped with the old round is refused.
|
|
883
|
+
v.round += 1;
|
|
884
|
+
r.actions.push({ type: "vote_round", voteId, open: v.open, round: v.round });
|
|
885
|
+
},
|
|
886
|
+
walk(p, to, opts) {
|
|
887
|
+
flow("walk");
|
|
888
|
+
for (const [id, w] of Object.entries(s.walks))
|
|
889
|
+
if (w.player === p.name)
|
|
890
|
+
delete s.walks[id];
|
|
891
|
+
const id = `walk:${s.nextId++}`;
|
|
892
|
+
const fallback = opts?.fallback ?? "teleport";
|
|
893
|
+
s.walks[id] = { player: p.name, fallback, ...to };
|
|
894
|
+
r.actions.push({ type: "walk", id, player: p.name, to: { ...to }, fallback });
|
|
895
|
+
},
|
|
896
|
+
batch(name, fn) {
|
|
897
|
+
flow("batch");
|
|
898
|
+
if (r.batch)
|
|
899
|
+
throw (0, errors_js_1.apiError)("batch_nested", "batch: a batch cannot open inside another");
|
|
900
|
+
if (Object.values(s.batches).includes(name)) {
|
|
901
|
+
throw (0, errors_js_1.apiError)("batch_pending", `batch("${name}"): a batch with that name is pending`, { value: name });
|
|
902
|
+
}
|
|
903
|
+
const effects = [];
|
|
904
|
+
r.batch = effects;
|
|
905
|
+
try {
|
|
906
|
+
assertSync(fn(), `batch("${name}")`);
|
|
907
|
+
}
|
|
908
|
+
finally {
|
|
909
|
+
r.batch = null;
|
|
910
|
+
}
|
|
911
|
+
// The transport id is unique for the run; the public name repeats once its batch is done.
|
|
912
|
+
const id = `batch:${s.nextId++}`;
|
|
913
|
+
s.batches[id] = name;
|
|
914
|
+
r.actions.push({ type: "batch", id, effects });
|
|
915
|
+
},
|
|
916
|
+
};
|
|
917
|
+
return g;
|
|
918
|
+
}
|
|
919
|
+
// ------------------------------------------------------------ dispatch
|
|
920
|
+
/** One event to the ordered global group, then, in the same entry, to the phase's handler. */
|
|
921
|
+
function fire(r, key, payload) {
|
|
922
|
+
const s = r.s;
|
|
923
|
+
if (r.req.end || s.status !== "PLAYING")
|
|
924
|
+
return;
|
|
925
|
+
const entry = s.phaseEntries;
|
|
926
|
+
const g = ctxOf(r, "full");
|
|
927
|
+
const e = viewPayload(r, payload);
|
|
928
|
+
for (const set of def.on) {
|
|
929
|
+
if (r.req.end)
|
|
930
|
+
break;
|
|
931
|
+
const h = set[key];
|
|
932
|
+
if (h)
|
|
933
|
+
run(r, "handler", h, g, e);
|
|
934
|
+
}
|
|
935
|
+
resolvePhaseRequest(r);
|
|
936
|
+
if (r.req.end || s.phaseEntries !== entry)
|
|
937
|
+
return;
|
|
938
|
+
const local = phaseSpec(s)?.on?.[key];
|
|
939
|
+
if (local)
|
|
940
|
+
run(r, "handler", local, g, e);
|
|
941
|
+
resolvePhaseRequest(r);
|
|
942
|
+
}
|
|
943
|
+
/** The contexts of a reduction, one per mode, built on first use. */
|
|
944
|
+
const ctxCache = new WeakMap();
|
|
945
|
+
function ctxOf(r, mode) {
|
|
946
|
+
let cache = ctxCache.get(r);
|
|
947
|
+
if (!cache) {
|
|
948
|
+
cache = {};
|
|
949
|
+
ctxCache.set(r, cache);
|
|
950
|
+
}
|
|
951
|
+
let ctx = cache[mode];
|
|
952
|
+
if (!ctx) {
|
|
953
|
+
ctx = makeCtx(r, mode);
|
|
954
|
+
cache[mode] = ctx;
|
|
955
|
+
}
|
|
956
|
+
return ctx;
|
|
957
|
+
}
|
|
958
|
+
function clearPhaseWork(s) {
|
|
959
|
+
const prefix = `phase:${s.phaseEntries}:`;
|
|
960
|
+
for (const key of Object.keys(s.fired))
|
|
961
|
+
if (key.startsWith(prefix))
|
|
962
|
+
delete s.fired[key];
|
|
963
|
+
for (const key of Object.keys(s.every))
|
|
964
|
+
if (key.startsWith(prefix))
|
|
965
|
+
delete s.every[key];
|
|
966
|
+
}
|
|
967
|
+
function resolveNext(r, spec) {
|
|
968
|
+
const next = typeof spec.next === "function" ? run(r, "pure", spec.next, ctxOf(r, "read")) : spec.next;
|
|
969
|
+
if (next === undefined)
|
|
970
|
+
throw (0, errors_js_1.apiError)("phase_no_next", "the phase's next returned nothing");
|
|
971
|
+
if (next !== "END" && !def.phases.includes(next))
|
|
972
|
+
throw (0, errors_js_1.notDeclared)("phases", String(next), def.phases);
|
|
973
|
+
return next;
|
|
974
|
+
}
|
|
975
|
+
/** Applies the pending phase request: exit once, enter once; a chained request from `enter` loops under the budget. */
|
|
976
|
+
function resolvePhaseRequest(r) {
|
|
977
|
+
const s = r.s;
|
|
978
|
+
while (r.req.phase !== null && r.req.end === null) {
|
|
979
|
+
const request = r.req.phase;
|
|
980
|
+
r.req.phase = null;
|
|
981
|
+
const target = "next" in request ? resolveNext(r, phaseSpec(s)) : request.target;
|
|
982
|
+
if (target === "END") {
|
|
983
|
+
r.req.end = { reason: "phase" };
|
|
984
|
+
return;
|
|
985
|
+
}
|
|
986
|
+
if (r.req.budget === 0) {
|
|
987
|
+
throw (0, errors_js_1.apiError)("phase_chain", `more than ${exports.MAX_TRANSITIONS_PER_EVENT} phase transitions in one event`);
|
|
988
|
+
}
|
|
989
|
+
r.req.budget -= 1;
|
|
990
|
+
if (s.phase !== null)
|
|
991
|
+
run(r, "exit", phaseSpec(s)?.exit, ctxOf(r, "full"));
|
|
992
|
+
clearPhaseWork(s);
|
|
993
|
+
s.phase = target;
|
|
994
|
+
s.phaseStartTick = s.tick;
|
|
995
|
+
s.phaseEntries += 1;
|
|
996
|
+
const spec = def.phaseSpecs[target];
|
|
997
|
+
s.phaseDuration = null;
|
|
998
|
+
if (spec?.duration !== undefined) {
|
|
999
|
+
const d = typeof spec.duration === "function"
|
|
1000
|
+
? run(r, "pure", spec.duration, ctxOf(r, "read"))
|
|
1001
|
+
: spec.duration;
|
|
1002
|
+
if (d === undefined)
|
|
1003
|
+
throw (0, errors_js_1.apiError)("bad_duration", `phases.${target}.duration returned nothing`);
|
|
1004
|
+
const t = (0, time_js_1.toTicks)(d);
|
|
1005
|
+
if (t < 1)
|
|
1006
|
+
throw (0, errors_js_1.apiError)("bad_duration", `phases.${target}.duration is ${t} ticks; at least 1`);
|
|
1007
|
+
s.phaseDuration = t;
|
|
1008
|
+
}
|
|
1009
|
+
(spec?.every ?? []).forEach((_e, i) => {
|
|
1010
|
+
s.every[`phase:${s.phaseEntries}:every:${i}`] = s.tick;
|
|
1011
|
+
});
|
|
1012
|
+
run(r, "handler", spec?.enter, ctxOf(r, "full"));
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
// ------------------------------------------------------------ scores, checkpoint, finalization
|
|
1016
|
+
/** The derived-score vector from the committed baseline: `outcome.scores` reads the previous scores as `p.score`. */
|
|
1017
|
+
function scoreVector(r) {
|
|
1018
|
+
const list = Object.values(r.s.players).sort((a, b) => a.seatNumber - b.seatNumber);
|
|
1019
|
+
const g = ctxOf(r, "read");
|
|
1020
|
+
const vector = {};
|
|
1021
|
+
r.scoreOverlay = r.previousScores;
|
|
1022
|
+
try {
|
|
1023
|
+
for (const p of list) {
|
|
1024
|
+
const v = run(r, "pure", def.outcome.scores, g, playerView(r, p, false));
|
|
1025
|
+
if (typeof v !== "number" || !Number.isFinite(v)) {
|
|
1026
|
+
throw (0, errors_js_1.apiError)("bad_score", `outcome.scores returned ${String(v)} for ${p.name}; a finite number is required`);
|
|
1027
|
+
}
|
|
1028
|
+
vector[p.name] = v;
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
finally {
|
|
1032
|
+
r.scoreOverlay = null;
|
|
1033
|
+
}
|
|
1034
|
+
return vector;
|
|
1035
|
+
}
|
|
1036
|
+
/** The candidate pass of the checkpoint: `until` and `endWhen` read it as `p.score`; nothing is committed. */
|
|
1037
|
+
function candidateScores(r) {
|
|
1038
|
+
if (!derivedScores)
|
|
1039
|
+
return;
|
|
1040
|
+
r.candidate = scoreVector(r);
|
|
1041
|
+
r.scoreOverlay = r.candidate;
|
|
1042
|
+
}
|
|
1043
|
+
/**
|
|
1044
|
+
* The one commit per reduction. `recompute` when a transition (exit, enter) changed the state after
|
|
1045
|
+
* the candidate pass, or at the end of the game: the final vector comes from the same baseline.
|
|
1046
|
+
*/
|
|
1047
|
+
function commitScores(r, recompute) {
|
|
1048
|
+
if (!derivedScores)
|
|
1049
|
+
return;
|
|
1050
|
+
const vector = recompute || r.candidate === null ? scoreVector(r) : r.candidate;
|
|
1051
|
+
r.candidate = null;
|
|
1052
|
+
r.scoreOverlay = null;
|
|
1053
|
+
for (const p of Object.values(r.s.players))
|
|
1054
|
+
p.score = vector[p.name] ?? p.score;
|
|
1055
|
+
}
|
|
1056
|
+
/** The checkpoint after every event's handlers (PLAYING only). */
|
|
1057
|
+
function checkpoint(r) {
|
|
1058
|
+
const s = r.s;
|
|
1059
|
+
if (s.status !== "PLAYING" || r.req.end)
|
|
1060
|
+
return;
|
|
1061
|
+
candidateScores(r);
|
|
1062
|
+
// The phase's own end, staged: until, a pass-through on a later tick, or the timeout.
|
|
1063
|
+
let staged = false;
|
|
1064
|
+
const spec = phaseSpec(s);
|
|
1065
|
+
if (spec) {
|
|
1066
|
+
if (spec.until) {
|
|
1067
|
+
staged = run(r, "pure", spec.until, ctxOf(r, "read")) === true;
|
|
1068
|
+
}
|
|
1069
|
+
else if (r.isTick && s.phaseDuration === null && s.tick > s.phaseStartTick) {
|
|
1070
|
+
staged = true;
|
|
1071
|
+
}
|
|
1072
|
+
if (!staged && r.isTick && s.phaseDuration !== null && s.tick - s.phaseStartTick >= s.phaseDuration) {
|
|
1073
|
+
staged = true;
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
// The game's end, on the pre-transition phase: it wins over the staged phase change.
|
|
1077
|
+
if (def.duration !== undefined && s.tick - s.startTick >= def.duration) {
|
|
1078
|
+
r.req.end = { reason: "time" };
|
|
1079
|
+
}
|
|
1080
|
+
else if (def.outcome.endWhen &&
|
|
1081
|
+
run(r, "pure", def.outcome.endWhen, ctxOf(r, "read")) === true) {
|
|
1082
|
+
r.req.end = { reason: "rule" };
|
|
1083
|
+
}
|
|
1084
|
+
// From here on `p.score` reads the committed scores again: exit and enter see the same values on every end path.
|
|
1085
|
+
r.scoreOverlay = null;
|
|
1086
|
+
if (r.req.end)
|
|
1087
|
+
return;
|
|
1088
|
+
if (staged) {
|
|
1089
|
+
r.req.phase = { next: true };
|
|
1090
|
+
resolvePhaseRequest(r);
|
|
1091
|
+
if (r.req.end)
|
|
1092
|
+
return;
|
|
1093
|
+
}
|
|
1094
|
+
commitScores(r, staged);
|
|
1095
|
+
}
|
|
1096
|
+
/** The end of the game: exit, final scores, winners and end state, the frozen result, the hooks, game_over. */
|
|
1097
|
+
function finalize(r) {
|
|
1098
|
+
const s = r.s;
|
|
1099
|
+
const end = r.req.end;
|
|
1100
|
+
if (!end || s.status === "OVER")
|
|
1101
|
+
return;
|
|
1102
|
+
if (s.phase !== null)
|
|
1103
|
+
run(r, "exit", phaseSpec(s)?.exit, ctxOf(r, "full"));
|
|
1104
|
+
commitScores(r, true);
|
|
1105
|
+
const gRead = ctxOf(r, "read");
|
|
1106
|
+
const names = new Set(Object.keys(s.players));
|
|
1107
|
+
const rawWinners = (run(r, "pure", def.outcome.winners, gRead) ?? []).map((p) => p.name);
|
|
1108
|
+
const winners = [];
|
|
1109
|
+
for (const name of rawWinners) {
|
|
1110
|
+
if (!names.has(name))
|
|
1111
|
+
throw (0, errors_js_1.apiError)("bad_winner", `outcome.winners named "${name}", not a participant`);
|
|
1112
|
+
if (!winners.includes(name))
|
|
1113
|
+
winners.push(name);
|
|
1114
|
+
}
|
|
1115
|
+
let endState = end.endState ?? run(r, "pure", def.outcome.endState, gRead) ?? "";
|
|
1116
|
+
if (endState !== "" && !endStateNames().includes(endState)) {
|
|
1117
|
+
throw (0, errors_js_1.notDeclared)("endStates", endState, endStateNames());
|
|
1118
|
+
}
|
|
1119
|
+
endState = String(endState);
|
|
1120
|
+
const elapsed = s.tick - s.startTick;
|
|
1121
|
+
const phaseElapsed = s.phase === null ? 0 : s.tick - s.phaseStartTick;
|
|
1122
|
+
s.frozen = { elapsed, phaseElapsed };
|
|
1123
|
+
const won = new Set(winners);
|
|
1124
|
+
const result = Object.freeze({
|
|
1125
|
+
winners: Object.freeze([...winners]),
|
|
1126
|
+
endState: endState,
|
|
1127
|
+
reason: end.reason,
|
|
1128
|
+
phase: s.phase,
|
|
1129
|
+
elapsed: elapsed,
|
|
1130
|
+
phaseElapsed: phaseElapsed,
|
|
1131
|
+
isWinner: (p) => won.has(p.name),
|
|
1132
|
+
});
|
|
1133
|
+
s.status = "OVER";
|
|
1134
|
+
s.endState = endState;
|
|
1135
|
+
s.winners = winners;
|
|
1136
|
+
s.reason = end.reason;
|
|
1137
|
+
run(r, "notify", def.onOver, ctxOf(r, "notify"), result);
|
|
1138
|
+
const judge = snapshot(run(r, "pure", def.judgeContext, gRead, result));
|
|
1139
|
+
const gameOver = { type: "game_over", winners, endState, reason: end.reason };
|
|
1140
|
+
if (judge !== undefined) {
|
|
1141
|
+
if (judge === null || typeof judge !== "object" || Array.isArray(judge)) {
|
|
1142
|
+
throw (0, errors_js_1.apiError)("bad_judge_context", "judgeContext must return an object");
|
|
1143
|
+
}
|
|
1144
|
+
assertJson(judge, "judgeContext");
|
|
1145
|
+
gameOver.judgeContext = judge;
|
|
1146
|
+
}
|
|
1147
|
+
r.actions.push(gameOver);
|
|
1148
|
+
}
|
|
1149
|
+
/** The SDK's own terminal result: no author callback runs. */
|
|
1150
|
+
function failRun(s, actions, endState, message, data) {
|
|
1151
|
+
actions.push(compact({ type: "log", message, data }));
|
|
1152
|
+
if (s.status === "OVER")
|
|
1153
|
+
return;
|
|
1154
|
+
s.status = "OVER";
|
|
1155
|
+
s.endState = endState;
|
|
1156
|
+
s.winners = [];
|
|
1157
|
+
s.reason = "error";
|
|
1158
|
+
s.frozen = { elapsed: s.tick - s.startTick, phaseElapsed: s.phase === null ? 0 : s.tick - s.phaseStartTick };
|
|
1159
|
+
actions.push({ type: "game_over", winners: [], endState, reason: "error" });
|
|
1160
|
+
}
|
|
1161
|
+
// ------------------------------------------------------------ lifecycle
|
|
1162
|
+
function worldGlobals() {
|
|
1163
|
+
if (!def.world)
|
|
1164
|
+
return [];
|
|
1165
|
+
const w = def.world;
|
|
1166
|
+
const out = [];
|
|
1167
|
+
if (w.spawn)
|
|
1168
|
+
out.push({ kind: "spawn", at: { ...w.spawn } });
|
|
1169
|
+
if (w.time !== false)
|
|
1170
|
+
out.push({ kind: "time", value: w.time });
|
|
1171
|
+
out.push({ kind: "gamerule", rule: "doDaylightCycle", value: w.daylightCycle });
|
|
1172
|
+
out.push({ kind: "gamerule", rule: "doMobSpawning", value: w.mobSpawning });
|
|
1173
|
+
out.push({ kind: "gamerule", rule: "naturalRegeneration", value: w.naturalRegeneration });
|
|
1174
|
+
return out;
|
|
1175
|
+
}
|
|
1176
|
+
function worldPlayers(s) {
|
|
1177
|
+
if (!def.world || def.world.maxHealth === false)
|
|
1178
|
+
return [];
|
|
1179
|
+
return Object.values(s.players).map((p) => ({
|
|
1180
|
+
kind: "attribute",
|
|
1181
|
+
player: p.name,
|
|
1182
|
+
attribute: "minecraft:generic.max_health",
|
|
1183
|
+
value: def.world?.maxHealth,
|
|
1184
|
+
}));
|
|
1185
|
+
}
|
|
1186
|
+
/** The state initializers, once, with the observed players. */
|
|
1187
|
+
function initialize(r) {
|
|
1188
|
+
const s = r.s;
|
|
1189
|
+
const { random, close: closeRandom } = makeRandom(r, "init.random");
|
|
1190
|
+
const roster = Object.values(s.players).sort((a, b) => a.seatNumber - b.seatNumber);
|
|
1191
|
+
// Every player has a position here: `allReady` gates this call.
|
|
1192
|
+
// One argument object per call, with its own copies of the data: an initializer may keep
|
|
1193
|
+
// `init.locations.hub` in its state, and two states never share an object.
|
|
1194
|
+
const initArg = () => ({
|
|
1195
|
+
params: snapshot(s.params),
|
|
1196
|
+
random,
|
|
1197
|
+
players: roster.map((p) => playerView(r, p, false)),
|
|
1198
|
+
locations: snapshot(s.locations),
|
|
1199
|
+
});
|
|
1200
|
+
s.game = (run(r, "init", def.state.game, initArg()) ?? {});
|
|
1201
|
+
for (const p of roster) {
|
|
1202
|
+
const forRole = def.state.players?.[p.role];
|
|
1203
|
+
const observed = playerView(r, p, false);
|
|
1204
|
+
const own = forRole
|
|
1205
|
+
? run(r, "init", forRole, observed, initArg())
|
|
1206
|
+
: run(r, "init", def.state.player, observed, initArg());
|
|
1207
|
+
p.state = (own ?? {});
|
|
1208
|
+
}
|
|
1209
|
+
closeRandom();
|
|
1210
|
+
s.initialized = true;
|
|
1211
|
+
assertJson({ game: s.game, players: Object.fromEntries(roster.map((p) => [p.name, p.state])) }, "state");
|
|
1212
|
+
}
|
|
1213
|
+
function sendSetupBatch(r, effects, index) {
|
|
1214
|
+
r.actions.push({ type: "batch", id: `setup:${index}`, effects });
|
|
1215
|
+
if (r.s.setup)
|
|
1216
|
+
r.s.setup.sentTick = r.s.tick;
|
|
1217
|
+
}
|
|
1218
|
+
/** After the initializers: the setup batches, or straight into the game. */
|
|
1219
|
+
function leaveConnecting(r) {
|
|
1220
|
+
const s = r.s;
|
|
1221
|
+
initialize(r);
|
|
1222
|
+
if (!def.setup) {
|
|
1223
|
+
play(r);
|
|
1224
|
+
return;
|
|
1225
|
+
}
|
|
1226
|
+
const effects = [...worldGlobals()];
|
|
1227
|
+
r.batch = effects;
|
|
1228
|
+
try {
|
|
1229
|
+
run(r, "setup", def.setup, ctxOf(r, "setup"));
|
|
1230
|
+
}
|
|
1231
|
+
finally {
|
|
1232
|
+
r.batch = null;
|
|
1233
|
+
}
|
|
1234
|
+
const batches = splitBatches(effects);
|
|
1235
|
+
s.status = "SETUP";
|
|
1236
|
+
// The batch list rides in the state until the last confirmation: every SETUP tick clones it (bounded by the batch limits).
|
|
1237
|
+
s.setup = { batches, confirmed: 0, sentTick: s.tick };
|
|
1238
|
+
sendSetupBatch(r, batches[0], 0);
|
|
1239
|
+
}
|
|
1240
|
+
function play(r) {
|
|
1241
|
+
const s = r.s;
|
|
1242
|
+
s.status = "PLAYING";
|
|
1243
|
+
s.startTick = s.tick;
|
|
1244
|
+
for (const p of Object.values(s.players)) {
|
|
1245
|
+
p.alive = true;
|
|
1246
|
+
p.deaths = 0;
|
|
1247
|
+
p.kills = 0;
|
|
1248
|
+
}
|
|
1249
|
+
const head = def.setup ? worldPlayers(s) : [...worldGlobals(), ...worldPlayers(s)];
|
|
1250
|
+
for (const e of head)
|
|
1251
|
+
r.actions.push({ type: "effect", effect: e });
|
|
1252
|
+
def.every.forEach((_e, i) => {
|
|
1253
|
+
s.every[`every:${i}`] = s.tick;
|
|
1254
|
+
});
|
|
1255
|
+
s.phase = null;
|
|
1256
|
+
run(r, "handler", def.start, ctxOf(r, "full"));
|
|
1257
|
+
resolvePhaseRequest(r);
|
|
1258
|
+
if (r.req.end)
|
|
1259
|
+
return;
|
|
1260
|
+
if (s.phase === null && def.phases.length > 0) {
|
|
1261
|
+
r.req.phase = { target: def.initialPhase ?? def.phases[0] };
|
|
1262
|
+
resolvePhaseRequest(r);
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
/** The connect-and-position condition for the current connection of every participant. */
|
|
1266
|
+
const allReady = (s) => {
|
|
1267
|
+
const roster = Object.values(s.players);
|
|
1268
|
+
return roster.length > 0 && roster.every((p) => p.connected && p.sampled && p.pos !== null);
|
|
1269
|
+
};
|
|
1270
|
+
function onTick(r) {
|
|
1271
|
+
const s = r.s;
|
|
1272
|
+
if (s.status === "CONNECTING") {
|
|
1273
|
+
if (!s.initialized && allReady(s))
|
|
1274
|
+
leaveConnecting(r);
|
|
1275
|
+
return;
|
|
1276
|
+
}
|
|
1277
|
+
if (s.status === "SETUP") {
|
|
1278
|
+
if (s.setup && s.tick - s.setup.sentTick > SETUP_BATCH_TIMEOUT_TICKS) {
|
|
1279
|
+
failRun(s, r.actions, "error_setup", "the engine did not confirm a setup batch in 30 s");
|
|
1280
|
+
}
|
|
1281
|
+
return;
|
|
1282
|
+
}
|
|
1283
|
+
if (s.status !== "PLAYING")
|
|
1284
|
+
return;
|
|
1285
|
+
const g = ctxOf(r, "full");
|
|
1286
|
+
const work = [];
|
|
1287
|
+
let order = 0;
|
|
1288
|
+
def.at.forEach((st, i) => {
|
|
1289
|
+
const key = `at:${i}`;
|
|
1290
|
+
const due = s.startTick + (0, time_js_1.toTicks)(st.at);
|
|
1291
|
+
if (s.tick >= due && !s.fired[key])
|
|
1292
|
+
work.push({
|
|
1293
|
+
due,
|
|
1294
|
+
order: order++,
|
|
1295
|
+
scope: "global",
|
|
1296
|
+
entry: 0,
|
|
1297
|
+
run: () => {
|
|
1298
|
+
s.fired[key] = true;
|
|
1299
|
+
run(r, "handler", st.run, g);
|
|
1300
|
+
},
|
|
1301
|
+
});
|
|
1302
|
+
});
|
|
1303
|
+
def.every.forEach((st, i) => {
|
|
1304
|
+
const key = `every:${i}`;
|
|
1305
|
+
const due = (s.every[key] ?? s.startTick) + (0, time_js_1.toTicks)(st.interval);
|
|
1306
|
+
if (s.tick >= due)
|
|
1307
|
+
work.push({
|
|
1308
|
+
due,
|
|
1309
|
+
order: order++,
|
|
1310
|
+
scope: "global",
|
|
1311
|
+
entry: 0,
|
|
1312
|
+
run: () => {
|
|
1313
|
+
s.every[key] = s.tick;
|
|
1314
|
+
run(r, "handler", st.run, g);
|
|
1315
|
+
},
|
|
1316
|
+
});
|
|
1317
|
+
});
|
|
1318
|
+
const spec = phaseSpec(s);
|
|
1319
|
+
if (spec) {
|
|
1320
|
+
const entry = s.phaseEntries;
|
|
1321
|
+
(spec.at ?? []).forEach((st, i) => {
|
|
1322
|
+
const key = `phase:${entry}:${i}`;
|
|
1323
|
+
let offset;
|
|
1324
|
+
if ("remaining" in st && st.remaining !== undefined) {
|
|
1325
|
+
if (s.phaseDuration === null)
|
|
1326
|
+
return;
|
|
1327
|
+
offset = s.phaseDuration - (0, time_js_1.toTicks)(st.remaining);
|
|
1328
|
+
}
|
|
1329
|
+
else
|
|
1330
|
+
offset = (0, time_js_1.toTicks)(st.elapsed);
|
|
1331
|
+
const due = s.phaseStartTick + offset;
|
|
1332
|
+
if (s.tick >= due && !s.fired[key])
|
|
1333
|
+
work.push({
|
|
1334
|
+
due,
|
|
1335
|
+
order: order++,
|
|
1336
|
+
scope: "phase",
|
|
1337
|
+
entry,
|
|
1338
|
+
run: () => {
|
|
1339
|
+
s.fired[key] = true;
|
|
1340
|
+
run(r, "handler", st.run, g);
|
|
1341
|
+
},
|
|
1342
|
+
});
|
|
1343
|
+
});
|
|
1344
|
+
(spec.every ?? []).forEach((st, i) => {
|
|
1345
|
+
const key = `phase:${entry}:every:${i}`;
|
|
1346
|
+
const due = (s.every[key] ?? s.phaseStartTick) + (0, time_js_1.toTicks)(st.interval);
|
|
1347
|
+
if (s.tick >= due)
|
|
1348
|
+
work.push({
|
|
1349
|
+
due,
|
|
1350
|
+
order: order++,
|
|
1351
|
+
scope: "phase",
|
|
1352
|
+
entry,
|
|
1353
|
+
run: () => {
|
|
1354
|
+
s.every[key] = s.tick;
|
|
1355
|
+
run(r, "handler", st.run, g);
|
|
1356
|
+
},
|
|
1357
|
+
});
|
|
1358
|
+
});
|
|
1359
|
+
}
|
|
1360
|
+
for (const [name, t] of Object.entries(s.timers)) {
|
|
1361
|
+
if (s.tick >= t.due) {
|
|
1362
|
+
const gen = t.gen;
|
|
1363
|
+
work.push({
|
|
1364
|
+
due: t.due,
|
|
1365
|
+
order: order++,
|
|
1366
|
+
scope: "timer",
|
|
1367
|
+
entry: 0,
|
|
1368
|
+
run: () => {
|
|
1369
|
+
// A sibling handler may have cancelled or rescheduled it on this same tick.
|
|
1370
|
+
if (s.timers[name]?.gen !== gen)
|
|
1371
|
+
return;
|
|
1372
|
+
delete s.timers[name];
|
|
1373
|
+
fire(r, "timer", { name });
|
|
1374
|
+
},
|
|
1375
|
+
});
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
work.sort((a, b) => a.due - b.due || a.order - b.order);
|
|
1379
|
+
for (const w of work) {
|
|
1380
|
+
if (r.req.end)
|
|
1381
|
+
return;
|
|
1382
|
+
if (w.scope === "phase" && w.entry !== s.phaseEntries)
|
|
1383
|
+
continue;
|
|
1384
|
+
w.run();
|
|
1385
|
+
resolvePhaseRequest(r);
|
|
1386
|
+
}
|
|
1387
|
+
if (r.req.end)
|
|
1388
|
+
return;
|
|
1389
|
+
fire(r, "tick", undefined);
|
|
1390
|
+
}
|
|
1391
|
+
/** Applies one sample's fields; returns the move, for the events that fire once the whole tick applied. */
|
|
1392
|
+
function applySample(p, d) {
|
|
1393
|
+
let move = null;
|
|
1394
|
+
if (d.pos) {
|
|
1395
|
+
const from = p.pos ?? { ...d.pos };
|
|
1396
|
+
p.pos = { ...d.pos };
|
|
1397
|
+
p.sampled = true;
|
|
1398
|
+
move = { p, from, to: { ...d.pos } };
|
|
1399
|
+
}
|
|
1400
|
+
if (d.yaw !== undefined)
|
|
1401
|
+
p.yaw = d.yaw;
|
|
1402
|
+
if (d.pitch !== undefined)
|
|
1403
|
+
p.pitch = d.pitch;
|
|
1404
|
+
if (d.headYaw !== undefined)
|
|
1405
|
+
p.headYaw = d.headYaw;
|
|
1406
|
+
if (d.id !== undefined)
|
|
1407
|
+
p.entityId = d.id;
|
|
1408
|
+
if (d.uuid !== undefined)
|
|
1409
|
+
p.uuid = d.uuid;
|
|
1410
|
+
if (d.equipment)
|
|
1411
|
+
p.equipment = (0, world_js_1.applyEquipment)(p.equipment, d.equipment);
|
|
1412
|
+
if (d.invisible !== undefined)
|
|
1413
|
+
p.invisible = d.invisible;
|
|
1414
|
+
if (d.health !== undefined)
|
|
1415
|
+
p.health = d.health;
|
|
1416
|
+
if (d.food !== undefined)
|
|
1417
|
+
p.food = d.food;
|
|
1418
|
+
if (d.gamemode !== undefined)
|
|
1419
|
+
p.gamemode = d.gamemode;
|
|
1420
|
+
if (d.held !== undefined)
|
|
1421
|
+
p.held = d.held;
|
|
1422
|
+
return move;
|
|
1423
|
+
}
|
|
1424
|
+
function zonesAt(pos) {
|
|
1425
|
+
if (!pos)
|
|
1426
|
+
return [];
|
|
1427
|
+
return declaredZones.filter((zone) => (0, world_js_1.inBox)(pos, def.zones[zone]));
|
|
1428
|
+
}
|
|
1429
|
+
/** The zone edges and the move of every player who moved this event, in roster order. */
|
|
1430
|
+
function fireMoves(r, moves) {
|
|
1431
|
+
const edges = moves
|
|
1432
|
+
.sort((a, b) => a.p.seatNumber - b.p.seatNumber)
|
|
1433
|
+
.map((m) => {
|
|
1434
|
+
const before = m.p.zones;
|
|
1435
|
+
const after = zonesAt(m.p.pos);
|
|
1436
|
+
m.p.zones = after;
|
|
1437
|
+
return { m, before, after };
|
|
1438
|
+
});
|
|
1439
|
+
if (r.s.status !== "PLAYING")
|
|
1440
|
+
return;
|
|
1441
|
+
for (const { m, before, after } of edges) {
|
|
1442
|
+
for (const zone of after)
|
|
1443
|
+
if (!before.includes(zone))
|
|
1444
|
+
fire(r, "zoneEntered", { player: m.p, zone });
|
|
1445
|
+
for (const zone of before)
|
|
1446
|
+
if (!after.includes(zone))
|
|
1447
|
+
fire(r, "zoneLeft", { player: m.p, zone });
|
|
1448
|
+
fire(r, "move", { player: m.p, from: m.from, to: { ...m.to } });
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
function sampleOther(s, d) {
|
|
1452
|
+
const prev = s.world.others[d.name];
|
|
1453
|
+
s.world = (0, world_js_1.putOther)(s.world, compact({
|
|
1454
|
+
name: d.name,
|
|
1455
|
+
pos: d.pos ? { ...d.pos } : (prev?.pos ?? null),
|
|
1456
|
+
health: d.health ?? prev?.health ?? 20,
|
|
1457
|
+
food: d.food ?? prev?.food ?? 20,
|
|
1458
|
+
gamemode: d.gamemode ?? prev?.gamemode ?? "survival",
|
|
1459
|
+
held: d.held !== undefined ? d.held : (prev?.held ?? null),
|
|
1460
|
+
uuid: d.uuid ?? prev?.uuid,
|
|
1461
|
+
entityId: d.id ?? prev?.entityId,
|
|
1462
|
+
yaw: d.yaw ?? prev?.yaw,
|
|
1463
|
+
pitch: d.pitch ?? prev?.pitch,
|
|
1464
|
+
headYaw: d.headYaw ?? prev?.headYaw,
|
|
1465
|
+
equipment: d.equipment ? (0, world_js_1.applyEquipment)(prev?.equipment, d.equipment) : prev?.equipment,
|
|
1466
|
+
invisible: d.invisible ?? prev?.invisible,
|
|
1467
|
+
}));
|
|
1468
|
+
}
|
|
1469
|
+
function applyContainer(r, e) {
|
|
1470
|
+
const s = r.s;
|
|
1471
|
+
const key = posKey(e.at);
|
|
1472
|
+
const prev = s.containers[key];
|
|
1473
|
+
const before = prev ? countsOf(prev.slots) : {};
|
|
1474
|
+
if (e.removed) {
|
|
1475
|
+
delete s.containers[key];
|
|
1476
|
+
if (s.status !== "PLAYING" || !prev)
|
|
1477
|
+
return;
|
|
1478
|
+
const changes = Object.entries(before).map(([item, n]) => ({ item: item, delta: -n }));
|
|
1479
|
+
fire(r, "container", { at: { ...e.at }, block: e.block, items: {}, changes, by: undefined, removed: true });
|
|
1480
|
+
return;
|
|
1481
|
+
}
|
|
1482
|
+
const slots = { ...(prev?.slots ?? {}) };
|
|
1483
|
+
for (const sl of e.slots) {
|
|
1484
|
+
const st = stackOf(sl);
|
|
1485
|
+
if (st)
|
|
1486
|
+
slots[sl.slot] = st;
|
|
1487
|
+
else
|
|
1488
|
+
delete slots[sl.slot];
|
|
1489
|
+
}
|
|
1490
|
+
s.containers[key] = { block: e.block, slots };
|
|
1491
|
+
if (s.status !== "PLAYING" || e.initial)
|
|
1492
|
+
return;
|
|
1493
|
+
const after = countsOf(slots);
|
|
1494
|
+
const changes = [];
|
|
1495
|
+
for (const item of new Set([...Object.keys(before), ...Object.keys(after)])) {
|
|
1496
|
+
const delta = (after[item] ?? 0) - (before[item] ?? 0);
|
|
1497
|
+
if (delta !== 0)
|
|
1498
|
+
changes.push({ item: item, delta });
|
|
1499
|
+
}
|
|
1500
|
+
if (changes.length === 0)
|
|
1501
|
+
return;
|
|
1502
|
+
const by = e.by === undefined ? undefined : s.players[e.by];
|
|
1503
|
+
fire(r, "container", { at: { ...e.at }, block: e.block, items: after, changes, by, removed: false });
|
|
1504
|
+
}
|
|
1505
|
+
/** Applies one event and dispatches its handlers. */
|
|
1506
|
+
function step(r, e) {
|
|
1507
|
+
const s = r.s;
|
|
1508
|
+
const playing = () => s.status === "PLAYING";
|
|
1509
|
+
const resolve = (name) => name === undefined ? undefined : s.players[name];
|
|
1510
|
+
switch (e.type) {
|
|
1511
|
+
case "run_started": {
|
|
1512
|
+
s.seed = e.seed;
|
|
1513
|
+
s.rng = e.seed >>> 0;
|
|
1514
|
+
s.params = { ...paramDefaults(), ...e.params };
|
|
1515
|
+
s.capabilities = e.capabilities;
|
|
1516
|
+
s.locations = e.locations ?? {};
|
|
1517
|
+
s.markers = (e.markers ?? []).map((m) => ({ ...m, at: { ...m.at }, tags: [...m.tags] }));
|
|
1518
|
+
e.participants.forEach((p, i) => {
|
|
1519
|
+
s.players[p.name] = attachDerived({
|
|
1520
|
+
name: p.name,
|
|
1521
|
+
role: p.role,
|
|
1522
|
+
seatNumber: i + 1,
|
|
1523
|
+
human: p.human === true,
|
|
1524
|
+
connected: false,
|
|
1525
|
+
alive: true,
|
|
1526
|
+
eliminated: false,
|
|
1527
|
+
deaths: 0,
|
|
1528
|
+
kills: 0,
|
|
1529
|
+
score: 0,
|
|
1530
|
+
pos: null,
|
|
1531
|
+
yaw: 0,
|
|
1532
|
+
pitch: 0,
|
|
1533
|
+
headYaw: 0,
|
|
1534
|
+
entityId: null,
|
|
1535
|
+
uuid: null,
|
|
1536
|
+
equipment: {},
|
|
1537
|
+
invisible: false,
|
|
1538
|
+
health: 20,
|
|
1539
|
+
food: 20,
|
|
1540
|
+
gamemode: "survival",
|
|
1541
|
+
held: null,
|
|
1542
|
+
slots: {},
|
|
1543
|
+
inventory: {},
|
|
1544
|
+
zones: [],
|
|
1545
|
+
state: {},
|
|
1546
|
+
sampled: false,
|
|
1547
|
+
});
|
|
1548
|
+
});
|
|
1549
|
+
const unknownRoles = e.participants.filter((p) => !(p.role in config.roles));
|
|
1550
|
+
if (unknownRoles.length > 0) {
|
|
1551
|
+
failRun(s, r.actions, "error_challenge", `unknown role: ${unknownRoles.map((p) => `${p.role} (${p.name})`).join(", ")}; the roles are ${roleNames.join(", ")}`);
|
|
1552
|
+
return;
|
|
1553
|
+
}
|
|
1554
|
+
const missing = missingCapabilities(e.capabilities);
|
|
1555
|
+
if (def.setup && !e.capabilities.includes("batch"))
|
|
1556
|
+
missing.push("batch");
|
|
1557
|
+
if (missing.length > 0) {
|
|
1558
|
+
failRun(s, r.actions, "error_unsupported_facts", `this engine cannot supply: ${missing.join(", ")}`, {
|
|
1559
|
+
missing,
|
|
1560
|
+
});
|
|
1561
|
+
}
|
|
1562
|
+
return;
|
|
1563
|
+
}
|
|
1564
|
+
case "player_joined": {
|
|
1565
|
+
const p = resolve(e.name);
|
|
1566
|
+
if (!p)
|
|
1567
|
+
return;
|
|
1568
|
+
p.connected = true;
|
|
1569
|
+
// A new connection needs a new sample before the roster counts as ready.
|
|
1570
|
+
p.sampled = false;
|
|
1571
|
+
if (e.uuid !== undefined)
|
|
1572
|
+
p.uuid = e.uuid;
|
|
1573
|
+
if (e.gamemode !== undefined)
|
|
1574
|
+
p.gamemode = e.gamemode;
|
|
1575
|
+
if (playing())
|
|
1576
|
+
fire(r, "joined", { player: p });
|
|
1577
|
+
break;
|
|
1578
|
+
}
|
|
1579
|
+
case "gamemode": {
|
|
1580
|
+
const p = resolve(e.name);
|
|
1581
|
+
if (!p) {
|
|
1582
|
+
const other = s.world.others[e.name];
|
|
1583
|
+
if (other)
|
|
1584
|
+
s.world = (0, world_js_1.putOther)(s.world, { ...other, gamemode: e.gamemode });
|
|
1585
|
+
return;
|
|
1586
|
+
}
|
|
1587
|
+
p.gamemode = e.gamemode;
|
|
1588
|
+
if (playing())
|
|
1589
|
+
fire(r, "gamemode", { player: p, gamemode: e.gamemode });
|
|
1590
|
+
break;
|
|
1591
|
+
}
|
|
1592
|
+
case "player_left": {
|
|
1593
|
+
const p = resolve(e.name);
|
|
1594
|
+
if (!p) {
|
|
1595
|
+
s.world = (0, world_js_1.removeOther)(s.world, e.name);
|
|
1596
|
+
return;
|
|
1597
|
+
}
|
|
1598
|
+
p.connected = false;
|
|
1599
|
+
if (playing())
|
|
1600
|
+
fire(r, "left", { player: p });
|
|
1601
|
+
break;
|
|
1602
|
+
}
|
|
1603
|
+
case "tick": {
|
|
1604
|
+
r.isTick = true;
|
|
1605
|
+
s.dt = e.serverTick - s.tick;
|
|
1606
|
+
s.tick = e.serverTick;
|
|
1607
|
+
if (e.entities)
|
|
1608
|
+
s.world = (0, world_js_1.moveEntities)(s.world, e.entities);
|
|
1609
|
+
if (e.time !== undefined && e.time !== s.world.time)
|
|
1610
|
+
s.world = { ...s.world, time: e.time };
|
|
1611
|
+
const moves = [];
|
|
1612
|
+
for (const d of e.players ?? []) {
|
|
1613
|
+
const p = resolve(d.name);
|
|
1614
|
+
if (p) {
|
|
1615
|
+
const move = applySample(p, d);
|
|
1616
|
+
if (move)
|
|
1617
|
+
moves.push(move);
|
|
1618
|
+
}
|
|
1619
|
+
else if (!d.participant)
|
|
1620
|
+
sampleOther(s, d);
|
|
1621
|
+
}
|
|
1622
|
+
fireMoves(r, moves);
|
|
1623
|
+
onTick(r);
|
|
1624
|
+
break;
|
|
1625
|
+
}
|
|
1626
|
+
case "effect_result": {
|
|
1627
|
+
if (e.id.startsWith("setup:")) {
|
|
1628
|
+
// Only the pending batch counts: a duplicate, stale or unknown acknowledgement neither resends nor advances.
|
|
1629
|
+
if (s.status !== "SETUP" || !s.setup || e.id !== `setup:${s.setup.confirmed}`) {
|
|
1630
|
+
r.actions.push({ type: "log", message: `ignored effect_result for ${e.id}: not the pending setup batch` });
|
|
1631
|
+
return;
|
|
1632
|
+
}
|
|
1633
|
+
const index = s.setup.confirmed;
|
|
1634
|
+
if (!e.ok) {
|
|
1635
|
+
const detail = e.failed?.map((f) => `#${f.index} ${f.detail}`).join("; ") ?? e.detail ?? "unknown";
|
|
1636
|
+
failRun(s, r.actions, "error_setup", `setup batch ${index} failed: ${detail}`, {
|
|
1637
|
+
failed: (e.failed ?? []),
|
|
1638
|
+
});
|
|
1639
|
+
return;
|
|
1640
|
+
}
|
|
1641
|
+
s.setup.confirmed = index + 1;
|
|
1642
|
+
if (s.setup.confirmed < s.setup.batches.length) {
|
|
1643
|
+
sendSetupBatch(r, s.setup.batches[s.setup.confirmed], s.setup.confirmed);
|
|
1644
|
+
return;
|
|
1645
|
+
}
|
|
1646
|
+
s.setup = null;
|
|
1647
|
+
play(r);
|
|
1648
|
+
return;
|
|
1649
|
+
}
|
|
1650
|
+
const batchName = s.batches[e.id];
|
|
1651
|
+
if (batchName !== undefined) {
|
|
1652
|
+
delete s.batches[e.id];
|
|
1653
|
+
if (playing())
|
|
1654
|
+
fire(r, "batchDone", { name: batchName, ok: e.ok, failed: e.failed ?? [] });
|
|
1655
|
+
return;
|
|
1656
|
+
}
|
|
1657
|
+
const w = s.walks[e.id];
|
|
1658
|
+
if (w) {
|
|
1659
|
+
delete s.walks[e.id];
|
|
1660
|
+
if (!e.ok && w.fallback === "teleport") {
|
|
1661
|
+
r.actions.push({
|
|
1662
|
+
type: "effect",
|
|
1663
|
+
effect: { kind: "teleport", player: w.player, to: { x: w.x, y: w.y, z: w.z } },
|
|
1664
|
+
});
|
|
1665
|
+
}
|
|
1666
|
+
const p = resolve(w.player);
|
|
1667
|
+
if (p && playing())
|
|
1668
|
+
fire(r, "walkDone", { player: p, ok: e.ok });
|
|
1669
|
+
}
|
|
1670
|
+
break;
|
|
1671
|
+
}
|
|
1672
|
+
case "effect_failed":
|
|
1673
|
+
r.actions.push({ type: "log", message: `effect failed: ${e.kind}: ${e.detail}` });
|
|
1674
|
+
if (playing()) {
|
|
1675
|
+
fire(r, "effectFailed", {
|
|
1676
|
+
kind: e.kind,
|
|
1677
|
+
detail: e.detail,
|
|
1678
|
+
effect: e.effect,
|
|
1679
|
+
batch: e.batch && s.batches[e.batch.id] !== undefined
|
|
1680
|
+
? { name: s.batches[e.batch.id], index: e.batch.index }
|
|
1681
|
+
: undefined,
|
|
1682
|
+
});
|
|
1683
|
+
}
|
|
1684
|
+
break;
|
|
1685
|
+
case "player_moved": {
|
|
1686
|
+
const p = resolve(e.name);
|
|
1687
|
+
if (!p)
|
|
1688
|
+
return;
|
|
1689
|
+
const move = applySample(p, { pos: e.to });
|
|
1690
|
+
if (move)
|
|
1691
|
+
fireMoves(r, [move]);
|
|
1692
|
+
if (s.status === "CONNECTING" && !s.initialized && allReady(s))
|
|
1693
|
+
leaveConnecting(r);
|
|
1694
|
+
break;
|
|
1695
|
+
}
|
|
1696
|
+
case "player_damaged": {
|
|
1697
|
+
const p = resolve(e.name);
|
|
1698
|
+
if (!p || !playing())
|
|
1699
|
+
return;
|
|
1700
|
+
fire(r, "damaged", { player: p, amount: e.amount, cause: e.cause, attacker: resolve(e.attacker) });
|
|
1701
|
+
break;
|
|
1702
|
+
}
|
|
1703
|
+
case "player_died": {
|
|
1704
|
+
const p = resolve(e.name);
|
|
1705
|
+
if (!p)
|
|
1706
|
+
return;
|
|
1707
|
+
p.alive = false;
|
|
1708
|
+
const killer = resolve(e.killer);
|
|
1709
|
+
if (!playing())
|
|
1710
|
+
return;
|
|
1711
|
+
p.deaths += 1;
|
|
1712
|
+
if (killer && killer !== p)
|
|
1713
|
+
killer.kills += 1;
|
|
1714
|
+
if (def.life === "single" && !p.eliminated) {
|
|
1715
|
+
p.eliminated = true;
|
|
1716
|
+
r.actions.push({ type: "effect", effect: { kind: "eliminate", player: p.name } });
|
|
1717
|
+
}
|
|
1718
|
+
fire(r, "died", { player: p, cause: e.cause, killer });
|
|
1719
|
+
break;
|
|
1720
|
+
}
|
|
1721
|
+
case "player_respawned": {
|
|
1722
|
+
const p = resolve(e.name);
|
|
1723
|
+
if (!p)
|
|
1724
|
+
return;
|
|
1725
|
+
if (!p.eliminated)
|
|
1726
|
+
p.alive = true;
|
|
1727
|
+
if (playing())
|
|
1728
|
+
fire(r, "respawned", { player: p });
|
|
1729
|
+
break;
|
|
1730
|
+
}
|
|
1731
|
+
case "item_picked_up": {
|
|
1732
|
+
const p = resolve(e.name);
|
|
1733
|
+
if (!p || !playing())
|
|
1734
|
+
return;
|
|
1735
|
+
fire(r, "pickup", { player: p, item: e.item, count: e.count, at: e.at });
|
|
1736
|
+
break;
|
|
1737
|
+
}
|
|
1738
|
+
case "inventory": {
|
|
1739
|
+
const p = resolve(e.name);
|
|
1740
|
+
if (!p)
|
|
1741
|
+
return;
|
|
1742
|
+
for (const sl of e.slots) {
|
|
1743
|
+
const st = stackOf(sl);
|
|
1744
|
+
if (st)
|
|
1745
|
+
p.slots[sl.slot] = st;
|
|
1746
|
+
else
|
|
1747
|
+
delete p.slots[sl.slot];
|
|
1748
|
+
}
|
|
1749
|
+
const before = p.inventory;
|
|
1750
|
+
const after = countsOf(p.slots);
|
|
1751
|
+
p.inventory = after;
|
|
1752
|
+
if (!playing())
|
|
1753
|
+
return;
|
|
1754
|
+
for (const item of new Set([...Object.keys(before), ...Object.keys(after)])) {
|
|
1755
|
+
if ((before[item] ?? 0) !== (after[item] ?? 0)) {
|
|
1756
|
+
fire(r, "inventory", { player: p, item: item, count: after[item] ?? 0 });
|
|
1757
|
+
}
|
|
1758
|
+
}
|
|
1759
|
+
break;
|
|
1760
|
+
}
|
|
1761
|
+
case "container":
|
|
1762
|
+
applyContainer(r, e);
|
|
1763
|
+
break;
|
|
1764
|
+
case "entity_spawned": {
|
|
1765
|
+
const entity = compact({
|
|
1766
|
+
id: e.id,
|
|
1767
|
+
type: e.entity,
|
|
1768
|
+
pos: { ...e.at },
|
|
1769
|
+
tags: [...(e.tags ?? [])],
|
|
1770
|
+
name: e.name,
|
|
1771
|
+
health: e.health,
|
|
1772
|
+
item: e.item,
|
|
1773
|
+
count: e.count,
|
|
1774
|
+
uuid: e.uuid,
|
|
1775
|
+
yaw: e.yaw,
|
|
1776
|
+
pitch: e.pitch,
|
|
1777
|
+
headYaw: e.headYaw,
|
|
1778
|
+
equipment: e.equipment ? (0, world_js_1.applyEquipment)(undefined, e.equipment) : undefined,
|
|
1779
|
+
invisible: e.invisible,
|
|
1780
|
+
villager: e.villager ? { ...e.villager } : undefined,
|
|
1781
|
+
});
|
|
1782
|
+
s.world = (0, world_js_1.putEntity)(s.world, entity);
|
|
1783
|
+
if (playing())
|
|
1784
|
+
fire(r, "entitySpawned", { entity: (0, world_js_1.freezeObservation)(entity) });
|
|
1785
|
+
break;
|
|
1786
|
+
}
|
|
1787
|
+
case "entity_changed": {
|
|
1788
|
+
const prev = s.world.entities[e.id];
|
|
1789
|
+
if (!prev)
|
|
1790
|
+
return;
|
|
1791
|
+
const next = { ...prev };
|
|
1792
|
+
if (e.tags)
|
|
1793
|
+
next.tags = [...e.tags];
|
|
1794
|
+
if (e.name !== undefined)
|
|
1795
|
+
next.name = e.name ?? undefined;
|
|
1796
|
+
if (e.health !== undefined)
|
|
1797
|
+
next.health = e.health;
|
|
1798
|
+
if (e.item !== undefined)
|
|
1799
|
+
next.item = e.item;
|
|
1800
|
+
if (e.count !== undefined)
|
|
1801
|
+
next.count = e.count;
|
|
1802
|
+
if (e.equipment)
|
|
1803
|
+
next.equipment = (0, world_js_1.applyEquipment)(prev.equipment, e.equipment);
|
|
1804
|
+
if (e.invisible !== undefined)
|
|
1805
|
+
next.invisible = e.invisible;
|
|
1806
|
+
if (e.villager)
|
|
1807
|
+
next.villager = { ...e.villager };
|
|
1808
|
+
s.world = (0, world_js_1.putEntity)(s.world, next);
|
|
1809
|
+
if (playing())
|
|
1810
|
+
fire(r, "entityChanged", { entity: (0, world_js_1.freezeObservation)(next), was: (0, world_js_1.freezeObservation)(prev) });
|
|
1811
|
+
break;
|
|
1812
|
+
}
|
|
1813
|
+
case "entity_removed": {
|
|
1814
|
+
const prev = s.world.entities[e.id];
|
|
1815
|
+
if (!prev)
|
|
1816
|
+
return;
|
|
1817
|
+
s.world = (0, world_js_1.removeEntity)(s.world, e.id);
|
|
1818
|
+
if (playing())
|
|
1819
|
+
fire(r, "entityRemoved", { entity: (0, world_js_1.freezeObservation)(prev), cause: e.cause, killer: resolve(e.killer) });
|
|
1820
|
+
break;
|
|
1821
|
+
}
|
|
1822
|
+
case "chunk":
|
|
1823
|
+
s.world = (0, world_js_1.applyChunk)(s.world, e);
|
|
1824
|
+
break;
|
|
1825
|
+
case "chunk_unloaded": {
|
|
1826
|
+
s.world = (0, world_js_1.removeChunk)(s.world, e.x, e.z);
|
|
1827
|
+
// The containers of the chunk are unknown now.
|
|
1828
|
+
for (const key of Object.keys(s.containers)) {
|
|
1829
|
+
const [x, , z] = key.split(",").map(Number);
|
|
1830
|
+
if (x >> 4 === e.x && z >> 4 === e.z)
|
|
1831
|
+
delete s.containers[key];
|
|
1832
|
+
}
|
|
1833
|
+
break;
|
|
1834
|
+
}
|
|
1835
|
+
case "blocks_changed": {
|
|
1836
|
+
s.world = (0, world_js_1.setBlocks)(s.world, e.changes);
|
|
1837
|
+
if (playing()) {
|
|
1838
|
+
fire(r, "blocksChanged", {
|
|
1839
|
+
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) })),
|
|
1840
|
+
});
|
|
1841
|
+
}
|
|
1842
|
+
break;
|
|
1843
|
+
}
|
|
1844
|
+
case "blocks_filled":
|
|
1845
|
+
s.world = (0, world_js_1.fillBlocks)(s.world, { from: e.from, to: e.to }, e.block);
|
|
1846
|
+
if (playing()) {
|
|
1847
|
+
fire(r, "blocksChanged", {
|
|
1848
|
+
changes: [{ box: { from: { ...e.from }, to: { ...e.to } }, block: e.block, cause: "fill" }],
|
|
1849
|
+
});
|
|
1850
|
+
}
|
|
1851
|
+
break;
|
|
1852
|
+
case "block_broken": {
|
|
1853
|
+
s.world = (0, world_js_1.setBlocks)(s.world, [{ ...e.at, block: e.after ?? world_js_1.AIR }]);
|
|
1854
|
+
const p = resolve(e.name);
|
|
1855
|
+
if (p && playing())
|
|
1856
|
+
fire(r, "blockBroken", { player: p, at: { ...e.at }, block: parseBlockState(e.block) });
|
|
1857
|
+
break;
|
|
1858
|
+
}
|
|
1859
|
+
case "block_placed": {
|
|
1860
|
+
const changes = [{ ...e.at, block: e.block }, ...(e.others ?? []).map((o) => ({ ...o.at, block: o.block }))];
|
|
1861
|
+
s.world = (0, world_js_1.setBlocks)(s.world, changes);
|
|
1862
|
+
const p = resolve(e.name);
|
|
1863
|
+
if (p && playing())
|
|
1864
|
+
fire(r, "blockPlaced", { player: p, at: { ...e.at }, block: parseBlockState(e.block) });
|
|
1865
|
+
break;
|
|
1866
|
+
}
|
|
1867
|
+
case "block_used": {
|
|
1868
|
+
const p = resolve(e.name);
|
|
1869
|
+
if (p && playing())
|
|
1870
|
+
fire(r, "blockUsed", { player: p, at: { ...e.at }, block: parseBlockState(e.block), item: e.item });
|
|
1871
|
+
break;
|
|
1872
|
+
}
|
|
1873
|
+
case "chat": {
|
|
1874
|
+
const p = resolve(e.from);
|
|
1875
|
+
if (p && playing())
|
|
1876
|
+
fire(r, "chat", { player: p, text: e.text });
|
|
1877
|
+
break;
|
|
1878
|
+
}
|
|
1879
|
+
case "animation": {
|
|
1880
|
+
const p = resolve(e.name);
|
|
1881
|
+
if (p && playing())
|
|
1882
|
+
fire(r, "animation", { player: p, animation: e.animation });
|
|
1883
|
+
break;
|
|
1884
|
+
}
|
|
1885
|
+
case "vote_attempt":
|
|
1886
|
+
voteAttempt(r, e);
|
|
1887
|
+
break;
|
|
1888
|
+
case "custom": {
|
|
1889
|
+
if (!playing())
|
|
1890
|
+
return;
|
|
1891
|
+
fire(r, "custom", { name: e.name, player: resolve(e.player), data: e.data });
|
|
1892
|
+
break;
|
|
1893
|
+
}
|
|
1894
|
+
}
|
|
1895
|
+
}
|
|
1896
|
+
function voteAttempt(r, e) {
|
|
1897
|
+
const s = r.s;
|
|
1898
|
+
const p = s.players[e.name];
|
|
1899
|
+
const reject = (feedback) => {
|
|
1900
|
+
r.actions.push(compact({
|
|
1901
|
+
type: "vote_decision",
|
|
1902
|
+
attemptId: e.attemptId,
|
|
1903
|
+
accepted: false,
|
|
1904
|
+
feedback,
|
|
1905
|
+
round: s.votes[e.voteId]?.round,
|
|
1906
|
+
}));
|
|
1907
|
+
};
|
|
1908
|
+
if (!p) {
|
|
1909
|
+
reject("You are not a participant in this run.");
|
|
1910
|
+
return;
|
|
1911
|
+
}
|
|
1912
|
+
if (s.status !== "PLAYING") {
|
|
1913
|
+
reject("The game is not running.");
|
|
1914
|
+
return;
|
|
1915
|
+
}
|
|
1916
|
+
const rule = def.votes[e.voteId];
|
|
1917
|
+
const v = s.votes[e.voteId];
|
|
1918
|
+
if (!rule || !v) {
|
|
1919
|
+
reject(`"${e.voteId}" is not a vote in this challenge.`);
|
|
1920
|
+
return;
|
|
1921
|
+
}
|
|
1922
|
+
if (!v.open) {
|
|
1923
|
+
reject(`The vote "${e.voteId}" is closed.`);
|
|
1924
|
+
return;
|
|
1925
|
+
}
|
|
1926
|
+
if (e.round === undefined) {
|
|
1927
|
+
reject(`Your vote for "${e.voteId}" carries no round; the arena stamps every request with the round it saw.`);
|
|
1928
|
+
return;
|
|
1929
|
+
}
|
|
1930
|
+
if (e.round !== v.round) {
|
|
1931
|
+
reject(`Your vote for "${e.voteId}" was for an earlier round. Vote again.`);
|
|
1932
|
+
return;
|
|
1933
|
+
}
|
|
1934
|
+
if (p.eliminated && !rule.allowEliminated) {
|
|
1935
|
+
reject("You are out of the game; your vote is refused.");
|
|
1936
|
+
return;
|
|
1937
|
+
}
|
|
1938
|
+
const isPlayerVote = e.voteId === types_js_1.PLAYER_VOTE;
|
|
1939
|
+
const options = isPlayerVote
|
|
1940
|
+
? Object.values(s.players)
|
|
1941
|
+
.filter((o) => (rule.allowSelf ? true : o.name !== p.name) && (rule.allowEliminated || !o.eliminated))
|
|
1942
|
+
.map((o) => o.name)
|
|
1943
|
+
: [...(rule.options ?? [])];
|
|
1944
|
+
if (!options.includes(e.option)) {
|
|
1945
|
+
reject(isPlayerVote
|
|
1946
|
+
? `"${e.option}" is not a player you can vote for. Players: ${options.join(", ")}.`
|
|
1947
|
+
: `"${e.option}" is not an option for "${e.voteId}". Options: ${options.join(", ")}.`);
|
|
1948
|
+
return;
|
|
1949
|
+
}
|
|
1950
|
+
const prior = v.cast[p.name];
|
|
1951
|
+
if (prior !== undefined && rule.final !== false) {
|
|
1952
|
+
reject(`You already voted "${prior}" for "${e.voteId}". Votes are final.`);
|
|
1953
|
+
return;
|
|
1954
|
+
}
|
|
1955
|
+
if (rule.accept) {
|
|
1956
|
+
const verdict = run(r, "pure", rule.accept, ctxOf(r, "read"), playerView(r, p, false), e.option);
|
|
1957
|
+
if (verdict !== true) {
|
|
1958
|
+
reject(typeof verdict === "string" ? verdict : "Your vote was refused.");
|
|
1959
|
+
return;
|
|
1960
|
+
}
|
|
1961
|
+
}
|
|
1962
|
+
v.cast[p.name] = e.option;
|
|
1963
|
+
r.actions.push({ type: "vote_decision", attemptId: e.attemptId, accepted: true, round: v.round });
|
|
1964
|
+
fire(r, "vote", { player: p, voteId: e.voteId, option: e.option });
|
|
1965
|
+
}
|
|
1966
|
+
// ------------------------------------------------------------ reduce
|
|
1967
|
+
const reduce = (input, event) => {
|
|
1968
|
+
if (input.status === "OVER")
|
|
1969
|
+
return { state: input, actions: [] };
|
|
1970
|
+
let r = {
|
|
1971
|
+
s: clone(input),
|
|
1972
|
+
actions: [],
|
|
1973
|
+
batch: null,
|
|
1974
|
+
previousScores: Object.fromEntries(Object.values(input.players).map((p) => [p.name, p.score])),
|
|
1975
|
+
scoreOverlay: null,
|
|
1976
|
+
candidate: null,
|
|
1977
|
+
views: { read: new WeakMap(), player: new WeakMap() },
|
|
1978
|
+
req: { phase: null, end: null, budget: exports.MAX_TRANSITIONS_PER_EVENT },
|
|
1979
|
+
frame: { kind: "handler" },
|
|
1980
|
+
closed: false,
|
|
1981
|
+
isTick: false,
|
|
1982
|
+
};
|
|
1983
|
+
try {
|
|
1984
|
+
step(r, event);
|
|
1985
|
+
if (r.s.status === "PLAYING" && !r.req.end)
|
|
1986
|
+
checkpoint(r);
|
|
1987
|
+
if (r.req.end)
|
|
1988
|
+
finalize(r);
|
|
1989
|
+
assertJson({ game: r.s.game, players: Object.fromEntries(Object.values(r.s.players).map((p) => [p.name, p.state])) }, "state");
|
|
1990
|
+
}
|
|
1991
|
+
catch (err) {
|
|
1992
|
+
// A challenge bug must neither kill the arena nor pass as a result. The crashed
|
|
1993
|
+
// reduction's state and effects are dropped; only the SDK's own values remain.
|
|
1994
|
+
r.closed = true;
|
|
1995
|
+
r = { ...r, s: clone(input), actions: [], batch: null, req: { phase: null, end: null, budget: 0 } };
|
|
1996
|
+
if (event.type === "tick")
|
|
1997
|
+
r.s.tick = event.serverTick;
|
|
1998
|
+
const detail = err instanceof errors_js_1.ChallengeError ? err.toJSON() : { message: String(err?.message ?? err) };
|
|
1999
|
+
failRun(r.s, r.actions, "error_challenge", `challenge error on ${event.type}: ${err.message}`, {
|
|
2000
|
+
event: event,
|
|
2001
|
+
error: detail,
|
|
2002
|
+
category: (0, errors_js_1.categoryOf)(err),
|
|
2003
|
+
});
|
|
2004
|
+
}
|
|
2005
|
+
r.closed = true;
|
|
2006
|
+
const s = r.s;
|
|
2007
|
+
// The SDK reports `score`; the author never emits it.
|
|
2008
|
+
const scores = [];
|
|
2009
|
+
for (const p of Object.values(s.players)) {
|
|
2010
|
+
if (input.players[p.name]?.score !== p.score)
|
|
2011
|
+
scores.push({ type: "score", player: p.name, score: p.score });
|
|
2012
|
+
}
|
|
2013
|
+
const tail = r.actions.filter((a) => a.type === "game_over");
|
|
2014
|
+
const head = r.actions.filter((a) => a.type !== "game_over");
|
|
2015
|
+
// Publication: nothing an author kept a reference to can change the published state. The mirror is
|
|
2016
|
+
// skipped: no context exposes it (reads return values, observations enter frozen) and it is large.
|
|
2017
|
+
for (const [k, v] of Object.entries(s))
|
|
2018
|
+
if (k !== "world")
|
|
2019
|
+
deepFreeze(v);
|
|
2020
|
+
Object.freeze(s);
|
|
2021
|
+
return { state: s, actions: [...head, ...scores, ...tail] };
|
|
2022
|
+
};
|
|
2023
|
+
return { init, reduce };
|
|
2024
|
+
}
|
|
2025
|
+
//# sourceMappingURL=reducer.js.map
|