@moku-labs/game 0.0.1 → 0.0.2

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.
@@ -0,0 +1,182 @@
1
+ import { n as isComponentDefinition } from "./component-DGg5DqKK.mjs";
2
+ //#region src/plugins/ui/jsx/flatten.ts
3
+ /** The type of the node `Fragment` builds. It never reaches a reconcile: `flatten` lifts it. */
4
+ const FRAGMENT = "#fragment";
5
+ /**
6
+ * Builds the node a bare string or number child becomes.
7
+ *
8
+ * @param content - What stood between the tags.
9
+ * @returns A text node carrying it.
10
+ * @example
11
+ * ```ts
12
+ * textNode(5); // { type: "text", props: { content: "5" }, children: [] }
13
+ * ```
14
+ */
15
+ function textNode(content) {
16
+ return {
17
+ type: "text",
18
+ props: { content: String(content) },
19
+ children: []
20
+ };
21
+ }
22
+ /**
23
+ * Tells a description node from the other things a child may be.
24
+ *
25
+ * @param child - One child of a tag.
26
+ * @returns True when it is a node.
27
+ * @example
28
+ * ```ts
29
+ * isNode({ type: "row", props: {}, children: [] }); // true
30
+ * ```
31
+ */
32
+ function isNode(child) {
33
+ return typeof child === "object" && child !== null && !Array.isArray(child) && "type" in child;
34
+ }
35
+ /**
36
+ * Turns whatever stood between two tags into the children of that tag.
37
+ *
38
+ * @param child - One child, an array of children, or nothing.
39
+ * @returns The children as nodes, fragments lifted and empty values dropped.
40
+ * @example
41
+ * ```ts
42
+ * flatten(["a", undefined, 2]).map(node => node.props.content); // ["a", "2"]
43
+ * ```
44
+ */
45
+ function flatten(child) {
46
+ if (child === null || child === void 0 || typeof child === "boolean") return [];
47
+ if (typeof child === "string" || typeof child === "number") return [textNode(child)];
48
+ if (Array.isArray(child)) {
49
+ const nodes = [];
50
+ for (const item of child) nodes.push(...flatten(item));
51
+ return nodes;
52
+ }
53
+ if (!isNode(child)) return [];
54
+ if (child.type === "#fragment") return [...child.children];
55
+ return [child];
56
+ }
57
+ //#endregion
58
+ //#region src/plugins/ui/jsx/runtime.ts
59
+ /**
60
+ * @file ui/jsx — the JSX runtime itself: the three factories a TypeScript build calls, the
61
+ * fragment, and the `JSX` namespace `"jsxImportSource": "@moku-labs/game"` resolves to. Pure and
62
+ * reachable only through `src/jsx-runtime.ts` and `src/jsx-dev-runtime.ts` (lint L9).
63
+ */
64
+ /**
65
+ * Splits the children out of the props the compiler built, so a node never carries them twice.
66
+ *
67
+ * @param props - What the compiler passed.
68
+ * @returns The props without `children`.
69
+ * @example
70
+ * ```ts
71
+ * withoutChildren({ style: {}, children: "a" }); // { style: {} }
72
+ * ```
73
+ */
74
+ function withoutChildren(props) {
75
+ const rest = { ...props };
76
+ delete rest.children;
77
+ return rest;
78
+ }
79
+ /**
80
+ * Wraps what a plain function component returned so it can stand where one node is expected.
81
+ *
82
+ * @param produced - What the function returned.
83
+ * @param key - The key the tag carried, if any.
84
+ * @returns One node; several results are lifted by the parent's `flatten`.
85
+ */
86
+ function asSingleNode(produced, key) {
87
+ const nodes = flatten(produced);
88
+ const single = nodes.length === 1 ? nodes[0] : void 0;
89
+ if (single === void 0) return {
90
+ type: FRAGMENT,
91
+ props: {},
92
+ children: nodes
93
+ };
94
+ if (key === void 0) return single;
95
+ return {
96
+ ...single,
97
+ key
98
+ };
99
+ }
100
+ /**
101
+ * Builds one description node. A string tag becomes an intrinsic, a component definition becomes
102
+ * one node the reconcile expands, and a plain function runs now.
103
+ *
104
+ * @param type - The tag: a string, `Fragment`, a component definition or a plain function.
105
+ * @param props - What the compiler collected, `children` included.
106
+ * @param key - The `key` attribute, passed as the third argument, never inside `props`.
107
+ * @returns The node.
108
+ * @example
109
+ * ```ts
110
+ * jsx("row", { style: { gap: 8 } }, "tabs"); // { type: "row", key: "tabs", props: { style: { gap: 8 } }, children: [] }
111
+ * ```
112
+ */
113
+ function jsx(type, props, key) {
114
+ const rest = withoutChildren(props);
115
+ if (type === Fragment) return {
116
+ type: FRAGMENT,
117
+ props: rest,
118
+ children: flatten(props.children)
119
+ };
120
+ if (isComponentDefinition(type)) {
121
+ const node = {
122
+ type: type.name,
123
+ props,
124
+ children: []
125
+ };
126
+ return key === void 0 ? node : {
127
+ ...node,
128
+ key
129
+ };
130
+ }
131
+ if (typeof type === "function") return asSingleNode(type(props), key);
132
+ const node = {
133
+ type: String(type),
134
+ props: rest,
135
+ children: flatten(props.children)
136
+ };
137
+ return key === void 0 ? node : {
138
+ ...node,
139
+ key
140
+ };
141
+ }
142
+ /**
143
+ * The factory for a tag with several static children. The same function as `jsx`.
144
+ */
145
+ const jsxs = jsx;
146
+ /**
147
+ * The development factory. The last three arguments are debug information the runtime drops.
148
+ *
149
+ * @param type - The tag.
150
+ * @param props - What the compiler collected.
151
+ * @param key - The `key` attribute.
152
+ * @param _isStatic - Whether the children list is static. Ignored.
153
+ * @param _source - The file and line of the tag. Ignored.
154
+ * @param _self - The `this` of the enclosing scope. Ignored.
155
+ * @returns The node `jsx` builds.
156
+ * @example
157
+ * ```ts
158
+ * jsxDEV("row", {}, "tabs", false, { fileName: "hud.tsx" }, undefined).key; // "tabs"
159
+ * ```
160
+ */
161
+ function jsxDEV(type, props, key, _isStatic, _source, _self) {
162
+ return jsx(type, props, key);
163
+ }
164
+ /**
165
+ * Groups children without a tag of its own. Its children are lifted into the parent.
166
+ *
167
+ * @param props - The children between the two ends of the fragment.
168
+ * @returns The fragment node, which `flatten` lifts.
169
+ * @example
170
+ * ```ts
171
+ * Fragment({ children: ["a"] }).children.length; // 1
172
+ * ```
173
+ */
174
+ function Fragment(props) {
175
+ return {
176
+ type: FRAGMENT,
177
+ props: {},
178
+ children: flatten(props.children)
179
+ };
180
+ }
181
+ //#endregion
182
+ export { jsxs as i, jsx as n, jsxDEV as r, Fragment as t };
@@ -0,0 +1,77 @@
1
+ import { a as JsxChild, m as UiIntrinsicElements, t as DescriptionNode } from "./types-yg_ywtT-.mjs";
2
+
3
+ //#region src/plugins/ui/jsx/runtime.d.ts
4
+ /** What the compiler passes as props: the children plus whatever the tag declared. */
5
+ type JsxProperties = Record<string, unknown> & {
6
+ children?: JsxChild;
7
+ };
8
+ /**
9
+ * Builds one description node. A string tag becomes an intrinsic, a component definition becomes
10
+ * one node the reconcile expands, and a plain function runs now.
11
+ *
12
+ * @param type - The tag: a string, `Fragment`, a component definition or a plain function.
13
+ * @param props - What the compiler collected, `children` included.
14
+ * @param key - The `key` attribute, passed as the third argument, never inside `props`.
15
+ * @returns The node.
16
+ * @example
17
+ * ```ts
18
+ * jsx("row", { style: { gap: 8 } }, "tabs"); // { type: "row", key: "tabs", props: { style: { gap: 8 } }, children: [] }
19
+ * ```
20
+ */
21
+ declare function jsx(type: unknown, props: JsxProperties, key?: string): DescriptionNode;
22
+ /**
23
+ * The factory for a tag with several static children. The same function as `jsx`.
24
+ */
25
+ declare const jsxs: typeof jsx;
26
+ /**
27
+ * The development factory. The last three arguments are debug information the runtime drops.
28
+ *
29
+ * @param type - The tag.
30
+ * @param props - What the compiler collected.
31
+ * @param key - The `key` attribute.
32
+ * @param _isStatic - Whether the children list is static. Ignored.
33
+ * @param _source - The file and line of the tag. Ignored.
34
+ * @param _self - The `this` of the enclosing scope. Ignored.
35
+ * @returns The node `jsx` builds.
36
+ * @example
37
+ * ```ts
38
+ * jsxDEV("row", {}, "tabs", false, { fileName: "hud.tsx" }, undefined).key; // "tabs"
39
+ * ```
40
+ */
41
+ declare function jsxDEV(type: unknown, props: JsxProperties, key?: string, _isStatic?: boolean, _source?: unknown, _self?: unknown): DescriptionNode;
42
+ /**
43
+ * Groups children without a tag of its own. Its children are lifted into the parent.
44
+ *
45
+ * @param props - The children between the two ends of the fragment.
46
+ * @returns The fragment node, which `flatten` lifts.
47
+ * @example
48
+ * ```ts
49
+ * Fragment({ children: ["a"] }).children.length; // 1
50
+ * ```
51
+ */
52
+ declare function Fragment(props: JsxProperties): DescriptionNode;
53
+ /**
54
+ * The namespace TypeScript reads a `.tsx` file against. A game sets `"jsx": "react-jsx"` and
55
+ * `"jsxImportSource": "@moku-labs/game"`, and both entry files export this namespace.
56
+ */
57
+ declare namespace JSX {
58
+ /** What a tag evaluates to. */
59
+ interface Element extends DescriptionNode {}
60
+ /** The thirteen tags a screen is written with. */
61
+ interface IntrinsicElements extends UiIntrinsicElements {}
62
+ /** The prop children are collected into. */
63
+ interface ElementChildrenAttribute {
64
+ children: unknown;
65
+ }
66
+ /** The attribute every tag takes next to its own props. */
67
+ interface IntrinsicAttributes {
68
+ key?: string;
69
+ }
70
+ /** The same attribute on a component tag. */
71
+ interface IntrinsicClassAttributes<Component> {
72
+ key?: string;
73
+ ref?: Component;
74
+ }
75
+ }
76
+ //#endregion
77
+ export { jsxs as a, jsxDEV as i, JSX as n, jsx as r, Fragment as t };
@@ -0,0 +1,145 @@
1
+ //#region src/plugins/flow/doors/define.ts
2
+ /** Lowercase-first camelCase words joined by dots, at least two: `game.position`. */
3
+ const idPattern = /^[a-z][a-zA-Z0-9]*(\.[a-z][a-zA-Z0-9]*)+$/;
4
+ /**
5
+ * Refuses an id that is not a dotted name.
6
+ *
7
+ * @param kind - `"source"` or `"command"`, for the message.
8
+ * @param id - The id to check.
9
+ * @throws {Error} When the id is not camelCase words joined by dots.
10
+ */
11
+ function checkId(kind, id) {
12
+ if (idPattern.test(id)) return;
13
+ throw new Error(`[game] The ${kind} id "${id}" is not a dotted name.\n Name it like "game.position": camelCase words joined by dots.`);
14
+ }
15
+ /**
16
+ * Declares a source: a read-only view the editor lists, reads and watches. The types flow from
17
+ * the object: the input from `input`, the app from the `read` parameter, the output from its
18
+ * result.
19
+ *
20
+ * @param source - The descriptor.
21
+ * @returns The same descriptor, frozen.
22
+ * @throws {Error} When the id is not a dotted name such as `"game.position"`.
23
+ * @example
24
+ * ```ts
25
+ * // A game's .dev module: the orders on the board, re-read after every edge.
26
+ * export const orders = defineSource({
27
+ * id: "timber.orders",
28
+ * title: "Orders",
29
+ * input: {},
30
+ * changes: "edge",
31
+ * read: app => app.flow.state().path
32
+ * });
33
+ * read(app, orders); // "board/awaitIntent"
34
+ * ```
35
+ */
36
+ function defineSource(source) {
37
+ checkId("source", source.id);
38
+ Object.freeze(source.input);
39
+ return Object.freeze(source);
40
+ }
41
+ /**
42
+ * Declares a command: a dev-only action the editor lists and runs. Its body starts with the
43
+ * inline dev guard, so a production build drops it.
44
+ *
45
+ * @param command - The descriptor.
46
+ * @returns The same descriptor, frozen.
47
+ * @throws {Error} When the id is not a dotted name such as `"game.answer"`.
48
+ * @example
49
+ * ```ts
50
+ * // A game's .dev module: open the shop from anywhere, through the graph.
51
+ * export const openShop = defineCommand({
52
+ * id: "timber.openShop",
53
+ * title: "Open the shop",
54
+ * input: {},
55
+ * effect: "route",
56
+ * run: app => {
57
+ * if (typeof __MOKU_GAME_DEV__ === "undefined" || !__MOKU_GAME_DEV__) throw controlRefused();
58
+ * return app.flow.walk([{ at: "home", intent: "shop" }]);
59
+ * }
60
+ * });
61
+ * (await run(app, openShop)).state.path; // "shop"
62
+ * ```
63
+ */
64
+ function defineCommand(command) {
65
+ checkId("command", command.id);
66
+ Object.freeze(command.input);
67
+ return Object.freeze(command);
68
+ }
69
+ //#endregion
70
+ //#region src/plugins/flow/doors/input.ts
71
+ /** The input of a call that passed none. */
72
+ const noInput = Object.freeze({});
73
+ /**
74
+ * Hands the given input on, or an empty one. `InputArguments` and `WatchInput` let a call leave
75
+ * the input out only when every field of the schema is optional, so `{}` is an input of that
76
+ * schema.
77
+ *
78
+ * @param input - The input of the call, if any.
79
+ * @returns The input to pass to the descriptor.
80
+ * @example
81
+ * ```ts
82
+ * inputOrEmpty<{ last: "number?" }>(undefined); // {}
83
+ * ```
84
+ */
85
+ function inputOrEmpty(input) {
86
+ return input ?? noInput;
87
+ }
88
+ //#endregion
89
+ //#region src/plugins/flow/doors/session.ts
90
+ /** The journal of an app that ran no cheat. */
91
+ const noCheats = Object.freeze([]);
92
+ const sessions = /*#__PURE__*/ new WeakMap();
93
+ /**
94
+ * Tells whether a cheat or a raw command ran on this app.
95
+ *
96
+ * @param app - The app.
97
+ * @returns True once a `cheat` or `raw` command ran.
98
+ * @example
99
+ * ```ts
100
+ * // After run(app, commands.restore, { repro }): a restore is raw.
101
+ * isTainted(app); // true
102
+ * ```
103
+ */
104
+ function isTainted(app) {
105
+ return sessions.get(app)?.tainted ?? false;
106
+ }
107
+ /**
108
+ * Reads the journal of cheat and raw commands of this app, oldest first.
109
+ *
110
+ * @param app - The app.
111
+ * @returns The frozen journal, the same list until the next cheat.
112
+ * @example
113
+ * ```ts
114
+ * // After run(app, commands.restore, { bookmark }) on frame 96.
115
+ * cheatsOf(app); // [{ id: "game.restore", input: { bookmark: { path: "home", ... } }, frame: 96 }]
116
+ * ```
117
+ */
118
+ function cheatsOf(app) {
119
+ return sessions.get(app)?.cheats ?? noCheats;
120
+ }
121
+ /**
122
+ * Taints the session of this app and journals the command. The input is copied, so the caller
123
+ * cannot rewrite the journal afterwards.
124
+ *
125
+ * @param app - The app.
126
+ * @param id - The command id.
127
+ * @param input - The input the command got.
128
+ * @param frame - The frame it runs on.
129
+ */
130
+ function recordCheat(app, id, input, frame) {
131
+ const session = sessions.get(app) ?? {
132
+ tainted: true,
133
+ cheats: noCheats
134
+ };
135
+ const entry = Object.freeze({
136
+ id,
137
+ input: structuredClone(input),
138
+ frame
139
+ });
140
+ session.tainted = true;
141
+ session.cheats = Object.freeze([...session.cheats.slice(-499), entry]);
142
+ sessions.set(app, session);
143
+ }
144
+ //#endregion
145
+ export { defineCommand as a, inputOrEmpty as i, isTainted as n, defineSource as o, recordCheat as r, cheatsOf as t };
@@ -1,125 +1,27 @@
1
- import { E as Json, L as RngState, M as ProviderCall, N as SaveDoc, S as FakeClock, U as Api, h as RouteStep, j as PlayerStateProvider, m as JournalEntry, p as FlowState, t as Api$1, y as Answer } from "./types-BfsmUzLC.mjs";
1
+ import { H as PlayerStateProvider, L as Json, U as ProviderCall, W as SaveDoc, it as FakeClock } from "./types-JNc_UQBo.mjs";
2
+ import { a as createHeadless, i as ReproResult, n as HeadlessGame, o as runRepro, r as Repro, s as stepFrames, t as HeadlessApp } from "./headless-KaSWcd0s.mjs";
2
3
 
