@moku-labs/game 0.9.0 → 0.11.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/README.md +81 -22
- package/bin/moku-game.mjs +4 -0
- package/dist/app/page.d.mts +44 -0
- package/dist/app/page.mjs +379 -0
- package/dist/app/system.d.mts +144 -0
- package/dist/app/system.mjs +329 -0
- package/dist/app.d.mts +37 -0
- package/dist/app.mjs +235 -0
- package/dist/assets-BHwTLVta.mjs +7391 -0
- package/dist/assets.d.mts +1 -1
- package/dist/assets.mjs +1 -7390
- package/dist/cli.d.mts +70 -0
- package/dist/cli.mjs +1685 -0
- package/dist/{commands-DuwCxK5t.d.mts → commands-C4QlqijJ.d.mts} +5 -5
- package/dist/config-CdARrrhU.mjs +81 -0
- package/dist/control.d.mts +11 -10
- package/dist/control.mjs +3 -3
- package/dist/{define-B6pHpdBi.d.mts → define-CCc0ozKe.d.mts} +2 -2
- package/dist/{dev-wKLgOw7c.mjs → dev-Cc50Dk7H.mjs} +1 -1
- package/dist/drafts-BawpSaVp.mjs +156 -0
- package/dist/fake-MpzwZhIZ.mjs +87 -0
- package/dist/{headless-SCQXJxQf.d.mts → headless-G4nuZ-xz.d.mts} +1 -1
- package/dist/hot.d.mts +4 -6
- package/dist/hot.mjs +8 -9
- package/dist/{index-BO9kZzY3.d.mts → index-8TiJQjQA.d.mts} +30 -17
- package/dist/index.d.mts +5 -5
- package/dist/index.mjs +1 -1
- package/dist/inspect-la_KkjCS.mjs +1 -0
- package/dist/inspect.d.mts +7 -7
- package/dist/inspect.mjs +3 -2
- package/dist/jsx-dev-runtime.d.mts +1 -1
- package/dist/jsx-runtime.d.mts +1 -1
- package/dist/lint.mjs +10 -6
- package/dist/memory-CWlqhS7l.mjs +136 -0
- package/dist/memory-DwDhfFWK.d.mts +50 -0
- package/dist/project.mjs +3 -3
- package/dist/{run-CofJEdYx.mjs → run-wYty_Lwo.mjs} +2 -2
- package/dist/{runtime-CdNJ0UBm.d.mts → runtime-DOBYM6g4.d.mts} +1 -1
- package/dist/{input-z_35Jwfn.mjs → session-DgukhR_7.mjs} +20 -20
- package/dist/{sources-DNnzyCYf.mjs → sources-Duzj6lhV.mjs} +2 -2
- package/dist/{src-536arXjV.mjs → src-BSbtHZWk.mjs} +35 -300
- package/dist/testing.d.mts +4 -50
- package/dist/testing.mjs +3 -87
- package/dist/{types-VZVWwqCg.d.mts → types-BM4shh8K.d.mts} +259 -259
- package/dist/{types-xXfyk56Q.d.mts → types-BUwDHKXm.d.mts} +1377 -1377
- package/dist/types-Ca_z_4tQ.d.mts +586 -0
- package/dist/{types-CQueODzo.d.mts → types-CefgYzFt.d.mts} +2 -2
- package/dist/{types-DUMoYPYe.d.mts → types-DAdwEJMp.d.mts} +2 -2
- package/dist/{types-B0v5VVa3.d.mts → types-ZQLYXo5X.d.mts} +304 -304
- package/dist/visual.d.mts +6 -6
- package/dist/visual.mjs +5 -5
- package/llms.txt +120 -43
- package/package.json +30 -2
package/dist/app.mjs
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import { H as platformPlugin, i as screen, jt as audioPlugin, t as createApp, xt as effectsPlugin } from "./src-BSbtHZWk.mjs";
|
|
2
|
+
import { t as memory } from "./memory-CWlqhS7l.mjs";
|
|
3
|
+
import { t as fakeClock } from "./fake-MpzwZhIZ.mjs";
|
|
4
|
+
//#region src/app/define.ts
|
|
5
|
+
/**
|
|
6
|
+
* The moment the default fake clock starts at, in epoch milliseconds. A test stamps times from it.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```ts
|
|
10
|
+
* game.headless().clock.now() === startMoment; // true
|
|
11
|
+
* ```
|
|
12
|
+
*/
|
|
13
|
+
const startMoment = 1e6;
|
|
14
|
+
/** The rng seed of a new save when neither the definition nor the seam names one. */
|
|
15
|
+
const defaultSeed = 42;
|
|
16
|
+
/** The plugins every app has: the engine's core five and the core plugins `log` and `env`. */
|
|
17
|
+
const corePluginNames = [
|
|
18
|
+
"time",
|
|
19
|
+
"lifecycle",
|
|
20
|
+
"model",
|
|
21
|
+
"clock",
|
|
22
|
+
"flow",
|
|
23
|
+
"log",
|
|
24
|
+
"env"
|
|
25
|
+
];
|
|
26
|
+
/**
|
|
27
|
+
* Builds the error of a list entry that is not a feature.
|
|
28
|
+
*
|
|
29
|
+
* @param path - The list and index, `features[2]`, or `shared`.
|
|
30
|
+
* @returns The error.
|
|
31
|
+
* @example
|
|
32
|
+
* ```ts
|
|
33
|
+
* notAFeature("features[2]").message; // "[game] defineGameApp: features[2] is not a feature.\n Make it with defineFeature(name, description)."
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
function notAFeature(path) {
|
|
37
|
+
return /* @__PURE__ */ new Error(`[game] defineGameApp: ${path} is not a feature.\n Make it with defineFeature(name, description).`);
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Throws for the first entry of a feature list that `defineFeature` did not make. The type already
|
|
41
|
+
* says so; a game in JavaScript, or one that casts, is caught here, at the definition.
|
|
42
|
+
*
|
|
43
|
+
* @param list - `features`, `headless.features`, or `[shared]`.
|
|
44
|
+
* @param pathOf - Names an entry by its index.
|
|
45
|
+
* @throws {Error} When an entry has no `logicOnly`.
|
|
46
|
+
*/
|
|
47
|
+
function checkFeatures(list, pathOf) {
|
|
48
|
+
for (const [index, entry] of list.entries()) if (entry?.logicOnly === void 0) throw notAFeature(pathOf(index));
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Checks what TypeScript cannot stop: a definition from JavaScript, or one that casts.
|
|
52
|
+
*
|
|
53
|
+
* @param definition - The game's definition.
|
|
54
|
+
* @throws {Error} When `flow` is missing, or a feature entry is not a feature.
|
|
55
|
+
*/
|
|
56
|
+
function checkDefinition(definition) {
|
|
57
|
+
if (definition.flow === void 0) throw new Error("[game] defineGameApp needs flow.\n Pass the main flow: defineGameApp({ flow: mainFlow, ... }).");
|
|
58
|
+
if (definition.shared !== void 0) checkFeatures([definition.shared], () => "shared");
|
|
59
|
+
checkFeatures(definition.features ?? [], (index) => `features[${index}]`);
|
|
60
|
+
checkFeatures(definition.headless?.features ?? [], (index) => `headless.features[${index}]`);
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Copies the keys of a seam whose value is set, so a seam left out never erases a game value.
|
|
64
|
+
*
|
|
65
|
+
* @param seam - One seam object, or `undefined`.
|
|
66
|
+
* @returns The set keys only.
|
|
67
|
+
* @example
|
|
68
|
+
* ```ts
|
|
69
|
+
* definedKeys({ journal: 200, context: undefined }); // { journal: 200 }
|
|
70
|
+
* ```
|
|
71
|
+
*/
|
|
72
|
+
function definedKeys(seam) {
|
|
73
|
+
if (seam === void 0) return {};
|
|
74
|
+
return Object.fromEntries(Object.entries(seam).filter(([, value]) => value !== void 0));
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Merges the seam-owned keys over the game's plugin configs, one plugin at a time: a key the game
|
|
78
|
+
* owns stays, a key the shell owns is written. With `screen` the screen keys are written too.
|
|
79
|
+
*
|
|
80
|
+
* @param definition - The game's definition.
|
|
81
|
+
* @param seams - The seams of this call.
|
|
82
|
+
* @param runtime - The clock and the save provider of this call.
|
|
83
|
+
* @param withScreen - Whether the app is the screen app.
|
|
84
|
+
* @returns The plugin configs of the app.
|
|
85
|
+
*/
|
|
86
|
+
function mergeConfigs(definition, seams, runtime, withScreen) {
|
|
87
|
+
const game = definition.pluginConfigs ?? {};
|
|
88
|
+
const logic = {
|
|
89
|
+
...game,
|
|
90
|
+
model: {
|
|
91
|
+
...game.model,
|
|
92
|
+
playerProvider: runtime.provider,
|
|
93
|
+
initialPlayer: seams.player ?? definition.player,
|
|
94
|
+
initialSession: seams.session ?? definition.session,
|
|
95
|
+
seed: seams.seed ?? definition.seed ?? defaultSeed
|
|
96
|
+
},
|
|
97
|
+
clock: { source: runtime.clock },
|
|
98
|
+
flow: {
|
|
99
|
+
...game.flow,
|
|
100
|
+
mainFlow: definition.flow,
|
|
101
|
+
safeNode: definition.safeNode
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
if (!withScreen) return logic;
|
|
105
|
+
return {
|
|
106
|
+
...logic,
|
|
107
|
+
platform: {
|
|
108
|
+
provider: seams.platform,
|
|
109
|
+
keepAwake: seams.keepAwake ?? false
|
|
110
|
+
},
|
|
111
|
+
renderer: {
|
|
112
|
+
...game.renderer,
|
|
113
|
+
...definedKeys(seams.renderer)
|
|
114
|
+
},
|
|
115
|
+
assets: {
|
|
116
|
+
...game.assets,
|
|
117
|
+
manifest: seams.manifest,
|
|
118
|
+
io: seams.io
|
|
119
|
+
},
|
|
120
|
+
audio: {
|
|
121
|
+
...game.audio,
|
|
122
|
+
...definedKeys(seams.audio)
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Keeps the configs of the plugins an app composes. Core takes unknown keys at run time, but the
|
|
128
|
+
* headless app's type would not: a headless app gets no screen or unlisted game plugin config.
|
|
129
|
+
*
|
|
130
|
+
* @param configs - The merged configs.
|
|
131
|
+
* @param plugins - The plugins the app composes beyond the core.
|
|
132
|
+
* @returns The configs of the composed plugins only.
|
|
133
|
+
*/
|
|
134
|
+
function composedOnly(configs, plugins) {
|
|
135
|
+
const names = /* @__PURE__ */ new Set([...corePluginNames, ...plugins.map((plugin) => plugin.name)]);
|
|
136
|
+
return Object.fromEntries(Object.entries(configs).filter(([name]) => names.has(name)));
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Picks the clock and the save provider of one call: the seams', or a fresh fake clock at
|
|
140
|
+
* `startMoment` and a fresh memory provider.
|
|
141
|
+
*
|
|
142
|
+
* @param seams - The seams of this call.
|
|
143
|
+
* @returns The clock and the provider.
|
|
144
|
+
*/
|
|
145
|
+
function runtimeOf(seams) {
|
|
146
|
+
return {
|
|
147
|
+
clock: seams.clock ?? fakeClock(1e6),
|
|
148
|
+
provider: seams.provider ?? memory()
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Creates one app over the engine: the plugins in order, the merged configs, and `referenceLong`
|
|
153
|
+
* only when the definition sets it.
|
|
154
|
+
*
|
|
155
|
+
* @param definition - The game's definition.
|
|
156
|
+
* @param plugins - The plugins beyond the core, in order.
|
|
157
|
+
* @param pluginConfigs - The merged configs.
|
|
158
|
+
* @returns The app, not started.
|
|
159
|
+
*/
|
|
160
|
+
function appOf(definition, plugins, pluginConfigs) {
|
|
161
|
+
return createApp({
|
|
162
|
+
plugins: [...plugins],
|
|
163
|
+
pluginConfigs,
|
|
164
|
+
...definition.referenceLong === void 0 ? {} : { config: { referenceLong: definition.referenceLong } }
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Makes the headless app of a definition: the logic of `headless.features`, then
|
|
169
|
+
* `headless.plugins`.
|
|
170
|
+
*
|
|
171
|
+
* @param definition - The game's definition.
|
|
172
|
+
* @param seams - The seams of this call.
|
|
173
|
+
* @returns The app, not started, and its clock and provider.
|
|
174
|
+
*/
|
|
175
|
+
function headlessOf(definition, seams) {
|
|
176
|
+
const runtime = runtimeOf(seams);
|
|
177
|
+
const plugins = [...(definition.headless?.features ?? []).map((feature) => feature.logicOnly), ...definition.headless?.plugins ?? []];
|
|
178
|
+
return {
|
|
179
|
+
app: appOf(definition, plugins, composedOnly(mergeConfigs(definition, seams, runtime, false), plugins)),
|
|
180
|
+
...runtime
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Makes the screen app of a definition: the screen set, `audio`, `effects`, `platform`, then
|
|
185
|
+
* `shared`, the features and the game's plugins.
|
|
186
|
+
*
|
|
187
|
+
* @param definition - The game's definition.
|
|
188
|
+
* @param seams - The seams of this call.
|
|
189
|
+
* @returns The app, not started, and its clock and provider.
|
|
190
|
+
*/
|
|
191
|
+
function screenOf(definition, seams) {
|
|
192
|
+
const runtime = runtimeOf(seams);
|
|
193
|
+
return {
|
|
194
|
+
app: appOf(definition, [
|
|
195
|
+
...screen,
|
|
196
|
+
audioPlugin,
|
|
197
|
+
effectsPlugin,
|
|
198
|
+
platformPlugin,
|
|
199
|
+
...definition.shared === void 0 ? [] : [definition.shared],
|
|
200
|
+
...definition.features ?? [],
|
|
201
|
+
...definition.plugins ?? []
|
|
202
|
+
], mergeConfigs(definition, seams, runtime, true)),
|
|
203
|
+
...runtime
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Turns a game's one data object into the game: `game.headless()` for logic tests and
|
|
208
|
+
* `game.screen()` for the page and the screen tests. Every type comes from the object: the plugin
|
|
209
|
+
* tuples, the player and the session. Checks the definition at once, so an error points at
|
|
210
|
+
* `index.ts`.
|
|
211
|
+
*
|
|
212
|
+
* @param definition - The game: its flow, starting state, features, plugins and plugin configs.
|
|
213
|
+
* @returns The game, with `headless()` and `screen()`; no app is created yet.
|
|
214
|
+
* @throws {Error} When `flow` is missing, or an entry of `shared`, `features` or `headless.features` is not a feature.
|
|
215
|
+
* @example
|
|
216
|
+
* ```ts
|
|
217
|
+
* // index.ts of a game: the whole game, no createApp call. Its tests import it as `game`.
|
|
218
|
+
* export default defineGameApp({
|
|
219
|
+
* flow: mainFlow, safeNode: "home", player: startingPlayer, session: startingSession,
|
|
220
|
+
* shared: sharedFeature, features: [homeFeature, boardFeature, rewardFeature], plugins: [loadingPlugin],
|
|
221
|
+
* headless: { features: [rewardFeature] }, pluginConfigs: { ui: { tapTargetPt: 48 } }
|
|
222
|
+
* });
|
|
223
|
+
* game.headless({ seed: 7 }).app.flow.state().running; // false: the test starts it
|
|
224
|
+
* ```
|
|
225
|
+
*/
|
|
226
|
+
function defineGameApp(definition) {
|
|
227
|
+
const wide = definition;
|
|
228
|
+
checkDefinition(wide);
|
|
229
|
+
return {
|
|
230
|
+
headless: (seams = {}) => headlessOf(wide, seams),
|
|
231
|
+
screen: (seams = {}) => screenOf(wide, seams)
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
//#endregion
|
|
235
|
+
export { defineGameApp, startMoment };
|