@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,630 @@
|
|
|
1
|
+
// Example: sudoku
|
|
2
|
+
// Source: examples/sudoku/src/sudoku.ts
|
|
3
|
+
// Pure module file — no DOM wiring
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Sudoku – Directive Module
|
|
7
|
+
*
|
|
8
|
+
* Constraint-driven Sudoku game. Sudoku IS a constraint satisfaction problem:
|
|
9
|
+
* no duplicates in rows, columns, or 3x3 boxes. The game rules map directly
|
|
10
|
+
* to Directive's constraint→resolver flow.
|
|
11
|
+
*
|
|
12
|
+
* Also demonstrates temporal constraints (countdown timer) and runtime
|
|
13
|
+
* reconfiguration (difficulty modes) – patterns not shown in checkers.
|
|
14
|
+
*
|
|
15
|
+
* Pure Sudoku logic lives in rules.ts; puzzle generation in generator.ts.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { type ModuleSchema, createModule, t } from "@directive-run/core";
|
|
19
|
+
import { generatePuzzle } from "./generator.js";
|
|
20
|
+
import {
|
|
21
|
+
type Conflict,
|
|
22
|
+
type Difficulty,
|
|
23
|
+
type Grid,
|
|
24
|
+
MAX_HINTS,
|
|
25
|
+
TIMER_CRITICAL_THRESHOLD,
|
|
26
|
+
TIMER_DURATIONS,
|
|
27
|
+
TIMER_EFFECT_CRITICAL,
|
|
28
|
+
TIMER_EFFECT_WARNING,
|
|
29
|
+
TIMER_WARNING_THRESHOLD,
|
|
30
|
+
createEmptyNotes,
|
|
31
|
+
findConflicts,
|
|
32
|
+
getCandidates,
|
|
33
|
+
getPeers,
|
|
34
|
+
isBoardComplete,
|
|
35
|
+
toRowCol,
|
|
36
|
+
} from "./rules.js";
|
|
37
|
+
|
|
38
|
+
// ============================================================================
|
|
39
|
+
// Schema
|
|
40
|
+
// ============================================================================
|
|
41
|
+
|
|
42
|
+
export const sudokuSchema = {
|
|
43
|
+
facts: {
|
|
44
|
+
grid: t.object<Grid>(),
|
|
45
|
+
solution: t.object<Grid>(),
|
|
46
|
+
givens: t.object<Set<number>>(),
|
|
47
|
+
selectedIndex: t.object<number | null>(),
|
|
48
|
+
difficulty: t.object<Difficulty>(),
|
|
49
|
+
timerRemaining: t.number(),
|
|
50
|
+
timerRunning: t.boolean(),
|
|
51
|
+
gameOver: t.boolean(),
|
|
52
|
+
won: t.boolean(),
|
|
53
|
+
message: t.string(),
|
|
54
|
+
notesMode: t.boolean(),
|
|
55
|
+
notes: t.object<Set<number>[]>(),
|
|
56
|
+
hintsUsed: t.number(),
|
|
57
|
+
errorsCount: t.number(),
|
|
58
|
+
hintRequested: t.boolean(),
|
|
59
|
+
},
|
|
60
|
+
derivations: {
|
|
61
|
+
conflicts: t.object<Conflict[]>(),
|
|
62
|
+
conflictIndices: t.object<Set<number>>(),
|
|
63
|
+
hasConflicts: t.boolean(),
|
|
64
|
+
filledCount: t.number(),
|
|
65
|
+
progress: t.number(),
|
|
66
|
+
isComplete: t.boolean(),
|
|
67
|
+
isSolved: t.boolean(),
|
|
68
|
+
selectedPeers: t.object<number[]>(),
|
|
69
|
+
highlightValue: t.number(),
|
|
70
|
+
sameValueIndices: t.object<Set<number>>(),
|
|
71
|
+
candidates: t.object<number[]>(),
|
|
72
|
+
timerDisplay: t.string(),
|
|
73
|
+
timerUrgency: t.object<"normal" | "warning" | "critical">(),
|
|
74
|
+
},
|
|
75
|
+
events: {
|
|
76
|
+
newGame: { difficulty: t.object<Difficulty>() },
|
|
77
|
+
selectCell: { index: t.number() },
|
|
78
|
+
inputNumber: { value: t.number() },
|
|
79
|
+
toggleNote: { value: t.number() },
|
|
80
|
+
toggleNotesMode: {},
|
|
81
|
+
requestHint: {},
|
|
82
|
+
tick: {},
|
|
83
|
+
},
|
|
84
|
+
requirements: {
|
|
85
|
+
SHOW_CONFLICT: {
|
|
86
|
+
index: t.number(),
|
|
87
|
+
value: t.number(),
|
|
88
|
+
row: t.number(),
|
|
89
|
+
col: t.number(),
|
|
90
|
+
},
|
|
91
|
+
GAME_WON: {
|
|
92
|
+
timeLeft: t.number(),
|
|
93
|
+
hintsUsed: t.number(),
|
|
94
|
+
errors: t.number(),
|
|
95
|
+
},
|
|
96
|
+
GAME_OVER: {
|
|
97
|
+
reason: t.string(),
|
|
98
|
+
},
|
|
99
|
+
REVEAL_HINT: {
|
|
100
|
+
index: t.number(),
|
|
101
|
+
value: t.number(),
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
} satisfies ModuleSchema;
|
|
105
|
+
|
|
106
|
+
// ============================================================================
|
|
107
|
+
// Module
|
|
108
|
+
// ============================================================================
|
|
109
|
+
|
|
110
|
+
export const sudokuGame = createModule("sudoku", {
|
|
111
|
+
schema: sudokuSchema,
|
|
112
|
+
snapshotEvents: ["inputNumber", "toggleNote", "requestHint", "newGame"],
|
|
113
|
+
|
|
114
|
+
init: (facts) => {
|
|
115
|
+
const { puzzle, solution } = generatePuzzle("easy");
|
|
116
|
+
const givens = new Set<number>();
|
|
117
|
+
for (let i = 0; i < 81; i++) {
|
|
118
|
+
if (puzzle[i] !== 0) {
|
|
119
|
+
givens.add(i);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
facts.grid = puzzle;
|
|
124
|
+
facts.solution = solution;
|
|
125
|
+
facts.givens = givens;
|
|
126
|
+
facts.selectedIndex = null;
|
|
127
|
+
facts.difficulty = "easy";
|
|
128
|
+
facts.timerRemaining = TIMER_DURATIONS.easy;
|
|
129
|
+
facts.timerRunning = true;
|
|
130
|
+
facts.gameOver = false;
|
|
131
|
+
facts.won = false;
|
|
132
|
+
facts.message =
|
|
133
|
+
"Fill in the grid. No duplicates in rows, columns, or boxes.";
|
|
134
|
+
facts.notesMode = false;
|
|
135
|
+
facts.notes = createEmptyNotes();
|
|
136
|
+
facts.hintsUsed = 0;
|
|
137
|
+
facts.errorsCount = 0;
|
|
138
|
+
facts.hintRequested = false;
|
|
139
|
+
},
|
|
140
|
+
|
|
141
|
+
// ============================================================================
|
|
142
|
+
// Derivations
|
|
143
|
+
// ============================================================================
|
|
144
|
+
|
|
145
|
+
derive: {
|
|
146
|
+
conflicts: (facts) => {
|
|
147
|
+
return findConflicts(facts.grid as Grid);
|
|
148
|
+
},
|
|
149
|
+
|
|
150
|
+
conflictIndices: (facts, derive) => {
|
|
151
|
+
const indices = new Set<number>();
|
|
152
|
+
const givens = facts.givens as Set<number>;
|
|
153
|
+
for (const c of derive.conflicts as Conflict[]) {
|
|
154
|
+
// Only highlight player-placed cells, not givens
|
|
155
|
+
if (!givens.has(c.index)) {
|
|
156
|
+
indices.add(c.index);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return indices;
|
|
161
|
+
},
|
|
162
|
+
|
|
163
|
+
hasConflicts: (_facts, derive) => {
|
|
164
|
+
return (derive.conflicts as Conflict[]).length > 0;
|
|
165
|
+
},
|
|
166
|
+
|
|
167
|
+
filledCount: (facts) => {
|
|
168
|
+
let count = 0;
|
|
169
|
+
const grid = facts.grid as Grid;
|
|
170
|
+
for (let i = 0; i < 81; i++) {
|
|
171
|
+
if (grid[i] !== 0) {
|
|
172
|
+
count++;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return count;
|
|
177
|
+
},
|
|
178
|
+
|
|
179
|
+
progress: (_facts, derive) => {
|
|
180
|
+
return Math.round(((derive.filledCount as number) / 81) * 100);
|
|
181
|
+
},
|
|
182
|
+
|
|
183
|
+
isComplete: (facts) => {
|
|
184
|
+
return isBoardComplete(facts.grid as Grid);
|
|
185
|
+
},
|
|
186
|
+
|
|
187
|
+
isSolved: (_facts, derive) => {
|
|
188
|
+
return (
|
|
189
|
+
(derive.isComplete as boolean) && !(derive.hasConflicts as boolean)
|
|
190
|
+
);
|
|
191
|
+
},
|
|
192
|
+
|
|
193
|
+
selectedPeers: (facts) => {
|
|
194
|
+
const sel = facts.selectedIndex as number | null;
|
|
195
|
+
if (sel === null) {
|
|
196
|
+
return [];
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return getPeers(sel);
|
|
200
|
+
},
|
|
201
|
+
|
|
202
|
+
highlightValue: (facts) => {
|
|
203
|
+
const sel = facts.selectedIndex as number | null;
|
|
204
|
+
if (sel === null) {
|
|
205
|
+
return 0;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return (facts.grid as Grid)[sel];
|
|
209
|
+
},
|
|
210
|
+
|
|
211
|
+
sameValueIndices: (facts, derive) => {
|
|
212
|
+
const val = derive.highlightValue as number;
|
|
213
|
+
if (val === 0) {
|
|
214
|
+
return new Set<number>();
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const indices = new Set<number>();
|
|
218
|
+
const grid = facts.grid as Grid;
|
|
219
|
+
for (let i = 0; i < 81; i++) {
|
|
220
|
+
if (grid[i] === val) {
|
|
221
|
+
indices.add(i);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return indices;
|
|
226
|
+
},
|
|
227
|
+
|
|
228
|
+
candidates: (facts) => {
|
|
229
|
+
const sel = facts.selectedIndex as number | null;
|
|
230
|
+
if (sel === null) {
|
|
231
|
+
return [];
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return getCandidates(facts.grid as Grid, sel);
|
|
235
|
+
},
|
|
236
|
+
|
|
237
|
+
timerDisplay: (facts) => {
|
|
238
|
+
const remaining = facts.timerRemaining as number;
|
|
239
|
+
const mins = Math.max(0, Math.floor(remaining / 60));
|
|
240
|
+
const secs = Math.max(0, remaining % 60);
|
|
241
|
+
|
|
242
|
+
return `${String(mins).padStart(2, "0")}:${String(secs).padStart(2, "0")}`;
|
|
243
|
+
},
|
|
244
|
+
|
|
245
|
+
timerUrgency: (facts) => {
|
|
246
|
+
const remaining = facts.timerRemaining as number;
|
|
247
|
+
if (remaining <= TIMER_CRITICAL_THRESHOLD) {
|
|
248
|
+
return "critical";
|
|
249
|
+
}
|
|
250
|
+
if (remaining <= TIMER_WARNING_THRESHOLD) {
|
|
251
|
+
return "warning";
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return "normal";
|
|
255
|
+
},
|
|
256
|
+
},
|
|
257
|
+
|
|
258
|
+
// ============================================================================
|
|
259
|
+
// Events
|
|
260
|
+
// ============================================================================
|
|
261
|
+
|
|
262
|
+
events: {
|
|
263
|
+
newGame: (facts, { difficulty }) => {
|
|
264
|
+
const { puzzle, solution } = generatePuzzle(difficulty);
|
|
265
|
+
const givens = new Set<number>();
|
|
266
|
+
for (let i = 0; i < 81; i++) {
|
|
267
|
+
if (puzzle[i] !== 0) {
|
|
268
|
+
givens.add(i);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
facts.grid = puzzle;
|
|
273
|
+
facts.solution = solution;
|
|
274
|
+
facts.givens = givens;
|
|
275
|
+
facts.selectedIndex = null;
|
|
276
|
+
facts.difficulty = difficulty;
|
|
277
|
+
facts.timerRemaining = TIMER_DURATIONS[difficulty];
|
|
278
|
+
facts.timerRunning = true;
|
|
279
|
+
facts.gameOver = false;
|
|
280
|
+
facts.won = false;
|
|
281
|
+
facts.message =
|
|
282
|
+
"Fill in the grid. No duplicates in rows, columns, or boxes.";
|
|
283
|
+
facts.notesMode = false;
|
|
284
|
+
facts.notes = createEmptyNotes();
|
|
285
|
+
facts.hintsUsed = 0;
|
|
286
|
+
facts.errorsCount = 0;
|
|
287
|
+
facts.hintRequested = false;
|
|
288
|
+
},
|
|
289
|
+
|
|
290
|
+
selectCell: (facts, { index }) => {
|
|
291
|
+
if (facts.gameOver) {
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
facts.selectedIndex = index;
|
|
295
|
+
},
|
|
296
|
+
|
|
297
|
+
inputNumber: (facts, { value }) => {
|
|
298
|
+
if (facts.gameOver) {
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const sel = facts.selectedIndex as number | null;
|
|
303
|
+
if (sel === null) {
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const givens = facts.givens as Set<number>;
|
|
308
|
+
if (givens.has(sel)) {
|
|
309
|
+
facts.message = "That cell is locked.";
|
|
310
|
+
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (facts.notesMode && value !== 0) {
|
|
315
|
+
// In notes mode, toggle the pencil mark instead
|
|
316
|
+
const notes = [...(facts.notes as Set<number>[])];
|
|
317
|
+
notes[sel] = new Set(notes[sel]);
|
|
318
|
+
if (notes[sel].has(value)) {
|
|
319
|
+
notes[sel].delete(value);
|
|
320
|
+
} else {
|
|
321
|
+
notes[sel].add(value);
|
|
322
|
+
}
|
|
323
|
+
facts.notes = notes;
|
|
324
|
+
facts.message = "";
|
|
325
|
+
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Place or clear a number
|
|
330
|
+
const grid = [...(facts.grid as Grid)];
|
|
331
|
+
grid[sel] = value;
|
|
332
|
+
facts.grid = grid;
|
|
333
|
+
|
|
334
|
+
// Clear notes for this cell when placing a number
|
|
335
|
+
if (value !== 0) {
|
|
336
|
+
const notes = [...(facts.notes as Set<number>[])];
|
|
337
|
+
notes[sel] = new Set();
|
|
338
|
+
// Also clear this value from peer notes
|
|
339
|
+
for (const peer of getPeers(sel)) {
|
|
340
|
+
if (notes[peer].has(value)) {
|
|
341
|
+
notes[peer] = new Set(notes[peer]);
|
|
342
|
+
notes[peer].delete(value);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
facts.notes = notes;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
facts.message = "";
|
|
349
|
+
},
|
|
350
|
+
|
|
351
|
+
toggleNote: (facts, { value }) => {
|
|
352
|
+
if (facts.gameOver) {
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const sel = facts.selectedIndex as number | null;
|
|
357
|
+
if (sel === null) {
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const givens = facts.givens as Set<number>;
|
|
362
|
+
if (givens.has(sel)) {
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Only allow notes on empty cells
|
|
367
|
+
if ((facts.grid as Grid)[sel] !== 0) {
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const notes = [...(facts.notes as Set<number>[])];
|
|
372
|
+
notes[sel] = new Set(notes[sel]);
|
|
373
|
+
if (notes[sel].has(value)) {
|
|
374
|
+
notes[sel].delete(value);
|
|
375
|
+
} else {
|
|
376
|
+
notes[sel].add(value);
|
|
377
|
+
}
|
|
378
|
+
facts.notes = notes;
|
|
379
|
+
},
|
|
380
|
+
|
|
381
|
+
toggleNotesMode: (facts) => {
|
|
382
|
+
facts.notesMode = !(facts.notesMode as boolean);
|
|
383
|
+
},
|
|
384
|
+
|
|
385
|
+
requestHint: (facts) => {
|
|
386
|
+
if (facts.gameOver) {
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
if (facts.hintsUsed >= MAX_HINTS) {
|
|
390
|
+
facts.message = "No hints remaining.";
|
|
391
|
+
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
const sel = facts.selectedIndex as number | null;
|
|
396
|
+
if (sel === null) {
|
|
397
|
+
facts.message = "Select a cell first.";
|
|
398
|
+
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const givens = facts.givens as Set<number>;
|
|
403
|
+
if (givens.has(sel)) {
|
|
404
|
+
facts.message = "That cell is already filled.";
|
|
405
|
+
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
if ((facts.grid as Grid)[sel] !== 0) {
|
|
410
|
+
facts.message = "Clear the cell first, or select an empty cell.";
|
|
411
|
+
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// Signal the hintAvailable constraint to fire
|
|
416
|
+
facts.hintRequested = true;
|
|
417
|
+
},
|
|
418
|
+
|
|
419
|
+
tick: (facts) => {
|
|
420
|
+
if (!facts.timerRunning || facts.gameOver) {
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
facts.timerRemaining = Math.max(0, (facts.timerRemaining as number) - 1);
|
|
424
|
+
},
|
|
425
|
+
},
|
|
426
|
+
|
|
427
|
+
// ============================================================================
|
|
428
|
+
// Constraints – The Showcase
|
|
429
|
+
// ============================================================================
|
|
430
|
+
|
|
431
|
+
constraints: {
|
|
432
|
+
// Highest priority: timer expiry ends the game immediately
|
|
433
|
+
timerExpired: {
|
|
434
|
+
priority: 200,
|
|
435
|
+
when: (facts) => {
|
|
436
|
+
if (facts.gameOver) {
|
|
437
|
+
return false;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
return (facts.timerRemaining as number) <= 0;
|
|
441
|
+
},
|
|
442
|
+
require: () => ({
|
|
443
|
+
type: "GAME_OVER",
|
|
444
|
+
reason: "Time's up!",
|
|
445
|
+
}),
|
|
446
|
+
},
|
|
447
|
+
|
|
448
|
+
// Detect conflicts on player-placed cells
|
|
449
|
+
detectConflict: {
|
|
450
|
+
priority: 100,
|
|
451
|
+
when: (facts) => {
|
|
452
|
+
if (facts.gameOver) {
|
|
453
|
+
return false;
|
|
454
|
+
}
|
|
455
|
+
const conflicts = findConflicts(facts.grid as Grid);
|
|
456
|
+
const givens = facts.givens as Set<number>;
|
|
457
|
+
|
|
458
|
+
return conflicts.some((c) => !givens.has(c.index));
|
|
459
|
+
},
|
|
460
|
+
require: (facts) => {
|
|
461
|
+
const conflicts = findConflicts(facts.grid as Grid);
|
|
462
|
+
const givens = facts.givens as Set<number>;
|
|
463
|
+
const playerConflict = conflicts.find((c) => !givens.has(c.index));
|
|
464
|
+
const idx = playerConflict?.index ?? 0;
|
|
465
|
+
const { row, col } = toRowCol(idx);
|
|
466
|
+
|
|
467
|
+
return {
|
|
468
|
+
type: "SHOW_CONFLICT",
|
|
469
|
+
index: idx,
|
|
470
|
+
value: playerConflict?.value ?? 0,
|
|
471
|
+
row: row + 1,
|
|
472
|
+
col: col + 1,
|
|
473
|
+
};
|
|
474
|
+
},
|
|
475
|
+
},
|
|
476
|
+
|
|
477
|
+
// Puzzle solved: all cells filled with no conflicts
|
|
478
|
+
puzzleSolved: {
|
|
479
|
+
priority: 90,
|
|
480
|
+
when: (facts) => {
|
|
481
|
+
if (facts.gameOver) {
|
|
482
|
+
return false;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
return (
|
|
486
|
+
isBoardComplete(facts.grid as Grid) &&
|
|
487
|
+
findConflicts(facts.grid as Grid).length === 0
|
|
488
|
+
);
|
|
489
|
+
},
|
|
490
|
+
require: (facts) => ({
|
|
491
|
+
type: "GAME_WON",
|
|
492
|
+
timeLeft: facts.timerRemaining as number,
|
|
493
|
+
hintsUsed: facts.hintsUsed as number,
|
|
494
|
+
errors: facts.errorsCount as number,
|
|
495
|
+
}),
|
|
496
|
+
},
|
|
497
|
+
|
|
498
|
+
// Hint available: player requested a hint on an empty cell
|
|
499
|
+
hintAvailable: {
|
|
500
|
+
priority: 70,
|
|
501
|
+
when: (facts) => {
|
|
502
|
+
if (facts.gameOver) {
|
|
503
|
+
return false;
|
|
504
|
+
}
|
|
505
|
+
if (!facts.hintRequested) {
|
|
506
|
+
return false;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
const sel = facts.selectedIndex as number | null;
|
|
510
|
+
if (sel === null) {
|
|
511
|
+
return false;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
return (facts.grid as Grid)[sel] === 0;
|
|
515
|
+
},
|
|
516
|
+
require: (facts) => {
|
|
517
|
+
const sel = facts.selectedIndex as number;
|
|
518
|
+
const solution = facts.solution as Grid;
|
|
519
|
+
|
|
520
|
+
return {
|
|
521
|
+
type: "REVEAL_HINT",
|
|
522
|
+
index: sel,
|
|
523
|
+
value: solution[sel],
|
|
524
|
+
};
|
|
525
|
+
},
|
|
526
|
+
},
|
|
527
|
+
},
|
|
528
|
+
|
|
529
|
+
// ============================================================================
|
|
530
|
+
// Resolvers
|
|
531
|
+
// ============================================================================
|
|
532
|
+
|
|
533
|
+
resolvers: {
|
|
534
|
+
showConflict: {
|
|
535
|
+
requirement: "SHOW_CONFLICT",
|
|
536
|
+
resolve: async (req, context) => {
|
|
537
|
+
context.facts.errorsCount = (context.facts.errorsCount as number) + 1;
|
|
538
|
+
context.facts.message = `Conflict at row ${req.row}, column ${req.col} – duplicate ${req.value}.`;
|
|
539
|
+
},
|
|
540
|
+
},
|
|
541
|
+
|
|
542
|
+
gameWon: {
|
|
543
|
+
requirement: "GAME_WON",
|
|
544
|
+
resolve: async (req, context) => {
|
|
545
|
+
context.facts.timerRunning = false;
|
|
546
|
+
context.facts.gameOver = true;
|
|
547
|
+
context.facts.won = true;
|
|
548
|
+
|
|
549
|
+
const mins = Math.floor(
|
|
550
|
+
(TIMER_DURATIONS[context.facts.difficulty as Difficulty] -
|
|
551
|
+
req.timeLeft) /
|
|
552
|
+
60,
|
|
553
|
+
);
|
|
554
|
+
const secs =
|
|
555
|
+
(TIMER_DURATIONS[context.facts.difficulty as Difficulty] -
|
|
556
|
+
req.timeLeft) %
|
|
557
|
+
60;
|
|
558
|
+
context.facts.message = `Solved in ${mins}m ${secs}s! Hints: ${req.hintsUsed}, Errors: ${req.errors}`;
|
|
559
|
+
},
|
|
560
|
+
},
|
|
561
|
+
|
|
562
|
+
gameOver: {
|
|
563
|
+
requirement: "GAME_OVER",
|
|
564
|
+
resolve: async (req, context) => {
|
|
565
|
+
context.facts.timerRunning = false;
|
|
566
|
+
context.facts.gameOver = true;
|
|
567
|
+
context.facts.won = false;
|
|
568
|
+
context.facts.message = req.reason;
|
|
569
|
+
},
|
|
570
|
+
},
|
|
571
|
+
|
|
572
|
+
revealHint: {
|
|
573
|
+
requirement: "REVEAL_HINT",
|
|
574
|
+
resolve: async (req, context) => {
|
|
575
|
+
const grid = [...(context.facts.grid as Grid)];
|
|
576
|
+
grid[req.index] = req.value;
|
|
577
|
+
context.facts.grid = grid;
|
|
578
|
+
|
|
579
|
+
// Clear notes for the hinted cell and remove value from peer notes
|
|
580
|
+
const notes = [...(context.facts.notes as Set<number>[])];
|
|
581
|
+
notes[req.index] = new Set();
|
|
582
|
+
for (const peer of getPeers(req.index)) {
|
|
583
|
+
if (notes[peer].has(req.value)) {
|
|
584
|
+
notes[peer] = new Set(notes[peer]);
|
|
585
|
+
notes[peer].delete(req.value);
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
context.facts.notes = notes;
|
|
589
|
+
|
|
590
|
+
context.facts.hintRequested = false;
|
|
591
|
+
context.facts.hintsUsed = (context.facts.hintsUsed as number) + 1;
|
|
592
|
+
context.facts.message = `Hint revealed! ${MAX_HINTS - (context.facts.hintsUsed as number)} remaining.`;
|
|
593
|
+
},
|
|
594
|
+
},
|
|
595
|
+
},
|
|
596
|
+
|
|
597
|
+
// ============================================================================
|
|
598
|
+
// Effects
|
|
599
|
+
// ============================================================================
|
|
600
|
+
|
|
601
|
+
effects: {
|
|
602
|
+
timerWarning: {
|
|
603
|
+
deps: ["timerRemaining"],
|
|
604
|
+
run: (facts) => {
|
|
605
|
+
const remaining = facts.timerRemaining as number;
|
|
606
|
+
if (remaining === TIMER_EFFECT_WARNING) {
|
|
607
|
+
console.log("[Sudoku] 1 minute remaining!");
|
|
608
|
+
}
|
|
609
|
+
if (remaining === TIMER_EFFECT_CRITICAL) {
|
|
610
|
+
console.log("[Sudoku] 30 seconds remaining!");
|
|
611
|
+
}
|
|
612
|
+
},
|
|
613
|
+
},
|
|
614
|
+
|
|
615
|
+
gameResult: {
|
|
616
|
+
deps: ["gameOver"],
|
|
617
|
+
run: (facts) => {
|
|
618
|
+
if (facts.gameOver) {
|
|
619
|
+
if (facts.won) {
|
|
620
|
+
console.log(
|
|
621
|
+
`[Sudoku] Puzzle solved! Difficulty: ${facts.difficulty}, Hints: ${facts.hintsUsed}, Errors: ${facts.errorsCount}`,
|
|
622
|
+
);
|
|
623
|
+
} else {
|
|
624
|
+
console.log(`[Sudoku] Game over: ${facts.message}`);
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
},
|
|
628
|
+
},
|
|
629
|
+
},
|
|
630
|
+
});
|