3
4
  //#region src/plugins/clock/fake.d.ts
4
5
  /**
5
- * Creates a fake source for tests. Timers fire synchronously inside `advance`, in due order.
6
+ * Creates a fake source for tests. Timers fire synchronously inside `advance`, in due order. A
7
+ * negative delay counts as zero and an unknown handle is ignored.
6
8
  *
7
9
  * @param start - Starting moment in epoch milliseconds. Defaults to 0.
8
10
  * @returns A clock source with the test-only controls `advance` and `set`.
9
11
  * @example
10
12
  * ```ts
13
+ * // An energy point refills 60 s after it was spent. The test moves the clock, not the minute.
11
14
  * const clock = fakeClock(1000);
12
- * clock.advance(5000);
13
- * ```
14
- */
15
- declare function fakeClock(start?: number): FakeClock;
16
- //#endregion
17
- //#region src/plugins/flow/headless.d.ts
18
- /**
19
- * The part of an app the headless helpers use. Structural on purpose: any app created with the
20
- * default plugins fits.
21
- *
22
- * @example
23
- * ```ts
24
- * const app: HeadlessApp = createApp({ pluginConfigs: { flow: { mainFlow } } });
25
- * ```
26
- */
27
- type HeadlessApp = {
28
- start(): Promise<void>;
29
- stop(): Promise<void>;
30
- time: Api;
31
- flow: Api$1;
32
- };
33
- /**
34
- * A game played without a screen: answers go through the gate, effects resolve instantly.
35
- *
36
- * @example
37
- * ```ts
38
- * const game: HeadlessGame = await createHeadless(app);
39
- * await game.walk([{ at: "home", intent: "play" }]);
40
- * ```
41
- */
42
- type HeadlessGame = {
43
- walk(route: readonly RouteStep[]): Promise<FlowState>;
44
- answer(answer: Answer): boolean;
45
- state(): FlowState;
46
- history(): readonly JournalEntry[];
47
- stop(): Promise<void>;
48
- };
49
- /**
50
- * A reproducible run: a starting state, an optional checkpoint and the route played from it.
15
+ * const app = createApp({ pluginConfigs: { clock: { source: clock } } });
16
+ * const seen: number[] = [];
51
17
  *
52
- * @example
53
- * ```ts
54
- * const repro: Repro = { player: { coins: 0 }, checkpoint: "home", route: [{ at: "home", intent: "play" }] };
18
+ * app.clock.onElapsed(({ now }) => seen.push(now));
19
+ * app.clock.scheduleAt(61_000);
20
+ * clock.advance(60_000); // the timer fires inside advance, nothing waits
21
+ * seen; // [61000]
55
22
  * ```
56
23
  */
