@arcane-engine/runtime 0.1.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.
Files changed (57) hide show
  1. package/README.md +38 -0
  2. package/index.ts +19 -0
  3. package/package.json +53 -0
  4. package/src/agent/agent.test.ts +384 -0
  5. package/src/agent/describe.ts +72 -0
  6. package/src/agent/index.ts +20 -0
  7. package/src/agent/protocol.ts +125 -0
  8. package/src/agent/types.ts +73 -0
  9. package/src/pathfinding/astar.test.ts +208 -0
  10. package/src/pathfinding/astar.ts +193 -0
  11. package/src/pathfinding/index.ts +2 -0
  12. package/src/pathfinding/types.ts +21 -0
  13. package/src/physics/aabb.ts +54 -0
  14. package/src/physics/index.ts +2 -0
  15. package/src/rendering/animation.test.ts +119 -0
  16. package/src/rendering/animation.ts +132 -0
  17. package/src/rendering/audio.test.ts +33 -0
  18. package/src/rendering/audio.ts +70 -0
  19. package/src/rendering/camera.ts +35 -0
  20. package/src/rendering/index.ts +56 -0
  21. package/src/rendering/input.test.ts +70 -0
  22. package/src/rendering/input.ts +82 -0
  23. package/src/rendering/lighting.ts +38 -0
  24. package/src/rendering/loop.ts +21 -0
  25. package/src/rendering/sprites.ts +60 -0
  26. package/src/rendering/text.test.ts +91 -0
  27. package/src/rendering/text.ts +184 -0
  28. package/src/rendering/texture.ts +31 -0
  29. package/src/rendering/tilemap.ts +46 -0
  30. package/src/rendering/types.ts +54 -0
  31. package/src/rendering/validate.ts +132 -0
  32. package/src/state/error.test.ts +45 -0
  33. package/src/state/error.ts +20 -0
  34. package/src/state/index.ts +70 -0
  35. package/src/state/observe.test.ts +173 -0
  36. package/src/state/observe.ts +110 -0
  37. package/src/state/prng.test.ts +221 -0
  38. package/src/state/prng.ts +162 -0
  39. package/src/state/query.test.ts +208 -0
  40. package/src/state/query.ts +144 -0
  41. package/src/state/store.test.ts +211 -0
  42. package/src/state/store.ts +109 -0
  43. package/src/state/transaction.test.ts +235 -0
  44. package/src/state/transaction.ts +280 -0
  45. package/src/state/types.test.ts +33 -0
  46. package/src/state/types.ts +30 -0
  47. package/src/systems/index.ts +2 -0
  48. package/src/systems/system.test.ts +217 -0
  49. package/src/systems/system.ts +150 -0
  50. package/src/systems/types.ts +35 -0
  51. package/src/testing/harness.ts +271 -0
  52. package/src/testing/mock-renderer.test.ts +93 -0
  53. package/src/testing/mock-renderer.ts +178 -0
  54. package/src/ui/index.ts +3 -0
  55. package/src/ui/primitives.test.ts +105 -0
  56. package/src/ui/primitives.ts +260 -0
  57. package/src/ui/types.ts +57 -0
