@directive-run/knowledge 0.8.2 → 0.8.4
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/LICENSE +5 -0
- package/core/plugins.md +83 -73
- package/examples/counter-react.ts +44 -0
- package/examples/counter-svelte.ts +44 -0
- package/examples/counter-vue.ts +44 -0
- package/examples/counter.ts +27 -335
- package/examples/number-match.ts +376 -0
- package/package.json +4 -4
package/LICENSE
CHANGED
|
@@ -2,6 +2,11 @@ MIT License
|
|
|
2
2
|
|
|
3
3
|
Copyright (c) 2026 Sizls LLC
|
|
4
4
|
|
|
5
|
+
This project is dual-licensed under MIT OR Apache-2.0. You may choose
|
|
6
|
+
either license. See LICENSE-APACHE for the Apache License, Version 2.0.
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
5
10
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
11
|
of this software and associated documentation files (the "Software"), to deal
|
|
7
12
|
in the Software without restriction, including without limitation the rights
|
package/core/plugins.md
CHANGED
|
@@ -28,7 +28,7 @@ const system = createSystem({
|
|
|
28
28
|
module: myModule,
|
|
29
29
|
plugins: [
|
|
30
30
|
devtoolsPlugin(),
|
|
31
|
-
loggingPlugin({
|
|
31
|
+
loggingPlugin({ level: "debug" }),
|
|
32
32
|
persistencePlugin({
|
|
33
33
|
key: "my-app-state",
|
|
34
34
|
storage: localStorage,
|
|
@@ -46,25 +46,27 @@ Logs state changes, requirements, and resolutions to the console.
|
|
|
46
46
|
```typescript
|
|
47
47
|
import { loggingPlugin } from "@directive-run/core/plugins";
|
|
48
48
|
|
|
49
|
-
// Default – logs facts changes and resolver start/complete
|
|
49
|
+
// Default – logs facts changes and resolver start/complete at "info" level
|
|
50
50
|
loggingPlugin()
|
|
51
51
|
|
|
52
|
-
//
|
|
53
|
-
loggingPlugin({
|
|
52
|
+
// Debug level – logs everything including derivation recomputation and constraint evaluation
|
|
53
|
+
loggingPlugin({ level: "debug" })
|
|
54
54
|
|
|
55
55
|
// Custom filter – only log specific events
|
|
56
56
|
loggingPlugin({
|
|
57
|
-
filter: (event) =>
|
|
58
|
-
|
|
59
|
-
if (event.type === "resolution:start" || event.type === "resolution:complete") {
|
|
60
|
-
return true;
|
|
61
|
-
}
|
|
57
|
+
filter: (event) => event.startsWith("resolver."),
|
|
58
|
+
})
|
|
62
59
|
|
|
63
|
-
|
|
64
|
-
|
|
60
|
+
// Custom logger and prefix
|
|
61
|
+
loggingPlugin({
|
|
62
|
+
level: "warn",
|
|
63
|
+
prefix: "[MyApp]",
|
|
64
|
+
logger: customLogger,
|
|
65
65
|
})
|
|
66
66
|
```
|
|
67
67
|
|
|
68
|
+
Options: `level` ("debug" | "info" | "warn" | "error"), `filter` (predicate on event name string), `logger` (Console-compatible object), `prefix` (string, default "[Directive]").
|
|
69
|
+
|
|
68
70
|
## devtoolsPlugin
|
|
69
71
|
|
|
70
72
|
Connects to the Directive DevTools browser extension for visual state inspection.
|
|
@@ -78,7 +80,9 @@ devtoolsPlugin()
|
|
|
78
80
|
// With options
|
|
79
81
|
devtoolsPlugin({
|
|
80
82
|
name: "My App", // Name shown in DevTools
|
|
81
|
-
|
|
83
|
+
maxEvents: 1000, // Max trace events to retain (default: 1000)
|
|
84
|
+
trace: true, // Enable trace logging
|
|
85
|
+
panel: true, // Show floating debug panel (dev mode only)
|
|
82
86
|
})
|
|
83
87
|
```
|
|
84
88
|
|
|
@@ -122,59 +126,56 @@ persistencePlugin({
|
|
|
122
126
|
exclude: ["tempData", "sessionId"], // Everything except these
|
|
123
127
|
})
|
|
124
128
|
|
|
125
|
-
//
|
|
129
|
+
// With callbacks
|
|
126
130
|
persistencePlugin({
|
|
127
131
|
key: "my-app",
|
|
128
132
|
storage: localStorage,
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
...oldState,
|
|
134
|
-
newField: "default",
|
|
135
|
-
};
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
return oldState;
|
|
139
|
-
},
|
|
133
|
+
debounce: 200, // Debounce saves (default: 100ms)
|
|
134
|
+
onRestore: (data) => console.log("Restored:", data),
|
|
135
|
+
onSave: (data) => console.log("Saved:", data),
|
|
136
|
+
onError: (err) => console.error("Persistence error:", err),
|
|
140
137
|
})
|
|
141
138
|
```
|
|
142
139
|
|
|
143
140
|
## createCircuitBreaker
|
|
144
141
|
|
|
145
|
-
|
|
142
|
+
Standalone utility (not a Plugin) that implements the circuit breaker pattern for resilience. Use it inside resolvers to protect against cascading failures from external services.
|
|
146
143
|
|
|
147
144
|
```typescript
|
|
148
145
|
import { createCircuitBreaker } from "@directive-run/core/plugins";
|
|
149
146
|
|
|
150
|
-
const
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
//
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
resetTimeout: 30000,
|
|
159
|
-
|
|
160
|
-
// Optional: only apply to specific resolver types
|
|
161
|
-
include: ["FETCH_DATA", "SYNC_REMOTE"],
|
|
147
|
+
const breaker = createCircuitBreaker({
|
|
148
|
+
failureThreshold: 5, // Failures before opening (default: 5)
|
|
149
|
+
recoveryTimeMs: 30000, // Time before HALF_OPEN (default: 30000)
|
|
150
|
+
halfOpenMaxRequests: 3, // Requests allowed in HALF_OPEN (default: 3)
|
|
151
|
+
failureWindowMs: 60000, // Window for counting failures (default: 60000)
|
|
152
|
+
name: "api-breaker", // Name for metrics/errors
|
|
153
|
+
onStateChange: (from, to) => console.log(`Circuit: ${from} -> ${to}`),
|
|
154
|
+
});
|
|
162
155
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
156
|
+
// Use inside a resolver
|
|
157
|
+
resolvers: {
|
|
158
|
+
fetchData: {
|
|
159
|
+
requirement: "FETCH_DATA",
|
|
160
|
+
resolve: async (req, context) => {
|
|
161
|
+
const result = await breaker.execute(async () => {
|
|
162
|
+
return await callExternalAPI();
|
|
163
|
+
});
|
|
164
|
+
context.facts.data = result;
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
},
|
|
167
168
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
}
|
|
169
|
+
// Wire circuit state into constraints
|
|
170
|
+
constraints: {
|
|
171
|
+
apiDown: {
|
|
172
|
+
when: () => breaker.getState() === "OPEN",
|
|
173
|
+
require: { type: "FALLBACK_RESPONSE" },
|
|
174
|
+
},
|
|
175
|
+
},
|
|
175
176
|
```
|
|
176
177
|
|
|
177
|
-
Circuit breaker states: **Closed** (normal) -> **Open** (failing, rejects immediately) -> **Half-Open** (testing
|
|
178
|
+
Circuit breaker states: **Closed** (normal) -> **Open** (failing, rejects immediately) -> **Half-Open** (testing limited requests).
|
|
178
179
|
|
|
179
180
|
## createObservability
|
|
180
181
|
|
|
@@ -212,14 +213,14 @@ const system = createSystem({
|
|
|
212
213
|
Plugins hook into the system lifecycle. Use these to build custom plugins.
|
|
213
214
|
|
|
214
215
|
```typescript
|
|
215
|
-
import type {
|
|
216
|
+
import type { Plugin, ModuleSchema } from "@directive-run/core";
|
|
216
217
|
|
|
217
|
-
const myPlugin:
|
|
218
|
+
const myPlugin: Plugin<ModuleSchema> = {
|
|
218
219
|
name: "my-custom-plugin",
|
|
219
220
|
|
|
220
221
|
// System lifecycle
|
|
221
222
|
onInit: (system) => {
|
|
222
|
-
// Called when system is created
|
|
223
|
+
// Called when system is created (only async hook)
|
|
223
224
|
},
|
|
224
225
|
onStart: (system) => {
|
|
225
226
|
// Called when system.start() is invoked
|
|
@@ -227,32 +228,42 @@ const myPlugin: DirectivePlugin = {
|
|
|
227
228
|
onStop: (system) => {
|
|
228
229
|
// Called when system.stop() is invoked
|
|
229
230
|
},
|
|
231
|
+
onDestroy: (system) => {
|
|
232
|
+
// Called when system.destroy() is invoked
|
|
233
|
+
},
|
|
230
234
|
|
|
231
|
-
//
|
|
232
|
-
|
|
233
|
-
// Called
|
|
234
|
-
|
|
235
|
-
|
|
235
|
+
// Fact tracking
|
|
236
|
+
onFactSet: (key, value, prev) => {
|
|
237
|
+
// Called when a single fact is set
|
|
238
|
+
},
|
|
239
|
+
onFactsBatch: (changes) => {
|
|
240
|
+
// Called after a batch of fact changes completes
|
|
236
241
|
},
|
|
237
242
|
|
|
238
243
|
// Requirement pipeline
|
|
239
|
-
|
|
244
|
+
onRequirementCreated: (requirement) => {
|
|
240
245
|
// Called when a constraint emits a requirement
|
|
241
|
-
// requirement.type, requirement.id
|
|
246
|
+
// requirement.type, requirement.id
|
|
247
|
+
},
|
|
248
|
+
onRequirementMet: (requirement, byResolver) => {
|
|
249
|
+
// Called when a requirement is fulfilled
|
|
242
250
|
},
|
|
243
|
-
|
|
251
|
+
|
|
252
|
+
// Resolver pipeline
|
|
253
|
+
onResolverStart: (resolverId, requirement) => {
|
|
244
254
|
// Called when a resolver begins executing
|
|
245
|
-
// resolution.resolverId, resolution.requirement
|
|
246
255
|
},
|
|
247
|
-
|
|
248
|
-
// Called when a resolver finishes
|
|
249
|
-
|
|
256
|
+
onResolverComplete: (resolverId, requirement, duration) => {
|
|
257
|
+
// Called when a resolver finishes successfully
|
|
258
|
+
},
|
|
259
|
+
onResolverError: (resolverId, requirement, error) => {
|
|
260
|
+
// Called when a resolver fails (after all retries)
|
|
250
261
|
},
|
|
251
262
|
|
|
252
263
|
// Error handling
|
|
253
|
-
onError: (error
|
|
254
|
-
// Called on any
|
|
255
|
-
//
|
|
264
|
+
onError: (error) => {
|
|
265
|
+
// Called on any DirectiveError in the system
|
|
266
|
+
// error.source, error.message, error.context
|
|
256
267
|
},
|
|
257
268
|
};
|
|
258
269
|
|
|
@@ -277,7 +288,7 @@ const system = createSystem({
|
|
|
277
288
|
const plugins = [];
|
|
278
289
|
if (process.env.NODE_ENV === "development") {
|
|
279
290
|
plugins.push(devtoolsPlugin());
|
|
280
|
-
plugins.push(loggingPlugin({
|
|
291
|
+
plugins.push(loggingPlugin({ level: "debug" }));
|
|
281
292
|
}
|
|
282
293
|
|
|
283
294
|
const system = createSystem({
|
|
@@ -286,21 +297,20 @@ const system = createSystem({
|
|
|
286
297
|
});
|
|
287
298
|
```
|
|
288
299
|
|
|
289
|
-
### Persistence without
|
|
300
|
+
### Persistence without filtering
|
|
290
301
|
|
|
291
302
|
```typescript
|
|
292
|
-
// WRONG –
|
|
303
|
+
// WRONG – persists everything including transient state
|
|
293
304
|
persistencePlugin({
|
|
294
305
|
key: "app-state",
|
|
295
306
|
storage: localStorage,
|
|
296
307
|
})
|
|
297
308
|
|
|
298
|
-
// CORRECT –
|
|
309
|
+
// CORRECT – use include/exclude to control what's persisted
|
|
299
310
|
persistencePlugin({
|
|
300
311
|
key: "app-state",
|
|
301
312
|
storage: localStorage,
|
|
302
|
-
|
|
303
|
-
migrate: (old, version) => old,
|
|
313
|
+
include: ["user", "preferences", "settings"],
|
|
304
314
|
})
|
|
305
315
|
```
|
|
306
316
|
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// Example: counter-react
|
|
2
|
+
// Source: examples/counter-react/src/module.ts
|
|
3
|
+
// Pure module file — no DOM wiring
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Shared counter module — same file used by all framework examples.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { createModule, createSystem, t, type ModuleSchema } from "@directive-run/core";
|
|
10
|
+
|
|
11
|
+
const schema = {
|
|
12
|
+
facts: { count: t.number() },
|
|
13
|
+
derivations: { doubled: t.number(), isPositive: t.boolean() },
|
|
14
|
+
events: { increment: {}, decrement: {}, reset: {} },
|
|
15
|
+
requirements: { CLAMP_TO_ZERO: {} },
|
|
16
|
+
} satisfies ModuleSchema;
|
|
17
|
+
|
|
18
|
+
export const counterModule = createModule("counter", {
|
|
19
|
+
schema,
|
|
20
|
+
init: (facts) => { facts.count = 0; },
|
|
21
|
+
derive: {
|
|
22
|
+
doubled: (facts) => facts.count * 2,
|
|
23
|
+
isPositive: (facts) => facts.count > 0,
|
|
24
|
+
},
|
|
25
|
+
events: {
|
|
26
|
+
increment: (facts) => { facts.count += 1; },
|
|
27
|
+
decrement: (facts) => { facts.count -= 1; },
|
|
28
|
+
reset: (facts) => { facts.count = 0; },
|
|
29
|
+
},
|
|
30
|
+
constraints: {
|
|
31
|
+
noNegative: {
|
|
32
|
+
when: (facts) => facts.count < 0,
|
|
33
|
+
require: { type: "CLAMP_TO_ZERO" },
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
resolvers: {
|
|
37
|
+
clamp: {
|
|
38
|
+
requirement: "CLAMP_TO_ZERO",
|
|
39
|
+
resolve: async (req, context) => { context.facts.count = 0; },
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
export const system = createSystem({ module: counterModule });
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// Example: counter-svelte
|
|
2
|
+
// Source: examples/counter-svelte/src/module.ts
|
|
3
|
+
// Pure module file — no DOM wiring
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Shared counter module — same file used by all framework examples.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { createModule, createSystem, t, type ModuleSchema } from "@directive-run/core";
|
|
10
|
+
|
|
11
|
+
const schema = {
|
|
12
|
+
facts: { count: t.number() },
|
|
13
|
+
derivations: { doubled: t.number(), isPositive: t.boolean() },
|
|
14
|
+
events: { increment: {}, decrement: {}, reset: {} },
|
|
15
|
+
requirements: { CLAMP_TO_ZERO: {} },
|
|
16
|
+
} satisfies ModuleSchema;
|
|
17
|
+
|
|
18
|
+
export const counterModule = createModule("counter", {
|
|
19
|
+
schema,
|
|
20
|
+
init: (facts) => { facts.count = 0; },
|
|
21
|
+
derive: {
|
|
22
|
+
doubled: (facts) => facts.count * 2,
|
|
23
|
+
isPositive: (facts) => facts.count > 0,
|
|
24
|
+
},
|
|
25
|
+
events: {
|
|
26
|
+
increment: (facts) => { facts.count += 1; },
|
|
27
|
+
decrement: (facts) => { facts.count -= 1; },
|
|
28
|
+
reset: (facts) => { facts.count = 0; },
|
|
29
|
+
},
|
|
30
|
+
constraints: {
|
|
31
|
+
noNegative: {
|
|
32
|
+
when: (facts) => facts.count < 0,
|
|
33
|
+
require: { type: "CLAMP_TO_ZERO" },
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
resolvers: {
|
|
37
|
+
clamp: {
|
|
38
|
+
requirement: "CLAMP_TO_ZERO",
|
|
39
|
+
resolve: async (req, context) => { context.facts.count = 0; },
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
export const system = createSystem({ module: counterModule });
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// Example: counter-vue
|
|
2
|
+
// Source: examples/counter-vue/src/module.ts
|
|
3
|
+
// Pure module file — no DOM wiring
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Shared counter module — same file used by all framework examples.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { createModule, createSystem, t, type ModuleSchema } from "@directive-run/core";
|
|
10
|
+
|
|
11
|
+
const schema = {
|
|
12
|
+
facts: { count: t.number() },
|
|
13
|
+
derivations: { doubled: t.number(), isPositive: t.boolean() },
|
|
14
|
+
events: { increment: {}, decrement: {}, reset: {} },
|
|
15
|
+
requirements: { CLAMP_TO_ZERO: {} },
|
|
16
|
+
} satisfies ModuleSchema;
|
|
17
|
+
|
|
18
|
+
export const counterModule = createModule("counter", {
|
|
19
|
+
schema,
|
|
20
|
+
init: (facts) => { facts.count = 0; },
|
|
21
|
+
derive: {
|
|
22
|
+
doubled: (facts) => facts.count * 2,
|
|
23
|
+
isPositive: (facts) => facts.count > 0,
|
|
24
|
+
},
|
|
25
|
+
events: {
|
|
26
|
+
increment: (facts) => { facts.count += 1; },
|
|
27
|
+
decrement: (facts) => { facts.count -= 1; },
|
|
28
|
+
reset: (facts) => { facts.count = 0; },
|
|
29
|
+
},
|
|
30
|
+
constraints: {
|
|
31
|
+
noNegative: {
|
|
32
|
+
when: (facts) => facts.count < 0,
|
|
33
|
+
require: { type: "CLAMP_TO_ZERO" },
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
resolvers: {
|
|
37
|
+
clamp: {
|
|
38
|
+
requirement: "CLAMP_TO_ZERO",
|
|
39
|
+
resolve: async (req, context) => { context.facts.count = 0; },
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
export const system = createSystem({ module: counterModule });
|
package/examples/counter.ts
CHANGED
|
@@ -3,374 +3,66 @@
|
|
|
3
3
|
// Pure module file — no DOM wiring
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
*
|
|
6
|
+
* Counter — The simplest Directive module.
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
8
|
+
* Demonstrates: facts, events, derivations, one constraint, one resolver.
|
|
9
|
+
* Total: ~40 lines.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import {
|
|
13
|
-
type ModuleSchema,
|
|
14
|
-
createModule,
|
|
15
|
-
createSystem,
|
|
16
|
-
t,
|
|
17
|
-
} from "@directive-run/core";
|
|
18
|
-
import { devtoolsPlugin } from "@directive-run/core/plugins";
|
|
12
|
+
import { createModule, createSystem, t, type ModuleSchema } from "@directive-run/core";
|
|
19
13
|
|
|
20
|
-
|
|
21
|
-
// Types
|
|
22
|
-
// ============================================================================
|
|
23
|
-
|
|
24
|
-
export interface Tile {
|
|
25
|
-
id: string;
|
|
26
|
-
value: number;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export interface TimelineEntry {
|
|
30
|
-
time: number;
|
|
31
|
-
event: string;
|
|
32
|
-
detail: string;
|
|
33
|
-
type: string;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// ============================================================================
|
|
37
|
-
// Helpers
|
|
38
|
-
// ============================================================================
|
|
39
|
-
|
|
40
|
-
// Create a pool of numbered tiles (1-9, four of each = 36 tiles)
|
|
41
|
-
function createPool(): Tile[] {
|
|
42
|
-
const tiles: Tile[] = [];
|
|
43
|
-
let id = 0;
|
|
44
|
-
for (let copy = 0; copy < 4; copy++) {
|
|
45
|
-
for (let value = 1; value <= 9; value++) {
|
|
46
|
-
tiles.push({ id: `t${id++}`, value });
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
// Shuffle
|
|
50
|
-
for (let i = tiles.length - 1; i > 0; i--) {
|
|
51
|
-
const j = Math.floor(Math.random() * (i + 1));
|
|
52
|
-
[tiles[i], tiles[j]] = [tiles[j], tiles[i]];
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
return tiles;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// ============================================================================
|
|
59
|
-
// Timeline
|
|
60
|
-
// ============================================================================
|
|
61
|
-
|
|
62
|
-
export const timeline: TimelineEntry[] = [];
|
|
63
|
-
|
|
64
|
-
export function addLog(msg: string) {
|
|
65
|
-
console.log(`[NumberMatch] ${msg}`);
|
|
66
|
-
|
|
67
|
-
// Classify and add significant events to the timeline
|
|
68
|
-
let event = "";
|
|
69
|
-
let detail = "";
|
|
70
|
-
let type = "info";
|
|
71
|
-
|
|
72
|
-
if (msg.startsWith("EVENT selectTile")) {
|
|
73
|
-
event = "tile selected";
|
|
74
|
-
const match = msg.match(/selectTile: (t\d+)/);
|
|
75
|
-
detail = match ? match[1] : "";
|
|
76
|
-
type = "selection";
|
|
77
|
-
} else if (msg.includes("pairAddsTen: TRUE")) {
|
|
78
|
-
event = "match found";
|
|
79
|
-
const match = msg.match(/\((.+)\)/);
|
|
80
|
-
detail = match ? match[1] : "";
|
|
81
|
-
type = "match";
|
|
82
|
-
} else if (msg === "RESOLVER removeTiles: DONE") {
|
|
83
|
-
event = "tiles removed";
|
|
84
|
-
detail = "";
|
|
85
|
-
type = "match";
|
|
86
|
-
} else if (msg.includes("refillTable: DONE")) {
|
|
87
|
-
event = "refill";
|
|
88
|
-
const match = msg.match(/table now: (\d+)/);
|
|
89
|
-
detail = match ? `table: ${match[1]} tiles` : "";
|
|
90
|
-
type = "refill";
|
|
91
|
-
} else if (msg.startsWith("RESOLVER endGame:")) {
|
|
92
|
-
event = "game over";
|
|
93
|
-
detail = msg.replace("RESOLVER endGame: ", "");
|
|
94
|
-
type = "gameover";
|
|
95
|
-
} else if (msg.includes("New game") || msg.includes("Game started")) {
|
|
96
|
-
event = "new game";
|
|
97
|
-
detail = msg;
|
|
98
|
-
type = "newgame";
|
|
99
|
-
} else {
|
|
100
|
-
// Skip verbose intermediate messages (RESOLVER steps, CONSTRAINT produce)
|
|
101
|
-
return;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
timeline.unshift({ time: Date.now(), event, detail, type });
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
// ============================================================================
|
|
108
|
-
// Schema
|
|
109
|
-
// ============================================================================
|
|
110
|
-
|
|
111
|
-
export const schema = {
|
|
14
|
+
const schema = {
|
|
112
15
|
facts: {
|
|
113
|
-
|
|
114
|
-
table: t.array<Tile>(),
|
|
115
|
-
removed: t.array<Tile>(),
|
|
116
|
-
selected: t.array<string>(),
|
|
117
|
-
message: t.string(),
|
|
118
|
-
moveCount: t.number(),
|
|
119
|
-
gameOver: t.boolean(),
|
|
16
|
+
count: t.number(),
|
|
120
17
|
},
|
|
121
18
|
derivations: {
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
selectedTiles: t.array<Tile>(),
|
|
125
|
-
hasValidMoves: t.boolean(),
|
|
19
|
+
doubled: t.number(),
|
|
20
|
+
isPositive: t.boolean(),
|
|
126
21
|
},
|
|
127
22
|
events: {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
clearSelection: {},
|
|
23
|
+
increment: {},
|
|
24
|
+
decrement: {},
|
|
25
|
+
reset: {},
|
|
132
26
|
},
|
|
133
27
|
requirements: {
|
|
134
|
-
|
|
135
|
-
REFILL_TABLE: { count: t.number() },
|
|
136
|
-
END_GAME: { reason: t.string() },
|
|
28
|
+
CLAMP_TO_ZERO: {},
|
|
137
29
|
},
|
|
138
30
|
} satisfies ModuleSchema;
|
|
139
31
|
|
|
140
|
-
|
|
141
|
-
// Module
|
|
142
|
-
// ============================================================================
|
|
143
|
-
|
|
144
|
-
const numberMatch = createModule("number-match", {
|
|
32
|
+
export const counterModule = createModule("counter", {
|
|
145
33
|
schema,
|
|
146
34
|
|
|
147
35
|
init: (facts) => {
|
|
148
|
-
|
|
149
|
-
facts.pool = pool.slice(9);
|
|
150
|
-
facts.table = pool.slice(0, 9);
|
|
151
|
-
facts.removed = [];
|
|
152
|
-
facts.selected = [];
|
|
153
|
-
facts.message = "Select two numbers that add to 10";
|
|
154
|
-
facts.moveCount = 0;
|
|
155
|
-
facts.gameOver = false;
|
|
36
|
+
facts.count = 0;
|
|
156
37
|
},
|
|
157
38
|
|
|
158
39
|
derive: {
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
selectedTiles: (facts) =>
|
|
162
|
-
facts.table.filter((tile: Tile) => facts.selected.includes(tile.id)),
|
|
163
|
-
hasValidMoves: (facts) => {
|
|
164
|
-
const nums = facts.table.map((t: Tile) => t.value);
|
|
165
|
-
for (let i = 0; i < nums.length; i++) {
|
|
166
|
-
for (let j = i + 1; j < nums.length; j++) {
|
|
167
|
-
if (nums[i] + nums[j] === 10) {
|
|
168
|
-
return true;
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
return false;
|
|
174
|
-
},
|
|
40
|
+
doubled: (facts) => facts.count * 2,
|
|
41
|
+
isPositive: (facts) => facts.count > 0,
|
|
175
42
|
},
|
|
176
43
|
|
|
177
44
|
events: {
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
facts.table = pool.slice(0, 9);
|
|
182
|
-
facts.removed = [];
|
|
183
|
-
facts.selected = [];
|
|
184
|
-
facts.message = "New game! Select two numbers that add to 10";
|
|
185
|
-
facts.moveCount = 0;
|
|
186
|
-
facts.gameOver = false;
|
|
187
|
-
},
|
|
188
|
-
selectTile: (facts, { tileId }) => {
|
|
189
|
-
if (!facts.selected.includes(tileId) && !facts.gameOver) {
|
|
190
|
-
facts.selected = [...facts.selected, tileId];
|
|
191
|
-
addLog(
|
|
192
|
-
`EVENT selectTile: ${tileId}, selected now: [${facts.selected}]`,
|
|
193
|
-
);
|
|
194
|
-
}
|
|
195
|
-
},
|
|
196
|
-
deselectTile: (facts, { tileId }) => {
|
|
197
|
-
facts.selected = facts.selected.filter((id: string) => id !== tileId);
|
|
198
|
-
},
|
|
199
|
-
clearSelection: (facts) => {
|
|
200
|
-
facts.selected = [];
|
|
201
|
-
},
|
|
45
|
+
increment: (facts) => { facts.count += 1; },
|
|
46
|
+
decrement: (facts) => { facts.count -= 1; },
|
|
47
|
+
reset: (facts) => { facts.count = 0; },
|
|
202
48
|
},
|
|
203
49
|
|
|
204
|
-
//
|
|
205
|
-
// Constraints
|
|
206
|
-
// ============================================================================
|
|
50
|
+
// When count goes negative, automatically fix it
|
|
207
51
|
constraints: {
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
when: (facts) => {
|
|
212
|
-
if (facts.gameOver) {
|
|
213
|
-
return false;
|
|
214
|
-
}
|
|
215
|
-
const selected = facts.table.filter((tile: Tile) =>
|
|
216
|
-
facts.selected.includes(tile.id),
|
|
217
|
-
);
|
|
218
|
-
if (selected.length !== 2) {
|
|
219
|
-
return false;
|
|
220
|
-
}
|
|
221
|
-
const result = selected[0].value + selected[1].value === 10;
|
|
222
|
-
if (result) {
|
|
223
|
-
addLog(
|
|
224
|
-
`CONSTRAINT pairAddsTen: TRUE (${selected[0].value} + ${selected[1].value})`,
|
|
225
|
-
);
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
return result;
|
|
229
|
-
},
|
|
230
|
-
require: (facts) => {
|
|
231
|
-
addLog("CONSTRAINT pairAddsTen: producing REMOVE_TILES");
|
|
232
|
-
|
|
233
|
-
return {
|
|
234
|
-
type: "REMOVE_TILES",
|
|
235
|
-
tileIds: [...facts.selected],
|
|
236
|
-
};
|
|
237
|
-
},
|
|
238
|
-
},
|
|
239
|
-
|
|
240
|
-
// Refill table when tiles are removed
|
|
241
|
-
refillTable: {
|
|
242
|
-
priority: 50,
|
|
243
|
-
when: (facts) => {
|
|
244
|
-
const result =
|
|
245
|
-
!facts.gameOver && facts.table.length < 9 && facts.pool.length > 0;
|
|
246
|
-
if (result) {
|
|
247
|
-
addLog(
|
|
248
|
-
`CONSTRAINT refillTable: TRUE (table: ${facts.table.length}, pool: ${facts.pool.length})`,
|
|
249
|
-
);
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
return result;
|
|
253
|
-
},
|
|
254
|
-
require: (facts) => {
|
|
255
|
-
const count = Math.min(9 - facts.table.length, facts.pool.length);
|
|
256
|
-
addLog(`CONSTRAINT refillTable: producing REFILL_TABLE count=${count}`);
|
|
257
|
-
|
|
258
|
-
return { type: "REFILL_TABLE", count };
|
|
259
|
-
},
|
|
260
|
-
},
|
|
261
|
-
|
|
262
|
-
// No moves left -> game over
|
|
263
|
-
noMovesLeft: {
|
|
264
|
-
priority: 190,
|
|
265
|
-
when: (facts) => {
|
|
266
|
-
if (facts.gameOver) {
|
|
267
|
-
return false;
|
|
268
|
-
}
|
|
269
|
-
if (facts.table.length === 0) {
|
|
270
|
-
return false;
|
|
271
|
-
}
|
|
272
|
-
if (facts.pool.length > 0) {
|
|
273
|
-
return false;
|
|
274
|
-
}
|
|
275
|
-
const nums = facts.table.map((t: Tile) => t.value);
|
|
276
|
-
for (let i = 0; i < nums.length; i++) {
|
|
277
|
-
for (let j = i + 1; j < nums.length; j++) {
|
|
278
|
-
if (nums[i] + nums[j] === 10) {
|
|
279
|
-
return false;
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
addLog("CONSTRAINT noMovesLeft: TRUE");
|
|
284
|
-
|
|
285
|
-
return true;
|
|
286
|
-
},
|
|
287
|
-
require: (facts) => ({
|
|
288
|
-
type: "END_GAME",
|
|
289
|
-
reason: `Game over! Removed ${facts.removed.length} of 36 tiles.`,
|
|
290
|
-
}),
|
|
291
|
-
},
|
|
292
|
-
|
|
293
|
-
// Win condition
|
|
294
|
-
allCleared: {
|
|
295
|
-
priority: 200,
|
|
296
|
-
when: (facts) => {
|
|
297
|
-
const result =
|
|
298
|
-
!facts.gameOver &&
|
|
299
|
-
facts.table.length === 0 &&
|
|
300
|
-
facts.pool.length === 0;
|
|
301
|
-
if (result) {
|
|
302
|
-
addLog("CONSTRAINT allCleared: TRUE");
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
return result;
|
|
306
|
-
},
|
|
307
|
-
require: (facts) => ({
|
|
308
|
-
type: "END_GAME",
|
|
309
|
-
reason: `You win! Cleared all tiles in ${facts.moveCount} moves!`,
|
|
310
|
-
}),
|
|
52
|
+
noNegative: {
|
|
53
|
+
when: (facts) => facts.count < 0,
|
|
54
|
+
require: { type: "CLAMP_TO_ZERO" },
|
|
311
55
|
},
|
|
312
56
|
},
|
|
313
57
|
|
|
314
|
-
// ============================================================================
|
|
315
|
-
// Resolvers
|
|
316
|
-
// ============================================================================
|
|
317
58
|
resolvers: {
|
|
318
|
-
|
|
319
|
-
requirement: "
|
|
320
|
-
resolve: async (req, context) => {
|
|
321
|
-
addLog("RESOLVER removeTiles: START");
|
|
322
|
-
const tilesToRemove = context.facts.table.filter((tile: Tile) =>
|
|
323
|
-
req.tileIds.includes(tile.id),
|
|
324
|
-
);
|
|
325
|
-
|
|
326
|
-
// Multiple fact mutations
|
|
327
|
-
addLog("RESOLVER removeTiles: setting table");
|
|
328
|
-
context.facts.table = context.facts.table.filter(
|
|
329
|
-
(tile: Tile) => !req.tileIds.includes(tile.id),
|
|
330
|
-
);
|
|
331
|
-
addLog("RESOLVER removeTiles: setting removed");
|
|
332
|
-
context.facts.removed = [...context.facts.removed, ...tilesToRemove];
|
|
333
|
-
addLog("RESOLVER removeTiles: clearing selected");
|
|
334
|
-
context.facts.selected = [];
|
|
335
|
-
addLog("RESOLVER removeTiles: incrementing moveCount");
|
|
336
|
-
context.facts.moveCount++;
|
|
337
|
-
addLog("RESOLVER removeTiles: setting message");
|
|
338
|
-
context.facts.message = `Removed ${tilesToRemove[0].value} + ${tilesToRemove[1].value} = 10!`;
|
|
339
|
-
addLog("RESOLVER removeTiles: DONE");
|
|
340
|
-
},
|
|
341
|
-
},
|
|
342
|
-
|
|
343
|
-
refillTable: {
|
|
344
|
-
requirement: "REFILL_TABLE",
|
|
59
|
+
clamp: {
|
|
60
|
+
requirement: "CLAMP_TO_ZERO",
|
|
345
61
|
resolve: async (req, context) => {
|
|
346
|
-
|
|
347
|
-
const newTiles = context.facts.pool.slice(0, req.count);
|
|
348
|
-
context.facts.pool = context.facts.pool.slice(req.count);
|
|
349
|
-
context.facts.table = [...context.facts.table, ...newTiles];
|
|
350
|
-
addLog(
|
|
351
|
-
`RESOLVER refillTable: DONE (table now: ${context.facts.table.length})`,
|
|
352
|
-
);
|
|
353
|
-
},
|
|
354
|
-
},
|
|
355
|
-
|
|
356
|
-
endGame: {
|
|
357
|
-
requirement: "END_GAME",
|
|
358
|
-
resolve: async (req, context) => {
|
|
359
|
-
addLog(`RESOLVER endGame: ${req.reason}`);
|
|
360
|
-
context.facts.gameOver = true;
|
|
361
|
-
context.facts.message = req.reason;
|
|
62
|
+
context.facts.count = 0;
|
|
362
63
|
},
|
|
363
64
|
},
|
|
364
65
|
},
|
|
365
66
|
});
|
|
366
67
|
|
|
367
|
-
|
|
368
|
-
// System
|
|
369
|
-
// ============================================================================
|
|
370
|
-
|
|
371
|
-
export const system = createSystem({
|
|
372
|
-
module: numberMatch,
|
|
373
|
-
plugins: [devtoolsPlugin({ name: "number-match" })],
|
|
374
|
-
history: true,
|
|
375
|
-
trace: true,
|
|
376
|
-
});
|
|
68
|
+
export const system = createSystem({ module: counterModule });
|
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
// Example: number-match
|
|
2
|
+
// Source: examples/number-match/src/module.ts
|
|
3
|
+
// Pure module file — no DOM wiring
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Number Match — Directive Module
|
|
7
|
+
*
|
|
8
|
+
* Types, schema, helpers, module definition, timeline, and system creation
|
|
9
|
+
* for a tile-matching game where pairs must add to 10.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
type ModuleSchema,
|
|
14
|
+
createModule,
|
|
15
|
+
createSystem,
|
|
16
|
+
t,
|
|
17
|
+
} from "@directive-run/core";
|
|
18
|
+
import { devtoolsPlugin } from "@directive-run/core/plugins";
|
|
19
|
+
|
|
20
|
+
// ============================================================================
|
|
21
|
+
// Types
|
|
22
|
+
// ============================================================================
|
|
23
|
+
|
|
24
|
+
export interface Tile {
|
|
25
|
+
id: string;
|
|
26
|
+
value: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface TimelineEntry {
|
|
30
|
+
time: number;
|
|
31
|
+
event: string;
|
|
32
|
+
detail: string;
|
|
33
|
+
type: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// ============================================================================
|
|
37
|
+
// Helpers
|
|
38
|
+
// ============================================================================
|
|
39
|
+
|
|
40
|
+
// Create a pool of numbered tiles (1-9, four of each = 36 tiles)
|
|
41
|
+
function createPool(): Tile[] {
|
|
42
|
+
const tiles: Tile[] = [];
|
|
43
|
+
let id = 0;
|
|
44
|
+
for (let copy = 0; copy < 4; copy++) {
|
|
45
|
+
for (let value = 1; value <= 9; value++) {
|
|
46
|
+
tiles.push({ id: `t${id++}`, value });
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
// Shuffle
|
|
50
|
+
for (let i = tiles.length - 1; i > 0; i--) {
|
|
51
|
+
const j = Math.floor(Math.random() * (i + 1));
|
|
52
|
+
[tiles[i], tiles[j]] = [tiles[j], tiles[i]];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return tiles;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ============================================================================
|
|
59
|
+
// Timeline
|
|
60
|
+
// ============================================================================
|
|
61
|
+
|
|
62
|
+
export const timeline: TimelineEntry[] = [];
|
|
63
|
+
|
|
64
|
+
export function addLog(msg: string) {
|
|
65
|
+
console.log(`[NumberMatch] ${msg}`);
|
|
66
|
+
|
|
67
|
+
// Classify and add significant events to the timeline
|
|
68
|
+
let event = "";
|
|
69
|
+
let detail = "";
|
|
70
|
+
let type = "info";
|
|
71
|
+
|
|
72
|
+
if (msg.startsWith("EVENT selectTile")) {
|
|
73
|
+
event = "tile selected";
|
|
74
|
+
const match = msg.match(/selectTile: (t\d+)/);
|
|
75
|
+
detail = match ? match[1] : "";
|
|
76
|
+
type = "selection";
|
|
77
|
+
} else if (msg.includes("pairAddsTen: TRUE")) {
|
|
78
|
+
event = "match found";
|
|
79
|
+
const match = msg.match(/\((.+)\)/);
|
|
80
|
+
detail = match ? match[1] : "";
|
|
81
|
+
type = "match";
|
|
82
|
+
} else if (msg === "RESOLVER removeTiles: DONE") {
|
|
83
|
+
event = "tiles removed";
|
|
84
|
+
detail = "";
|
|
85
|
+
type = "match";
|
|
86
|
+
} else if (msg.includes("refillTable: DONE")) {
|
|
87
|
+
event = "refill";
|
|
88
|
+
const match = msg.match(/table now: (\d+)/);
|
|
89
|
+
detail = match ? `table: ${match[1]} tiles` : "";
|
|
90
|
+
type = "refill";
|
|
91
|
+
} else if (msg.startsWith("RESOLVER endGame:")) {
|
|
92
|
+
event = "game over";
|
|
93
|
+
detail = msg.replace("RESOLVER endGame: ", "");
|
|
94
|
+
type = "gameover";
|
|
95
|
+
} else if (msg.includes("New game") || msg.includes("Game started")) {
|
|
96
|
+
event = "new game";
|
|
97
|
+
detail = msg;
|
|
98
|
+
type = "newgame";
|
|
99
|
+
} else {
|
|
100
|
+
// Skip verbose intermediate messages (RESOLVER steps, CONSTRAINT produce)
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
timeline.unshift({ time: Date.now(), event, detail, type });
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ============================================================================
|
|
108
|
+
// Schema
|
|
109
|
+
// ============================================================================
|
|
110
|
+
|
|
111
|
+
export const schema = {
|
|
112
|
+
facts: {
|
|
113
|
+
pool: t.array<Tile>(),
|
|
114
|
+
table: t.array<Tile>(),
|
|
115
|
+
removed: t.array<Tile>(),
|
|
116
|
+
selected: t.array<string>(),
|
|
117
|
+
message: t.string(),
|
|
118
|
+
moveCount: t.number(),
|
|
119
|
+
gameOver: t.boolean(),
|
|
120
|
+
},
|
|
121
|
+
derivations: {
|
|
122
|
+
poolCount: t.number(),
|
|
123
|
+
removedCount: t.number(),
|
|
124
|
+
selectedTiles: t.array<Tile>(),
|
|
125
|
+
hasValidMoves: t.boolean(),
|
|
126
|
+
},
|
|
127
|
+
events: {
|
|
128
|
+
newGame: {},
|
|
129
|
+
selectTile: { tileId: t.string() },
|
|
130
|
+
deselectTile: { tileId: t.string() },
|
|
131
|
+
clearSelection: {},
|
|
132
|
+
},
|
|
133
|
+
requirements: {
|
|
134
|
+
REMOVE_TILES: { tileIds: t.array<string>() },
|
|
135
|
+
REFILL_TABLE: { count: t.number() },
|
|
136
|
+
END_GAME: { reason: t.string() },
|
|
137
|
+
},
|
|
138
|
+
} satisfies ModuleSchema;
|
|
139
|
+
|
|
140
|
+
// ============================================================================
|
|
141
|
+
// Module
|
|
142
|
+
// ============================================================================
|
|
143
|
+
|
|
144
|
+
const numberMatch = createModule("number-match", {
|
|
145
|
+
schema,
|
|
146
|
+
|
|
147
|
+
init: (facts) => {
|
|
148
|
+
const pool = createPool();
|
|
149
|
+
facts.pool = pool.slice(9);
|
|
150
|
+
facts.table = pool.slice(0, 9);
|
|
151
|
+
facts.removed = [];
|
|
152
|
+
facts.selected = [];
|
|
153
|
+
facts.message = "Select two numbers that add to 10";
|
|
154
|
+
facts.moveCount = 0;
|
|
155
|
+
facts.gameOver = false;
|
|
156
|
+
},
|
|
157
|
+
|
|
158
|
+
derive: {
|
|
159
|
+
poolCount: (facts) => facts.pool.length,
|
|
160
|
+
removedCount: (facts) => facts.removed.length,
|
|
161
|
+
selectedTiles: (facts) =>
|
|
162
|
+
facts.table.filter((tile: Tile) => facts.selected.includes(tile.id)),
|
|
163
|
+
hasValidMoves: (facts) => {
|
|
164
|
+
const nums = facts.table.map((t: Tile) => t.value);
|
|
165
|
+
for (let i = 0; i < nums.length; i++) {
|
|
166
|
+
for (let j = i + 1; j < nums.length; j++) {
|
|
167
|
+
if (nums[i] + nums[j] === 10) {
|
|
168
|
+
return true;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return false;
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
|
|
177
|
+
events: {
|
|
178
|
+
newGame: (facts) => {
|
|
179
|
+
const pool = createPool();
|
|
180
|
+
facts.pool = pool.slice(9);
|
|
181
|
+
facts.table = pool.slice(0, 9);
|
|
182
|
+
facts.removed = [];
|
|
183
|
+
facts.selected = [];
|
|
184
|
+
facts.message = "New game! Select two numbers that add to 10";
|
|
185
|
+
facts.moveCount = 0;
|
|
186
|
+
facts.gameOver = false;
|
|
187
|
+
},
|
|
188
|
+
selectTile: (facts, { tileId }) => {
|
|
189
|
+
if (!facts.selected.includes(tileId) && !facts.gameOver) {
|
|
190
|
+
facts.selected = [...facts.selected, tileId];
|
|
191
|
+
addLog(
|
|
192
|
+
`EVENT selectTile: ${tileId}, selected now: [${facts.selected}]`,
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
},
|
|
196
|
+
deselectTile: (facts, { tileId }) => {
|
|
197
|
+
facts.selected = facts.selected.filter((id: string) => id !== tileId);
|
|
198
|
+
},
|
|
199
|
+
clearSelection: (facts) => {
|
|
200
|
+
facts.selected = [];
|
|
201
|
+
},
|
|
202
|
+
},
|
|
203
|
+
|
|
204
|
+
// ============================================================================
|
|
205
|
+
// Constraints
|
|
206
|
+
// ============================================================================
|
|
207
|
+
constraints: {
|
|
208
|
+
// When two selected tiles add to 10 -> remove them
|
|
209
|
+
pairAddsTen: {
|
|
210
|
+
priority: 100,
|
|
211
|
+
when: (facts) => {
|
|
212
|
+
if (facts.gameOver) {
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
const selected = facts.table.filter((tile: Tile) =>
|
|
216
|
+
facts.selected.includes(tile.id),
|
|
217
|
+
);
|
|
218
|
+
if (selected.length !== 2) {
|
|
219
|
+
return false;
|
|
220
|
+
}
|
|
221
|
+
const result = selected[0].value + selected[1].value === 10;
|
|
222
|
+
if (result) {
|
|
223
|
+
addLog(
|
|
224
|
+
`CONSTRAINT pairAddsTen: TRUE (${selected[0].value} + ${selected[1].value})`,
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return result;
|
|
229
|
+
},
|
|
230
|
+
require: (facts) => {
|
|
231
|
+
addLog("CONSTRAINT pairAddsTen: producing REMOVE_TILES");
|
|
232
|
+
|
|
233
|
+
return {
|
|
234
|
+
type: "REMOVE_TILES",
|
|
235
|
+
tileIds: [...facts.selected],
|
|
236
|
+
};
|
|
237
|
+
},
|
|
238
|
+
},
|
|
239
|
+
|
|
240
|
+
// Refill table when tiles are removed
|
|
241
|
+
refillTable: {
|
|
242
|
+
priority: 50,
|
|
243
|
+
when: (facts) => {
|
|
244
|
+
const result =
|
|
245
|
+
!facts.gameOver && facts.table.length < 9 && facts.pool.length > 0;
|
|
246
|
+
if (result) {
|
|
247
|
+
addLog(
|
|
248
|
+
`CONSTRAINT refillTable: TRUE (table: ${facts.table.length}, pool: ${facts.pool.length})`,
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return result;
|
|
253
|
+
},
|
|
254
|
+
require: (facts) => {
|
|
255
|
+
const count = Math.min(9 - facts.table.length, facts.pool.length);
|
|
256
|
+
addLog(`CONSTRAINT refillTable: producing REFILL_TABLE count=${count}`);
|
|
257
|
+
|
|
258
|
+
return { type: "REFILL_TABLE", count };
|
|
259
|
+
},
|
|
260
|
+
},
|
|
261
|
+
|
|
262
|
+
// No moves left -> game over
|
|
263
|
+
noMovesLeft: {
|
|
264
|
+
priority: 190,
|
|
265
|
+
when: (facts) => {
|
|
266
|
+
if (facts.gameOver) {
|
|
267
|
+
return false;
|
|
268
|
+
}
|
|
269
|
+
if (facts.table.length === 0) {
|
|
270
|
+
return false;
|
|
271
|
+
}
|
|
272
|
+
if (facts.pool.length > 0) {
|
|
273
|
+
return false;
|
|
274
|
+
}
|
|
275
|
+
const nums = facts.table.map((t: Tile) => t.value);
|
|
276
|
+
for (let i = 0; i < nums.length; i++) {
|
|
277
|
+
for (let j = i + 1; j < nums.length; j++) {
|
|
278
|
+
if (nums[i] + nums[j] === 10) {
|
|
279
|
+
return false;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
addLog("CONSTRAINT noMovesLeft: TRUE");
|
|
284
|
+
|
|
285
|
+
return true;
|
|
286
|
+
},
|
|
287
|
+
require: (facts) => ({
|
|
288
|
+
type: "END_GAME",
|
|
289
|
+
reason: `Game over! Removed ${facts.removed.length} of 36 tiles.`,
|
|
290
|
+
}),
|
|
291
|
+
},
|
|
292
|
+
|
|
293
|
+
// Win condition
|
|
294
|
+
allCleared: {
|
|
295
|
+
priority: 200,
|
|
296
|
+
when: (facts) => {
|
|
297
|
+
const result =
|
|
298
|
+
!facts.gameOver &&
|
|
299
|
+
facts.table.length === 0 &&
|
|
300
|
+
facts.pool.length === 0;
|
|
301
|
+
if (result) {
|
|
302
|
+
addLog("CONSTRAINT allCleared: TRUE");
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return result;
|
|
306
|
+
},
|
|
307
|
+
require: (facts) => ({
|
|
308
|
+
type: "END_GAME",
|
|
309
|
+
reason: `You win! Cleared all tiles in ${facts.moveCount} moves!`,
|
|
310
|
+
}),
|
|
311
|
+
},
|
|
312
|
+
},
|
|
313
|
+
|
|
314
|
+
// ============================================================================
|
|
315
|
+
// Resolvers
|
|
316
|
+
// ============================================================================
|
|
317
|
+
resolvers: {
|
|
318
|
+
removeTiles: {
|
|
319
|
+
requirement: "REMOVE_TILES",
|
|
320
|
+
resolve: async (req, context) => {
|
|
321
|
+
addLog("RESOLVER removeTiles: START");
|
|
322
|
+
const tilesToRemove = context.facts.table.filter((tile: Tile) =>
|
|
323
|
+
req.tileIds.includes(tile.id),
|
|
324
|
+
);
|
|
325
|
+
|
|
326
|
+
// Multiple fact mutations
|
|
327
|
+
addLog("RESOLVER removeTiles: setting table");
|
|
328
|
+
context.facts.table = context.facts.table.filter(
|
|
329
|
+
(tile: Tile) => !req.tileIds.includes(tile.id),
|
|
330
|
+
);
|
|
331
|
+
addLog("RESOLVER removeTiles: setting removed");
|
|
332
|
+
context.facts.removed = [...context.facts.removed, ...tilesToRemove];
|
|
333
|
+
addLog("RESOLVER removeTiles: clearing selected");
|
|
334
|
+
context.facts.selected = [];
|
|
335
|
+
addLog("RESOLVER removeTiles: incrementing moveCount");
|
|
336
|
+
context.facts.moveCount++;
|
|
337
|
+
addLog("RESOLVER removeTiles: setting message");
|
|
338
|
+
context.facts.message = `Removed ${tilesToRemove[0].value} + ${tilesToRemove[1].value} = 10!`;
|
|
339
|
+
addLog("RESOLVER removeTiles: DONE");
|
|
340
|
+
},
|
|
341
|
+
},
|
|
342
|
+
|
|
343
|
+
refillTable: {
|
|
344
|
+
requirement: "REFILL_TABLE",
|
|
345
|
+
resolve: async (req, context) => {
|
|
346
|
+
addLog(`RESOLVER refillTable: START (count: ${req.count})`);
|
|
347
|
+
const newTiles = context.facts.pool.slice(0, req.count);
|
|
348
|
+
context.facts.pool = context.facts.pool.slice(req.count);
|
|
349
|
+
context.facts.table = [...context.facts.table, ...newTiles];
|
|
350
|
+
addLog(
|
|
351
|
+
`RESOLVER refillTable: DONE (table now: ${context.facts.table.length})`,
|
|
352
|
+
);
|
|
353
|
+
},
|
|
354
|
+
},
|
|
355
|
+
|
|
356
|
+
endGame: {
|
|
357
|
+
requirement: "END_GAME",
|
|
358
|
+
resolve: async (req, context) => {
|
|
359
|
+
addLog(`RESOLVER endGame: ${req.reason}`);
|
|
360
|
+
context.facts.gameOver = true;
|
|
361
|
+
context.facts.message = req.reason;
|
|
362
|
+
},
|
|
363
|
+
},
|
|
364
|
+
},
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
// ============================================================================
|
|
368
|
+
// System
|
|
369
|
+
// ============================================================================
|
|
370
|
+
|
|
371
|
+
export const system = createSystem({
|
|
372
|
+
module: numberMatch,
|
|
373
|
+
plugins: [devtoolsPlugin({ name: "number-match" })],
|
|
374
|
+
history: true,
|
|
375
|
+
trace: true,
|
|
376
|
+
});
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@directive-run/knowledge",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.4",
|
|
4
4
|
"description": "Knowledge files, examples, and validation for Directive — the constraint-driven TypeScript runtime.",
|
|
5
|
-
"license": "MIT",
|
|
5
|
+
"license": "(MIT OR Apache-2.0)",
|
|
6
6
|
"author": "Jason Comes",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
@@ -50,8 +50,8 @@
|
|
|
50
50
|
"tsx": "^4.19.2",
|
|
51
51
|
"typescript": "^5.7.2",
|
|
52
52
|
"vitest": "^3.0.0",
|
|
53
|
-
"@directive-run/core": "0.8.
|
|
54
|
-
"@directive-run/ai": "0.8.
|
|
53
|
+
"@directive-run/core": "0.8.4",
|
|
54
|
+
"@directive-run/ai": "0.8.4"
|
|
55
55
|
},
|
|
56
56
|
"scripts": {
|
|
57
57
|
"build": "tsx scripts/generate-api-skeleton.ts && tsx scripts/extract-examples.ts && tsup",
|