57
- type Repro = {
58
- player: Json;
59
- session?: Json;
60
- rng?: RngState;
61
- checkpoint?: string;
62
- route: RouteStep[];
63
- };
64
- /**
65
- * What a repro run ends with: the path where the loop rests and the committed state.
66
- *
67
- * @example
68
- * ```ts
69
- * const { path, player }: ReproResult = await runRepro(app, repro);
70
- * ```
71
- */
72
- type ReproResult = {
73
- path: string[];
74
- player: Json;
75
- session: Json;
76
- };
77
- /**
78
- * Starts an app headless: sets flow mode `"fast"`, awaits `app.start()`, starts `flow.run()`
79
- * unless the app's own `onStart` already did, and waits until the loop rests for the first time.
80
- * `game.state().path` therefore names the first rest node of the graph as soon as the call
81
- * resolves. A fatal error of the loop rejects this call, and a later one is re-thrown by `walk`
82
- * and by `stop`, so a headless test never loses it.
83
- *
84
- * @param app - An app that is not started yet.
85
- * @returns The game: walk it, answer it, read it, stop it.
86
- * @throws {Error} When the loop failed fatally before it reached its first rest node.
87
- * @example
88
- * ```ts
89
- * const game = await createHeadless(app);
90
- * expect(game.state().path).toBe("home");
91
- * await game.walk([{ at: "board/awaitIntent", intent: "merge", payload: { from: "c2", to: "c3" } }]);
92
- * await game.stop();
93
- * ```
94
- */
95
- declare function createHeadless(app: HeadlessApp): Promise<HeadlessGame>;
96
- /**
97
- * Plays a repro: starts the app headless, restores the bookmark built from its state and
98
- * checkpoint, then walks its route. The app keeps running, so the caller can read more than the
99
- * result and stops it itself.
100
- *
101
- * @param app - An app that is not started yet.
102
- * @param repro - Starting state, optional checkpoint and the route.
103
- * @returns The path the run ended at, and the committed state.
104
- * @example
105
- * ```ts
106
- * const result = await runRepro(app, { player: saved, checkpoint: "home", route });
107
- * ```
108
- */
109
- declare function runRepro(app: HeadlessApp, repro: Repro): Promise<ReproResult>;
110
- /**
111
- * Steps the frame loop by hand: `count` calls of `app.time.step(deltaMs)`. A headless game has no
112
- * frame source, so this is the only thing that moves the frame phases.
113
- *
114
- * @param app - A started app.
115
- * @param count - Number of frames.
116
- * @param deltaMs - Milliseconds per frame.
117
- * @example
118
- * ```ts
119
- * stepFrames(app, 60, 16);
120
- * ```
121
- */
122
- declare function stepFrames(app: HeadlessApp, count: number, deltaMs: number): void;
24
+ declare function fakeClock(start?: number): FakeClock;
123
25
  //#endregion
