@directive-run/knowledge 0.2.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 +21 -0
- package/README.md +63 -0
- package/ai/ai-adapters.md +250 -0
- package/ai/ai-agents-streaming.md +269 -0
- package/ai/ai-budget-resilience.md +235 -0
- package/ai/ai-communication.md +281 -0
- package/ai/ai-debug-observability.md +243 -0
- package/ai/ai-guardrails-memory.md +332 -0
- package/ai/ai-mcp-rag.md +288 -0
- package/ai/ai-multi-agent.md +274 -0
- package/ai/ai-orchestrator.md +227 -0
- package/ai/ai-security.md +293 -0
- package/ai/ai-tasks.md +261 -0
- package/ai/ai-testing-evals.md +378 -0
- package/api-skeleton.md +5 -0
- package/core/anti-patterns.md +382 -0
- package/core/constraints.md +263 -0
- package/core/core-patterns.md +228 -0
- package/core/error-boundaries.md +322 -0
- package/core/multi-module.md +315 -0
- package/core/naming.md +283 -0
- package/core/plugins.md +344 -0
- package/core/react-adapter.md +262 -0
- package/core/resolvers.md +357 -0
- package/core/schema-types.md +262 -0
- package/core/system-api.md +271 -0
- package/core/testing.md +257 -0
- package/core/time-travel.md +238 -0
- package/dist/index.cjs +111 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +10 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +102 -0
- package/dist/index.js.map +1 -0
- package/examples/ab-testing.ts +385 -0
- package/examples/ai-checkpoint.ts +509 -0
- package/examples/ai-guardrails.ts +319 -0
- package/examples/ai-orchestrator.ts +589 -0
- package/examples/async-chains.ts +287 -0
- package/examples/auth-flow.ts +371 -0
- package/examples/batch-resolver.ts +341 -0
- package/examples/checkers.ts +589 -0
- package/examples/contact-form.ts +176 -0
- package/examples/counter.ts +393 -0
- package/examples/dashboard-loader.ts +512 -0
- package/examples/debounce-constraints.ts +105 -0
- package/examples/dynamic-modules.ts +293 -0
- package/examples/error-boundaries.ts +430 -0
- package/examples/feature-flags.ts +220 -0
- package/examples/form-wizard.ts +347 -0
- package/examples/fraud-analysis.ts +663 -0
- package/examples/goal-heist.ts +341 -0
- package/examples/multi-module.ts +57 -0
- package/examples/newsletter.ts +241 -0
- package/examples/notifications.ts +210 -0
- package/examples/optimistic-updates.ts +317 -0
- package/examples/pagination.ts +260 -0
- package/examples/permissions.ts +337 -0
- package/examples/provider-routing.ts +403 -0
- package/examples/server.ts +316 -0
- package/examples/shopping-cart.ts +422 -0
- package/examples/sudoku.ts +630 -0
- package/examples/theme-locale.ts +204 -0
- package/examples/time-machine.ts +225 -0
- package/examples/topic-guard.ts +306 -0
- package/examples/url-sync.ts +333 -0
- package/examples/websocket.ts +404 -0
- package/package.json +65 -0
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
// Example: contact-form
|
|
2
|
+
// Source: examples/contact-form/src/main.ts
|
|
3
|
+
// Extracted for AI rules — DOM wiring stripped
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Contact Form — DOM Rendering & System Wiring
|
|
7
|
+
*
|
|
8
|
+
* Creates the Directive system, subscribes to state changes,
|
|
9
|
+
* renders the form and event timeline.
|
|
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
|
+
// Constants
|
|
22
|
+
// ============================================================================
|
|
23
|
+
|
|
24
|
+
const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
25
|
+
const RATE_LIMIT_MS = 10_000; // 10 seconds (shorter for demo)
|
|
26
|
+
|
|
27
|
+
// ============================================================================
|
|
28
|
+
// Timeline
|
|
29
|
+
// ============================================================================
|
|
30
|
+
|
|
31
|
+
interface TimelineEntry {
|
|
32
|
+
time: number;
|
|
33
|
+
event: string;
|
|
34
|
+
detail: string;
|
|
35
|
+
type: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const timeline: TimelineEntry[] = [];
|
|
39
|
+
|
|
40
|
+
function addTimelineEntry(event: string, detail: string, type: string) {
|
|
41
|
+
timeline.unshift({ time: Date.now(), event, detail, type });
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function log(msg: string) {
|
|
45
|
+
console.log(`[contact-form] ${msg}`);
|
|
46
|
+
|
|
47
|
+
// Classify and add to timeline
|
|
48
|
+
if (msg.startsWith("Sending:")) {
|
|
49
|
+
addTimelineEntry("submit", msg.replace("Sending: ", ""), "submit");
|
|
50
|
+
} else if (msg.includes("succeeded")) {
|
|
51
|
+
addTimelineEntry("success", msg, "submit");
|
|
52
|
+
} else if (msg.includes("failed")) {
|
|
53
|
+
addTimelineEntry("error", msg, "error");
|
|
54
|
+
} else if (msg.startsWith("Status:")) {
|
|
55
|
+
addTimelineEntry("status", msg.replace("Status: ", ""), "field");
|
|
56
|
+
} else if (msg.includes("Auto-resetting")) {
|
|
57
|
+
addTimelineEntry("auto-reset", msg, "reset");
|
|
58
|
+
} else if (msg === "Form reset") {
|
|
59
|
+
addTimelineEntry("reset", "Form cleared", "reset");
|
|
60
|
+
} else if (msg.includes("ready")) {
|
|
61
|
+
addTimelineEntry("init", msg, "field");
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ============================================================================
|
|
66
|
+
// Schema
|
|
67
|
+
// ============================================================================
|
|
68
|
+
|
|
69
|
+
const schema = {
|
|
70
|
+
facts: {
|
|
71
|
+
name: t.string(),
|
|
72
|
+
email: t.string(),
|
|
73
|
+
subject: t.string(),
|
|
74
|
+
message: t.string(),
|
|
75
|
+
touched: t.object<Record<string, boolean>>(),
|
|
76
|
+
status: t.string<"idle" | "submitting" | "success" | "error">(),
|
|
77
|
+
errorMessage: t.string(),
|
|
78
|
+
lastSubmittedAt: t.number(),
|
|
79
|
+
submissionCount: t.number(),
|
|
80
|
+
},
|
|
81
|
+
derivations: {
|
|
82
|
+
nameError: t.string(),
|
|
83
|
+
emailError: t.string(),
|
|
84
|
+
subjectError: t.string(),
|
|
85
|
+
messageError: t.string(),
|
|
86
|
+
isValid: t.boolean(),
|
|
87
|
+
canSubmit: t.boolean(),
|
|
88
|
+
messageCharCount: t.number(),
|
|
89
|
+
},
|
|
90
|
+
events: {
|
|
91
|
+
updateField: { field: t.string(), value: t.string() },
|
|
92
|
+
touchField: { field: t.string() },
|
|
93
|
+
submit: {},
|
|
94
|
+
reset: {},
|
|
95
|
+
},
|
|
96
|
+
requirements: {
|
|
97
|
+
SEND_MESSAGE: {},
|
|
98
|
+
RESET_AFTER_DELAY: {},
|
|
99
|
+
},
|
|
100
|
+
} satisfies ModuleSchema;
|
|
101
|
+
|
|
102
|
+
// ============================================================================
|
|
103
|
+
// Module
|
|
104
|
+
// ============================================================================
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
// ============================================================================
|
|
108
|
+
// System
|
|
109
|
+
// ============================================================================
|
|
110
|
+
|
|
111
|
+
const system = createSystem({
|
|
112
|
+
module: contactForm,
|
|
113
|
+
debug: { runHistory: true },
|
|
114
|
+
plugins: [devtoolsPlugin({ name: "contact-form" })],
|
|
115
|
+
});
|
|
116
|
+
system.start();
|
|
117
|
+
|
|
118
|
+
// ============================================================================
|
|
119
|
+
// DOM References
|
|
120
|
+
// ============================================================================
|
|
121
|
+
|
|
122
|
+
// Form inputs
|
|
123
|
+
|
|
124
|
+
// Timeline
|
|
125
|
+
|
|
126
|
+
// ============================================================================
|
|
127
|
+
// Input Handlers
|
|
128
|
+
// ============================================================================
|
|
129
|
+
|
|
130
|
+
for (const [el, field] of [
|
|
131
|
+
[nameInput, "name"],
|
|
132
|
+
[emailInput, "email"],
|
|
133
|
+
[subjectInput, "subject"],
|
|
134
|
+
[messageInput, "message"],
|
|
135
|
+
] as const) {
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
// ============================================================================
|
|
140
|
+
// Render
|
|
141
|
+
// ============================================================================
|
|
142
|
+
|
|
143
|
+
function escapeHtml(text: string): string {
|
|
144
|
+
|
|
145
|
+
return div.innerHTML;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
// Subscribe to all relevant facts and derivations
|
|
150
|
+
system.subscribe(
|
|
151
|
+
[
|
|
152
|
+
"name",
|
|
153
|
+
"email",
|
|
154
|
+
"subject",
|
|
155
|
+
"message",
|
|
156
|
+
"touched",
|
|
157
|
+
"status",
|
|
158
|
+
"errorMessage",
|
|
159
|
+
"lastSubmittedAt",
|
|
160
|
+
"submissionCount",
|
|
161
|
+
"nameError",
|
|
162
|
+
"emailError",
|
|
163
|
+
"subjectError",
|
|
164
|
+
"messageError",
|
|
165
|
+
"isValid",
|
|
166
|
+
"canSubmit",
|
|
167
|
+
"messageCharCount",
|
|
168
|
+
],
|
|
169
|
+
render,
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
// Initial render
|
|
173
|
+
render();
|
|
174
|
+
log("Contact form ready. Fill in all fields and submit.");
|
|
175
|
+
|
|
176
|
+
// Signal to tests that initialization is complete
|
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
// Example: counter
|
|
2
|
+
// Source: examples/counter/src/main.ts
|
|
3
|
+
// Extracted for AI rules — DOM wiring stripped
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Number Match — DOM Rendering & System Wiring
|
|
7
|
+
*
|
|
8
|
+
* Creates the Directive system, subscribes to state changes,
|
|
9
|
+
* renders the game grid and event timeline.
|
|
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
|
+
interface Tile {
|
|
25
|
+
id: string;
|
|
26
|
+
value: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface TimelineEntry {
|
|
30
|
+
time: number;
|
|
31
|
+
event: string;
|
|
32
|
+
detail: string;
|
|
33
|
+
type: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Create a pool of numbered tiles (1-9, four of each = 36 tiles)
|
|
37
|
+
function createPool(): Tile[] {
|
|
38
|
+
const tiles: Tile[] = [];
|
|
39
|
+
let id = 0;
|
|
40
|
+
for (let copy = 0; copy < 4; copy++) {
|
|
41
|
+
for (let value = 1; value <= 9; value++) {
|
|
42
|
+
tiles.push({ id: `t${id++}`, value });
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
// Shuffle
|
|
46
|
+
for (let i = tiles.length - 1; i > 0; i--) {
|
|
47
|
+
const j = Math.floor(Math.random() * (i + 1));
|
|
48
|
+
[tiles[i], tiles[j]] = [tiles[j], tiles[i]];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return tiles;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ============================================================================
|
|
55
|
+
// Timeline
|
|
56
|
+
// ============================================================================
|
|
57
|
+
|
|
58
|
+
const timeline: TimelineEntry[] = [];
|
|
59
|
+
|
|
60
|
+
function log(msg: string) {
|
|
61
|
+
console.log(`[NumberMatch] ${msg}`);
|
|
62
|
+
|
|
63
|
+
// Classify and add significant events to the timeline
|
|
64
|
+
let event = "";
|
|
65
|
+
let detail = "";
|
|
66
|
+
let type = "info";
|
|
67
|
+
|
|
68
|
+
if (msg.startsWith("EVENT selectTile")) {
|
|
69
|
+
event = "tile selected";
|
|
70
|
+
const match = msg.match(/selectTile: (t\d+)/);
|
|
71
|
+
detail = match ? match[1] : "";
|
|
72
|
+
type = "selection";
|
|
73
|
+
} else if (msg.includes("pairAddsTen: TRUE")) {
|
|
74
|
+
event = "match found";
|
|
75
|
+
const match = msg.match(/\((.+)\)/);
|
|
76
|
+
detail = match ? match[1] : "";
|
|
77
|
+
type = "match";
|
|
78
|
+
} else if (msg === "RESOLVER removeTiles: DONE") {
|
|
79
|
+
event = "tiles removed";
|
|
80
|
+
detail = "";
|
|
81
|
+
type = "match";
|
|
82
|
+
} else if (msg.includes("refillTable: DONE")) {
|
|
83
|
+
event = "refill";
|
|
84
|
+
const match = msg.match(/table now: (\d+)/);
|
|
85
|
+
detail = match ? `table: ${match[1]} tiles` : "";
|
|
86
|
+
type = "refill";
|
|
87
|
+
} else if (msg.startsWith("RESOLVER endGame:")) {
|
|
88
|
+
event = "game over";
|
|
89
|
+
detail = msg.replace("RESOLVER endGame: ", "");
|
|
90
|
+
type = "gameover";
|
|
91
|
+
} else if (msg.includes("New game") || msg.includes("Game started")) {
|
|
92
|
+
event = "new game";
|
|
93
|
+
detail = msg;
|
|
94
|
+
type = "newgame";
|
|
95
|
+
} else {
|
|
96
|
+
// Skip verbose intermediate messages (RESOLVER steps, CONSTRAINT produce)
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
timeline.unshift({ time: Date.now(), event, detail, type });
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ============================================================================
|
|
104
|
+
// Schema - same structure as eleven-up
|
|
105
|
+
// ============================================================================
|
|
106
|
+
|
|
107
|
+
const schema = {
|
|
108
|
+
facts: {
|
|
109
|
+
pool: t.object<Tile[]>(),
|
|
110
|
+
table: t.object<Tile[]>(),
|
|
111
|
+
removed: t.object<Tile[]>(),
|
|
112
|
+
selected: t.object<string[]>(),
|
|
113
|
+
message: t.string(),
|
|
114
|
+
moveCount: t.number(),
|
|
115
|
+
gameOver: t.boolean(),
|
|
116
|
+
},
|
|
117
|
+
derivations: {
|
|
118
|
+
poolCount: t.number(),
|
|
119
|
+
removedCount: t.number(),
|
|
120
|
+
selectedTiles: t.object<Tile[]>(),
|
|
121
|
+
hasValidMoves: t.boolean(),
|
|
122
|
+
},
|
|
123
|
+
events: {
|
|
124
|
+
newGame: {},
|
|
125
|
+
selectTile: { tileId: t.string() },
|
|
126
|
+
deselectTile: { tileId: t.string() },
|
|
127
|
+
clearSelection: {},
|
|
128
|
+
},
|
|
129
|
+
requirements: {
|
|
130
|
+
REMOVE_TILES: { tileIds: t.object<string[]>() },
|
|
131
|
+
REFILL_TABLE: { count: t.number() },
|
|
132
|
+
END_GAME: { reason: t.string() },
|
|
133
|
+
},
|
|
134
|
+
} satisfies ModuleSchema;
|
|
135
|
+
|
|
136
|
+
// ============================================================================
|
|
137
|
+
// Module
|
|
138
|
+
// ============================================================================
|
|
139
|
+
|
|
140
|
+
const numberMatch = createModule("number-match", {
|
|
141
|
+
schema,
|
|
142
|
+
|
|
143
|
+
init: (facts) => {
|
|
144
|
+
const pool = createPool();
|
|
145
|
+
facts.pool = pool.slice(9);
|
|
146
|
+
facts.table = pool.slice(0, 9);
|
|
147
|
+
facts.removed = [];
|
|
148
|
+
facts.selected = [];
|
|
149
|
+
facts.message = "Select two numbers that add to 10";
|
|
150
|
+
facts.moveCount = 0;
|
|
151
|
+
facts.gameOver = false;
|
|
152
|
+
},
|
|
153
|
+
|
|
154
|
+
derive: {
|
|
155
|
+
poolCount: (facts) => facts.pool.length,
|
|
156
|
+
removedCount: (facts) => facts.removed.length,
|
|
157
|
+
selectedTiles: (facts) =>
|
|
158
|
+
facts.table.filter((tile: Tile) => facts.selected.includes(tile.id)),
|
|
159
|
+
hasValidMoves: (facts) => {
|
|
160
|
+
const nums = facts.table.map((t: Tile) => t.value);
|
|
161
|
+
for (let i = 0; i < nums.length; i++) {
|
|
162
|
+
for (let j = i + 1; j < nums.length; j++) {
|
|
163
|
+
if (nums[i] + nums[j] === 10) return true;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return false;
|
|
167
|
+
},
|
|
168
|
+
},
|
|
169
|
+
|
|
170
|
+
events: {
|
|
171
|
+
newGame: (facts) => {
|
|
172
|
+
const pool = createPool();
|
|
173
|
+
facts.pool = pool.slice(9);
|
|
174
|
+
facts.table = pool.slice(0, 9);
|
|
175
|
+
facts.removed = [];
|
|
176
|
+
facts.selected = [];
|
|
177
|
+
facts.message = "New game! Select two numbers that add to 10";
|
|
178
|
+
facts.moveCount = 0;
|
|
179
|
+
facts.gameOver = false;
|
|
180
|
+
},
|
|
181
|
+
selectTile: (facts, { tileId }) => {
|
|
182
|
+
if (!facts.selected.includes(tileId) && !facts.gameOver) {
|
|
183
|
+
facts.selected = [...facts.selected, tileId];
|
|
184
|
+
log(`EVENT selectTile: ${tileId}, selected now: [${facts.selected}]`);
|
|
185
|
+
}
|
|
186
|
+
},
|
|
187
|
+
deselectTile: (facts, { tileId }) => {
|
|
188
|
+
facts.selected = facts.selected.filter((id: string) => id !== tileId);
|
|
189
|
+
},
|
|
190
|
+
clearSelection: (facts) => {
|
|
191
|
+
facts.selected = [];
|
|
192
|
+
},
|
|
193
|
+
},
|
|
194
|
+
|
|
195
|
+
// ============================================================================
|
|
196
|
+
// Constraints - same pattern as eleven-up
|
|
197
|
+
// ============================================================================
|
|
198
|
+
constraints: {
|
|
199
|
+
// When two selected tiles add to 10 -> remove them
|
|
200
|
+
pairAddsTen: {
|
|
201
|
+
priority: 100,
|
|
202
|
+
when: (facts) => {
|
|
203
|
+
if (facts.gameOver) return false;
|
|
204
|
+
const selected = facts.table.filter((tile: Tile) =>
|
|
205
|
+
facts.selected.includes(tile.id),
|
|
206
|
+
);
|
|
207
|
+
if (selected.length !== 2) return false;
|
|
208
|
+
const result = selected[0].value + selected[1].value === 10;
|
|
209
|
+
if (result)
|
|
210
|
+
log(
|
|
211
|
+
`CONSTRAINT pairAddsTen: TRUE (${selected[0].value} + ${selected[1].value})`,
|
|
212
|
+
);
|
|
213
|
+
return result;
|
|
214
|
+
},
|
|
215
|
+
require: (facts) => {
|
|
216
|
+
log("CONSTRAINT pairAddsTen: producing REMOVE_TILES");
|
|
217
|
+
return {
|
|
218
|
+
type: "REMOVE_TILES",
|
|
219
|
+
tileIds: [...facts.selected],
|
|
220
|
+
};
|
|
221
|
+
},
|
|
222
|
+
},
|
|
223
|
+
|
|
224
|
+
// Refill table when tiles are removed
|
|
225
|
+
refillTable: {
|
|
226
|
+
priority: 50,
|
|
227
|
+
when: (facts) => {
|
|
228
|
+
const result =
|
|
229
|
+
!facts.gameOver && facts.table.length < 9 && facts.pool.length > 0;
|
|
230
|
+
if (result)
|
|
231
|
+
log(
|
|
232
|
+
`CONSTRAINT refillTable: TRUE (table: ${facts.table.length}, pool: ${facts.pool.length})`,
|
|
233
|
+
);
|
|
234
|
+
return result;
|
|
235
|
+
},
|
|
236
|
+
require: (facts) => {
|
|
237
|
+
const count = Math.min(9 - facts.table.length, facts.pool.length);
|
|
238
|
+
log(`CONSTRAINT refillTable: producing REFILL_TABLE count=${count}`);
|
|
239
|
+
return { type: "REFILL_TABLE", count };
|
|
240
|
+
},
|
|
241
|
+
},
|
|
242
|
+
|
|
243
|
+
// No moves left -> game over
|
|
244
|
+
noMovesLeft: {
|
|
245
|
+
priority: 190,
|
|
246
|
+
when: (facts) => {
|
|
247
|
+
if (facts.gameOver) return false;
|
|
248
|
+
if (facts.table.length === 0) return false;
|
|
249
|
+
if (facts.pool.length > 0) return false;
|
|
250
|
+
const nums = facts.table.map((t: Tile) => t.value);
|
|
251
|
+
for (let i = 0; i < nums.length; i++) {
|
|
252
|
+
for (let j = i + 1; j < nums.length; j++) {
|
|
253
|
+
if (nums[i] + nums[j] === 10) return false;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
log("CONSTRAINT noMovesLeft: TRUE");
|
|
257
|
+
return true;
|
|
258
|
+
},
|
|
259
|
+
require: (facts) => ({
|
|
260
|
+
type: "END_GAME",
|
|
261
|
+
reason: `Game over! Removed ${facts.removed.length} of 36 tiles.`,
|
|
262
|
+
}),
|
|
263
|
+
},
|
|
264
|
+
|
|
265
|
+
// Win condition
|
|
266
|
+
allCleared: {
|
|
267
|
+
priority: 200,
|
|
268
|
+
when: (facts) => {
|
|
269
|
+
const result =
|
|
270
|
+
!facts.gameOver &&
|
|
271
|
+
facts.table.length === 0 &&
|
|
272
|
+
facts.pool.length === 0;
|
|
273
|
+
if (result) log("CONSTRAINT allCleared: TRUE");
|
|
274
|
+
return result;
|
|
275
|
+
},
|
|
276
|
+
require: (facts) => ({
|
|
277
|
+
type: "END_GAME",
|
|
278
|
+
reason: `You win! Cleared all tiles in ${facts.moveCount} moves!`,
|
|
279
|
+
}),
|
|
280
|
+
},
|
|
281
|
+
},
|
|
282
|
+
|
|
283
|
+
// ============================================================================
|
|
284
|
+
// Resolvers - same multi-fact mutation pattern as eleven-up
|
|
285
|
+
// ============================================================================
|
|
286
|
+
resolvers: {
|
|
287
|
+
removeTiles: {
|
|
288
|
+
requirement: "REMOVE_TILES",
|
|
289
|
+
resolve: async (req, context) => {
|
|
290
|
+
log("RESOLVER removeTiles: START");
|
|
291
|
+
const tilesToRemove = context.facts.table.filter((tile: Tile) =>
|
|
292
|
+
req.tileIds.includes(tile.id),
|
|
293
|
+
);
|
|
294
|
+
|
|
295
|
+
// Multiple fact mutations
|
|
296
|
+
log("RESOLVER removeTiles: setting table");
|
|
297
|
+
context.facts.table = context.facts.table.filter(
|
|
298
|
+
(tile: Tile) => !req.tileIds.includes(tile.id),
|
|
299
|
+
);
|
|
300
|
+
log("RESOLVER removeTiles: setting removed");
|
|
301
|
+
context.facts.removed = [...context.facts.removed, ...tilesToRemove];
|
|
302
|
+
log("RESOLVER removeTiles: clearing selected");
|
|
303
|
+
context.facts.selected = [];
|
|
304
|
+
log("RESOLVER removeTiles: incrementing moveCount");
|
|
305
|
+
context.facts.moveCount++;
|
|
306
|
+
log("RESOLVER removeTiles: setting message");
|
|
307
|
+
context.facts.message = `Removed ${tilesToRemove[0].value} + ${tilesToRemove[1].value} = 10!`;
|
|
308
|
+
log("RESOLVER removeTiles: DONE");
|
|
309
|
+
},
|
|
310
|
+
},
|
|
311
|
+
|
|
312
|
+
refillTable: {
|
|
313
|
+
requirement: "REFILL_TABLE",
|
|
314
|
+
resolve: async (req, context) => {
|
|
315
|
+
log(`RESOLVER refillTable: START (count: ${req.count})`);
|
|
316
|
+
const newTiles = context.facts.pool.slice(0, req.count);
|
|
317
|
+
context.facts.pool = context.facts.pool.slice(req.count);
|
|
318
|
+
context.facts.table = [...context.facts.table, ...newTiles];
|
|
319
|
+
log(
|
|
320
|
+
`RESOLVER refillTable: DONE (table now: ${context.facts.table.length})`,
|
|
321
|
+
);
|
|
322
|
+
},
|
|
323
|
+
},
|
|
324
|
+
|
|
325
|
+
endGame: {
|
|
326
|
+
requirement: "END_GAME",
|
|
327
|
+
resolve: async (req, context) => {
|
|
328
|
+
log(`RESOLVER endGame: ${req.reason}`);
|
|
329
|
+
context.facts.gameOver = true;
|
|
330
|
+
context.facts.message = req.reason;
|
|
331
|
+
},
|
|
332
|
+
},
|
|
333
|
+
},
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
// ============================================================================
|
|
337
|
+
// System
|
|
338
|
+
// ============================================================================
|
|
339
|
+
|
|
340
|
+
const system = createSystem({
|
|
341
|
+
module: numberMatch,
|
|
342
|
+
plugins: [devtoolsPlugin({ name: "number-match" })],
|
|
343
|
+
debug: { timeTravel: true, runHistory: true },
|
|
344
|
+
});
|
|
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
|