@homie-rocks/studio 0.35.0 → 0.36.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 +37 -1
- package/agents/agents.ts +8 -6
- package/bin/homie-studio.mjs +9 -6
- package/lab/lab.ts +15 -0
- package/lib/build.mjs +20 -15
- package/lib/dev.mjs +16 -1
- package/lib/lab-check.mjs +18 -3
- package/lib/lab.mjs +21 -7
- package/lib/mcp-tools.mjs +2 -1
- package/lib/rules-build.mjs +97 -49
- package/lib/rules-check-worker.mjs +405 -0
- package/lib/rules-check.mjs +69 -0
- package/lib/rules-compiler.mjs +30 -0
- package/lib/rules-guard.mjs +26 -7
- package/lib/rules-types.mjs +167 -0
- package/lib/shop-catalog.mjs +20 -1
- package/lib/shop-links.mjs +3 -1
- package/lib/shop.mjs +14 -8
- package/lib/standalone-files.mjs +2 -2
- package/lib/standalone.mjs +6 -2
- package/lib/stripe-connect.mjs +18 -7
- package/lib/template-history.json +83 -0
- package/lib/typecheck.mjs +52 -3
- package/netplay/NETPLAY.md +123 -6
- package/netplay/netplay.ts +289 -53
- package/netplay/rules-output.mjs +85 -0
- package/package.json +3 -2
- package/rules/browser.ts +100 -0
- package/rules/core.ts +159 -58
- package/rules/guard.ts +11 -4
- package/rules/host.ts +90 -15
- package/rules/math.ts +16 -0
- package/rules/pack.ts +94 -17
- package/rules/rules.ts +21 -13
- package/rules/types.ts +63 -0
- package/rules/view.ts +36 -33
- package/standalone/shell/shell.js +8 -6
- package/worker/brain.mjs +2 -1
- package/worker/hosted.mjs +1 -1
- package/worker/index.mjs +7 -11
- package/worker/limits.mjs +41 -0
- package/worker/pages.mjs +4 -2
- package/worker/room.mjs +136 -63
- package/worker/stripe.mjs +2 -2
- package/worker/version.mjs +1 -1
package/lib/rules-build.mjs
CHANGED
|
@@ -2,33 +2,36 @@
|
|
|
2
2
|
* What `homie-studio build` does for a game written as rules plus view (rooms-milestone-1-design.md, section 8).
|
|
3
3
|
*
|
|
4
4
|
* A rules game is games/<id>/src/rules.ts, move.ts and view.ts, with game.json ("entry": "src/view.ts"),
|
|
5
|
-
* tunables.json and map/. Its build makes
|
|
5
|
+
* tunables.json and map/. Its build makes the view and the guarded rules:
|
|
6
6
|
*
|
|
7
7
|
* the view's bundle view.ts and move.ts, for the browser, as every game's bundle is made (lib/build.mjs). The
|
|
8
8
|
* move code in it is the guarded, counted module the server runs, not the source; none of
|
|
9
|
-
* the rules'
|
|
10
|
-
* the
|
|
9
|
+
* the rules' handlers enter the view itself, only their declarations as data.
|
|
10
|
+
* the offline module the guarded rules, host runtime and all tunables, fetched after playable when offline is
|
|
11
|
+
* enabled. Browser-hosted games import it eagerly; private server games do not ship it.
|
|
12
|
+
* the server module rules.ts and move.ts as ONE JavaScript module, types removed and guards added
|
|
11
13
|
* (lib/rules-guard.mjs), written to site/src/rules/<id>.mjs with the game's tunables, map and
|
|
12
14
|
* settings beside it (<id>.data.mjs) and a table of every such game (index.mjs). The studio's
|
|
13
15
|
* site/src/worker.mjs imports that table, so the rules reach the Table in the Worker's own
|
|
14
16
|
* bundle. These files are build output that is committed with the studio, like wrangler.jsonc:
|
|
15
17
|
* the Worker cannot be bundled without them.
|
|
16
18
|
*
|
|
17
|
-
* Before anything is written the
|
|
18
|
-
* (
|
|
19
|
-
*
|
|
20
|
-
* longer on this computer's own clock than the room's period (`smokeRun`), stops the build with its name. (The full
|
|
21
|
-
* build check, with generated runs compared tick by tick, is a later release's.)
|
|
19
|
+
* Before anything is written, the toolkit's own compiler checks rules, movement and view against the game's
|
|
20
|
+
* declarations (lib/typecheck.mjs, on every build), and the guarded module is played with generated input in a worker
|
|
21
|
+
* thread (lib/rules-check.mjs). The module that is played is the module that is written.
|
|
22
22
|
*/
|
|
23
23
|
import { createHash } from 'node:crypto';
|
|
24
24
|
import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs';
|
|
25
25
|
import { join } from 'node:path';
|
|
26
26
|
import { pathToFileURL } from 'node:url';
|
|
27
27
|
import { GUARD_MODULE, RULES_MODULE, guardRules, guardedPlugin, problemLine } from './rules-guard.mjs';
|
|
28
|
-
import {
|
|
28
|
+
import { capOf, RULES_FRAMES, rulesRates } from '../worker/limits.mjs';
|
|
29
29
|
import { PACKAGE_ROOT, isRulesGame, rulesIndex } from './studio.mjs';
|
|
30
|
+
import { ALLOWANCE, LONG_ALLOWANCE, runRulesWorker } from './rules-check.mjs';
|
|
31
|
+
import { typecheckRules } from './typecheck.mjs';
|
|
30
32
|
|
|
31
33
|
export { isRulesGame };
|
|
34
|
+
export { stateHash } from './rules-check.mjs';
|
|
32
35
|
|
|
33
36
|
const readJson = (path) => { try { return JSON.parse(readFileSync(path, 'utf8')); } catch { return null; } };
|
|
34
37
|
const RULES_FILES = { [RULES_MODULE]: 'rules.ts', [GUARD_MODULE]: 'guard.ts', [`${RULES_MODULE}/view`]: 'view.ts', [`${RULES_MODULE}/host`]: 'host.ts' };
|
|
@@ -58,33 +61,56 @@ function readMap(g) {
|
|
|
58
61
|
* `W` (guard.ts, the wall), `P` (pack.ts) and `M` (math.ts). They are one instance together, apart from any other
|
|
59
62
|
* copy of the runtime in this process, so the guard's budget counter is theirs alone.
|
|
60
63
|
*/
|
|
61
|
-
export async function loadRules(esbuild, root, id, code) {
|
|
64
|
+
export async function loadRules(esbuild, root, id, code, { bundleOnly = false } = {}) {
|
|
62
65
|
const dir = join(root, '.studio', 'rules');
|
|
63
66
|
mkdirSync(dir, { recursive: true });
|
|
64
|
-
const gen = join(dir, `${id}.linked.mjs`);
|
|
67
|
+
const gen = join(dir, `${id}-${process.pid}-${++loads}.linked.mjs`);
|
|
65
68
|
writeFileSync(gen, code);
|
|
66
69
|
const lib = (name) => JSON.stringify(join(PACKAGE_ROOT, 'rules', name));
|
|
67
70
|
const entry = `import def from ${JSON.stringify(gen)};\nimport * as R from ${lib('rules.ts')};\nimport * as H from ${lib('host.ts')};\nimport * as C from ${lib('core.ts')};\nimport * as W from ${lib('guard.ts')};\nimport * as P from ${lib('pack.ts')};\nimport * as M from ${lib('math.ts')};\nexport { def, R, H, C, W, P, M };\n`;
|
|
68
|
-
|
|
71
|
+
try {
|
|
72
|
+
const res = await esbuild.build({ stdin: { contents: entry, resolveDir: root, loader: 'js' }, bundle: true, format: bundleOnly ? 'cjs' : 'esm', target: 'es2022', platform: 'neutral', write: false, logLevel: 'silent', plugins: [rulesModulesPlugin()] });
|
|
69
73
|
const text = res.outputFiles[0].text;
|
|
74
|
+
if (bundleOnly) { rmSync(gen, { force: true }); return text; }
|
|
70
75
|
const out = join(dir, `${id}.check-${createHash('sha256').update(text).digest('hex').slice(0, 12)}-${process.pid}-${(loads += 1)}.mjs`);
|
|
71
76
|
writeFileSync(out, text);
|
|
72
|
-
try { return await import(pathToFileURL(out).href); } finally { rmSync(out, { force: true });
|
|
77
|
+
try { return await import(pathToFileURL(out).href); } finally { rmSync(out, { force: true }); }
|
|
78
|
+
} catch (error) {
|
|
79
|
+
if (/No matching export.*default/s.test(error.message)) throw new Error(`games/${id}/src/rules.ts: the rules need a default export. Use export default defineRules({ ... }).`);
|
|
80
|
+
throw error;
|
|
81
|
+
} finally { rmSync(gen, { force: true }); }
|
|
73
82
|
}
|
|
74
83
|
let loads = 0;
|
|
75
84
|
|
|
76
85
|
/**
|
|
77
|
-
* Three seconds of the room's clock with bots and one seated player, on a clock of the
|
|
86
|
+
* Three seconds of the room's clock with bots and one seated player, on a clock of the caller's own.
|
|
78
87
|
*
|
|
79
|
-
* Every tick is also timed
|
|
80
|
-
*
|
|
81
|
-
*
|
|
88
|
+
* Every tick is also timed (`timer`, milliseconds). A tick has one period of the room's clock to run in. One slow tick
|
|
89
|
+
* on a busy computer proves nothing, so the line is three ticks of the run over the period, or one tick over four
|
|
90
|
+
* periods: either throws, naming the handler that used the most.
|
|
91
|
+
*
|
|
92
|
+
* The build calls this with a constant timer solely to measure browser frame sizes and rates. Real CPU timing is
|
|
93
|
+
* only for manual measurements: what it would decide depends on the computer and on what else that computer is doing,
|
|
94
|
+
* and a build's verdict may not (lib/rules-check.mjs plays the game instead, and counts in budget units).
|
|
82
95
|
*/
|
|
83
|
-
export function smokeRun(H, compiled, id, { timer = () =>
|
|
84
|
-
let now =
|
|
85
|
-
|
|
96
|
+
export function smokeRun(H, compiled, id, { timer = () => { const t = process.cpuUsage(); return (t.user + t.system) / 1000; }, vocab = null } = {}) {
|
|
97
|
+
let now = 1_000_000_000_000;
|
|
98
|
+
let control = [];
|
|
99
|
+
let snapshotBytes = 0; let checkpointBytes = 0;
|
|
100
|
+
const frameBytes = {}; const frameRates = {}; const sentAt = {};
|
|
101
|
+
const measure = (m, text) => {
|
|
102
|
+
if (!RULES_FRAMES.includes(m.t)) return;
|
|
103
|
+
frameBytes[m.t] = Math.max(frameBytes[m.t] ?? 0, Buffer.byteLength(JSON.stringify({ ...m, rules: true })));
|
|
104
|
+
const times = (sentAt[m.t] ??= []);
|
|
105
|
+
// The large synthetic clock loses a fraction of a millisecond at sixty ticks per second.
|
|
106
|
+
while (times.length && now - times[0] >= 1000 - 0.1) times.shift();
|
|
107
|
+
times.push(now);
|
|
108
|
+
frameRates[m.t] = Math.max(frameRates[m.t] ?? 0, times.length);
|
|
109
|
+
if (m.t === 'snap') { snapshotBytes = frameBytes.snap; control = m.c.map(([seat, r, ack]) => [seat, r, 1, ack]); }
|
|
110
|
+
};
|
|
111
|
+
const host = H.createHost({ game: id, compiled, check: true, send: measure, clock: { now: () => now, setTimer: () => 0, clearTimer() {} }, random: () => 0.5 });
|
|
86
112
|
if (vocab) { host.frame({ t: 'vocabulary', vocab }); host.frame({ t: 'policy', policy: { kind: 'beginner', guides: 1, aiSeats: 0, bots: 'fill', brain: 'script' } }); }
|
|
87
|
-
host.frame({ t: 'join', peer: { id: 'build', seat: 0, name: 'Build', occ: 1 } });
|
|
113
|
+
if (typeof compiled.join === 'function') host.frame({ t: 'join', peer: { id: 'build', seat: 0, name: 'Build', occ: 1 } });
|
|
88
114
|
const period = 1000 / compiled.settings.tickHz;
|
|
89
115
|
let slowest = 0; let slowestTick = 0; let over = 0;
|
|
90
116
|
for (let i = 0; i < 3 * compiled.settings.tickHz; i += 1) {
|
|
@@ -92,6 +118,7 @@ export function smokeRun(H, compiled, id, { timer = () => performance.now(), voc
|
|
|
92
118
|
const t0 = timer();
|
|
93
119
|
host.tickNow();
|
|
94
120
|
const ms = timer() - t0;
|
|
121
|
+
checkpointBytes = Math.max(checkpointBytes, Buffer.byteLength(JSON.stringify({ t: 'ckpt', rules: true, k: host.tick, st: now, c: control, d: { rules: '0000000000000000', data: JSON.parse(new TextDecoder().decode(host.save())) } })));
|
|
95
122
|
if (ms > slowest) { slowest = ms; slowestTick = host.tick; }
|
|
96
123
|
if (ms > period) over += 1;
|
|
97
124
|
}
|
|
@@ -101,40 +128,50 @@ export function smokeRun(H, compiled, id, { timer = () => performance.now(), voc
|
|
|
101
128
|
if (facts.faults) throw new Error(`games/${id}: its rules ran for three seconds with bots and the runtime itself failed ${facts.faults} ${facts.faults === 1 ? 'time' : 'times'}. The last: ${facts.lastFault}`);
|
|
102
129
|
if (s.errors) throw new Error(`games/${id}: its rules ran for three seconds with bots and ${s.errors} ${s.errors === 1 ? 'handler' : 'handlers'} failed. The last: ${s.lastError}`);
|
|
103
130
|
if (over >= 3 || slowest > 4 * period) throw new Error(`games/${id}: its rules ran for three seconds with bots and were too slow: tick ${slowestTick} took ${Math.round(slowest)} ms${over > 1 ? `, and ${over} ticks took longer than a tick lasts` : ''}. A tick lasts ${Math.round(period)} ms at ${compiled.settings.tickHz} ticks a second. The handler that used the most: ${s.worst || 'none'} (${s.maxUnits} budget units); the busiest tick used ${s.maxTickUnits} of ${compiled.settings.budget.tick}`);
|
|
104
|
-
return { ...s, slowestMs: slowest };
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/** The ordered declarations a save depends on. Functions contribute their presence, never their implementation. */
|
|
108
|
-
export function stateHash(c) {
|
|
109
|
-
const { kinds, events, commands, effects, shared, rounds, bots, roomOn, asks, map, dims, contract, seats } = c;
|
|
110
|
-
return createHash('sha256').update(JSON.stringify({ kinds, events, commands, effects, shared, rounds, bots, roomOn, asks, map, dims, contract, seats, tickHz: c.settings.tickHz }, (k, v) => typeof v === 'function' ? true : v)).digest('hex').slice(0, 32);
|
|
131
|
+
return { ...s, slowestMs: slowest, snapshotBytes, checkpointBytes, frameBytes: { ...frameBytes, ckpt: checkpointBytes }, frameRates };
|
|
111
132
|
}
|
|
112
133
|
|
|
113
134
|
/**
|
|
114
135
|
* Everything a rules game's build needs, or an Error that says what to fix (each refused line named).
|
|
115
|
-
* Returns { code, tune, map, settings, seats, schema, publicTune, build, files, rounds,
|
|
136
|
+
* Returns { code, tune, map, settings, seats, stateHash, schema, publicTune, build, files, rounds, units, tickUnits, check }.
|
|
137
|
+
* `longCheck`: eight times the generated play (`homie-studio build --long-check`).
|
|
116
138
|
*/
|
|
117
|
-
export async function prepareRules(esbuild, root, g, { log = () => {} } = {}) {
|
|
139
|
+
export async function prepareRules(esbuild, root, g, { log = () => {}, longCheck = false } = {}) {
|
|
118
140
|
const guarded = await guardRules(esbuild, root, g.dir);
|
|
119
141
|
if (!guarded.ok) throw new Error(`games/${g.id}: its rules were refused.\n${guarded.problems.slice(0, 20).map((p) => ` ${problemLine(p)}`).join('\n')}${guarded.problems.length > 20 ? `\n … and ${guarded.problems.length - 20} more` : ''}`);
|
|
120
142
|
const tune = readJson(join(g.dir, 'tunables.json')) ?? {};
|
|
121
143
|
const map = readMap(g);
|
|
122
|
-
const { def, R, H } = await loadRules(esbuild, root, g.id, guarded.code);
|
|
123
|
-
const { settings, problems } = R.roomSettings(g.room);
|
|
124
|
-
for (const p of problems) log(`warning: games/${g.id}/game.json: ${p}`);
|
|
125
|
-
if (settings.host === 'browser') throw new Error(`games/${g.id}/game.json asks for "room": { "host": "browser" }. Rules hosted by a player's browser arrive in a later release; until then a rules game is hosted by the server ("host": "server", the default).`);
|
|
126
144
|
const seats = Math.max(1, Math.min(32, Math.floor(Number(g.players?.max)) || 8));
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
145
|
+
const bundle = await loadRules(esbuild, root, g.id, guarded.code, { bundleOnly: true });
|
|
146
|
+
const data = { id: g.id, tune, map, room: g.room, seats, vocab: readJson(join(g.dir, 'agents.json')), sites: guarded.sites };
|
|
147
|
+
// The declarations first, for the type check: a wrong program is named by the compiler before anything is played.
|
|
148
|
+
await typecheckRules(root, g, await runRulesWorker(bundle, data, { declarationsOnly: true }), tune, { log });
|
|
149
|
+
const checked = await runRulesWorker(bundle, data, { allowance: longCheck ? LONG_ALLOWANCE : ALLOWANCE });
|
|
150
|
+
for (const p of checked.problems) log(`warning: games/${g.id}/game.json: ${p}`);
|
|
151
|
+
for (const line of checked.information) log(`info: ${line}`);
|
|
152
|
+
const { settings, schema, publicTune, rounds, stateHash } = checked;
|
|
153
|
+
const { def, R, H } = await loadRules(esbuild, root, g.id, guarded.code);
|
|
154
|
+
const compiled = R.compileRules(def, { tune, map: R.compileMap(map, map.name), settings, seats });
|
|
155
|
+
const stats = smokeRun(H, compiled, g.id, { timer: () => 0, vocab: data.vocab });
|
|
156
|
+
const snapshotCap = capOf('snap', seats); const checkpointCap = capOf('ckpt', seats);
|
|
157
|
+
const exceeded = RULES_FRAMES.flatMap(t => {
|
|
158
|
+
const bytes = stats.frameBytes[t] ?? 0; const cap = capOf(t, seats);
|
|
159
|
+
const rate = stats.frameRates[t] ?? 0; const rateCap = rulesRates(settings.tickHz, seats)[t];
|
|
160
|
+
return [...(bytes > cap ? [`${t} ${bytes} B (cap ${cap} B; over by ${bytes - cap} B)`] : []),
|
|
161
|
+
...(t !== 'snap' && rate > rateCap ? [`${t} ${rate}/s (cap ${rateCap}/s; over by ${rate - rateCap}/s)`] : [])];
|
|
162
|
+
});
|
|
163
|
+
if (exceeded.length) {
|
|
164
|
+
const message = `games/${g.id}: browser rules smoke run measured snapshot ${stats.snapshotBytes} B (cap ${snapshotCap} B), checkpoint ${stats.checkpointBytes} B (cap ${checkpointCap} B). Exceeded: ${exceeded.join(', ')}. Reduce these frames or use room.host: server; offline play has no relay caps.`;
|
|
165
|
+
if (settings.host === 'browser') throw new Error(message);
|
|
166
|
+
log(`warning: ${message}`);
|
|
130
167
|
}
|
|
131
|
-
|
|
132
|
-
try { compiled = R.compileRules(def, { tune, map: R.compileMap(map, map.name), settings, seats }); } catch (error) { throw new Error(`games/${g.id}/src/rules.ts: ${error.message}`); }
|
|
133
|
-
const stats = smokeRun(H, compiled, g.id, { vocab: readJson(join(g.dir, 'agents.json')) });
|
|
134
|
-
const data = { tune, map, settings, seats };
|
|
168
|
+
const built = { tune, map, settings, seats };
|
|
135
169
|
return {
|
|
136
|
-
code: guarded.code, ...
|
|
137
|
-
build: createHash('sha256').update(guarded.code).update(JSON.stringify(
|
|
170
|
+
code: guarded.code, ...built, stateHash, schema, publicTune, files: guarded.files, rounds,
|
|
171
|
+
build: createHash('sha256').update(guarded.code).update(JSON.stringify(built)).digest('hex'),
|
|
172
|
+
snapshotCap, checkpointCap, snapshotBytes: stats.snapshotBytes, checkpointBytes: stats.checkpointBytes,
|
|
173
|
+
units: checked.maxUnits, tickUnits: checked.maxTickUnits,
|
|
174
|
+
check: { information: checked.information, plays: checked.plays, ticks: checked.ticks, rounds: checked.roundsPlayed, restores: checked.restores, units: checked.units, largestSaveBytes: checked.largestSaveBytes },
|
|
138
175
|
};
|
|
139
176
|
}
|
|
140
177
|
|
|
@@ -143,17 +180,28 @@ export async function prepareRules(esbuild, root, g, { log = () => {} } = {}) {
|
|
|
143
180
|
* declarations and its guarded move code handed to the view library (`setGame`), then the game's own view.ts. So
|
|
144
181
|
* `openRoom()` in view.ts needs no argument, and `move` in the bundle is the module the server runs.
|
|
145
182
|
*/
|
|
146
|
-
export function viewPlugin(g, rules, entry) {
|
|
183
|
+
export function viewPlugin(g, rules, entry, { lab = false } = {}) {
|
|
147
184
|
const move = join(g.dir, 'src', 'move.ts');
|
|
148
|
-
const game = { id: g.id, schema: rules.schema, tune: rules.publicTune, map: rules.map, vocab: readJson(join(g.dir, 'agents.json')) };
|
|
185
|
+
const game = { id: g.id, schema: lab ? { ...rules.schema, settings: { ...rules.settings, offline: true } } : rules.schema, tune: rules.publicTune, map: rules.map, vocab: readJson(join(g.dir, 'agents.json')) };
|
|
186
|
+
const local = lab || rules.settings.host === 'browser';
|
|
187
|
+
const available = local || rules.settings.offline;
|
|
188
|
+
const factory = local ? `import { makeHost } from 'homie:host';\nconst loadHost = () => Promise.resolve(makeHost);` : available ? `const loadHost = () => import('homie:host').then(m => m.makeHost);` : 'const loadHost = undefined;';
|
|
149
189
|
const guarded = guardedPlugin(rules.files, { stub: [join(g.dir, 'src', 'rules.ts')] });
|
|
150
190
|
return {
|
|
151
191
|
name: 'homie-rules-view',
|
|
152
192
|
setup(b) {
|
|
153
|
-
|
|
154
|
-
|
|
193
|
+
if (!lab) {
|
|
194
|
+
b.onResolve({ filter: /lab\/lab\.ts$/ }, () => ({ path: 'production-lab', namespace: 'homie-no-lab' }));
|
|
195
|
+
b.onLoad({ filter: /.*/, namespace: 'homie-no-lab' }, () => ({ contents: 'export const lab = { rulesTune: x => x, stage: null };', loader: 'js' }));
|
|
196
|
+
}
|
|
197
|
+
b.onResolve({ filter: /^homie:(?:view|game|host|rules)$/ }, (a) => ({ path: a.path, namespace: 'homie-view' }));
|
|
198
|
+
b.onLoad({ filter: /.*/, namespace: 'homie-view' }, (a) => {
|
|
199
|
+
if (a.path === 'homie:rules') return { contents: rules.code, resolveDir: g.dir, loader: 'js' };
|
|
200
|
+
if (a.path === 'homie:host') return { contents: `import def from 'homie:rules';\nimport { browserHost } from ${JSON.stringify(join(PACKAGE_ROOT, 'rules', 'browser.ts'))};\nexport const makeHost = browserHost(def, ${JSON.stringify({ id: g.id, build: rules.build, tune: rules.tune, map: rules.map, settings: rules.settings, seats: rules.seats, vocab: game.vocab })});\n`, resolveDir: g.dir, loader: 'js' };
|
|
201
|
+
return (a.path === 'homie:view'
|
|
155
202
|
? { contents: `import 'homie:game';\nimport ${JSON.stringify(entry)};\n`, resolveDir: g.dir, loader: 'js' }
|
|
156
|
-
: { contents: `${existsSync(move) ? `import { move } from ${JSON.stringify(move)};` : 'const move = {};'}\nimport { setGame${game.vocab ? ', setAgentFactory' : ''} } from '${RULES_MODULE}/view';\n${game.vocab ? `import { useAgents } from ${JSON.stringify(join(PACKAGE_ROOT, 'agents/agents.ts'))}; setAgentFactory(useAgents);` : ''}\nsetGame(${JSON.stringify(game)}, move);\n`, resolveDir: g.dir, loader: 'js' })
|
|
203
|
+
: { contents: `${existsSync(move) ? `import { move } from ${JSON.stringify(move)};` : 'const move = {};'}\nimport { setGame${game.vocab ? ', setAgentFactory' : ''} } from '${RULES_MODULE}/view';\n${game.vocab ? `import { useAgents } from ${JSON.stringify(join(PACKAGE_ROOT, 'agents/agents.ts'))}; setAgentFactory(useAgents);` : ''}\n${factory}\nsetGame(${JSON.stringify(game)}, move, loadHost);\n`, resolveDir: g.dir, loader: 'js' });
|
|
204
|
+
});
|
|
157
205
|
rulesModulesPlugin().setup(b);
|
|
158
206
|
guarded.setup(b);
|
|
159
207
|
},
|
|
@@ -170,7 +218,7 @@ const HEADER = '// Written by `homie-studio build` from this studio\'s games. Do
|
|
|
170
218
|
export function writeRules(root, built, ids) {
|
|
171
219
|
const dir = join(root, 'site', 'src', 'rules');
|
|
172
220
|
mkdirSync(dir, { recursive: true });
|
|
173
|
-
for (const r of built) {
|
|
221
|
+
for (const r of built.filter((r) => r.settings.host === 'server')) {
|
|
174
222
|
writeFileSync(join(dir, `${r.id}.mjs`), `${HEADER}// The rules of games/${r.id}, checked and guarded (build ${r.build}).\n${r.code}`);
|
|
175
223
|
writeFileSync(join(dir, `${r.id}.data.mjs`), `${HEADER}export default ${JSON.stringify({ tune: r.tune, map: r.map, settings: r.settings, seats: r.seats, build: r.build, stateHash: r.stateHash }, null, 2)};\n`);
|
|
176
224
|
}
|