124
26
  //#region src/plugins/model/store/providers/memory.d.ts
125
27
  /**
@@ -129,13 +31,21 @@ declare function stepFrames(app: HeadlessApp, count: number, deltaMs: number): v
129
31
  * long as the provider does, so a second app over the same instance reads what the first one
130
32
  * wrote, and nothing survives the process.
131
33
  *
34
+ * `load()` reads the fixture, or everything committed since; without either, the player is new.
35
+ * `commitDurable()` resolves at once: there is no disk behind it.
36
+ *
132
37
  * @param fixture - The save `load()` starts from. Omitted: a new player.
133
38
  * @param fixture.state - The saved document.
134
39
  * @param fixture.version - Schema version of the saved document.
135
40
  * @returns A provider that keeps its document and records its calls.
136
41
  * @example
137
42
  * ```ts
138
- * const provider = memory({ state: saveOf({ coins: 5 }, 42), version: 1 });
43
+ * // A new player is saved at once. One roll reaches the provider at the next rest node.
44
+ * const provider = memory();
45
+ * const game = await createHeadless(createGame(provider));
46
+ *
47
+ * await game.walk([{ at: "home", intent: "roll" }]);
48
+ * provider.calls.map(call => call.method); // ["load", "commit", "commit"]
139
49
  * ```
140
50
  */
141
51
  declare function memory(fixture?: {
@@ -153,6 +63,8 @@ declare function memory(fixture?: {
153
63
  * @returns The save document.
154
64
  * @example
155
65
  * ```ts
66
+ * // A returning player with 5 coins. The test starts from this save, not from `initialPlayer`.
67
+ * saveOf({ coins: 5 }, 42); // { player: { coins: 5 }, rng: { seed: 42, streams: {} } }
156
68
  * const provider = memory({ state: saveOf({ coins: 5 }, 42), version: 1 });
157
69
  * ```
158
70
  */