logic-puzzle-generator 1.1.9 → 1.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/README.md +38 -6
- package/dist/src/engine/GenerativeSession.d.ts +16 -0
- package/dist/src/engine/GenerativeSession.js +113 -26
- package/dist/src/types.d.ts +14 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -192,8 +192,11 @@ The main class. Use `new Generator(seed)` to initialize.
|
|
|
192
192
|
- `options.targetClueCount`: Attempt to find exact solution length. Avoids early termination.
|
|
193
193
|
- `options.maxCandidates`: Performance tuning (default 50). Limits the heuristic search width.
|
|
194
194
|
- `options.timeoutMs`: Abort generation if it exceeds this limit (default 10000ms).
|
|
195
|
-
- `options.constraints`: Filter
|
|
196
|
-
- `allowedClueTypes`: `ClueType[]
|
|
195
|
+
- `options.constraints`: Filter/control generated clues.
|
|
196
|
+
- `allowedClueTypes`: `ClueType[]`.
|
|
197
|
+
- `includeSubjects`: `string[]` (New in v1.1.X). Restrict to clues involving these values.
|
|
198
|
+
- `excludeSubjects`: `string[]` (New in v1.1.X). Exclude clues involving these values.
|
|
199
|
+
- `minDeductions`: `number` (New in v1.1.X). Min new info required (default 1). Set to 0 for filler.
|
|
197
200
|
- `options.onTrace`: **(Debug)** Callback `(msg: string) => void`. Receives real-time logs about the generation process.
|
|
198
201
|
- `generatePuzzleAsync(...)`: **New in v1.1.0**. Non-blocking version of `generatePuzzle`. Returns `Promise<Puzzle>`.
|
|
199
202
|
- `getClueCountBounds(categories, target)`: Returns plausible Min/Max clue counts.
|
|
@@ -233,7 +236,32 @@ The logical engine responsible for applying clues and performing deductions.
|
|
|
233
236
|
Manages a stateful, step-by-step puzzle generation process.
|
|
234
237
|
- `getNextClue(constraints?)`: Returns `{ clue: Clue | null, remaining: number, solved: boolean }`.
|
|
235
238
|
- Generates and selects the next best clue based on the current grid state.
|
|
236
|
-
|
|
239
|
+
-### `session.getNextClue(options?: ClueGenerationConstraints)`
|
|
240
|
+
|
|
241
|
+
Generates the next step in the puzzle, returning the best available clue based on internal scoring and provided constraints.
|
|
242
|
+
|
|
243
|
+
### `session.getTotalClueCount()`
|
|
244
|
+
|
|
245
|
+
Returns the total number of valid clues remaining in the pool.
|
|
246
|
+
|
|
247
|
+
### `session.getMatchingClueCount(options?: ClueGenerationConstraints)`
|
|
248
|
+
|
|
249
|
+
Returns the number of clues that match the current filtering criteria.
|
|
250
|
+
|
|
251
|
+
### `session.getScoredMatchingClues(options?: ClueGenerationConstraints, limit: number = 50)`
|
|
252
|
+
|
|
253
|
+
Returns a list of clues matching the constraints, sorted by heuristic score.
|
|
254
|
+
Each result object contains:
|
|
255
|
+
- `clue`: The Clue object.
|
|
256
|
+
- `score`: The calculated helpfulness score.
|
|
257
|
+
- `deductions`: The number of new cell updates this clue provides.
|
|
258
|
+
- `isDirectAnswer`: Boolean indicating if this clue directly reveals the puzzle's goal.
|
|
259
|
+
|
|
260
|
+
### `session.useClue(clue: Clue)`
|
|
261
|
+
|
|
262
|
+
Manually applies a specific clue to the board. Useful for interactive search features.
|
|
263
|
+
|
|
264
|
+
### `session.rollbackLastClue()`: Returns `{ success: boolean, clue: Clue | null }`. Undoes the last step.
|
|
237
265
|
- `getNextClueAsync(constraints?)`: **New in v1.1.1**. Non-blocking version. Returns `Promise<{ clue, remaining, solved }>`.
|
|
238
266
|
- `rollbackLastClue()`: Returns `{ success: boolean, clue: Clue | null }`. Undoes the last step.
|
|
239
267
|
- `getGrid()`: Returns the current `LogicGrid` state.
|
|
@@ -277,7 +305,13 @@ let solved = false;
|
|
|
277
305
|
while (!solved) {
|
|
278
306
|
// 1. Get the next best clue (optionally force a specific type)
|
|
279
307
|
const result = session.getNextClue({
|
|
280
|
-
|
|
308
|
+
// 1. Get the next best clue (optionally force a specific type)
|
|
309
|
+
const result = session.getNextClue({
|
|
310
|
+
allowedClueTypes: [ClueType.BINARY, ClueType.ORDINAL],
|
|
311
|
+
includeSubjects: ['Mustard', 'Plum'], // Only clues about these entities
|
|
312
|
+
excludeSubjects: ['Revolver'], // No clues dealing with Revolvers
|
|
313
|
+
minDeductions: 0 // Allow "useless" clues (flavor text)
|
|
314
|
+
});
|
|
281
315
|
});
|
|
282
316
|
|
|
283
317
|
if (result.clue) {
|
|
@@ -294,8 +328,6 @@ while (!solved) {
|
|
|
294
328
|
|
|
295
329
|
The library uses specific error types to help you debug configuration issues.
|
|
296
330
|
|
|
297
|
-
| Method | Throws | Reason |
|
|
298
|
-
| :--- | :--- | :--- |
|
|
299
331
|
| Method | Throws | Reason |
|
|
300
332
|
| :--- | :--- | :--- |
|
|
301
333
|
| `new Generator()` | `Error` | If `seed` is invalid (NaN). |
|
|
@@ -15,6 +15,21 @@ export declare class GenerativeSession {
|
|
|
15
15
|
private solver;
|
|
16
16
|
private historyStack;
|
|
17
17
|
constructor(generator: Generator, categories: CategoryConfig[], solution: Solution, reverseSolution: Map<string, Map<ValueLabel, ValueLabel>>, valueMap: Map<ValueLabel, Record<string, ValueLabel>>, targetFact: TargetFact);
|
|
18
|
+
getTotalClueCount(): number;
|
|
19
|
+
getMatchingClueCount(constraints?: ClueGenerationConstraints): number;
|
|
20
|
+
getMatchingClues(constraints?: ClueGenerationConstraints, limit?: number): Clue[];
|
|
21
|
+
getScoredMatchingClues(constraints?: ClueGenerationConstraints, limit?: number): {
|
|
22
|
+
clue: Clue;
|
|
23
|
+
score: number;
|
|
24
|
+
deductions: number;
|
|
25
|
+
isDirectAnswer: boolean;
|
|
26
|
+
}[];
|
|
27
|
+
useClue(clue: Clue): {
|
|
28
|
+
remaining: number;
|
|
29
|
+
solved: boolean;
|
|
30
|
+
};
|
|
31
|
+
private filterClues;
|
|
32
|
+
private applyAndSave;
|
|
18
33
|
getNextClue(constraints?: ClueGenerationConstraints): {
|
|
19
34
|
clue: Clue | null;
|
|
20
35
|
remaining: number;
|
|
@@ -38,4 +53,5 @@ export declare class GenerativeSession {
|
|
|
38
53
|
getProofChain(): Clue[];
|
|
39
54
|
getSolution(): Solution;
|
|
40
55
|
getValueMap(): Map<ValueLabel, Record<string, ValueLabel>>;
|
|
56
|
+
private extractValuesFromClue;
|
|
41
57
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.GenerativeSession = void 0;
|
|
4
|
+
const types_1 = require("../types");
|
|
4
5
|
const LogicGrid_1 = require("./LogicGrid");
|
|
5
6
|
const Solver_1 = require("./Solver");
|
|
6
7
|
class GenerativeSession {
|
|
@@ -21,41 +22,116 @@ class GenerativeSession {
|
|
|
21
22
|
// We will call a public method on generator.
|
|
22
23
|
this.availableClues = this.generator.generateAllPossibleClues(categories, undefined, reverseSolution, valueMap);
|
|
23
24
|
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
25
|
+
getTotalClueCount() {
|
|
26
|
+
return this.availableClues.length;
|
|
27
|
+
}
|
|
28
|
+
getMatchingClueCount(constraints) {
|
|
29
|
+
return this.filterClues(constraints).length;
|
|
30
|
+
}
|
|
31
|
+
getMatchingClues(constraints, limit = 50) {
|
|
32
|
+
const clues = this.filterClues(constraints);
|
|
33
|
+
return clues.slice(0, limit);
|
|
34
|
+
}
|
|
35
|
+
getScoredMatchingClues(constraints, limit = 50) {
|
|
36
|
+
const clues = this.filterClues(constraints);
|
|
37
|
+
// Score all (or up to a reasonable hard limit to avoid perf issues, say 200)
|
|
38
|
+
// Then sort and take top 'limit'
|
|
39
|
+
const candidateLimit = 200;
|
|
40
|
+
const candidates = clues.slice(0, candidateLimit);
|
|
41
|
+
const scored = candidates.map(clue => {
|
|
42
|
+
// Score Logic - similar to getNextClue but without the constraints checks (already done)
|
|
43
|
+
const tempGrid = this.grid.clone();
|
|
44
|
+
const { deductions } = this.solver.applyClue(tempGrid, clue);
|
|
45
|
+
// Calculate real score
|
|
46
|
+
const score = this.generator.calculateClueScore(this.grid, this.targetFact, deductions, clue, this.proofChain, this.solution, this.reverseSolution);
|
|
47
|
+
// Check if this is the "Direct Answer" (Positive link for the target fact)
|
|
48
|
+
let isDirectAnswer = false;
|
|
49
|
+
if (clue.type === types_1.ClueType.BINARY) {
|
|
50
|
+
// Check if it links the target subject to the target category
|
|
51
|
+
// Positive operators: IS (0). Assuming enum values.
|
|
52
|
+
const isPositive = clue.operator === types_1.BinaryOperator.IS;
|
|
53
|
+
if (isPositive) {
|
|
54
|
+
const c = clue;
|
|
55
|
+
const matchForward = c.cat1 === this.targetFact.category1Id && c.val1 === this.targetFact.value1 && c.cat2 === this.targetFact.category2Id;
|
|
56
|
+
const matchReverse = c.cat2 === this.targetFact.category1Id && c.val2 === this.targetFact.value1 && c.cat1 === this.targetFact.category2Id;
|
|
57
|
+
if (matchForward || matchReverse) {
|
|
58
|
+
isDirectAnswer = true;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return { clue, score, deductions, isDirectAnswer };
|
|
63
|
+
});
|
|
64
|
+
// Sort by score descending
|
|
65
|
+
scored.sort((a, b) => b.score - a.score);
|
|
66
|
+
return scored.slice(0, limit);
|
|
67
|
+
}
|
|
68
|
+
useClue(clue) {
|
|
69
|
+
// Ensure the clue is removed from available if it was there
|
|
70
|
+
// (If searching, the UI passes a Clue object that should exist in our availableClues)
|
|
71
|
+
this.applyAndSave(clue);
|
|
72
|
+
return { remaining: this.availableClues.length, solved: this.generator.isPuzzleSolved(this.grid, this.solution, this.reverseSolution) };
|
|
73
|
+
}
|
|
74
|
+
filterClues(constraints) {
|
|
75
|
+
return this.availableClues.filter(clue => {
|
|
30
76
|
// 2. Constraints
|
|
31
77
|
if (constraints?.allowedClueTypes && !constraints.allowedClueTypes.includes(clue.type))
|
|
32
78
|
return false;
|
|
79
|
+
// Validation: Check for intersection
|
|
80
|
+
if (constraints?.includeSubjects && constraints?.excludeSubjects) {
|
|
81
|
+
const intersection = constraints.includeSubjects.filter(s => constraints.excludeSubjects.includes(s));
|
|
82
|
+
if (intersection.length > 0) {
|
|
83
|
+
throw new Error(`Constraint Error: The following subjects are both included and excluded: ${intersection.join(', ')}`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
// Subject Constraints
|
|
87
|
+
if (constraints?.includeSubjects || constraints?.excludeSubjects) {
|
|
88
|
+
const valuesInClue = this.extractValuesFromClue(clue);
|
|
89
|
+
if (constraints.includeSubjects) {
|
|
90
|
+
const hasMatch = constraints.includeSubjects.some(s => valuesInClue.includes(s));
|
|
91
|
+
if (!hasMatch)
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
if (constraints.excludeSubjects) {
|
|
95
|
+
const hasMatch = constraints.excludeSubjects.some(s => valuesInClue.includes(s));
|
|
96
|
+
if (hasMatch)
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
33
100
|
return true;
|
|
34
101
|
});
|
|
102
|
+
}
|
|
103
|
+
applyAndSave(clue) {
|
|
104
|
+
// Save state
|
|
105
|
+
this.historyStack.push(this.grid.clone());
|
|
106
|
+
// Apply it
|
|
107
|
+
this.solver.applyClue(this.grid, clue);
|
|
108
|
+
this.proofChain.push(clue);
|
|
109
|
+
// Remove from available
|
|
110
|
+
const idx = this.availableClues.indexOf(clue);
|
|
111
|
+
if (idx > -1)
|
|
112
|
+
this.availableClues.splice(idx, 1);
|
|
113
|
+
}
|
|
114
|
+
getNextClue(constraints) {
|
|
115
|
+
const validClues = this.filterClues(constraints);
|
|
35
116
|
// Score them
|
|
36
117
|
let bestClue = null;
|
|
37
118
|
let bestScore = -Infinity;
|
|
38
|
-
//
|
|
39
|
-
// For interactive session, speed is less critical than quality, but we want variety.
|
|
40
|
-
// Shuffle validClues first.
|
|
41
|
-
// In-place shuffle copy
|
|
119
|
+
// Shuffle validClues first to ensure variety.
|
|
42
120
|
const candidates = [...validClues].sort(() => Math.random() - 0.5);
|
|
43
121
|
// Take top N candidates?
|
|
44
122
|
const searchLimit = 50;
|
|
45
123
|
let checked = 0;
|
|
124
|
+
const minDeductions = constraints?.minDeductions ?? 0;
|
|
46
125
|
for (const clue of candidates) {
|
|
47
126
|
checked++;
|
|
48
127
|
if (checked > searchLimit && bestClue)
|
|
49
128
|
break; // found something good enough?
|
|
50
|
-
|
|
51
|
-
//
|
|
52
|
-
);
|
|
53
|
-
// Wait, publicCalculateScore needs `deductions`.
|
|
54
|
-
// So we must clone grid and apply.
|
|
129
|
+
// Score Logic
|
|
130
|
+
// We must clone grid and apply to see deductions.
|
|
55
131
|
const tempGrid = this.grid.clone();
|
|
56
132
|
const { deductions } = this.solver.applyClue(tempGrid, clue);
|
|
57
|
-
|
|
58
|
-
|
|
133
|
+
// Check Deduction Floor
|
|
134
|
+
if (deductions < minDeductions) {
|
|
59
135
|
continue;
|
|
60
136
|
}
|
|
61
137
|
// Re-calc score with deductions
|
|
@@ -66,15 +142,7 @@ class GenerativeSession {
|
|
|
66
142
|
}
|
|
67
143
|
}
|
|
68
144
|
if (bestClue) {
|
|
69
|
-
|
|
70
|
-
this.historyStack.push(this.grid.clone());
|
|
71
|
-
// Apply it
|
|
72
|
-
this.solver.applyClue(this.grid, bestClue);
|
|
73
|
-
this.proofChain.push(bestClue);
|
|
74
|
-
// Remove from available
|
|
75
|
-
const idx = this.availableClues.indexOf(bestClue);
|
|
76
|
-
if (idx > -1)
|
|
77
|
-
this.availableClues.splice(idx, 1);
|
|
145
|
+
this.applyAndSave(bestClue);
|
|
78
146
|
return { clue: bestClue, remaining: this.availableClues.length, solved: this.generator.isPuzzleSolved(this.grid, this.solution, this.reverseSolution) };
|
|
79
147
|
}
|
|
80
148
|
return { clue: null, remaining: validClues.length, solved: this.generator.isPuzzleSolved(this.grid, this.solution, this.reverseSolution) };
|
|
@@ -122,5 +190,24 @@ class GenerativeSession {
|
|
|
122
190
|
getProofChain() { return this.proofChain; }
|
|
123
191
|
getSolution() { return this.solution; }
|
|
124
192
|
getValueMap() { return this.valueMap; }
|
|
193
|
+
extractValuesFromClue(clue) {
|
|
194
|
+
const values = [];
|
|
195
|
+
// Extract based on type (simpler than full traversal)
|
|
196
|
+
// We cast values to string for easy comparison
|
|
197
|
+
const add = (v) => { if (v !== undefined)
|
|
198
|
+
values.push(String(v)); };
|
|
199
|
+
// Common fields
|
|
200
|
+
// @ts-ignore
|
|
201
|
+
add(clue.val1);
|
|
202
|
+
// @ts-ignore
|
|
203
|
+
add(clue.val2);
|
|
204
|
+
// @ts-ignore
|
|
205
|
+
add(clue.item1Val);
|
|
206
|
+
// @ts-ignore
|
|
207
|
+
add(clue.item2Val);
|
|
208
|
+
// @ts-ignore
|
|
209
|
+
add(clue.targetVal);
|
|
210
|
+
return values;
|
|
211
|
+
}
|
|
125
212
|
}
|
|
126
213
|
exports.GenerativeSession = GenerativeSession;
|
package/dist/src/types.d.ts
CHANGED
|
@@ -83,4 +83,18 @@ export interface ClueGenerationConstraints {
|
|
|
83
83
|
* Use this to control difficulty or puzzle attributes.
|
|
84
84
|
*/
|
|
85
85
|
allowedClueTypes?: ClueType[];
|
|
86
|
+
/**
|
|
87
|
+
* If provided, only clues that refer to these specific values (as subjects or objects) will be generated.
|
|
88
|
+
* Useful for "Ask this suspect" mechanics.
|
|
89
|
+
*/
|
|
90
|
+
includeSubjects?: string[];
|
|
91
|
+
/**
|
|
92
|
+
* If provided, clues referring to these values will be excluded.
|
|
93
|
+
*/
|
|
94
|
+
excludeSubjects?: string[];
|
|
95
|
+
/**
|
|
96
|
+
* Minimum number of new deductions this clue must provide to be considered valid.
|
|
97
|
+
* Default is 0 (allow all clues). Set to 1 to ensure progress.
|
|
98
|
+
*/
|
|
99
|
+
minDeductions?: number;
|
|
86
100
|
}
|