@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,150 @@
1
+ import type { Condition, Action, Rule, SystemDef, RuleResult, ExtendOptions } from "./types.ts";
2
+ import { createError } from "../state/error.ts";
3
+
4
+ /** Create a system definition from a name and list of rules. */
5
+ export function system<S>(name: string, rules: readonly Rule<S>[]): SystemDef<S> {
6
+ return { name, rules };
7
+ }
8
+
9
+ // --- Fluent rule builder ---
10
+
11
+ type RuleBuilderWithConditions<S> = {
12
+ then(...actions: Action<S>[]): Rule<S>;
13
+ };
14
+
15
+ type RuleBuilderBase<S> = {
16
+ when(...conditions: Condition<S>[]): RuleBuilderWithConditions<S>;
17
+ then(...actions: Action<S>[]): Rule<S>;
18
+ replaces(targetName: string): {
19
+ when(...conditions: Condition<S>[]): RuleBuilderWithConditions<S>;
20
+ then(...actions: Action<S>[]): Rule<S>;
21
+ };
22
+ };
23
+
24
+ /** Fluent builder for creating rules. */
25
+ export function rule<S>(name: string): RuleBuilderBase<S> {
26
+ let replacesName: string | undefined;
27
+
28
+ const makeRule = (conditions: readonly Condition<S>[], actions: readonly Action<S>[]): Rule<S> => ({
29
+ name,
30
+ conditions,
31
+ actions,
32
+ ...(replacesName !== undefined ? { replaces: replacesName } : {}),
33
+ });
34
+
35
+ const withConditions = (conditions: readonly Condition<S>[]): RuleBuilderWithConditions<S> => ({
36
+ then(...actions: Action<S>[]) {
37
+ return makeRule(conditions, actions);
38
+ },
39
+ });
40
+
41
+ return {
42
+ when(...conditions: Condition<S>[]) {
43
+ return withConditions(conditions);
44
+ },
45
+ then(...actions: Action<S>[]) {
46
+ return makeRule([], actions);
47
+ },
48
+ replaces(targetName: string) {
49
+ replacesName = targetName;
50
+ return {
51
+ when(...conditions: Condition<S>[]) {
52
+ return withConditions(conditions);
53
+ },
54
+ then(...actions: Action<S>[]) {
55
+ return makeRule([], actions);
56
+ },
57
+ };
58
+ },
59
+ };
60
+ }
61
+
62
+ /** Find a rule by name, apply conditions, chain actions. */
63
+ export function applyRule<S>(
64
+ sys: SystemDef<S>,
65
+ ruleName: string,
66
+ state: S,
67
+ args: Record<string, unknown> = {},
68
+ ): RuleResult<S> {
69
+ const r = sys.rules.find((r) => r.name === ruleName);
70
+ if (!r) {
71
+ return {
72
+ ok: false,
73
+ state,
74
+ ruleName,
75
+ error: createError("UNKNOWN_RULE", `Rule "${ruleName}" not found in system "${sys.name}"`, {
76
+ action: "applyRule",
77
+ reason: `No rule named "${ruleName}"`,
78
+ suggestion: `Available rules: ${sys.rules.map((r) => r.name).join(", ")}`,
79
+ }),
80
+ };
81
+ }
82
+
83
+ for (const condition of r.conditions) {
84
+ if (!condition(state, args)) {
85
+ return {
86
+ ok: false,
87
+ state,
88
+ ruleName,
89
+ error: createError("CONDITION_FAILED", `Conditions not met for rule "${ruleName}"`, {
90
+ action: "applyRule",
91
+ reason: "One or more conditions returned false",
92
+ }),
93
+ };
94
+ }
95
+ }
96
+
97
+ let current = state;
98
+ for (const action of r.actions) {
99
+ current = action(current, args);
100
+ }
101
+
102
+ return { ok: true, state: current, ruleName };
103
+ }
104
+
105
+ /** Return names of rules whose conditions are all met. */
106
+ export function getApplicableRules<S>(
107
+ sys: SystemDef<S>,
108
+ state: S,
109
+ args: Record<string, unknown> = {},
110
+ ): string[] {
111
+ return sys.rules
112
+ .filter((r) => r.conditions.every((c) => c(state, args)))
113
+ .map((r) => r.name);
114
+ }
115
+
116
+ /** Extend a system: replace rules by name, add new rules, remove rules by name. */
117
+ export function extend<S>(base: SystemDef<S>, options: ExtendOptions<S>): SystemDef<S> {
118
+ const removeSet = new Set(options.remove ?? []);
119
+ const newRules = options.rules ?? [];
120
+
121
+ // Build replacement map from new rules that have `replaces`
122
+ const replaceMap = new Map<string, Rule<S>>();
123
+ const additions: Rule<S>[] = [];
124
+ for (const r of newRules) {
125
+ if (r.replaces) {
126
+ replaceMap.set(r.replaces, r);
127
+ } else {
128
+ additions.push(r);
129
+ }
130
+ }
131
+
132
+ // Process base rules: replace or keep (unless removed)
133
+ const result: Rule<S>[] = [];
134
+ for (const r of base.rules) {
135
+ if (removeSet.has(r.name)) continue;
136
+ const replacement = replaceMap.get(r.name);
137
+ if (replacement) {
138
+ result.push(replacement);
139
+ } else {
140
+ result.push(r);
141
+ }
142
+ }
143
+
144
+ // Append non-replacement additions
145
+ for (const r of additions) {
146
+ result.push(r);
147
+ }
148
+
149
+ return { name: base.name, rules: result };
150
+ }
@@ -0,0 +1,35 @@
1
+ import type { ArcaneError } from "../state/error.ts";
2
+
3
+ /** A condition that must be true for a rule to fire. */
4
+ export type Condition<S> = (state: S, args: Record<string, unknown>) => boolean;
5
+
6
+ /** An action that transforms state when a rule fires. */
7
+ export type Action<S> = (state: S, args: Record<string, unknown>) => S;
8
+
9
+ /** A named rule with conditions and actions. */
10
+ export type Rule<S> = Readonly<{
11
+ name: string;
12
+ conditions: readonly Condition<S>[];
13
+ actions: readonly Action<S>[];
14
+ replaces?: string;
15
+ }>;
16
+
17
+ /** A named system: a collection of rules. */
18
+ export type SystemDef<S> = Readonly<{
19
+ name: string;
20
+ rules: readonly Rule<S>[];
21
+ }>;
22
+
23
+ /** Result of applying a single rule. */
24
+ export type RuleResult<S> = Readonly<{
25
+ ok: boolean;
26
+ state: S;
27
+ ruleName: string;
28
+ error?: ArcaneError;
29
+ }>;
30
+
31
+ /** Options for extending a system. */
32
+ export type ExtendOptions<S> = {
33
+ rules?: readonly Rule<S>[];
34
+ remove?: readonly string[];
35
+ };
@@ -0,0 +1,271 @@
1
+ // Universal test harness — works in both Node.js and V8 (deno_core).
2
+ //
3
+ // Node mode: delegates to node:test and node:assert.
4
+ // V8 mode: standalone implementations with result reporting via
5
+ // globalThis.__reportTest(suite, test, passed, error?)
6
+
7
+ type TestFn = () => void | Promise<void>;
8
+ type DescribeFn = (name: string, fn: () => void) => void;
9
+ type ItFn = (name: string, fn: TestFn) => void;
10
+
11
+ interface Assert {
12
+ equal(actual: unknown, expected: unknown, message?: string): void;
13
+ deepEqual(actual: unknown, expected: unknown, message?: string): void;
14
+ notEqual(actual: unknown, expected: unknown, message?: string): void;
15
+ notDeepEqual(actual: unknown, expected: unknown, message?: string): void;
16
+ ok(value: unknown, message?: string): void;
17
+ match(actual: string, expected: RegExp, message?: string): void;
18
+ throws(fn: () => unknown, expected?: RegExp, message?: string): void;
19
+ }
20
+
21
+ // ---------------------------------------------------------------------------
22
+ // Environment detection
23
+ // ---------------------------------------------------------------------------
24
+
25
+ const isNode =
26
+ typeof globalThis.process !== "undefined" &&
27
+ typeof globalThis.process.versions?.node === "string";
28
+
29
+ // ---------------------------------------------------------------------------
30
+ // V8 standalone implementations
31
+ // ---------------------------------------------------------------------------
32
+
33
+ function deepEqualImpl(a: unknown, b: unknown): boolean {
34
+ if (a === b) return true;
35
+ if (a === null || b === null) return false;
36
+ if (typeof a !== typeof b) return false;
37
+
38
+ if (typeof a !== "object") return false;
39
+
40
+ // Both are non-null objects at this point
41
+ const aObj = a as Record<string, unknown>;
42
+ const bObj = b as Record<string, unknown>;
43
+
44
+ const aIsArray = Array.isArray(aObj);
45
+ const bIsArray = Array.isArray(bObj);
46
+ if (aIsArray !== bIsArray) return false;
47
+
48
+ if (aIsArray) {
49
+ const aArr = a as unknown[];
50
+ const bArr = b as unknown[];
51
+ if (aArr.length !== bArr.length) return false;
52
+ for (let i = 0; i < aArr.length; i++) {
53
+ if (!deepEqualImpl(aArr[i], bArr[i])) return false;
54
+ }
55
+ return true;
56
+ }
57
+
58
+ const aKeys = Object.keys(aObj).sort();
59
+ const bKeys = Object.keys(bObj).sort();
60
+ if (aKeys.length !== bKeys.length) return false;
61
+ for (let i = 0; i < aKeys.length; i++) {
62
+ if (aKeys[i] !== bKeys[i]) return false;
63
+ }
64
+ for (const key of aKeys) {
65
+ if (!deepEqualImpl(aObj[key], bObj[key])) return false;
66
+ }
67
+ return true;
68
+ }
69
+
70
+ function formatValue(v: unknown): string {
71
+ if (typeof v === "string") return JSON.stringify(v);
72
+ if (typeof v === "object" && v !== null) {
73
+ try {
74
+ return JSON.stringify(v);
75
+ } catch {
76
+ return String(v);
77
+ }
78
+ }
79
+ return String(v);
80
+ }
81
+
82
+ function fail(message: string): never {
83
+ throw new Error(message);
84
+ }
85
+
86
+ const v8Assert: Assert = {
87
+ equal(actual, expected, message?) {
88
+ if (actual !== expected) {
89
+ fail(
90
+ message ??
91
+ `Expected ${formatValue(actual)} to strictly equal ${formatValue(expected)}`,
92
+ );
93
+ }
94
+ },
95
+ deepEqual(actual, expected, message?) {
96
+ if (!deepEqualImpl(actual, expected)) {
97
+ fail(
98
+ message ??
99
+ `Expected deep equality.\nActual: ${formatValue(actual)}\nExpected: ${formatValue(expected)}`,
100
+ );
101
+ }
102
+ },
103
+ notEqual(actual, expected, message?) {
104
+ if (actual === expected) {
105
+ fail(
106
+ message ??
107
+ `Expected ${formatValue(actual)} to not strictly equal ${formatValue(expected)}`,
108
+ );
109
+ }
110
+ },
111
+ notDeepEqual(actual, expected, message?) {
112
+ if (deepEqualImpl(actual, expected)) {
113
+ fail(
114
+ message ??
115
+ `Expected values to not be deeply equal.\nValue: ${formatValue(actual)}`,
116
+ );
117
+ }
118
+ },
119
+ ok(value, message?) {
120
+ if (!value) {
121
+ fail(message ?? `Expected truthy value, got ${formatValue(value)}`);
122
+ }
123
+ },
124
+ match(actual, expected, message?) {
125
+ if (!expected.test(actual)) {
126
+ fail(
127
+ message ??
128
+ `Expected ${formatValue(actual)} to match ${expected}`,
129
+ );
130
+ }
131
+ },
132
+ throws(fn, expected?, message?) {
133
+ let threw = false;
134
+ let error: unknown;
135
+ try {
136
+ fn();
137
+ } catch (e) {
138
+ threw = true;
139
+ error = e;
140
+ }
141
+ if (!threw) {
142
+ fail(message ?? "Expected function to throw");
143
+ }
144
+ if (expected instanceof RegExp) {
145
+ const errMsg =
146
+ error instanceof Error ? error.message : String(error);
147
+ if (!expected.test(errMsg)) {
148
+ fail(
149
+ message ??
150
+ `Expected thrown error message to match ${expected}, got ${formatValue(errMsg)}`,
151
+ );
152
+ }
153
+ }
154
+ },
155
+ };
156
+
157
+ // ---------------------------------------------------------------------------
158
+ // V8 test runner — collects describe/it blocks, runs them on demand
159
+ // ---------------------------------------------------------------------------
160
+
161
+ interface TestCase {
162
+ name: string;
163
+ fn: TestFn;
164
+ }
165
+
166
+ interface Suite {
167
+ name: string;
168
+ tests: TestCase[];
169
+ }
170
+
171
+ const suites: Suite[] = [];
172
+ let currentSuite: Suite | null = null;
173
+
174
+ const v8Describe: DescribeFn = (name, fn) => {
175
+ const suite: Suite = { name, tests: [] };
176
+ const parentSuite = currentSuite;
177
+ currentSuite = suite;
178
+ fn();
179
+ currentSuite = parentSuite;
180
+
181
+ if (parentSuite) {
182
+ // Nested describe: prefix child tests with parent name
183
+ for (const test of suite.tests) {
184
+ parentSuite.tests.push({
185
+ name: `${name} > ${test.name}`,
186
+ fn: test.fn,
187
+ });
188
+ }
189
+ } else {
190
+ suites.push(suite);
191
+ }
192
+ };
193
+
194
+ const v8It: ItFn = (name, fn) => {
195
+ if (!currentSuite) {
196
+ const anon: Suite = { name: "<root>", tests: [{ name, fn }] };
197
+ suites.push(anon);
198
+ } else {
199
+ currentSuite.tests.push({ name, fn });
200
+ }
201
+ };
202
+
203
+ // Declare the reporter op that Rust will provide
204
+ declare const __reportTest: (
205
+ suite: string,
206
+ test: string,
207
+ passed: boolean,
208
+ error?: string,
209
+ ) => void;
210
+
211
+ (globalThis as any).__runTests = async () => {
212
+ let total = 0;
213
+ let passed = 0;
214
+ let failed = 0;
215
+
216
+ const hasReporter = typeof (globalThis as any).__reportTest === "function";
217
+
218
+ for (const suite of suites) {
219
+ for (const test of suite.tests) {
220
+ total++;
221
+ try {
222
+ const result = test.fn();
223
+ if (result && typeof (result as any).then === "function") {
224
+ await result;
225
+ }
226
+ passed++;
227
+ if (hasReporter) {
228
+ (globalThis as any).__reportTest(suite.name, test.name, true);
229
+ }
230
+ } catch (e) {
231
+ failed++;
232
+ const errMsg = e instanceof Error ? e.message : String(e);
233
+ if (hasReporter) {
234
+ (globalThis as any).__reportTest(suite.name, test.name, false, errMsg);
235
+ }
236
+ }
237
+ }
238
+ }
239
+
240
+ return { total, passed, failed };
241
+ };
242
+
243
+ // ---------------------------------------------------------------------------
244
+ // Node.js — delegate to built-in modules
245
+ // ---------------------------------------------------------------------------
246
+
247
+ let describe: DescribeFn;
248
+ let it: ItFn;
249
+ let assert: Assert;
250
+
251
+ if (isNode) {
252
+ const nodeTest = await import("node:test");
253
+ const nodeAssert = await import("node:assert");
254
+ describe = nodeTest.describe;
255
+ it = nodeTest.it;
256
+ assert = {
257
+ equal: nodeAssert.strict.equal,
258
+ deepEqual: nodeAssert.strict.deepEqual,
259
+ notEqual: nodeAssert.strict.notEqual,
260
+ notDeepEqual: nodeAssert.strict.notDeepEqual,
261
+ ok: nodeAssert.strict.ok,
262
+ match: nodeAssert.strict.match,
263
+ throws: nodeAssert.strict.throws as any, // Node's throws has more overloads
264
+ };
265
+ } else {
266
+ describe = v8Describe;
267
+ it = v8It;
268
+ assert = v8Assert;
269
+ }
270
+
271
+ export { describe, it, assert };
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Tests demonstrating mock renderer usage
3
+ */
4
+
5
+ // @ts-nocheck - Uses Node-specific test APIs (beforeEach/afterEach) not in harness
6
+ import { describe, it, assert } from "./harness.ts";
7
+ import { mockRenderer, installMockRenderer, restoreRenderer } from "./mock-renderer.ts";
8
+
9
+ describe("Mock Renderer", () => {
10
+ beforeEach(() => {
11
+ mockRenderer.reset();
12
+ });
13
+
14
+ it("should validate drawRect parameters", () => {
15
+ mockRenderer.drawRect(10.0, 20.0, 100.0, 50.0, { color: { r: 1.0, g: 0.5, b: 0.0 } });
16
+
17
+ mockRenderer.assertNoErrors();
18
+ mockRenderer.assertCalled("drawRect", 1);
19
+ });
20
+
21
+ it("should catch wrong parameter types", () => {
22
+ // Wrong: passing object instead of separate params
23
+ mockRenderer.drawRect({ x: 10 } as any, undefined, undefined, undefined);
24
+
25
+ assert(mockRenderer.hasErrors(), "Should have errors");
26
+ const errors = mockRenderer.getErrors();
27
+ assert(errors.some(e => e.includes("must be number")), "Should complain about type");
28
+ });
29
+
30
+ it("should catch invalid color values", () => {
31
+ // Color values must be 0.0-1.0
32
+ mockRenderer.drawRect(0, 0, 100, 100, { color: { r: 255, g: 255, b: 255 } });
33
+
34
+ assert(mockRenderer.hasErrors(), "Should have errors");
35
+ const errors = mockRenderer.getErrors();
36
+ assert(errors.some(e => e.includes("color.r must be 0.0-1.0")), "Should catch invalid color");
37
+ });
38
+
39
+ it("should validate drawText parameters", () => {
40
+ mockRenderer.drawText("Hello", { x: 10.0, y: 20.0, size: 16.0, color: { r: 1.0, g: 1.0, b: 1.0 } });
41
+
42
+ mockRenderer.assertNoErrors();
43
+ mockRenderer.assertCalled("drawText", 1);
44
+ });
45
+
46
+ it("should catch NaN values", () => {
47
+ mockRenderer.drawRect(NaN, 0, 100, 100);
48
+
49
+ assert(mockRenderer.hasErrors(), "Should catch NaN");
50
+ const errors = mockRenderer.getErrors();
51
+ assert(errors.some(e => e.includes("NaN")), "Should complain about NaN");
52
+ });
53
+ });
54
+
55
+ // Example: Testing a game's rendering code
56
+ describe("Game Visual Tests", () => {
57
+ beforeEach(() => {
58
+ mockRenderer.reset();
59
+ installMockRenderer();
60
+ });
61
+
62
+ afterEach(() => {
63
+ restoreRenderer();
64
+ });
65
+
66
+ it("should render HUD correctly", () => {
67
+ // Import game code that uses rendering
68
+ function renderHUD(health: number, gold: number) {
69
+ (globalThis as any).drawText(`HP: ${health}`, {
70
+ x: 10.0,
71
+ y: 10.0,
72
+ size: 16.0,
73
+ color: { r: 1.0, g: 0.0, b: 0.0 }
74
+ });
75
+
76
+ (globalThis as any).drawText(`Gold: ${gold}`, {
77
+ x: 10.0,
78
+ y: 30.0,
79
+ size: 16.0,
80
+ color: { r: 1.0, g: 0.84, b: 0.0 }
81
+ });
82
+ }
83
+
84
+ renderHUD(100, 50);
85
+
86
+ mockRenderer.assertNoErrors();
87
+ mockRenderer.assertCalled("drawText", 2);
88
+
89
+ const calls = mockRenderer.getCalls("drawText");
90
+ assert(calls[0].params[0] === "HP: 100", "First text correct");
91
+ assert(calls[1].params[0] === "Gold: 50", "Second text correct");
92
+ });
93
+ });