@directive-run/knowledge 0.2.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.md +3 -3
- package/ai/ai-adapters.md +7 -7
- package/ai/ai-agents-streaming.md +8 -8
- package/ai/ai-budget-resilience.md +5 -5
- package/ai/ai-communication.md +1 -1
- package/ai/ai-guardrails-memory.md +7 -7
- package/ai/ai-mcp-rag.md +5 -5
- package/ai/ai-multi-agent.md +14 -14
- package/ai/ai-orchestrator.md +8 -8
- package/ai/ai-security.md +2 -2
- package/ai/ai-tasks.md +9 -9
- package/ai/ai-testing-evals.md +2 -2
- package/core/anti-patterns.md +39 -39
- package/core/constraints.md +15 -15
- package/core/core-patterns.md +9 -9
- package/core/error-boundaries.md +7 -7
- package/core/multi-module.md +16 -16
- package/core/naming.md +21 -21
- package/core/plugins.md +14 -14
- package/core/react-adapter.md +13 -13
- package/core/resolvers.md +14 -14
- package/core/schema-types.md +22 -22
- package/core/system-api.md +16 -16
- package/core/testing.md +5 -5
- package/core/time-travel.md +20 -20
- package/dist/index.cjs +6 -105
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +7 -97
- package/dist/index.js.map +1 -1
- package/examples/ab-testing.ts +18 -90
- package/examples/ai-checkpoint.ts +68 -87
- package/examples/ai-guardrails.ts +20 -70
- package/examples/auth-flow.ts +2 -2
- package/examples/batch-resolver.ts +19 -59
- package/examples/contact-form.ts +220 -69
- package/examples/counter.ts +77 -95
- package/examples/dashboard-loader.ts +38 -56
- package/examples/debounce-constraints.ts +0 -2
- package/examples/dynamic-modules.ts +17 -20
- package/examples/error-boundaries.ts +30 -81
- package/examples/form-wizard.ts +6 -6
- package/examples/newsletter.ts +24 -51
- package/examples/notifications.ts +24 -23
- package/examples/optimistic-updates.ts +36 -41
- package/examples/pagination.ts +2 -2
- package/examples/permissions.ts +22 -32
- package/examples/provider-routing.ts +26 -83
- package/examples/shopping-cart.ts +12 -12
- package/examples/sudoku.ts +60 -67
- package/examples/theme-locale.ts +4 -7
- package/examples/time-machine.ts +12 -90
- package/examples/topic-guard.ts +31 -39
- package/examples/url-sync.ts +8 -8
- package/examples/websocket.ts +5 -5
- package/package.json +3 -3
package/examples/contact-form.ts
CHANGED
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
// Example: contact-form
|
|
2
|
-
// Source: examples/contact-form/src/
|
|
3
|
-
//
|
|
2
|
+
// Source: examples/contact-form/src/module.ts
|
|
3
|
+
// Pure module file — no DOM wiring
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
* Contact Form —
|
|
6
|
+
* Contact Form — Directive Module
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
8
|
+
* Multi-field contact form showcasing validation, constraints, and resolvers:
|
|
9
|
+
* - Facts: name, email, subject, message, touched, status, errorMessage, etc.
|
|
10
|
+
* - Derivations: field errors, isValid, canSubmit, messageCharCount
|
|
11
|
+
* - Events: updateField, touchField, submit, reset
|
|
12
|
+
* - Constraints: submitForm, resetAfterSuccess
|
|
13
|
+
* - Resolvers: simulated async send, auto-reset after delay
|
|
14
|
+
* - Effects: logging status transitions
|
|
15
|
+
*
|
|
16
|
+
* Uses a simulated setTimeout instead of a real API so no account is needed.
|
|
10
17
|
*/
|
|
11
18
|
|
|
12
19
|
import {
|
|
@@ -25,23 +32,35 @@ const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
|
25
32
|
const RATE_LIMIT_MS = 10_000; // 10 seconds (shorter for demo)
|
|
26
33
|
|
|
27
34
|
// ============================================================================
|
|
28
|
-
//
|
|
35
|
+
// Types
|
|
29
36
|
// ============================================================================
|
|
30
37
|
|
|
31
|
-
interface TimelineEntry {
|
|
38
|
+
export interface TimelineEntry {
|
|
32
39
|
time: number;
|
|
33
40
|
event: string;
|
|
34
41
|
detail: string;
|
|
35
42
|
type: string;
|
|
36
43
|
}
|
|
37
44
|
|
|
38
|
-
|
|
45
|
+
// ============================================================================
|
|
46
|
+
// Timeline (external mutable array, same pattern as fraud-analysis)
|
|
47
|
+
// ============================================================================
|
|
48
|
+
|
|
49
|
+
export const timeline: TimelineEntry[] = [];
|
|
39
50
|
|
|
40
|
-
function addTimelineEntry(
|
|
51
|
+
export function addTimelineEntry(
|
|
52
|
+
event: string,
|
|
53
|
+
detail: string,
|
|
54
|
+
type: string,
|
|
55
|
+
): void {
|
|
41
56
|
timeline.unshift({ time: Date.now(), event, detail, type });
|
|
42
57
|
}
|
|
43
58
|
|
|
44
|
-
|
|
59
|
+
// ============================================================================
|
|
60
|
+
// Logs helper
|
|
61
|
+
// ============================================================================
|
|
62
|
+
|
|
63
|
+
export function log(msg: string): void {
|
|
45
64
|
console.log(`[contact-form] ${msg}`);
|
|
46
65
|
|
|
47
66
|
// Classify and add to timeline
|
|
@@ -66,7 +85,7 @@ function log(msg: string) {
|
|
|
66
85
|
// Schema
|
|
67
86
|
// ============================================================================
|
|
68
87
|
|
|
69
|
-
const schema = {
|
|
88
|
+
export const schema = {
|
|
70
89
|
facts: {
|
|
71
90
|
name: t.string(),
|
|
72
91
|
email: t.string(),
|
|
@@ -103,74 +122,206 @@ const schema = {
|
|
|
103
122
|
// Module
|
|
104
123
|
// ============================================================================
|
|
105
124
|
|
|
125
|
+
const contactForm = createModule("contact-form", {
|
|
126
|
+
schema,
|
|
106
127
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
128
|
+
init: (facts) => {
|
|
129
|
+
facts.name = "";
|
|
130
|
+
facts.email = "";
|
|
131
|
+
facts.subject = "";
|
|
132
|
+
facts.message = "";
|
|
133
|
+
facts.touched = {};
|
|
134
|
+
facts.status = "idle";
|
|
135
|
+
facts.errorMessage = "";
|
|
136
|
+
facts.lastSubmittedAt = 0;
|
|
137
|
+
facts.submissionCount = 0;
|
|
138
|
+
},
|
|
110
139
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
}
|
|
116
|
-
|
|
140
|
+
derive: {
|
|
141
|
+
nameError: (facts) => {
|
|
142
|
+
if (!facts.touched.name) {
|
|
143
|
+
return "";
|
|
144
|
+
}
|
|
145
|
+
if (!facts.name.trim()) {
|
|
146
|
+
return "Name is required";
|
|
147
|
+
}
|
|
148
|
+
if (facts.name.trim().length < 2) {
|
|
149
|
+
return "Name must be at least 2 characters";
|
|
150
|
+
}
|
|
117
151
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
// ============================================================================
|
|
152
|
+
return "";
|
|
153
|
+
},
|
|
121
154
|
|
|
122
|
-
|
|
155
|
+
emailError: (facts) => {
|
|
156
|
+
if (!facts.touched.email) {
|
|
157
|
+
return "";
|
|
158
|
+
}
|
|
159
|
+
if (!facts.email.trim()) {
|
|
160
|
+
return "Email is required";
|
|
161
|
+
}
|
|
162
|
+
if (!EMAIL_REGEX.test(facts.email)) {
|
|
163
|
+
return "Enter a valid email address";
|
|
164
|
+
}
|
|
123
165
|
|
|
124
|
-
|
|
166
|
+
return "";
|
|
167
|
+
},
|
|
125
168
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
169
|
+
subjectError: (facts) => {
|
|
170
|
+
if (!facts.touched.subject) {
|
|
171
|
+
return "";
|
|
172
|
+
}
|
|
173
|
+
if (!facts.subject) {
|
|
174
|
+
return "Please select a subject";
|
|
175
|
+
}
|
|
129
176
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
[emailInput, "email"],
|
|
133
|
-
[subjectInput, "subject"],
|
|
134
|
-
[messageInput, "message"],
|
|
135
|
-
] as const) {
|
|
136
|
-
}
|
|
177
|
+
return "";
|
|
178
|
+
},
|
|
137
179
|
|
|
180
|
+
messageError: (facts) => {
|
|
181
|
+
if (!facts.touched.message) {
|
|
182
|
+
return "";
|
|
183
|
+
}
|
|
184
|
+
if (!facts.message.trim()) {
|
|
185
|
+
return "Message is required";
|
|
186
|
+
}
|
|
187
|
+
if (facts.message.trim().length < 10) {
|
|
188
|
+
return "Message must be at least 10 characters";
|
|
189
|
+
}
|
|
138
190
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
// ============================================================================
|
|
191
|
+
return "";
|
|
192
|
+
},
|
|
142
193
|
|
|
143
|
-
|
|
194
|
+
isValid: (facts) =>
|
|
195
|
+
facts.name.trim().length >= 2 &&
|
|
196
|
+
EMAIL_REGEX.test(facts.email) &&
|
|
197
|
+
facts.subject !== "" &&
|
|
198
|
+
facts.message.trim().length >= 10,
|
|
144
199
|
|
|
145
|
-
|
|
146
|
-
|
|
200
|
+
canSubmit: (facts, derived) => {
|
|
201
|
+
if (!derived.isValid) {
|
|
202
|
+
return false;
|
|
203
|
+
}
|
|
204
|
+
if (facts.status !== "idle") {
|
|
205
|
+
return false;
|
|
206
|
+
}
|
|
207
|
+
if (
|
|
208
|
+
facts.lastSubmittedAt > 0 &&
|
|
209
|
+
Date.now() - facts.lastSubmittedAt < RATE_LIMIT_MS
|
|
210
|
+
) {
|
|
211
|
+
return false;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return true;
|
|
215
|
+
},
|
|
147
216
|
|
|
217
|
+
messageCharCount: (facts) => facts.message.length,
|
|
218
|
+
},
|
|
219
|
+
|
|
220
|
+
events: {
|
|
221
|
+
updateField: (facts, { field, value }) => {
|
|
222
|
+
const key = field as "name" | "email" | "subject" | "message";
|
|
223
|
+
if (key in facts && typeof facts[key] === "string") {
|
|
224
|
+
(facts as unknown as Record<string, string>)[key] = value;
|
|
225
|
+
}
|
|
226
|
+
},
|
|
148
227
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
228
|
+
touchField: (facts, { field }) => {
|
|
229
|
+
facts.touched = { ...facts.touched, [field]: true };
|
|
230
|
+
},
|
|
231
|
+
|
|
232
|
+
submit: (facts) => {
|
|
233
|
+
facts.touched = { name: true, email: true, subject: true, message: true };
|
|
234
|
+
facts.status = "submitting";
|
|
235
|
+
},
|
|
236
|
+
|
|
237
|
+
reset: (facts) => {
|
|
238
|
+
facts.name = "";
|
|
239
|
+
facts.email = "";
|
|
240
|
+
facts.subject = "";
|
|
241
|
+
facts.message = "";
|
|
242
|
+
facts.touched = {};
|
|
243
|
+
facts.status = "idle";
|
|
244
|
+
facts.errorMessage = "";
|
|
245
|
+
},
|
|
246
|
+
},
|
|
247
|
+
|
|
248
|
+
constraints: {
|
|
249
|
+
submitForm: {
|
|
250
|
+
when: (facts) => facts.status === "submitting",
|
|
251
|
+
require: { type: "SEND_MESSAGE" },
|
|
252
|
+
},
|
|
253
|
+
|
|
254
|
+
resetAfterSuccess: {
|
|
255
|
+
when: (facts) => facts.status === "success",
|
|
256
|
+
require: { type: "RESET_AFTER_DELAY" },
|
|
257
|
+
},
|
|
258
|
+
},
|
|
259
|
+
|
|
260
|
+
resolvers: {
|
|
261
|
+
sendMessage: {
|
|
262
|
+
requirement: "SEND_MESSAGE",
|
|
263
|
+
resolve: async (req, context) => {
|
|
264
|
+
log(
|
|
265
|
+
`Sending: ${context.facts.name} <${context.facts.email}> [${context.facts.subject}]`,
|
|
266
|
+
);
|
|
267
|
+
|
|
268
|
+
await new Promise((resolve) => setTimeout(resolve, 1500));
|
|
269
|
+
|
|
270
|
+
if (Math.random() < 0.2) {
|
|
271
|
+
context.facts.status = "error";
|
|
272
|
+
context.facts.errorMessage =
|
|
273
|
+
"Simulated error — try again (20% failure rate for demo).";
|
|
274
|
+
log("Submission failed (simulated)");
|
|
275
|
+
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
context.facts.status = "success";
|
|
280
|
+
context.facts.lastSubmittedAt = Date.now();
|
|
281
|
+
context.facts.submissionCount++;
|
|
282
|
+
log(`Submission #${context.facts.submissionCount} succeeded`);
|
|
283
|
+
},
|
|
284
|
+
},
|
|
285
|
+
|
|
286
|
+
resetAfterDelay: {
|
|
287
|
+
requirement: "RESET_AFTER_DELAY",
|
|
288
|
+
resolve: async (req, context) => {
|
|
289
|
+
log("Auto-resetting in 3 seconds...");
|
|
290
|
+
await new Promise((resolve) => setTimeout(resolve, 3000));
|
|
291
|
+
context.facts.name = "";
|
|
292
|
+
context.facts.email = "";
|
|
293
|
+
context.facts.subject = "";
|
|
294
|
+
context.facts.message = "";
|
|
295
|
+
context.facts.touched = {};
|
|
296
|
+
context.facts.status = "idle";
|
|
297
|
+
context.facts.errorMessage = "";
|
|
298
|
+
log("Form reset");
|
|
299
|
+
},
|
|
300
|
+
},
|
|
301
|
+
},
|
|
302
|
+
|
|
303
|
+
effects: {
|
|
304
|
+
logSubmission: {
|
|
305
|
+
deps: ["status", "submissionCount"],
|
|
306
|
+
run: (facts, prev) => {
|
|
307
|
+
if (!prev) {
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (facts.status !== prev.status) {
|
|
312
|
+
log(`Status: ${prev.status} → ${facts.status}`);
|
|
313
|
+
}
|
|
314
|
+
},
|
|
315
|
+
},
|
|
316
|
+
},
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
// ============================================================================
|
|
320
|
+
// System
|
|
321
|
+
// ============================================================================
|
|
322
|
+
|
|
323
|
+
export const system = createSystem({
|
|
324
|
+
module: contactForm,
|
|
325
|
+
debug: { runHistory: true },
|
|
326
|
+
plugins: [devtoolsPlugin({ name: "contact-form" })],
|
|
327
|
+
});
|
package/examples/counter.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
// Example: counter
|
|
2
|
-
// Source: examples/counter/src/
|
|
3
|
-
//
|
|
2
|
+
// Source: examples/counter/src/module.ts
|
|
3
|
+
// Pure module file — no DOM wiring
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
* Number Match —
|
|
6
|
+
* Number Match — Directive Module
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
8
|
+
* Types, schema, helpers, module definition, timeline, and system creation
|
|
9
|
+
* for a tile-matching game where pairs must add to 10.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import {
|
|
@@ -21,18 +21,22 @@ import { devtoolsPlugin } from "@directive-run/core/plugins";
|
|
|
21
21
|
// Types
|
|
22
22
|
// ============================================================================
|
|
23
23
|
|
|
24
|
-
interface Tile {
|
|
24
|
+
export interface Tile {
|
|
25
25
|
id: string;
|
|
26
26
|
value: number;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
interface TimelineEntry {
|
|
29
|
+
export interface TimelineEntry {
|
|
30
30
|
time: number;
|
|
31
31
|
event: string;
|
|
32
32
|
detail: string;
|
|
33
33
|
type: string;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
// ============================================================================
|
|
37
|
+
// Helpers
|
|
38
|
+
// ============================================================================
|
|
39
|
+
|
|
36
40
|
// Create a pool of numbered tiles (1-9, four of each = 36 tiles)
|
|
37
41
|
function createPool(): Tile[] {
|
|
38
42
|
const tiles: Tile[] = [];
|
|
@@ -55,9 +59,9 @@ function createPool(): Tile[] {
|
|
|
55
59
|
// Timeline
|
|
56
60
|
// ============================================================================
|
|
57
61
|
|
|
58
|
-
const timeline: TimelineEntry[] = [];
|
|
62
|
+
export const timeline: TimelineEntry[] = [];
|
|
59
63
|
|
|
60
|
-
function
|
|
64
|
+
export function addLog(msg: string) {
|
|
61
65
|
console.log(`[NumberMatch] ${msg}`);
|
|
62
66
|
|
|
63
67
|
// Classify and add significant events to the timeline
|
|
@@ -101,15 +105,15 @@ function log(msg: string) {
|
|
|
101
105
|
}
|
|
102
106
|
|
|
103
107
|
// ============================================================================
|
|
104
|
-
// Schema
|
|
108
|
+
// Schema
|
|
105
109
|
// ============================================================================
|
|
106
110
|
|
|
107
|
-
const schema = {
|
|
111
|
+
export const schema = {
|
|
108
112
|
facts: {
|
|
109
|
-
pool: t.
|
|
110
|
-
table: t.
|
|
111
|
-
removed: t.
|
|
112
|
-
selected: t.
|
|
113
|
+
pool: t.array<Tile>(),
|
|
114
|
+
table: t.array<Tile>(),
|
|
115
|
+
removed: t.array<Tile>(),
|
|
116
|
+
selected: t.array<string>(),
|
|
113
117
|
message: t.string(),
|
|
114
118
|
moveCount: t.number(),
|
|
115
119
|
gameOver: t.boolean(),
|
|
@@ -117,7 +121,7 @@ const schema = {
|
|
|
117
121
|
derivations: {
|
|
118
122
|
poolCount: t.number(),
|
|
119
123
|
removedCount: t.number(),
|
|
120
|
-
selectedTiles: t.
|
|
124
|
+
selectedTiles: t.array<Tile>(),
|
|
121
125
|
hasValidMoves: t.boolean(),
|
|
122
126
|
},
|
|
123
127
|
events: {
|
|
@@ -127,7 +131,7 @@ const schema = {
|
|
|
127
131
|
clearSelection: {},
|
|
128
132
|
},
|
|
129
133
|
requirements: {
|
|
130
|
-
REMOVE_TILES: { tileIds: t.
|
|
134
|
+
REMOVE_TILES: { tileIds: t.array<string>() },
|
|
131
135
|
REFILL_TABLE: { count: t.number() },
|
|
132
136
|
END_GAME: { reason: t.string() },
|
|
133
137
|
},
|
|
@@ -160,9 +164,12 @@ const numberMatch = createModule("number-match", {
|
|
|
160
164
|
const nums = facts.table.map((t: Tile) => t.value);
|
|
161
165
|
for (let i = 0; i < nums.length; i++) {
|
|
162
166
|
for (let j = i + 1; j < nums.length; j++) {
|
|
163
|
-
if (nums[i] + nums[j] === 10)
|
|
167
|
+
if (nums[i] + nums[j] === 10) {
|
|
168
|
+
return true;
|
|
169
|
+
}
|
|
164
170
|
}
|
|
165
171
|
}
|
|
172
|
+
|
|
166
173
|
return false;
|
|
167
174
|
},
|
|
168
175
|
},
|
|
@@ -181,7 +188,9 @@ const numberMatch = createModule("number-match", {
|
|
|
181
188
|
selectTile: (facts, { tileId }) => {
|
|
182
189
|
if (!facts.selected.includes(tileId) && !facts.gameOver) {
|
|
183
190
|
facts.selected = [...facts.selected, tileId];
|
|
184
|
-
|
|
191
|
+
addLog(
|
|
192
|
+
`EVENT selectTile: ${tileId}, selected now: [${facts.selected}]`,
|
|
193
|
+
);
|
|
185
194
|
}
|
|
186
195
|
},
|
|
187
196
|
deselectTile: (facts, { tileId }) => {
|
|
@@ -193,27 +202,34 @@ const numberMatch = createModule("number-match", {
|
|
|
193
202
|
},
|
|
194
203
|
|
|
195
204
|
// ============================================================================
|
|
196
|
-
// Constraints
|
|
205
|
+
// Constraints
|
|
197
206
|
// ============================================================================
|
|
198
207
|
constraints: {
|
|
199
208
|
// When two selected tiles add to 10 -> remove them
|
|
200
209
|
pairAddsTen: {
|
|
201
210
|
priority: 100,
|
|
202
211
|
when: (facts) => {
|
|
203
|
-
if (facts.gameOver)
|
|
212
|
+
if (facts.gameOver) {
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
204
215
|
const selected = facts.table.filter((tile: Tile) =>
|
|
205
216
|
facts.selected.includes(tile.id),
|
|
206
217
|
);
|
|
207
|
-
if (selected.length !== 2)
|
|
218
|
+
if (selected.length !== 2) {
|
|
219
|
+
return false;
|
|
220
|
+
}
|
|
208
221
|
const result = selected[0].value + selected[1].value === 10;
|
|
209
|
-
if (result)
|
|
210
|
-
|
|
222
|
+
if (result) {
|
|
223
|
+
addLog(
|
|
211
224
|
`CONSTRAINT pairAddsTen: TRUE (${selected[0].value} + ${selected[1].value})`,
|
|
212
225
|
);
|
|
226
|
+
}
|
|
227
|
+
|
|
213
228
|
return result;
|
|
214
229
|
},
|
|
215
230
|
require: (facts) => {
|
|
216
|
-
|
|
231
|
+
addLog("CONSTRAINT pairAddsTen: producing REMOVE_TILES");
|
|
232
|
+
|
|
217
233
|
return {
|
|
218
234
|
type: "REMOVE_TILES",
|
|
219
235
|
tileIds: [...facts.selected],
|
|
@@ -227,15 +243,18 @@ const numberMatch = createModule("number-match", {
|
|
|
227
243
|
when: (facts) => {
|
|
228
244
|
const result =
|
|
229
245
|
!facts.gameOver && facts.table.length < 9 && facts.pool.length > 0;
|
|
230
|
-
if (result)
|
|
231
|
-
|
|
246
|
+
if (result) {
|
|
247
|
+
addLog(
|
|
232
248
|
`CONSTRAINT refillTable: TRUE (table: ${facts.table.length}, pool: ${facts.pool.length})`,
|
|
233
249
|
);
|
|
250
|
+
}
|
|
251
|
+
|
|
234
252
|
return result;
|
|
235
253
|
},
|
|
236
254
|
require: (facts) => {
|
|
237
255
|
const count = Math.min(9 - facts.table.length, facts.pool.length);
|
|
238
|
-
|
|
256
|
+
addLog(`CONSTRAINT refillTable: producing REFILL_TABLE count=${count}`);
|
|
257
|
+
|
|
239
258
|
return { type: "REFILL_TABLE", count };
|
|
240
259
|
},
|
|
241
260
|
},
|
|
@@ -244,16 +263,25 @@ const numberMatch = createModule("number-match", {
|
|
|
244
263
|
noMovesLeft: {
|
|
245
264
|
priority: 190,
|
|
246
265
|
when: (facts) => {
|
|
247
|
-
if (facts.gameOver)
|
|
248
|
-
|
|
249
|
-
|
|
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
|
+
}
|
|
250
275
|
const nums = facts.table.map((t: Tile) => t.value);
|
|
251
276
|
for (let i = 0; i < nums.length; i++) {
|
|
252
277
|
for (let j = i + 1; j < nums.length; j++) {
|
|
253
|
-
if (nums[i] + nums[j] === 10)
|
|
278
|
+
if (nums[i] + nums[j] === 10) {
|
|
279
|
+
return false;
|
|
280
|
+
}
|
|
254
281
|
}
|
|
255
282
|
}
|
|
256
|
-
|
|
283
|
+
addLog("CONSTRAINT noMovesLeft: TRUE");
|
|
284
|
+
|
|
257
285
|
return true;
|
|
258
286
|
},
|
|
259
287
|
require: (facts) => ({
|
|
@@ -270,7 +298,10 @@ const numberMatch = createModule("number-match", {
|
|
|
270
298
|
!facts.gameOver &&
|
|
271
299
|
facts.table.length === 0 &&
|
|
272
300
|
facts.pool.length === 0;
|
|
273
|
-
if (result)
|
|
301
|
+
if (result) {
|
|
302
|
+
addLog("CONSTRAINT allCleared: TRUE");
|
|
303
|
+
}
|
|
304
|
+
|
|
274
305
|
return result;
|
|
275
306
|
},
|
|
276
307
|
require: (facts) => ({
|
|
@@ -281,42 +312,42 @@ const numberMatch = createModule("number-match", {
|
|
|
281
312
|
},
|
|
282
313
|
|
|
283
314
|
// ============================================================================
|
|
284
|
-
// Resolvers
|
|
315
|
+
// Resolvers
|
|
285
316
|
// ============================================================================
|
|
286
317
|
resolvers: {
|
|
287
318
|
removeTiles: {
|
|
288
319
|
requirement: "REMOVE_TILES",
|
|
289
320
|
resolve: async (req, context) => {
|
|
290
|
-
|
|
321
|
+
addLog("RESOLVER removeTiles: START");
|
|
291
322
|
const tilesToRemove = context.facts.table.filter((tile: Tile) =>
|
|
292
323
|
req.tileIds.includes(tile.id),
|
|
293
324
|
);
|
|
294
325
|
|
|
295
326
|
// Multiple fact mutations
|
|
296
|
-
|
|
327
|
+
addLog("RESOLVER removeTiles: setting table");
|
|
297
328
|
context.facts.table = context.facts.table.filter(
|
|
298
329
|
(tile: Tile) => !req.tileIds.includes(tile.id),
|
|
299
330
|
);
|
|
300
|
-
|
|
331
|
+
addLog("RESOLVER removeTiles: setting removed");
|
|
301
332
|
context.facts.removed = [...context.facts.removed, ...tilesToRemove];
|
|
302
|
-
|
|
333
|
+
addLog("RESOLVER removeTiles: clearing selected");
|
|
303
334
|
context.facts.selected = [];
|
|
304
|
-
|
|
335
|
+
addLog("RESOLVER removeTiles: incrementing moveCount");
|
|
305
336
|
context.facts.moveCount++;
|
|
306
|
-
|
|
337
|
+
addLog("RESOLVER removeTiles: setting message");
|
|
307
338
|
context.facts.message = `Removed ${tilesToRemove[0].value} + ${tilesToRemove[1].value} = 10!`;
|
|
308
|
-
|
|
339
|
+
addLog("RESOLVER removeTiles: DONE");
|
|
309
340
|
},
|
|
310
341
|
},
|
|
311
342
|
|
|
312
343
|
refillTable: {
|
|
313
344
|
requirement: "REFILL_TABLE",
|
|
314
345
|
resolve: async (req, context) => {
|
|
315
|
-
|
|
346
|
+
addLog(`RESOLVER refillTable: START (count: ${req.count})`);
|
|
316
347
|
const newTiles = context.facts.pool.slice(0, req.count);
|
|
317
348
|
context.facts.pool = context.facts.pool.slice(req.count);
|
|
318
349
|
context.facts.table = [...context.facts.table, ...newTiles];
|
|
319
|
-
|
|
350
|
+
addLog(
|
|
320
351
|
`RESOLVER refillTable: DONE (table now: ${context.facts.table.length})`,
|
|
321
352
|
);
|
|
322
353
|
},
|
|
@@ -325,7 +356,7 @@ const numberMatch = createModule("number-match", {
|
|
|
325
356
|
endGame: {
|
|
326
357
|
requirement: "END_GAME",
|
|
327
358
|
resolve: async (req, context) => {
|
|
328
|
-
|
|
359
|
+
addLog(`RESOLVER endGame: ${req.reason}`);
|
|
329
360
|
context.facts.gameOver = true;
|
|
330
361
|
context.facts.message = req.reason;
|
|
331
362
|
},
|
|
@@ -337,57 +368,8 @@ const numberMatch = createModule("number-match", {
|
|
|
337
368
|
// System
|
|
338
369
|
// ============================================================================
|
|
339
370
|
|
|
340
|
-
const system = createSystem({
|
|
371
|
+
export const system = createSystem({
|
|
341
372
|
module: numberMatch,
|
|
342
373
|
plugins: [devtoolsPlugin({ name: "number-match" })],
|
|
343
374
|
debug: { timeTravel: true, runHistory: true },
|
|
344
375
|
});
|
|
345
|
-
system.start();
|
|
346
|
-
|
|
347
|
-
// ============================================================================
|
|
348
|
-
// DOM References
|
|
349
|
-
// ============================================================================
|
|
350
|
-
|
|
351
|
-
// Stats
|
|
352
|
-
|
|
353
|
-
// Timeline
|
|
354
|
-
|
|
355
|
-
// ============================================================================
|
|
356
|
-
// Render
|
|
357
|
-
// ============================================================================
|
|
358
|
-
|
|
359
|
-
function escapeHtml(text: string): string {
|
|
360
|
-
|
|
361
|
-
return div.innerHTML;
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
// ============================================================================
|
|
366
|
-
// Subscribe
|
|
367
|
-
// ============================================================================
|
|
368
|
-
|
|
369
|
-
system.subscribe(
|
|
370
|
-
[
|
|
371
|
-
"table",
|
|
372
|
-
"selected",
|
|
373
|
-
"pool",
|
|
374
|
-
"removed",
|
|
375
|
-
"moveCount",
|
|
376
|
-
"message",
|
|
377
|
-
"gameOver",
|
|
378
|
-
"poolCount",
|
|
379
|
-
"removedCount",
|
|
380
|
-
"selectedTiles",
|
|
381
|
-
"hasValidMoves",
|
|
382
|
-
],
|
|
383
|
-
render,
|
|
384
|
-
);
|
|
385
|
-
|
|
386
|
-
// Button handlers
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
// Initial render
|
|
390
|
-
render();
|
|
391
|
-
log("Game started. Select two numbers that add to 10.");
|
|
392
|
-
|
|
393
|
-
// Signal to tests that initialization is complete
|