1/**2 * Guest-side prelude evaluated inside each tribe's QuickJS context BEFORE the3 * submitted script. Everything lives in one closure; the only globals it adds4 * are the three frozen host hooks (`__begin`, `__runTick`, `__end`).5 *6 * It also neutralizes the remaining ambient nondeterminism:7 * - `Date` is already absent (intrinsic disabled at context creation).8 * - `Math.random` is replaced with a throwing stub (use `api.rng()`).9 * - global `eval` is deleted (the Eval intrinsic must stay on because the10 * host itself drives the VM through `evalCode`).11 *12 * All intrinsic functions the hooks rely on (JSON.stringify/parse, Math.imul,13 * Object.keys, ...) are captured in the closure before user code runs, so a14 * script that vandalizes its own globals can only break itself — never the15 * host protocol.16 *17 * The api methods coerce their arguments to primitives IMMEDIATELY (any18 * valueOf/toString side effects run inside the metered tick), then push plain19 * fresh objects. Final strict shape validation happens host-side after the20 * JSON transfer (src/index.ts validateOrder).21 *22 * The prelude source is fixed (modulo the injected seed/limit literals), so23 * its execution cost is deterministic too.24 */2526/**27 * Build the prelude source.28 *29 * @param seed32 32-bit unsigned seed for the guest splitmix32 stream.30 * @param memoryMaxBytes cap on `JSON.stringify(memory).length` (UTF-16 code31 * units — a lower bound of UTF-8 bytes; the VM memory limit is the hard cap).32 */33export function buildPrelude(seed32: number, memoryMaxBytes: number): string {34 return (35 "(function (SEED, MEMORY_MAX) {\n" +36 ' "use strict";\n' +37 " // Capture intrinsics before any user code can touch them.\n" +38 " var stringify = JSON.stringify;\n" +39 " var parse = JSON.parse;\n" +40 " var imul = Math.imul;\n" +41 " var ownKeys = Object.keys;\n" +42 " var defineProperty = Object.defineProperty;\n" +43 " var freeze = Object.freeze;\n" +44 "\n" +45 " // --- neutralize ambient nondeterminism --------------------------------\n" +46 " Math.random = function () {\n" +47 ' throw new Error("Math.random is disabled; use api.rng() for the seeded stream");\n' +48 " };\n" +49 " try { delete globalThis.eval; } catch (e) {}\n" +50 ' if (typeof globalThis.eval !== "undefined") {\n' +51 " try { globalThis.eval = undefined; } catch (e) {}\n" +52 " }\n" +53 "\n" +54 " // --- seeded PRNG: splitmix32, cheap in-VM calls -----------------------\n" +55 " var rngState = SEED >>> 0;\n" +56 " function rng() {\n" +57 " rngState = (rngState + 0x9e3779b9) | 0;\n" +58 " var z = rngState;\n" +59 " z = imul(z ^ (z >>> 16), 0x21f0aaad);\n" +60 " z = imul(z ^ (z >>> 15), 0x735a2d97);\n" +61 " z = (z ^ (z >>> 15)) >>> 0;\n" +62 " return z / 4294967296; // float in [0, 1), like Math.random\n" +63 " }\n" +64 "\n" +65 " // --- log ring buffer ---------------------------------------------------\n" +66 " var logs = [];\n" +67 " var LOG_CAP = 128;\n" +68 " var LOG_LINE_MAX = 512;\n" +69 " function log(msg) {\n" +70 " var s;\n" +71 ' if (typeof msg === "string") s = msg;\n' +72 " else {\n" +73 " try { s = stringify(msg); } catch (e) { s = undefined; }\n" +74 ' if (typeof s !== "string") s = String(msg);\n' +75 " }\n" +76 ' if (s.length > LOG_LINE_MAX) s = s.slice(0, LOG_LINE_MAX) + "...";\n' +77 " if (logs.length >= LOG_CAP) logs.shift();\n" +78 " logs[logs.length] = s;\n" +79 " }\n" +80 "\n" +81 " // --- order buffer ------------------------------------------------------\n" +82 " // api methods coerce args to primitives immediately, then push plain\n" +83 " // fresh objects. Final shape validation happens host-side after JSON\n" +84 " // transfer (the host also enforces LIMITS.ACTIONS_PER_TICK).\n" +85 " var orders = [];\n" +86 " var MAX_ORDERS = 1024;\n" +87 " function pushOrder(o) {\n" +88 " if (orders.length < MAX_ORDERS) orders[orders.length] = o;\n" +89 " }\n" +90 " // Copy a supplies bag into a fresh plain object with numeric values.\n" +91 " // Only own enumerable keys; assignment through [] means a hostile\n" +92 ' // "__proto__" key silently no-ops. The host re-validates every key\n' +93 " // against the Resource enum anyway.\n" +94 " function copySupplies(supplies) {\n" +95 " var out = {};\n" +96 ' if (supplies !== null && typeof supplies === "object") {\n' +97 " var keys = ownKeys(supplies);\n" +98 " for (var i = 0; i < keys.length; i++) out[keys[i]] = +supplies[keys[i]];\n" +99 " }\n" +100 " return out;\n" +101 " }\n" +102 "\n" +103 " var api = freeze({\n" +104 " // Persistent labor policy: per-mille of population to a profession.\n" +105 " allocate: function (profession, permille, settlementId) {\n" +106 ' var o = { kind: "allocate", profession: "" + profession, permille: +permille };\n' +107 " if (settlementId !== undefined) o.settlementId = +settlementId;\n" +108 " pushOrder(o);\n" +109 " },\n" +110 " // Persistent workshop split (smithy: tools/weapons/armor; grain: mill/brew/fodder).\n" +111 " craft: function (target, permille) {\n" +112 ' pushOrder({ kind: "craft", target: "" + target, permille: +permille });\n' +113 " },\n" +114 " build: function (building, settlementId, count) {\n" +115 ' var o = { kind: "build", building: "" + building, settlementId: +settlementId };\n' +116 " if (count !== undefined) o.count = +count;\n" +117 " pushOrder(o);\n" +118 " },\n" +119 " demolish: function (building, settlementId) {\n" +120 ' pushOrder({ kind: "demolish", building: "" + building, settlementId: +settlementId });\n' +121 " },\n" +122 " found: function (x, y, settlers, supplies) {\n" +123 " pushOrder({\n" +124 ' kind: "found",\n' +125 " x: +x,\n" +126 " y: +y,\n" +127 " settlers: +settlers,\n" +128 " supplies: copySupplies(supplies),\n" +129 " });\n" +130 " },\n" +131 " // Persistent logistics: ship permille of `resource` surplus from -> to.\n" +132 " route: function (fromId, toId, resource, permilleOfSurplus) {\n" +133 " pushOrder({\n" +134 ' kind: "route",\n' +135 " fromId: +fromId,\n" +136 " toId: +toId,\n" +137 ' resource: "" + resource,\n' +138 " permilleOfSurplus: +permilleOfSurplus,\n" +139 " });\n" +140 " },\n" +141 " muster: function (settlementId, size, unitKind) {\n" +142 " pushOrder({\n" +143 ' kind: "muster",\n' +144 " settlementId: +settlementId,\n" +145 " size: +size,\n" +146 ' unitKind: "" + unitKind,\n' +147 " });\n" +148 " },\n" +149 " // Standing warband order: { type, target } where target is a settlement\n" +150 " // or caravan id, an {x, y} point, or null (disband).\n" +151 " command: function (warbandId, order) {\n" +152 ' var type = "";\n' +153 " var target = null;\n" +154 ' if (order !== null && typeof order === "object") {\n' +155 ' type = "" + order.type;\n' +156 " var t = order.target;\n" +157 " if (t === null || t === undefined) target = null;\n" +158 ' else if (typeof t === "object") target = { x: +t.x, y: +t.y };\n' +159 " else target = +t;\n" +160 " }\n" +161 ' pushOrder({ kind: "command", warbandId: +warbandId, order: { type: type, target: target } });\n' +162 " },\n" +163 " // Standing (repeat) or one-shot trade offer. tribe null = anyone\n" +164 " // non-hostile; wantAmount 0 makes it a gift/tribute.\n" +165 " trade: function (offer) {\n" +166 ' if (offer === null || typeof offer !== "object") offer = {};\n' +167 " pushOrder({\n" +168 ' kind: "trade_offer",\n' +169 ' give: "" + offer.give,\n' +170 " giveAmount: +offer.giveAmount,\n" +171 ' want: "" + offer.want,\n' +172 " wantAmount: +offer.wantAmount,\n" +173 " tribe: offer.tribe === null || offer.tribe === undefined ? null : +offer.tribe,\n" +174 " repeat: !!offer.repeat,\n" +175 " });\n" +176 " },\n" +177 " cancelTrade: function (offerId) {\n" +178 ' pushOrder({ kind: "trade_cancel", offerId: +offerId });\n' +179 " },\n" +180 " stance: function (tribeId, posture) {\n" +181 ' pushOrder({ kind: "stance", tribeId: +tribeId, posture: "" + posture });\n' +182 " },\n" +183 " research: function (tech) {\n" +184 ' pushOrder({ kind: "research", tech: "" + tech });\n' +185 " },\n" +186 " decree: function (key, value) {\n" +187 ' pushOrder({ kind: "decree", key: "" + key, value: +value });\n' +188 " },\n" +189 " // One-shot faith spend. target: settlement id or null (tribe-wide).\n" +190 " rite: function (rite, target) {\n" +191 " pushOrder({\n" +192 ' kind: "rite",\n' +193 ' rite: "" + rite,\n' +194 " target: target === null || target === undefined ? null : +target,\n" +195 " });\n" +196 " },\n" +197 " advance: function () {\n" +198 ' pushOrder({ kind: \"advance\" });\n' +199 " },\n" +200 " say: function (text) {\n" +201 ' pushOrder({ kind: "say", text: "" + text });\n' +202 " },\n" +203 " rng: rng,\n" +204 " log: log,\n" +205 " });\n" +206 "\n" +207 " // --- persistent memory & per-tick state --------------------------------\n" +208 " var memory = {};\n" +209 " var state;\n" +210 "\n" +211 " // --- host hooks (frozen onto globalThis) --------------------------------\n" +212 " function hook(name, fn) {\n" +213 " defineProperty(globalThis, name, {\n" +214 " value: fn,\n" +215 " writable: false,\n" +216 " configurable: false,\n" +217 " enumerable: false,\n" +218 " });\n" +219 " }\n" +220 "\n" +221 " hook(\"__begin\", function (json) {\n" +222 " orders.length = 0;\n" +223 " state = parse(json);\n" +224 " });\n" +225 "\n" +226 " hook(\"__runTick\", function () {\n" +227 " var t = globalThis.tick;\n" +228 ' if (typeof t !== "function") {\n' +229 ' throw new Error("script must define a global function tick(state, memory, api)");\n' +230 " }\n" +231 " t(state, memory, api);\n" +232 " });\n" +233 "\n" +234 " hook(\"__end\", function () {\n" +235 " var drained = logs.slice(0);\n" +236 " logs.length = 0;\n" +237 " var memoryExceeded = false;\n" +238 " var memJson;\n" +239 " try { memJson = stringify(memory); } catch (e) { memJson = undefined; }\n" +240 ' if (typeof memJson !== "string" || memJson.length > MEMORY_MAX) {\n' +241 " memoryExceeded = true;\n" +242 " memory = {}; // reset so the script can recover next tick\n" +243 " }\n" +244 " return stringify({ orders: orders, logs: drained, memoryExceeded: memoryExceeded });\n" +245 " });\n" +246 "})(" +247 String(seed32 >>> 0) +248 ", " +249 String(memoryMaxBytes) +250 ");\n"251 );252}