@@ -0,0 +1,211 @@
1
+ import { describe, it, assert } from "../testing/harness.ts";
2
+ import { createStore } from "./store.ts";
3
+ import { set, update, push } from "./transaction.ts";
4
+ import { lt } from "./query.ts";
5
+
6
+ type GameState = {
7
+ turn: number;
8
+ party: { id: string; hp: number; role: string }[];
9
+ score: number;
10
+ };
11
+
12
+ const initial: GameState = {
13
+ turn: 1,
14
+ party: [
15
+ { id: "alice", hp: 20, role: "fighter" },
16
+ { id: "bob", hp: 15, role: "mage" },
17
+ ],
18
+ score: 0,
19
+ };
20
+
21
+ describe("createStore", () => {
22
+ it("returns a store with the initial state", () => {
23
+ const store = createStore(initial);
24
+ assert.deepEqual(store.getState(), initial);
25
+ });
26
+ });
27
+
28
+ describe("dispatch", () => {
29
+ it("applies mutations and updates state", () => {
30
+ const store = createStore(initial);
31
+ const result = store.dispatch([
32
+ set<GameState>("turn", 2),
33
+ set<GameState>("score", 100),
34
+ ]);
35
+
36
+ assert.equal(result.valid, true);
37
+ assert.equal(store.getState().turn, 2);
38
+ assert.equal(store.getState().score, 100);
39
+ });
40
+
41
+ it("returns the transaction result with diff", () => {
42
+ const store = createStore(initial);
43
+ const result = store.dispatch([set<GameState>("turn", 2)]);
44
+
45
+ assert.equal(result.valid, true);
46
+ const turnEntry = result.diff.entries.find((e) => e.path === "turn");
47
+ assert.notEqual(turnEntry, undefined);
48
+ assert.equal(turnEntry!.from, 1);
49
+ assert.equal(turnEntry!.to, 2);
50
+ });
51
+
52
+ it("does not update state on invalid transaction", () => {
53
+ const store = createStore(initial);
54
+ const result = store.dispatch([
55
+ set<GameState>("turn", 2),
56
+ push<GameState>("turn", "invalid"), // turn is number, not array
57
+ ]);
58
+
59
+ assert.equal(result.valid, false);
60
+ assert.equal(store.getState().turn, 1); // unchanged
61
+ });
62
+
63
+ it("records transaction in history", () => {
64
+ const store = createStore(initial);
65
+ assert.equal(store.getHistory().length, 0);
66
+
67
+ store.dispatch([set<GameState>("turn", 2)]);
68
+ assert.equal(store.getHistory().length, 1);
69
+
70
+ store.dispatch([set<GameState>("turn", 3)]);
71
+ assert.equal(store.getHistory().length, 2);
72
+ });
73
+
74
+ it("does not record failed transactions", () => {
75
+ const store = createStore(initial);
76
+ store.dispatch([push<GameState>("turn", "invalid")]);
77
+ assert.equal(store.getHistory().length, 0);
78
+ });
79
+ });
80
+
81
+ describe("observe via store", () => {
82
+ it("notifies observers after dispatch", () => {
83
+ const store = createStore(initial);
84
+ const observed: { newVal: unknown; oldVal: unknown }[] = [];
85
+
86
+ store.observe("turn", (newVal, oldVal) => {
87
+ observed.push({ newVal, oldVal });
88
+ });
89
+
90
+ store.dispatch([set<GameState>("turn", 2)]);
91
+
92
+ assert.equal(observed.length, 1);
93
+ assert.equal(observed[0].newVal, 2);
94
+ assert.equal(observed[0].oldVal, 1);
95
+ });
96
+
97
+ it("does not notify on failed dispatch", () => {
98
+ const store = createStore(initial);
99
+ let notified = false;
100
+
101
+ store.observe("turn", () => {
102
+ notified = true;
103
+ });
104
+
105
+ store.dispatch([
106
+ set<GameState>("turn", 2),
107
+ push<GameState>("turn", "invalid"),
108
+ ]);
109
+
110
+ assert.equal(notified, false);
111
+ });
112
+
113
+ it("supports unsubscribe", () => {
114
+ const store = createStore(initial);
115
+ let count = 0;
116
+
117
+ const unsub = store.observe("turn", () => count++);
118
+ store.dispatch([set<GameState>("turn", 2)]);
119
+ assert.equal(count, 1);
120
+
121
+ unsub();
122
+ store.dispatch([set<GameState>("turn", 3)]);
123
+ assert.equal(count, 1);
124
+ });
125
+ });
126
+
127
+ describe("query/get/has via store", () => {
128
+ it("queries current state", () => {
129
+ const store = createStore(initial);
130
+ const fighters = store.query("party", { role: "fighter" });
131
+ assert.equal(fighters.length, 1);
132
+ });
133
+
134
+ it("gets a value from current state", () => {
135
+ const store = createStore(initial);
136
+ assert.equal(store.get("turn"), 1);
137
+ assert.equal(store.get("party.0.id"), "alice");
138
+ });
139
+
140
+ it("checks existence in current state", () => {
141
+ const store = createStore(initial);
142
+ assert.equal(store.has("turn"), true);
143
+ assert.equal(store.has("nonexistent"), false);
144
+ });
145
+
146
+ it("queries reflect dispatched changes", () => {
147
+ const store = createStore(initial);
148
+ store.dispatch([set<GameState>("party.0.hp", 5)]);
149
+
150
+ const wounded = store.query("party", { hp: lt(10) });
151
+ assert.equal(wounded.length, 1);
152
+ });
153
+ });
154
+
155
+ describe("replaceState", () => {
156
+ it("replaces the entire state", () => {
157
+ const store = createStore(initial);
158
+ const newState: GameState = {
159
+ turn: 10,
160
+ party: [],
161
+ score: 999,
162
+ };
163
+
164
+ store.replaceState(newState);
165
+ assert.deepEqual(store.getState(), newState);
166
+ });
167
+
168
+ it("enables undo/redo (time travel)", () => {
169
+ const store = createStore(initial);
170
+ const snapshot = store.getState();
171
+
172
+ store.dispatch([set<GameState>("turn", 2)]);
173
+ store.dispatch([set<GameState>("turn", 3)]);
174
+ assert.equal(store.getState().turn, 3);
175
+
176
+ // Undo to the original state
177
+ store.replaceState(snapshot as GameState);
178
+ assert.equal(store.getState().turn, 1);
179
+ });
180
+ });
181
+
182
+ describe("integration: full dispatch → observe → query cycle", () => {
183
+ it("works end-to-end", () => {
184
+ const store = createStore(initial);
185
+ const hpChanges: { who: string; from: number; to: number }[] = [];
186
+
187
+ store.observe("party.*.hp", (newVal, oldVal, ctx) => {
188
+ const index = ctx.path.split(".")[1];
189
+ const member = store.get<{ id: string }>(`party.${index}`);
190
+ hpChanges.push({
191
+ who: member!.id,
192
+ from: oldVal as number,
193
+ to: newVal as number,
194
+ });
195
+ });
196
+
197
+ store.dispatch([
198
+ update<GameState>("party.0.hp", (hp) => (hp as number) - 5),
199
+ update<GameState>("party.1.hp", (hp) => (hp as number) - 3),
200
+ update<GameState>("turn", (t) => (t as number) + 1),
201
+ ]);
202
+
203
+ assert.equal(hpChanges.length, 2);
204
+ assert.deepEqual(hpChanges[0], { who: "alice", from: 20, to: 15 });
205
+ assert.deepEqual(hpChanges[1], { who: "bob", from: 15, to: 12 });
206
+
207
+ assert.equal(store.getState().turn, 2);
208
+ assert.equal(store.get("party.0.hp"), 15);
209
+ assert.equal(store.get("party.1.hp"), 12);
210
+ });
211
+ });
@@ -0,0 +1,109 @@
1
+ import type { DeepReadonly } from "./types.ts";
2
+ import type { Mutation, Diff, TransactionResult } from "./transaction.ts";
3
+ import { transaction } from "./transaction.ts";
4
+ import type { Predicate } from "./query.ts";
5
+ import { query, get, has } from "./query.ts";
6
+ import type { PathPattern, ObserverCallback, Unsubscribe } from "./observe.ts";
7
+ import { createObserverRegistry } from "./observe.ts";
8
+
9
+ /** The game store: ties state + transactions + observers together */
10
+ export type GameStore<S> = Readonly<{
11
+ /** Current state (readonly snapshot) */
12
+ getState: () => DeepReadonly<S>;
13
+
14
+ /** Apply mutations as a transaction, update state, notify observers */
15
+ dispatch: (mutations: readonly Mutation<S>[]) => TransactionResult<S>;
16
+
17
+ /** Subscribe to state changes at a path pattern */
18
+ observe: <T = unknown>(
19
+ pattern: PathPattern,
20
+ callback: ObserverCallback<T>,
21
+ ) => Unsubscribe;
22
+
23
+ /** Query current state */
24
+ query: <R = unknown>(
25
+ path: string,
26
+ filter?: Predicate<R> | Record<string, unknown>,
27
+ ) => readonly R[];
28
+
29
+ /** Get a value from current state */
30
+ get: <R = unknown>(path: string) => R | undefined;
31
+
32
+ /** Check existence in current state */
33
+ has: (path: string, predicate?: Predicate<unknown>) => boolean;
34
+
35
+ /** Replace the entire state (for deserialization / time travel) */
36
+ replaceState: (state: S) => void;
37
+
38
+ /** Get the transaction history (for recording/replay) */
39
+ getHistory: () => readonly TransactionRecord<S>[];
40
+ }>;
41
+
42
+ /** A recorded transaction for replay */
43
+ export type TransactionRecord<S> = Readonly<{
44
+ timestamp: number;
45
+ mutations: readonly Mutation<S>[];
46
+ diff: Diff;
47
+ }>;
48
+
49
+ /** Create a game store with initial state */
50
+ export function createStore<S>(initialState: S): GameStore<S> {
51
+ let state: S = initialState;
52
+ const observers = createObserverRegistry<S>();
53
+ const history: TransactionRecord<S>[] = [];
54
+
55
+ return {
56
+ getState(): DeepReadonly<S> {
57
+ return state as DeepReadonly<S>;
58
+ },
59
+
60
+ dispatch(mutations: readonly Mutation<S>[]): TransactionResult<S> {
61
+ const oldState = state;
62
+ const result = transaction(state, mutations);
63
+
64
+ if (result.valid) {
65
+ state = result.state;
66
+
67
+ history.push({
68
+ timestamp: Date.now(),
69
+ mutations,
70
+ diff: result.diff,
71
+ });
72
+
73
+ observers.notify(oldState, state, result.diff);
74
+ }
75
+
76
+ return result;
77
+ },
78
+
79
+ observe<T = unknown>(
80
+ pattern: PathPattern,
81
+ callback: ObserverCallback<T>,
82
+ ): Unsubscribe {
83
+ return observers.observe(pattern, callback);
84
+ },
85
+
86
+ query<R = unknown>(
87
+ path: string,
88
+ filter?: Predicate<R> | Record<string, unknown>,
89
+ ): readonly R[] {
90
+ return query(state, path, filter);
91
+ },
92
+
93
+ get<R = unknown>(path: string): R | undefined {
94
+ return get(state, path);
95
+ },
96
+
97
+ has(path: string, predicate?: Predicate<unknown>): boolean {
98
+ return has(state, path, predicate);
99
+ },
100
+
101
+ replaceState(newState: S): void {
102
+ state = newState;
103
+ },
104
+
105
+ getHistory(): readonly TransactionRecord<S>[] {
106
+ return history;
107
+ },
108
+ };
109
+ }
@@ -0,0 +1,235 @@
1
+ import { describe, it, assert } from "../testing/harness.ts";
2
+ import {
3
+ set,
4
+ update,
5
+ push,
6
+ removeWhere,
7
+ removeKey,
8
+ transaction,
9
+ computeDiff,
10
+ } from "./transaction.ts";
11
+
12
+ type TestState = {
13
+ hp: number;
14
+ name: string;
15
+ items: string[];
16
+ party: { id: string; hp: number }[];
17
+ nested: { a: { b: { c: number } } };
18
+ };
19
+
20
+ const initial: TestState = {
21
+ hp: 10,
22
+ name: "hero",
23
+ items: ["sword", "shield", "potion"],
24
+ party: [
25
+ { id: "alice", hp: 20 },
26
+ { id: "bob", hp: 15 },
27
+ ],
28
+ nested: { a: { b: { c: 42 } } },
29
+ };
30
+
31
+ describe("set", () => {
32
+ it("sets a top-level value", () => {
33
+ const m = set<TestState>("hp", 5);
34
+ const result = m.apply(initial);
35
+ assert.equal(result.hp, 5);
36
+ assert.equal(initial.hp, 10); // original unchanged
37
+ });
38
+
39
+ it("sets a nested value", () => {
40
+ const m = set<TestState>("nested.a.b.c", 99);
41
+ const result = m.apply(initial);
42
+ assert.equal(result.nested.a.b.c, 99);
43
+ });
44
+
45
+ it("sets a value inside an array element", () => {
46
+ const m = set<TestState>("party.0.hp", 0);
47
+ const result = m.apply(initial);
48
+ assert.equal(result.party[0].hp, 0);
49
+ assert.equal(result.party[1].hp, 15); // untouched
50
+ });
51
+
52
+ it("has correct metadata", () => {
53
+ const m = set<TestState>("hp", 5);
54
+ assert.equal(m.type, "set");
55
+ assert.equal(m.path, "hp");
56
+ assert.ok(m.description.includes("hp"));
57
+ });
58
+ });
59
+
60
+ describe("update", () => {
61
+ it("updates a value with a function", () => {
62
+ const m = update<TestState>("hp", (hp) => (hp as number) - 3);
63
+ const result = m.apply(initial);
64
+ assert.equal(result.hp, 7);
65
+ });
66
+
67
+ it("updates a nested value", () => {
68
+ const m = update<TestState>("nested.a.b.c", (c) => (c as number) * 2);
69
+ const result = m.apply(initial);
70
+ assert.equal(result.nested.a.b.c, 84);
71
+ });
72
+ });
73
+
74
+ describe("push", () => {
75
+ it("pushes an item onto an array", () => {
76
+ const m = push<TestState>("items", "bow");
77
+ const result = m.apply(initial);
78
+ assert.deepEqual(result.items, ["sword", "shield", "potion", "bow"]);
79
+ assert.equal(initial.items.length, 3); // original unchanged
80
+ });
81
+
82
+ it("throws when target is not an array", () => {
83
+ const m = push<TestState>("hp", "invalid");
84
+ assert.throws(() => m.apply(initial), /Expected array/);
85
+ });
86
+ });
87
+
88
+ describe("removeWhere", () => {
89
+ it("removes matching items from an array", () => {
90
+ const m = removeWhere<TestState>("items", (item) => item === "shield");
91
+ const result = m.apply(initial);
92
+ assert.deepEqual(result.items, ["sword", "potion"]);
93
+ });
94
+
95
+ it("removes nothing when no match", () => {
96
+ const m = removeWhere<TestState>("items", (item) => item === "axe");
97
+ const result = m.apply(initial);
98
+ assert.deepEqual(result.items, initial.items);
99
+ });
100
+
101
+ it("can remove by predicate on objects", () => {
102
+ const m = removeWhere<TestState>(
103
+ "party",
104
+ (member: any) => member.hp < 18,
105
+ );
106
+ const result = m.apply(initial);
107
+ assert.equal(result.party.length, 1);
108
+ assert.equal(result.party[0].id, "alice");
109
+ });
110
+ });
111
+
112
+ describe("removeKey", () => {
113
+ it("removes a key from a nested object", () => {
114
+ const m = removeKey<TestState>("nested.a.b");
115
+ const result = m.apply(initial);
116
+ assert.deepEqual(result.nested.a, {});
117
+ });
118
+ });
119
+
120
+ describe("transaction", () => {
121
+ it("applies multiple mutations atomically", () => {
122
+ const result = transaction(initial, [
123
+ set<TestState>("hp", 5),
124
+ push<TestState>("items", "bow"),
125
+ set<TestState>("party.0.hp", 18),
126
+ ]);
127
+
128
+ assert.equal(result.valid, true);
129
+ assert.equal(result.state.hp, 5);
130
+ assert.deepEqual(result.state.items, ["sword", "shield", "potion", "bow"]);
131
+ assert.equal(result.state.party[0].hp, 18);
132
+ assert.equal(result.error, undefined);
133
+ });
134
+
135
+ it("rolls back all changes on failure", () => {
136
+ const result = transaction(initial, [
137
+ set<TestState>("hp", 0),
138
+ push<TestState>("hp", "invalid"), // hp is a number, not array — will throw
139
+ ]);
140
+
141
+ assert.equal(result.valid, false);
142
+ assert.equal(result.state, initial); // original state returned
143
+ assert.notEqual(result.error, undefined);
144
+ assert.equal(result.error!.code, "TRANSACTION_FAILED");
145
+ });
146
+
147
+ it("produces a diff", () => {
148
+ const result = transaction(initial, [
149
+ set<TestState>("hp", 5),
150
+ set<TestState>("name", "villain"),
151
+ ]);
152
+
153
+ assert.equal(result.valid, true);
154
+ const hpEntry = result.diff.entries.find((e) => e.path === "hp");
155
+ assert.notEqual(hpEntry, undefined);
156
+ assert.equal(hpEntry!.from, 10);
157
+ assert.equal(hpEntry!.to, 5);
158
+
159
+ const nameEntry = result.diff.entries.find((e) => e.path === "name");
160
+ assert.notEqual(nameEntry, undefined);
161
+ assert.equal(nameEntry!.from, "hero");
162
+ assert.equal(nameEntry!.to, "villain");
163
+ });
164
+
165
+ it("produces empty diff when nothing changes", () => {
166
+ const result = transaction(initial, [set<TestState>("hp", 10)]);
167
+ assert.equal(result.valid, true);
168
+ assert.equal(result.diff.entries.length, 0);
169
+ });
170
+
171
+ it("does not mutate the original state", () => {
172
+ const frozen = JSON.parse(JSON.stringify(initial));
173
+ transaction(initial, [
174
+ set<TestState>("hp", 0),
175
+ push<TestState>("items", "new_item"),
176
+ ]);
177
+ assert.deepEqual(initial, frozen);
178
+ });
179
+ });
180
+
181
+ describe("computeDiff", () => {
182
+ it("detects simple value changes", () => {
183
+ const before = { x: 1, y: 2 };
184
+ const after = { x: 1, y: 3 };
185
+ const diff = computeDiff(before, after);
186
+ assert.equal(diff.entries.length, 1);
187
+ assert.equal(diff.entries[0].path, "y");
188
+ assert.equal(diff.entries[0].from, 2);
189
+ assert.equal(diff.entries[0].to, 3);
190
+ });
191
+
192
+ it("detects nested changes", () => {
193
+ const before = { a: { b: 1 } };
194
+ const after = { a: { b: 2 } };
195
+ const diff = computeDiff(before, after);
196
+ assert.equal(diff.entries.length, 1);
197
+ assert.equal(diff.entries[0].path, "a.b");
198
+ });
199
+
200
+ it("detects added keys", () => {
201
+ const before = { x: 1 } as any;
202
+ const after = { x: 1, y: 2 };
203
+ const diff = computeDiff(before, after);
204
+ const yEntry = diff.entries.find((e) => e.path === "y");
205
+ assert.notEqual(yEntry, undefined);
206
+ assert.equal(yEntry!.from, undefined);
207
+ assert.equal(yEntry!.to, 2);
208
+ });
209
+
210
+ it("detects removed keys", () => {
211
+ const before = { x: 1, y: 2 };
212
+ const after = { x: 1 } as any;
213
+ const diff = computeDiff(before, after);
214
+ const yEntry = diff.entries.find((e) => e.path === "y");
215
+ assert.notEqual(yEntry, undefined);
216
+ assert.equal(yEntry!.from, 2);
217
+ assert.equal(yEntry!.to, undefined);
218
+ });
219
+
220
+ it("detects array changes", () => {
221
+ const before = { items: ["a", "b"] };
222
+ const after = { items: ["a", "c"] };
223
+ const diff = computeDiff(before, after);
224
+ const entry = diff.entries.find((e) => e.path === "items.1");
225
+ assert.notEqual(entry, undefined);
226
+ assert.equal(entry!.from, "b");
227
+ assert.equal(entry!.to, "c");
228
+ });
229
+
230
+ it("returns empty diff for identical objects", () => {
231
+ const obj = { a: 1, b: { c: 2 } };
232
+ const diff = computeDiff(obj, obj);
233
+ assert.equal(diff.entries.length, 0);
234
+ });
235
+ });