@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
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
// Example: provider-routing
|
|
2
|
-
// Source: examples/provider-routing/src/
|
|
3
|
-
//
|
|
2
|
+
// Source: examples/provider-routing/src/module.ts
|
|
3
|
+
// Pure module file — no DOM wiring
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
* Smart Provider Router —
|
|
6
|
+
* Smart Provider Router — Module Definition
|
|
7
7
|
*
|
|
8
8
|
* 3 mock providers (OpenAI, Anthropic, Ollama). Constraint router selects based
|
|
9
9
|
* on cost, error rates, circuit state. Provider fallback chain.
|
|
@@ -25,7 +25,7 @@ import {
|
|
|
25
25
|
// Types
|
|
26
26
|
// ============================================================================
|
|
27
27
|
|
|
28
|
-
interface ProviderStats {
|
|
28
|
+
export interface ProviderStats {
|
|
29
29
|
name: string;
|
|
30
30
|
callCount: number;
|
|
31
31
|
errorCount: number;
|
|
@@ -34,7 +34,7 @@ interface ProviderStats {
|
|
|
34
34
|
circuitState: CircuitState;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
interface TimelineEntry {
|
|
37
|
+
export interface TimelineEntry {
|
|
38
38
|
time: number;
|
|
39
39
|
event: string;
|
|
40
40
|
detail: string;
|
|
@@ -57,13 +57,14 @@ const providerErrors: Record<string, boolean> = {
|
|
|
57
57
|
ollama: false,
|
|
58
58
|
};
|
|
59
59
|
|
|
60
|
-
const circuitBreakers = {
|
|
60
|
+
export const circuitBreakers = {
|
|
61
61
|
openai: createCircuitBreaker({
|
|
62
62
|
name: "openai",
|
|
63
63
|
failureThreshold: 3,
|
|
64
64
|
recoveryTimeMs: 5000,
|
|
65
65
|
halfOpenMaxRequests: 2,
|
|
66
66
|
onStateChange: (from, to) =>
|
|
67
|
+
addTimeline("circuit", `openai: ${from} → ${to}`, "circuit"),
|
|
67
68
|
}),
|
|
68
69
|
anthropic: createCircuitBreaker({
|
|
69
70
|
name: "anthropic",
|
|
@@ -71,6 +72,7 @@ const circuitBreakers = {
|
|
|
71
72
|
recoveryTimeMs: 5000,
|
|
72
73
|
halfOpenMaxRequests: 2,
|
|
73
74
|
onStateChange: (from, to) =>
|
|
75
|
+
addTimeline("circuit", `anthropic: ${from} → ${to}`, "circuit"),
|
|
74
76
|
}),
|
|
75
77
|
ollama: createCircuitBreaker({
|
|
76
78
|
name: "ollama",
|
|
@@ -78,6 +80,7 @@ const circuitBreakers = {
|
|
|
78
80
|
recoveryTimeMs: 5000,
|
|
79
81
|
halfOpenMaxRequests: 2,
|
|
80
82
|
onStateChange: (from, to) =>
|
|
83
|
+
addTimeline("circuit", `ollama: ${from} → ${to}`, "circuit"),
|
|
81
84
|
}),
|
|
82
85
|
};
|
|
83
86
|
|
|
@@ -85,9 +88,9 @@ const circuitBreakers = {
|
|
|
85
88
|
// Timeline
|
|
86
89
|
// ============================================================================
|
|
87
90
|
|
|
88
|
-
const timeline: TimelineEntry[] = [];
|
|
91
|
+
export const timeline: TimelineEntry[] = [];
|
|
89
92
|
|
|
90
|
-
function addTimeline(
|
|
93
|
+
export function addTimeline(
|
|
91
94
|
event: string,
|
|
92
95
|
detail: string,
|
|
93
96
|
type: TimelineEntry["type"],
|
|
@@ -102,7 +105,7 @@ function addTimeline(
|
|
|
102
105
|
// Schema
|
|
103
106
|
// ============================================================================
|
|
104
107
|
|
|
105
|
-
const schema = {
|
|
108
|
+
export const schema = {
|
|
106
109
|
facts: {
|
|
107
110
|
openaiStats: t.object<ProviderStats>(),
|
|
108
111
|
anthropicStats: t.object<ProviderStats>(),
|
|
@@ -216,20 +219,19 @@ const routerModule = createModule("router", {
|
|
|
216
219
|
// System
|
|
217
220
|
// ============================================================================
|
|
218
221
|
|
|
219
|
-
const system = createSystem({
|
|
222
|
+
export const system = createSystem({
|
|
220
223
|
module: routerModule,
|
|
221
224
|
debug: { runHistory: true },
|
|
222
225
|
plugins: [devtoolsPlugin({ name: "provider-routing" })],
|
|
223
226
|
});
|
|
224
|
-
system.start();
|
|
225
227
|
|
|
226
228
|
// ============================================================================
|
|
227
229
|
// Routing Logic
|
|
228
230
|
// ============================================================================
|
|
229
231
|
|
|
230
232
|
function selectProvider(): string | null {
|
|
231
|
-
const budget = system.facts.budgetRemaining
|
|
232
|
-
const preferCheapest = system.facts.preferCheapest
|
|
233
|
+
const budget = system.facts.budgetRemaining;
|
|
234
|
+
const preferCheapest = system.facts.preferCheapest;
|
|
233
235
|
|
|
234
236
|
// Collect available providers (circuit breaker allows + within budget)
|
|
235
237
|
const available: { id: string; cost: number }[] = [];
|
|
@@ -280,7 +282,7 @@ async function executeProvider(providerId: string): Promise<boolean> {
|
|
|
280
282
|
}
|
|
281
283
|
});
|
|
282
284
|
|
|
283
|
-
const stats = system.facts[statsKey]
|
|
285
|
+
const stats = system.facts[statsKey];
|
|
284
286
|
const cost = config.costPer1k;
|
|
285
287
|
const latency = config.baseLatency + Math.random() * 100;
|
|
286
288
|
system.facts[statsKey] = {
|
|
@@ -294,35 +296,38 @@ async function executeProvider(providerId: string): Promise<boolean> {
|
|
|
294
296
|
circuitState: breaker.getState(),
|
|
295
297
|
};
|
|
296
298
|
system.facts.budgetRemaining =
|
|
297
|
-
Math.round((
|
|
298
|
-
1000;
|
|
299
|
+
Math.round((system.facts.budgetRemaining - cost) * 1000) / 1000;
|
|
299
300
|
system.facts.lastError = "";
|
|
301
|
+
addTimeline("success", `${config.name}: ok ($${cost})`, "success");
|
|
300
302
|
|
|
301
303
|
return true;
|
|
302
304
|
} catch (err) {
|
|
303
|
-
const stats = system.facts[statsKey]
|
|
305
|
+
const stats = system.facts[statsKey];
|
|
304
306
|
system.facts[statsKey] = {
|
|
305
307
|
...stats,
|
|
306
308
|
errorCount: stats.errorCount + 1,
|
|
307
309
|
circuitState: breaker.getState(),
|
|
308
310
|
};
|
|
309
311
|
system.facts.lastError = err instanceof Error ? err.message : String(err);
|
|
312
|
+
addTimeline("error", `${config.name}: ${system.facts.lastError}`, "error");
|
|
310
313
|
|
|
311
314
|
return false;
|
|
312
315
|
}
|
|
313
316
|
}
|
|
314
317
|
|
|
315
|
-
async function sendRequest() {
|
|
316
|
-
system.facts.totalRequests =
|
|
318
|
+
export async function sendRequest(): Promise<void> {
|
|
319
|
+
system.facts.totalRequests = system.facts.totalRequests + 1;
|
|
317
320
|
|
|
318
321
|
const providerId = selectProvider();
|
|
319
322
|
if (!providerId) {
|
|
320
323
|
system.facts.lastError = "All providers unavailable or over budget";
|
|
324
|
+
addTimeline("error", "no available providers", "error");
|
|
321
325
|
|
|
322
326
|
return;
|
|
323
327
|
}
|
|
324
328
|
|
|
325
329
|
system.facts.lastProvider = providerId;
|
|
330
|
+
addTimeline("route", `→ ${providerId}`, "route");
|
|
326
331
|
|
|
327
332
|
const success = await executeProvider(providerId);
|
|
328
333
|
if (success) {
|
|
@@ -332,72 +337,10 @@ async function sendRequest() {
|
|
|
332
337
|
// Primary failed — try fallback
|
|
333
338
|
const fallbackId = selectProvider();
|
|
334
339
|
if (fallbackId && fallbackId !== providerId) {
|
|
340
|
+
addTimeline("fallback", `falling back to ${fallbackId}`, "fallback");
|
|
335
341
|
system.facts.lastProvider = fallbackId;
|
|
336
342
|
await executeProvider(fallbackId);
|
|
337
343
|
}
|
|
338
344
|
}
|
|
339
345
|
|
|
340
|
-
|
|
341
|
-
// DOM References
|
|
342
|
-
// ============================================================================
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
// ============================================================================
|
|
346
|
-
// Render
|
|
347
|
-
// ============================================================================
|
|
348
|
-
|
|
349
|
-
function escapeHtml(text: string): string {
|
|
350
|
-
|
|
351
|
-
return div.innerHTML;
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
function circuitBadge(state: CircuitState): string {
|
|
355
|
-
const cls =
|
|
356
|
-
state === "CLOSED" ? "closed" : state === "OPEN" ? "open" : "half-open";
|
|
357
|
-
|
|
358
|
-
return `<span class="pr-circuit-badge ${cls}">${state}</span>`;
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
stats: ProviderStats,
|
|
362
|
-
state: CircuitState,
|
|
363
|
-
): void {
|
|
364
|
-
${circuitBadge(state)}
|
|
365
|
-
<span style="font-size:0.55rem;color:var(--brand-text-dim)">${stats.callCount} calls, ${stats.errorCount} err, $${stats.totalCost}</span>
|
|
366
|
-
`;
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
// ============================================================================
|
|
371
|
-
// Subscribe
|
|
372
|
-
// ============================================================================
|
|
373
|
-
|
|
374
|
-
const allKeys = [
|
|
375
|
-
...Object.keys(schema.facts),
|
|
376
|
-
...Object.keys(schema.derivations),
|
|
377
|
-
];
|
|
378
|
-
system.subscribe(allKeys, render);
|
|
379
|
-
|
|
380
|
-
setInterval(render, 1000);
|
|
381
|
-
|
|
382
|
-
// ============================================================================
|
|
383
|
-
// Controls
|
|
384
|
-
// ============================================================================
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
for (const id of ["openai", "anthropic", "ollama"]) {
|
|
388
|
-
system.events.toggleProviderError({ provider: id });
|
|
389
|
-
});
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
"input",
|
|
393
|
-
(e) => {
|
|
394
|
-
system.events.setBudget({ value });
|
|
395
|
-
},
|
|
396
|
-
);
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
// ============================================================================
|
|
400
|
-
// Initial Render
|
|
401
|
-
// ============================================================================
|
|
402
|
-
|
|
403
|
-
render();
|
|
346
|
+
export { providerErrors };
|
|
@@ -187,23 +187,23 @@ export const cartModule = createModule("cart", {
|
|
|
187
187
|
return facts.self.items.length === 0;
|
|
188
188
|
},
|
|
189
189
|
|
|
190
|
-
discount: (facts,
|
|
191
|
-
const sub =
|
|
190
|
+
discount: (facts, derived) => {
|
|
191
|
+
const sub = derived.subtotal;
|
|
192
192
|
|
|
193
193
|
return sub * (facts.self.couponDiscount / 100);
|
|
194
194
|
},
|
|
195
195
|
|
|
196
|
-
tax: (facts,
|
|
197
|
-
const sub =
|
|
198
|
-
const disc =
|
|
196
|
+
tax: (facts, derived) => {
|
|
197
|
+
const sub = derived.subtotal;
|
|
198
|
+
const disc = derived.discount;
|
|
199
199
|
|
|
200
200
|
return (sub - disc) * 0.08;
|
|
201
201
|
},
|
|
202
202
|
|
|
203
|
-
total: (_facts,
|
|
204
|
-
const sub =
|
|
205
|
-
const disc =
|
|
206
|
-
const tx =
|
|
203
|
+
total: (_facts, derived) => {
|
|
204
|
+
const sub = derived.subtotal;
|
|
205
|
+
const disc = derived.discount;
|
|
206
|
+
const tx = derived.tax;
|
|
207
207
|
|
|
208
208
|
return sub - disc + tx;
|
|
209
209
|
},
|
|
@@ -214,8 +214,8 @@ export const cartModule = createModule("cart", {
|
|
|
214
214
|
);
|
|
215
215
|
},
|
|
216
216
|
|
|
217
|
-
freeShipping: (_facts,
|
|
218
|
-
const sub =
|
|
217
|
+
freeShipping: (_facts, derived) => {
|
|
218
|
+
const sub = derived.subtotal;
|
|
219
219
|
|
|
220
220
|
return sub >= 75;
|
|
221
221
|
},
|
|
@@ -317,7 +317,7 @@ export const cartModule = createModule("cart", {
|
|
|
317
317
|
priority: 60,
|
|
318
318
|
after: ["quantityLimit", "couponValidation"],
|
|
319
319
|
when: (facts) => {
|
|
320
|
-
const items = facts.self.items
|
|
320
|
+
const items = facts.self.items;
|
|
321
321
|
const notEmpty = items.length > 0;
|
|
322
322
|
const noOverstock = !items.some(
|
|
323
323
|
(item: CartItem) => item.quantity > item.maxStock,
|
package/examples/sudoku.ts
CHANGED
|
@@ -52,23 +52,23 @@ export const sudokuSchema = {
|
|
|
52
52
|
won: t.boolean(),
|
|
53
53
|
message: t.string(),
|
|
54
54
|
notesMode: t.boolean(),
|
|
55
|
-
notes: t.
|
|
55
|
+
notes: t.array<Set<number>>(),
|
|
56
56
|
hintsUsed: t.number(),
|
|
57
57
|
errorsCount: t.number(),
|
|
58
58
|
hintRequested: t.boolean(),
|
|
59
59
|
},
|
|
60
60
|
derivations: {
|
|
61
|
-
conflicts: t.
|
|
61
|
+
conflicts: t.array<Conflict>(),
|
|
62
62
|
conflictIndices: t.object<Set<number>>(),
|
|
63
63
|
hasConflicts: t.boolean(),
|
|
64
64
|
filledCount: t.number(),
|
|
65
65
|
progress: t.number(),
|
|
66
66
|
isComplete: t.boolean(),
|
|
67
67
|
isSolved: t.boolean(),
|
|
68
|
-
selectedPeers: t.
|
|
68
|
+
selectedPeers: t.array<number>(),
|
|
69
69
|
highlightValue: t.number(),
|
|
70
70
|
sameValueIndices: t.object<Set<number>>(),
|
|
71
|
-
candidates: t.
|
|
71
|
+
candidates: t.array<number>(),
|
|
72
72
|
timerDisplay: t.string(),
|
|
73
73
|
timerUrgency: t.object<"normal" | "warning" | "critical">(),
|
|
74
74
|
},
|
|
@@ -144,13 +144,13 @@ export const sudokuGame = createModule("sudoku", {
|
|
|
144
144
|
|
|
145
145
|
derive: {
|
|
146
146
|
conflicts: (facts) => {
|
|
147
|
-
return findConflicts(facts.grid
|
|
147
|
+
return findConflicts(facts.grid);
|
|
148
148
|
},
|
|
149
149
|
|
|
150
|
-
conflictIndices: (facts,
|
|
150
|
+
conflictIndices: (facts, derived) => {
|
|
151
151
|
const indices = new Set<number>();
|
|
152
|
-
const givens = facts.givens
|
|
153
|
-
for (const c of
|
|
152
|
+
const givens = facts.givens;
|
|
153
|
+
for (const c of derived.conflicts) {
|
|
154
154
|
// Only highlight player-placed cells, not givens
|
|
155
155
|
if (!givens.has(c.index)) {
|
|
156
156
|
indices.add(c.index);
|
|
@@ -160,13 +160,13 @@ export const sudokuGame = createModule("sudoku", {
|
|
|
160
160
|
return indices;
|
|
161
161
|
},
|
|
162
162
|
|
|
163
|
-
hasConflicts: (_facts,
|
|
164
|
-
return
|
|
163
|
+
hasConflicts: (_facts, derived) => {
|
|
164
|
+
return derived.conflicts.length > 0;
|
|
165
165
|
},
|
|
166
166
|
|
|
167
167
|
filledCount: (facts) => {
|
|
168
168
|
let count = 0;
|
|
169
|
-
const grid = facts.grid
|
|
169
|
+
const grid = facts.grid;
|
|
170
170
|
for (let i = 0; i < 81; i++) {
|
|
171
171
|
if (grid[i] !== 0) {
|
|
172
172
|
count++;
|
|
@@ -176,22 +176,20 @@ export const sudokuGame = createModule("sudoku", {
|
|
|
176
176
|
return count;
|
|
177
177
|
},
|
|
178
178
|
|
|
179
|
-
progress: (_facts,
|
|
180
|
-
return Math.round((
|
|
179
|
+
progress: (_facts, derived) => {
|
|
180
|
+
return Math.round((derived.filledCount / 81) * 100);
|
|
181
181
|
},
|
|
182
182
|
|
|
183
183
|
isComplete: (facts) => {
|
|
184
|
-
return isBoardComplete(facts.grid
|
|
184
|
+
return isBoardComplete(facts.grid);
|
|
185
185
|
},
|
|
186
186
|
|
|
187
|
-
isSolved: (_facts,
|
|
188
|
-
return
|
|
189
|
-
(derive.isComplete as boolean) && !(derive.hasConflicts as boolean)
|
|
190
|
-
);
|
|
187
|
+
isSolved: (_facts, derived) => {
|
|
188
|
+
return derived.isComplete && !derived.hasConflicts;
|
|
191
189
|
},
|
|
192
190
|
|
|
193
191
|
selectedPeers: (facts) => {
|
|
194
|
-
const sel = facts.selectedIndex
|
|
192
|
+
const sel = facts.selectedIndex;
|
|
195
193
|
if (sel === null) {
|
|
196
194
|
return [];
|
|
197
195
|
}
|
|
@@ -200,22 +198,22 @@ export const sudokuGame = createModule("sudoku", {
|
|
|
200
198
|
},
|
|
201
199
|
|
|
202
200
|
highlightValue: (facts) => {
|
|
203
|
-
const sel = facts.selectedIndex
|
|
201
|
+
const sel = facts.selectedIndex;
|
|
204
202
|
if (sel === null) {
|
|
205
203
|
return 0;
|
|
206
204
|
}
|
|
207
205
|
|
|
208
|
-
return
|
|
206
|
+
return facts.grid[sel];
|
|
209
207
|
},
|
|
210
208
|
|
|
211
|
-
sameValueIndices: (facts,
|
|
212
|
-
const val =
|
|
209
|
+
sameValueIndices: (facts, derived) => {
|
|
210
|
+
const val = derived.highlightValue;
|
|
213
211
|
if (val === 0) {
|
|
214
212
|
return new Set<number>();
|
|
215
213
|
}
|
|
216
214
|
|
|
217
215
|
const indices = new Set<number>();
|
|
218
|
-
const grid = facts.grid
|
|
216
|
+
const grid = facts.grid;
|
|
219
217
|
for (let i = 0; i < 81; i++) {
|
|
220
218
|
if (grid[i] === val) {
|
|
221
219
|
indices.add(i);
|
|
@@ -226,16 +224,16 @@ export const sudokuGame = createModule("sudoku", {
|
|
|
226
224
|
},
|
|
227
225
|
|
|
228
226
|
candidates: (facts) => {
|
|
229
|
-
const sel = facts.selectedIndex
|
|
227
|
+
const sel = facts.selectedIndex;
|
|
230
228
|
if (sel === null) {
|
|
231
229
|
return [];
|
|
232
230
|
}
|
|
233
231
|
|
|
234
|
-
return getCandidates(facts.grid
|
|
232
|
+
return getCandidates(facts.grid, sel);
|
|
235
233
|
},
|
|
236
234
|
|
|
237
235
|
timerDisplay: (facts) => {
|
|
238
|
-
const remaining = facts.timerRemaining
|
|
236
|
+
const remaining = facts.timerRemaining;
|
|
239
237
|
const mins = Math.max(0, Math.floor(remaining / 60));
|
|
240
238
|
const secs = Math.max(0, remaining % 60);
|
|
241
239
|
|
|
@@ -243,7 +241,7 @@ export const sudokuGame = createModule("sudoku", {
|
|
|
243
241
|
},
|
|
244
242
|
|
|
245
243
|
timerUrgency: (facts) => {
|
|
246
|
-
const remaining = facts.timerRemaining
|
|
244
|
+
const remaining = facts.timerRemaining;
|
|
247
245
|
if (remaining <= TIMER_CRITICAL_THRESHOLD) {
|
|
248
246
|
return "critical";
|
|
249
247
|
}
|
|
@@ -299,12 +297,12 @@ export const sudokuGame = createModule("sudoku", {
|
|
|
299
297
|
return;
|
|
300
298
|
}
|
|
301
299
|
|
|
302
|
-
const sel = facts.selectedIndex
|
|
300
|
+
const sel = facts.selectedIndex;
|
|
303
301
|
if (sel === null) {
|
|
304
302
|
return;
|
|
305
303
|
}
|
|
306
304
|
|
|
307
|
-
const givens = facts.givens
|
|
305
|
+
const givens = facts.givens;
|
|
308
306
|
if (givens.has(sel)) {
|
|
309
307
|
facts.message = "That cell is locked.";
|
|
310
308
|
|
|
@@ -313,7 +311,7 @@ export const sudokuGame = createModule("sudoku", {
|
|
|
313
311
|
|
|
314
312
|
if (facts.notesMode && value !== 0) {
|
|
315
313
|
// In notes mode, toggle the pencil mark instead
|
|
316
|
-
const notes = [...
|
|
314
|
+
const notes = [...facts.notes];
|
|
317
315
|
notes[sel] = new Set(notes[sel]);
|
|
318
316
|
if (notes[sel].has(value)) {
|
|
319
317
|
notes[sel].delete(value);
|
|
@@ -327,13 +325,13 @@ export const sudokuGame = createModule("sudoku", {
|
|
|
327
325
|
}
|
|
328
326
|
|
|
329
327
|
// Place or clear a number
|
|
330
|
-
const grid = [...
|
|
328
|
+
const grid = [...facts.grid];
|
|
331
329
|
grid[sel] = value;
|
|
332
330
|
facts.grid = grid;
|
|
333
331
|
|
|
334
332
|
// Clear notes for this cell when placing a number
|
|
335
333
|
if (value !== 0) {
|
|
336
|
-
const notes = [...
|
|
334
|
+
const notes = [...facts.notes];
|
|
337
335
|
notes[sel] = new Set();
|
|
338
336
|
// Also clear this value from peer notes
|
|
339
337
|
for (const peer of getPeers(sel)) {
|
|
@@ -353,22 +351,22 @@ export const sudokuGame = createModule("sudoku", {
|
|
|
353
351
|
return;
|
|
354
352
|
}
|
|
355
353
|
|
|
356
|
-
const sel = facts.selectedIndex
|
|
354
|
+
const sel = facts.selectedIndex;
|
|
357
355
|
if (sel === null) {
|
|
358
356
|
return;
|
|
359
357
|
}
|
|
360
358
|
|
|
361
|
-
const givens = facts.givens
|
|
359
|
+
const givens = facts.givens;
|
|
362
360
|
if (givens.has(sel)) {
|
|
363
361
|
return;
|
|
364
362
|
}
|
|
365
363
|
|
|
366
364
|
// Only allow notes on empty cells
|
|
367
|
-
if (
|
|
365
|
+
if (facts.grid[sel] !== 0) {
|
|
368
366
|
return;
|
|
369
367
|
}
|
|
370
368
|
|
|
371
|
-
const notes = [...
|
|
369
|
+
const notes = [...facts.notes];
|
|
372
370
|
notes[sel] = new Set(notes[sel]);
|
|
373
371
|
if (notes[sel].has(value)) {
|
|
374
372
|
notes[sel].delete(value);
|
|
@@ -379,7 +377,7 @@ export const sudokuGame = createModule("sudoku", {
|
|
|
379
377
|
},
|
|
380
378
|
|
|
381
379
|
toggleNotesMode: (facts) => {
|
|
382
|
-
facts.notesMode = !
|
|
380
|
+
facts.notesMode = !facts.notesMode;
|
|
383
381
|
},
|
|
384
382
|
|
|
385
383
|
requestHint: (facts) => {
|
|
@@ -392,21 +390,21 @@ export const sudokuGame = createModule("sudoku", {
|
|
|
392
390
|
return;
|
|
393
391
|
}
|
|
394
392
|
|
|
395
|
-
const sel = facts.selectedIndex
|
|
393
|
+
const sel = facts.selectedIndex;
|
|
396
394
|
if (sel === null) {
|
|
397
395
|
facts.message = "Select a cell first.";
|
|
398
396
|
|
|
399
397
|
return;
|
|
400
398
|
}
|
|
401
399
|
|
|
402
|
-
const givens = facts.givens
|
|
400
|
+
const givens = facts.givens;
|
|
403
401
|
if (givens.has(sel)) {
|
|
404
402
|
facts.message = "That cell is already filled.";
|
|
405
403
|
|
|
406
404
|
return;
|
|
407
405
|
}
|
|
408
406
|
|
|
409
|
-
if (
|
|
407
|
+
if (facts.grid[sel] !== 0) {
|
|
410
408
|
facts.message = "Clear the cell first, or select an empty cell.";
|
|
411
409
|
|
|
412
410
|
return;
|
|
@@ -420,7 +418,7 @@ export const sudokuGame = createModule("sudoku", {
|
|
|
420
418
|
if (!facts.timerRunning || facts.gameOver) {
|
|
421
419
|
return;
|
|
422
420
|
}
|
|
423
|
-
facts.timerRemaining = Math.max(0,
|
|
421
|
+
facts.timerRemaining = Math.max(0, facts.timerRemaining - 1);
|
|
424
422
|
},
|
|
425
423
|
},
|
|
426
424
|
|
|
@@ -437,7 +435,7 @@ export const sudokuGame = createModule("sudoku", {
|
|
|
437
435
|
return false;
|
|
438
436
|
}
|
|
439
437
|
|
|
440
|
-
return
|
|
438
|
+
return facts.timerRemaining <= 0;
|
|
441
439
|
},
|
|
442
440
|
require: () => ({
|
|
443
441
|
type: "GAME_OVER",
|
|
@@ -452,14 +450,14 @@ export const sudokuGame = createModule("sudoku", {
|
|
|
452
450
|
if (facts.gameOver) {
|
|
453
451
|
return false;
|
|
454
452
|
}
|
|
455
|
-
const conflicts = findConflicts(facts.grid
|
|
456
|
-
const givens = facts.givens
|
|
453
|
+
const conflicts = findConflicts(facts.grid);
|
|
454
|
+
const givens = facts.givens;
|
|
457
455
|
|
|
458
456
|
return conflicts.some((c) => !givens.has(c.index));
|
|
459
457
|
},
|
|
460
458
|
require: (facts) => {
|
|
461
|
-
const conflicts = findConflicts(facts.grid
|
|
462
|
-
const givens = facts.givens
|
|
459
|
+
const conflicts = findConflicts(facts.grid);
|
|
460
|
+
const givens = facts.givens;
|
|
463
461
|
const playerConflict = conflicts.find((c) => !givens.has(c.index));
|
|
464
462
|
const idx = playerConflict?.index ?? 0;
|
|
465
463
|
const { row, col } = toRowCol(idx);
|
|
@@ -483,15 +481,14 @@ export const sudokuGame = createModule("sudoku", {
|
|
|
483
481
|
}
|
|
484
482
|
|
|
485
483
|
return (
|
|
486
|
-
isBoardComplete(facts.grid
|
|
487
|
-
findConflicts(facts.grid as Grid).length === 0
|
|
484
|
+
isBoardComplete(facts.grid) && findConflicts(facts.grid).length === 0
|
|
488
485
|
);
|
|
489
486
|
},
|
|
490
487
|
require: (facts) => ({
|
|
491
488
|
type: "GAME_WON",
|
|
492
|
-
timeLeft: facts.timerRemaining
|
|
493
|
-
hintsUsed: facts.hintsUsed
|
|
494
|
-
errors: facts.errorsCount
|
|
489
|
+
timeLeft: facts.timerRemaining,
|
|
490
|
+
hintsUsed: facts.hintsUsed,
|
|
491
|
+
errors: facts.errorsCount,
|
|
495
492
|
}),
|
|
496
493
|
},
|
|
497
494
|
|
|
@@ -506,16 +503,16 @@ export const sudokuGame = createModule("sudoku", {
|
|
|
506
503
|
return false;
|
|
507
504
|
}
|
|
508
505
|
|
|
509
|
-
const sel = facts.selectedIndex
|
|
506
|
+
const sel = facts.selectedIndex;
|
|
510
507
|
if (sel === null) {
|
|
511
508
|
return false;
|
|
512
509
|
}
|
|
513
510
|
|
|
514
|
-
return
|
|
511
|
+
return facts.grid[sel] === 0;
|
|
515
512
|
},
|
|
516
513
|
require: (facts) => {
|
|
517
514
|
const sel = facts.selectedIndex as number;
|
|
518
|
-
const solution = facts.solution
|
|
515
|
+
const solution = facts.solution;
|
|
519
516
|
|
|
520
517
|
return {
|
|
521
518
|
type: "REVEAL_HINT",
|
|
@@ -534,7 +531,7 @@ export const sudokuGame = createModule("sudoku", {
|
|
|
534
531
|
showConflict: {
|
|
535
532
|
requirement: "SHOW_CONFLICT",
|
|
536
533
|
resolve: async (req, context) => {
|
|
537
|
-
context.facts.errorsCount =
|
|
534
|
+
context.facts.errorsCount = context.facts.errorsCount + 1;
|
|
538
535
|
context.facts.message = `Conflict at row ${req.row}, column ${req.col} – duplicate ${req.value}.`;
|
|
539
536
|
},
|
|
540
537
|
},
|
|
@@ -547,14 +544,10 @@ export const sudokuGame = createModule("sudoku", {
|
|
|
547
544
|
context.facts.won = true;
|
|
548
545
|
|
|
549
546
|
const mins = Math.floor(
|
|
550
|
-
(TIMER_DURATIONS[context.facts.difficulty
|
|
551
|
-
req.timeLeft) /
|
|
552
|
-
60,
|
|
547
|
+
(TIMER_DURATIONS[context.facts.difficulty] - req.timeLeft) / 60,
|
|
553
548
|
);
|
|
554
549
|
const secs =
|
|
555
|
-
(TIMER_DURATIONS[context.facts.difficulty
|
|
556
|
-
req.timeLeft) %
|
|
557
|
-
60;
|
|
550
|
+
(TIMER_DURATIONS[context.facts.difficulty] - req.timeLeft) % 60;
|
|
558
551
|
context.facts.message = `Solved in ${mins}m ${secs}s! Hints: ${req.hintsUsed}, Errors: ${req.errors}`;
|
|
559
552
|
},
|
|
560
553
|
},
|
|
@@ -572,12 +565,12 @@ export const sudokuGame = createModule("sudoku", {
|
|
|
572
565
|
revealHint: {
|
|
573
566
|
requirement: "REVEAL_HINT",
|
|
574
567
|
resolve: async (req, context) => {
|
|
575
|
-
const grid = [...
|
|
568
|
+
const grid = [...context.facts.grid];
|
|
576
569
|
grid[req.index] = req.value;
|
|
577
570
|
context.facts.grid = grid;
|
|
578
571
|
|
|
579
572
|
// Clear notes for the hinted cell and remove value from peer notes
|
|
580
|
-
const notes = [...
|
|
573
|
+
const notes = [...context.facts.notes];
|
|
581
574
|
notes[req.index] = new Set();
|
|
582
575
|
for (const peer of getPeers(req.index)) {
|
|
583
576
|
if (notes[peer].has(req.value)) {
|
|
@@ -588,8 +581,8 @@ export const sudokuGame = createModule("sudoku", {
|
|
|
588
581
|
context.facts.notes = notes;
|
|
589
582
|
|
|
590
583
|
context.facts.hintRequested = false;
|
|
591
|
-
context.facts.hintsUsed =
|
|
592
|
-
context.facts.message = `Hint revealed! ${MAX_HINTS -
|
|
584
|
+
context.facts.hintsUsed = context.facts.hintsUsed + 1;
|
|
585
|
+
context.facts.message = `Hint revealed! ${MAX_HINTS - context.facts.hintsUsed} remaining.`;
|
|
593
586
|
},
|
|
594
587
|
},
|
|
595
588
|
},
|
|
@@ -602,7 +595,7 @@ export const sudokuGame = createModule("sudoku", {
|
|
|
602
595
|
timerWarning: {
|
|
603
596
|
deps: ["timerRemaining"],
|
|
604
597
|
run: (facts) => {
|
|
605
|
-
const remaining = facts.timerRemaining
|
|
598
|
+
const remaining = facts.timerRemaining;
|
|
606
599
|
if (remaining === TIMER_EFFECT_WARNING) {
|
|
607
600
|
console.log("[Sudoku] 1 minute remaining!");
|
|
608
601
|
}
|