logic-puzzle-generator 1.0.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 +221 -0
- package/dist/Clue.d.ts +81 -0
- package/dist/Clue.js +37 -0
- package/dist/Generator.d.ts +58 -0
- package/dist/Generator.js +433 -0
- package/dist/LogicGrid.d.ts +70 -0
- package/dist/LogicGrid.js +188 -0
- package/dist/Solver.d.ts +29 -0
- package/dist/Solver.js +242 -0
- package/dist/examples/benchmark.d.ts +1 -0
- package/dist/examples/benchmark.js +129 -0
- package/dist/examples/cli.d.ts +1 -0
- package/dist/examples/cli.js +84 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +21 -0
- package/dist/run_generator.d.ts +1 -0
- package/dist/run_generator.js +84 -0
- package/dist/src/defaults.d.ts +5 -0
- package/dist/src/defaults.js +24 -0
- package/dist/src/engine/BoundsCalculator.d.ts +6 -0
- package/dist/src/engine/BoundsCalculator.js +40 -0
- package/dist/src/engine/Clue.d.ts +75 -0
- package/dist/src/engine/Clue.js +10 -0
- package/dist/src/engine/DifficultyBounds.d.ts +12 -0
- package/dist/src/engine/DifficultyBounds.js +67 -0
- package/dist/src/engine/GenerativeSession.d.ts +32 -0
- package/dist/src/engine/GenerativeSession.js +109 -0
- package/dist/src/engine/Generator.d.ts +119 -0
- package/dist/src/engine/Generator.js +1058 -0
- package/dist/src/engine/LogicGrid.d.ts +70 -0
- package/dist/src/engine/LogicGrid.js +190 -0
- package/dist/src/engine/Solver.d.ts +30 -0
- package/dist/src/engine/Solver.js +613 -0
- package/dist/src/errors.d.ts +12 -0
- package/dist/src/errors.js +23 -0
- package/dist/src/index.d.ts +8 -0
- package/dist/src/index.js +24 -0
- package/dist/src/scripts/GenerateBoundsDeprecated.d.ts +1 -0
- package/dist/src/scripts/GenerateBoundsDeprecated.js +27 -0
- package/dist/src/scripts/StressTestBacktracking.d.ts +1 -0
- package/dist/src/scripts/StressTestBacktracking.js +49 -0
- package/dist/src/types.d.ts +86 -0
- package/dist/src/types.js +58 -0
- package/dist/types.d.ts +37 -0
- package/dist/types.js +13 -0
- package/package.json +40 -0
package/dist/Solver.d.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { LogicGrid } from './LogicGrid';
|
|
2
|
+
import { Clue } from './Clue';
|
|
3
|
+
/**
|
|
4
|
+
* The logical engine responsible for applying clues to a LogicGrid and deducing consequences.
|
|
5
|
+
*
|
|
6
|
+
* It implements various deduction strategies including basic elimination, uniqueness checks,
|
|
7
|
+
* and transitive logic logic (if A=B and B=C, then A=C).
|
|
8
|
+
*/
|
|
9
|
+
export declare class Solver {
|
|
10
|
+
/**
|
|
11
|
+
* Applies a single clue to the grid and propagates logical deductions.
|
|
12
|
+
*
|
|
13
|
+
* This method runs a loop that explicitly applies the clue and then repeatedly
|
|
14
|
+
* triggers the internal deduction engine until no further eliminations can be made.
|
|
15
|
+
*
|
|
16
|
+
* @param grid - The LogicGrid to modify.
|
|
17
|
+
* @param clue - The Clue to apply.
|
|
18
|
+
* @returns An object containing the modified grid and the total count of eliminations made.
|
|
19
|
+
*/
|
|
20
|
+
applyClue(grid: LogicGrid, clue: Clue): {
|
|
21
|
+
grid: LogicGrid;
|
|
22
|
+
deductions: number;
|
|
23
|
+
};
|
|
24
|
+
private applyUnaryClue;
|
|
25
|
+
private applyBinaryClue;
|
|
26
|
+
private runDeductionLoop;
|
|
27
|
+
private applyOrdinalClue;
|
|
28
|
+
private applySuperlativeClue;
|
|
29
|
+
}
|
package/dist/Solver.js
ADDED
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Solver = void 0;
|
|
4
|
+
const Clue_1 = require("./Clue");
|
|
5
|
+
const types_1 = require("./types");
|
|
6
|
+
/**
|
|
7
|
+
* The logical engine responsible for applying clues to a LogicGrid and deducing consequences.
|
|
8
|
+
*
|
|
9
|
+
* It implements various deduction strategies including basic elimination, uniqueness checks,
|
|
10
|
+
* and transitive logic logic (if A=B and B=C, then A=C).
|
|
11
|
+
*/
|
|
12
|
+
class Solver {
|
|
13
|
+
/**
|
|
14
|
+
* Applies a single clue to the grid and propagates logical deductions.
|
|
15
|
+
*
|
|
16
|
+
* This method runs a loop that explicitly applies the clue and then repeatedly
|
|
17
|
+
* triggers the internal deduction engine until no further eliminations can be made.
|
|
18
|
+
*
|
|
19
|
+
* @param grid - The LogicGrid to modify.
|
|
20
|
+
* @param clue - The Clue to apply.
|
|
21
|
+
* @returns An object containing the modified grid and the total count of eliminations made.
|
|
22
|
+
*/
|
|
23
|
+
applyClue(grid, clue) {
|
|
24
|
+
let deductions = 0;
|
|
25
|
+
switch (clue.type) {
|
|
26
|
+
case Clue_1.ClueType.BINARY:
|
|
27
|
+
deductions += this.applyBinaryClue(grid, clue);
|
|
28
|
+
break;
|
|
29
|
+
case Clue_1.ClueType.SUPERLATIVE:
|
|
30
|
+
deductions += this.applySuperlativeClue(grid, clue);
|
|
31
|
+
break;
|
|
32
|
+
case Clue_1.ClueType.ORDINAL:
|
|
33
|
+
deductions += this.applyOrdinalClue(grid, clue);
|
|
34
|
+
break;
|
|
35
|
+
case Clue_1.ClueType.UNARY:
|
|
36
|
+
deductions += this.applyUnaryClue(grid, clue);
|
|
37
|
+
break;
|
|
38
|
+
}
|
|
39
|
+
let newDeductions;
|
|
40
|
+
do {
|
|
41
|
+
newDeductions = this.runDeductionLoop(grid);
|
|
42
|
+
deductions += newDeductions;
|
|
43
|
+
} while (newDeductions > 0);
|
|
44
|
+
return { grid, deductions };
|
|
45
|
+
}
|
|
46
|
+
applyUnaryClue(grid, clue) {
|
|
47
|
+
let deductions = 0;
|
|
48
|
+
const categories = grid.categories;
|
|
49
|
+
const ordinalCatConfig = categories.find(c => c.id === clue.ordinalCat);
|
|
50
|
+
if (!ordinalCatConfig || ordinalCatConfig.type !== types_1.CategoryType.ORDINAL)
|
|
51
|
+
return 0;
|
|
52
|
+
if (!ordinalCatConfig.values.every(v => typeof v === 'number'))
|
|
53
|
+
return 0;
|
|
54
|
+
const isEven = clue.filter === Clue_1.UnaryFilter.IS_EVEN;
|
|
55
|
+
for (const ordVal of ordinalCatConfig.values) {
|
|
56
|
+
const ordNum = ordVal;
|
|
57
|
+
const shouldEliminate = isEven ? ordNum % 2 !== 0 : ordNum % 2 === 0;
|
|
58
|
+
if (shouldEliminate) {
|
|
59
|
+
if (grid.isPossible(clue.targetCat, clue.targetVal, clue.ordinalCat, ordVal)) {
|
|
60
|
+
grid.setPossibility(clue.targetCat, clue.targetVal, clue.ordinalCat, ordVal, false);
|
|
61
|
+
deductions++;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return deductions;
|
|
66
|
+
}
|
|
67
|
+
applyBinaryClue(grid, clue) {
|
|
68
|
+
let deductions = 0;
|
|
69
|
+
const categories = grid.categories;
|
|
70
|
+
const cat1Config = categories.find(c => c.id === clue.cat1);
|
|
71
|
+
const cat2Config = categories.find(c => c.id === clue.cat2);
|
|
72
|
+
if (!cat1Config || !cat2Config)
|
|
73
|
+
return 0;
|
|
74
|
+
if (clue.operator === Clue_1.BinaryOperator.IS) {
|
|
75
|
+
if (grid.isPossible(clue.cat1, clue.val1, clue.cat2, clue.val2)) {
|
|
76
|
+
// This is not a deduction, but a fact application. Still, we need to eliminate other possibilities.
|
|
77
|
+
}
|
|
78
|
+
grid.setPossibility(clue.cat1, clue.val1, clue.cat2, clue.val2, true);
|
|
79
|
+
for (const val of cat2Config.values) {
|
|
80
|
+
if (val !== clue.val2) {
|
|
81
|
+
if (grid.isPossible(clue.cat1, clue.val1, clue.cat2, val)) {
|
|
82
|
+
grid.setPossibility(clue.cat1, clue.val1, clue.cat2, val, false);
|
|
83
|
+
deductions++;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
for (const val of cat1Config.values) {
|
|
88
|
+
if (val !== clue.val1) {
|
|
89
|
+
if (grid.isPossible(clue.cat1, val, clue.cat2, clue.val2)) {
|
|
90
|
+
grid.setPossibility(clue.cat1, val, clue.cat2, clue.val2, false);
|
|
91
|
+
deductions++;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
else { // IS_NOT
|
|
97
|
+
if (grid.isPossible(clue.cat1, clue.val1, clue.cat2, clue.val2)) {
|
|
98
|
+
grid.setPossibility(clue.cat1, clue.val1, clue.cat2, clue.val2, false);
|
|
99
|
+
deductions++;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return deductions;
|
|
103
|
+
}
|
|
104
|
+
runDeductionLoop(grid) {
|
|
105
|
+
let deductions = 0;
|
|
106
|
+
const categories = grid.categories;
|
|
107
|
+
for (const cat1 of categories) {
|
|
108
|
+
for (const val1 of cat1.values) {
|
|
109
|
+
for (const cat2 of categories) {
|
|
110
|
+
if (cat1.id === cat2.id)
|
|
111
|
+
continue;
|
|
112
|
+
// Uniqueness Check
|
|
113
|
+
const possibleValues = cat2.values.filter(val2 => grid.isPossible(cat1.id, val1, cat2.id, val2));
|
|
114
|
+
if (possibleValues.length === 1) {
|
|
115
|
+
const val2 = possibleValues[0];
|
|
116
|
+
// If val1 is uniquely associated with val2, then no other val from cat1 can be associated with val2
|
|
117
|
+
for (const otherVal1 of cat1.values) {
|
|
118
|
+
if (otherVal1 !== val1) {
|
|
119
|
+
if (grid.isPossible(cat1.id, otherVal1, cat2.id, val2)) {
|
|
120
|
+
grid.setPossibility(cat1.id, otherVal1, cat2.id, val2, false);
|
|
121
|
+
deductions++;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
// Transitivity
|
|
127
|
+
for (const cat3 of categories) {
|
|
128
|
+
if (cat1.id === cat3.id || cat2.id === cat3.id)
|
|
129
|
+
continue;
|
|
130
|
+
// Positive Transitivity
|
|
131
|
+
const definiteVal2 = possibleValues.length === 1 ? possibleValues[0] : null;
|
|
132
|
+
if (definiteVal2) {
|
|
133
|
+
const possibleVal3s = cat3.values.filter(val3 => grid.isPossible(cat2.id, definiteVal2, cat3.id, val3));
|
|
134
|
+
if (possibleVal3s.length === 1) {
|
|
135
|
+
const definiteVal3 = possibleVal3s[0];
|
|
136
|
+
if (grid.isPossible(cat1.id, val1, cat3.id, definiteVal3) === false) {
|
|
137
|
+
// This indicates a contradiction, but for now we are just making deductions.
|
|
138
|
+
}
|
|
139
|
+
else if (grid.getPossibilitiesCount(cat1.id, val1, cat3.id) > 1) {
|
|
140
|
+
grid.setPossibility(cat1.id, val1, cat3.id, definiteVal3, true);
|
|
141
|
+
deductions++;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
// Negative Transitivity
|
|
146
|
+
for (const val3 of cat3.values) {
|
|
147
|
+
if (grid.isPossible(cat1.id, val1, cat3.id, val3)) {
|
|
148
|
+
const isPathPossible = cat2.values.some(val2 => grid.isPossible(cat1.id, val1, cat2.id, val2) && grid.isPossible(cat2.id, val2, cat3.id, val3));
|
|
149
|
+
if (!isPathPossible) {
|
|
150
|
+
grid.setPossibility(cat1.id, val1, cat3.id, val3, false);
|
|
151
|
+
deductions++;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return deductions;
|
|
160
|
+
}
|
|
161
|
+
applyOrdinalClue(grid, constraint) {
|
|
162
|
+
let deductions = 0;
|
|
163
|
+
const categories = grid.categories;
|
|
164
|
+
const ordCatConfig = categories.find(c => c.id === constraint.ordinalCat);
|
|
165
|
+
if (!ordCatConfig || ordCatConfig.type !== types_1.CategoryType.ORDINAL)
|
|
166
|
+
return 0;
|
|
167
|
+
const possibleVals1 = ordCatConfig.values
|
|
168
|
+
.map((v, i) => ({ val: v, idx: i }))
|
|
169
|
+
.filter(v => grid.isPossible(constraint.item1Cat, constraint.item1Val, constraint.ordinalCat, v.val));
|
|
170
|
+
const possibleVals2 = ordCatConfig.values
|
|
171
|
+
.map((v, i) => ({ val: v, idx: i }))
|
|
172
|
+
.filter(v => grid.isPossible(constraint.item2Cat, constraint.item2Val, constraint.ordinalCat, v.val));
|
|
173
|
+
if (possibleVals1.length === 0 || possibleVals2.length === 0)
|
|
174
|
+
return 0;
|
|
175
|
+
if (constraint.operator === Clue_1.OrdinalOperator.GREATER_THAN) { // item1 > item2
|
|
176
|
+
// Prune item1's possibilities
|
|
177
|
+
for (const pval1 of possibleVals1) {
|
|
178
|
+
const canBeGreaterThan = possibleVals2.some(pval2 => pval1.idx > pval2.idx);
|
|
179
|
+
if (!canBeGreaterThan) {
|
|
180
|
+
if (grid.isPossible(constraint.item1Cat, constraint.item1Val, constraint.ordinalCat, pval1.val)) {
|
|
181
|
+
grid.setPossibility(constraint.item1Cat, constraint.item1Val, constraint.ordinalCat, pval1.val, false);
|
|
182
|
+
deductions++;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
// Prune item2's possibilities
|
|
187
|
+
for (const pval2 of possibleVals2) {
|
|
188
|
+
const canBeLessThan = possibleVals1.some(pval1 => pval1.idx > pval2.idx);
|
|
189
|
+
if (!canBeLessThan) {
|
|
190
|
+
if (grid.isPossible(constraint.item2Cat, constraint.item2Val, constraint.ordinalCat, pval2.val)) {
|
|
191
|
+
grid.setPossibility(constraint.item2Cat, constraint.item2Val, constraint.ordinalCat, pval2.val, false);
|
|
192
|
+
deductions++;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
else if (constraint.operator === Clue_1.OrdinalOperator.LESS_THAN) { // item1 < item2
|
|
198
|
+
// Prune item1's possibilities
|
|
199
|
+
for (const pval1 of possibleVals1) {
|
|
200
|
+
const canBeLessThan = possibleVals2.some(pval2 => pval1.idx < pval2.idx);
|
|
201
|
+
if (!canBeLessThan) {
|
|
202
|
+
if (grid.isPossible(constraint.item1Cat, constraint.item1Val, constraint.ordinalCat, pval1.val)) {
|
|
203
|
+
grid.setPossibility(constraint.item1Cat, constraint.item1Val, constraint.ordinalCat, pval1.val, false);
|
|
204
|
+
deductions++;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
// Prune item2's possibilities
|
|
209
|
+
for (const pval2 of possibleVals2) {
|
|
210
|
+
const canBeGreaterThan = possibleVals1.some(pval1 => pval1.idx < pval2.idx);
|
|
211
|
+
if (!canBeGreaterThan) {
|
|
212
|
+
if (grid.isPossible(constraint.item2Cat, constraint.item2Val, constraint.ordinalCat, pval2.val)) {
|
|
213
|
+
grid.setPossibility(constraint.item2Cat, constraint.item2Val, constraint.ordinalCat, pval2.val, false);
|
|
214
|
+
deductions++;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return deductions;
|
|
220
|
+
}
|
|
221
|
+
applySuperlativeClue(grid, clue) {
|
|
222
|
+
const categories = grid.categories;
|
|
223
|
+
const ordinalCatConfig = categories.find(c => c.id === clue.ordinalCat);
|
|
224
|
+
if (!ordinalCatConfig || ordinalCatConfig.type !== types_1.CategoryType.ORDINAL)
|
|
225
|
+
return 0;
|
|
226
|
+
// Assuming values are sorted in ascending order for ordinal categories
|
|
227
|
+
const extremeValue = clue.operator === Clue_1.SuperlativeOperator.MAX
|
|
228
|
+
? ordinalCatConfig.values[ordinalCatConfig.values.length - 1]
|
|
229
|
+
: ordinalCatConfig.values[0];
|
|
230
|
+
// This clue is essentially a binary IS clue.
|
|
231
|
+
const binaryClue = {
|
|
232
|
+
type: Clue_1.ClueType.BINARY,
|
|
233
|
+
cat1: clue.targetCat,
|
|
234
|
+
val1: clue.targetVal,
|
|
235
|
+
cat2: clue.ordinalCat,
|
|
236
|
+
val2: extremeValue,
|
|
237
|
+
operator: Clue_1.BinaryOperator.IS,
|
|
238
|
+
};
|
|
239
|
+
return this.applyBinaryClue(grid, binaryClue);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
exports.Solver = Solver;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const Generator_1 = require("../src/engine/Generator");
|
|
4
|
+
const types_1 = require("../src/types");
|
|
5
|
+
const Clue_1 = require("../src/engine/Clue");
|
|
6
|
+
const perf_hooks_1 = require("perf_hooks");
|
|
7
|
+
// Helper to generate random config
|
|
8
|
+
function generateConfig(numCats, numVals) {
|
|
9
|
+
const cats = [];
|
|
10
|
+
for (let c = 0; c < numCats; c++) {
|
|
11
|
+
const isOrdinal = c === numCats - 1;
|
|
12
|
+
const values = [];
|
|
13
|
+
for (let v = 0; v < numVals; v++) {
|
|
14
|
+
if (isOrdinal)
|
|
15
|
+
values.push((v + 1) * 10);
|
|
16
|
+
else
|
|
17
|
+
values.push(`V${v}`);
|
|
18
|
+
}
|
|
19
|
+
cats.push({
|
|
20
|
+
id: `C${c}`,
|
|
21
|
+
type: isOrdinal ? types_1.CategoryType.ORDINAL : types_1.CategoryType.NOMINAL,
|
|
22
|
+
values: values
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
return cats;
|
|
26
|
+
}
|
|
27
|
+
const MATRIX = [
|
|
28
|
+
{ name: 'Standard (3x4)', cats: 3, vals: 4, iters: 10 },
|
|
29
|
+
{ name: 'Medium (4x5)', cats: 4, vals: 5, iters: 5 },
|
|
30
|
+
{ name: 'Large (5x5)', cats: 5, vals: 5, iters: 2 },
|
|
31
|
+
{ name: 'Wide (6x4)', cats: 6, vals: 4, iters: 2 },
|
|
32
|
+
{ name: 'Tall (3x8)', cats: 3, vals: 8, iters: 2 },
|
|
33
|
+
{ name: 'Stress (8x5)', cats: 8, vals: 5, iters: 1 },
|
|
34
|
+
{ name: 'Deep (3x10)', cats: 3, vals: 10, iters: 1 },
|
|
35
|
+
];
|
|
36
|
+
console.log('--- Scalability Benchmark ---');
|
|
37
|
+
for (const test of MATRIX) {
|
|
38
|
+
const seed = Math.floor(Math.random() * 10000);
|
|
39
|
+
const generator = new Generator_1.Generator(seed);
|
|
40
|
+
const config = generateConfig(test.cats, test.vals);
|
|
41
|
+
// Target: Cat0:Val0 -> Cat1
|
|
42
|
+
const target = {
|
|
43
|
+
category1Id: config[0].id,
|
|
44
|
+
value1: config[0].values[0],
|
|
45
|
+
category2Id: config[1].id,
|
|
46
|
+
};
|
|
47
|
+
const start = perf_hooks_1.performance.now();
|
|
48
|
+
let success = 0;
|
|
49
|
+
process.stdout.write(`Running ${test.name} ... `);
|
|
50
|
+
for (let i = 0; i < test.iters; i++) {
|
|
51
|
+
try {
|
|
52
|
+
let options = undefined;
|
|
53
|
+
if (test.cats >= 8)
|
|
54
|
+
options = { maxCandidates: 50 };
|
|
55
|
+
else if (test.cats >= 6)
|
|
56
|
+
options = { maxCandidates: 200 };
|
|
57
|
+
const puzzle = generator.generatePuzzle(config, target, options);
|
|
58
|
+
if (puzzle)
|
|
59
|
+
success++;
|
|
60
|
+
}
|
|
61
|
+
catch (e) {
|
|
62
|
+
console.error(e);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
const end = perf_hooks_1.performance.now();
|
|
66
|
+
const duration = end - start;
|
|
67
|
+
const avg = duration / test.iters;
|
|
68
|
+
console.log(`${avg.toFixed(2)}ms / puzzle (Success: ${success}/${test.iters})`);
|
|
69
|
+
}
|
|
70
|
+
// Stats Benchmark
|
|
71
|
+
console.log('--- Clue Variety Benchmark ---');
|
|
72
|
+
{
|
|
73
|
+
const ITERATIONS = 20;
|
|
74
|
+
const generator = new Generator_1.Generator(999);
|
|
75
|
+
const config = generateConfig(4, 5);
|
|
76
|
+
const target = { category1Id: config[0].id, value1: config[0].values[0], category2Id: config[1].id };
|
|
77
|
+
// Stats
|
|
78
|
+
const stats = {};
|
|
79
|
+
process.stdout.write(`Running Stats (4x5, N=${ITERATIONS}) ... `);
|
|
80
|
+
let totalClues = 0;
|
|
81
|
+
for (let i = 0; i < ITERATIONS; i++) {
|
|
82
|
+
try {
|
|
83
|
+
const p = generator.generatePuzzle(config, target);
|
|
84
|
+
if (p) {
|
|
85
|
+
for (const c of p.clues) {
|
|
86
|
+
totalClues++;
|
|
87
|
+
let key = 'UNKNOWN';
|
|
88
|
+
if (c.type === Clue_1.ClueType.BINARY)
|
|
89
|
+
key = `BINARY_${c.operator === Clue_1.BinaryOperator.IS ? 'IS' : 'IS_NOT'}`;
|
|
90
|
+
else if (c.type === Clue_1.ClueType.ORDINAL) {
|
|
91
|
+
const op = c.operator;
|
|
92
|
+
if (op === 0)
|
|
93
|
+
key = 'ORD_GT';
|
|
94
|
+
else if (op === 1)
|
|
95
|
+
key = 'ORD_LT';
|
|
96
|
+
else if (op === 2)
|
|
97
|
+
key = 'ORD_NOT_GT'; // <=
|
|
98
|
+
else if (op === 3)
|
|
99
|
+
key = 'ORD_NOT_LT'; // >=
|
|
100
|
+
}
|
|
101
|
+
else if (c.type === Clue_1.ClueType.SUPERLATIVE) {
|
|
102
|
+
const op = c.operator;
|
|
103
|
+
if (op === 0)
|
|
104
|
+
key = 'SUP_MIN';
|
|
105
|
+
else if (op === 1)
|
|
106
|
+
key = 'SUP_MAX';
|
|
107
|
+
else if (op === 2)
|
|
108
|
+
key = 'SUP_NOT_MIN';
|
|
109
|
+
else if (op === 3)
|
|
110
|
+
key = 'SUP_NOT_MAX';
|
|
111
|
+
}
|
|
112
|
+
else if (c.type === Clue_1.ClueType.UNARY)
|
|
113
|
+
key = 'UNARY';
|
|
114
|
+
else if (c.type === Clue_1.ClueType.CROSS_ORDINAL)
|
|
115
|
+
key = 'CROSS_ORDINAL';
|
|
116
|
+
stats[key] = (stats[key] || 0) + 1;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
catch (e) { }
|
|
121
|
+
}
|
|
122
|
+
console.log('Done.');
|
|
123
|
+
console.log(`Total Clues: ${totalClues}`);
|
|
124
|
+
Object.keys(stats).sort().forEach(k => {
|
|
125
|
+
const pct = ((stats[k] / totalClues) * 100).toFixed(1);
|
|
126
|
+
console.log(` ${k}: ${stats[k]} (${pct}%)`);
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
console.log('-----------------------------');
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const types_1 = require("../src/types");
|
|
4
|
+
const Generator_1 = require("../src/engine/Generator");
|
|
5
|
+
const Solver_1 = require("../src/engine/Solver");
|
|
6
|
+
const LogicGrid_1 = require("../src/engine/LogicGrid");
|
|
7
|
+
const Clue_1 = require("../src/engine/Clue");
|
|
8
|
+
const categories = [
|
|
9
|
+
{ id: 'Name', type: types_1.CategoryType.NOMINAL, values: ['Alice', 'Bob', 'Charlie', 'David'] },
|
|
10
|
+
{ id: 'Genre', type: types_1.CategoryType.NOMINAL, values: ['Horror', 'Sci-Fi', 'Comedy', 'Drama'] },
|
|
11
|
+
{ id: 'Snack', type: types_1.CategoryType.NOMINAL, values: ['Chips', 'Popcorn', 'Candy', 'Chocolate'] },
|
|
12
|
+
{ id: 'Age', type: types_1.CategoryType.ORDINAL, values: [20, 30, 40, 50] },
|
|
13
|
+
];
|
|
14
|
+
const targetFact = {
|
|
15
|
+
category1Id: 'Name',
|
|
16
|
+
value1: 'David',
|
|
17
|
+
category2Id: 'Snack',
|
|
18
|
+
};
|
|
19
|
+
const seed = 1234;
|
|
20
|
+
const generator = new Generator_1.Generator(seed);
|
|
21
|
+
const puzzle = generator.generatePuzzle(categories, targetFact);
|
|
22
|
+
console.log(`## ✨ Generated Puzzle (Seed: ${seed})`);
|
|
23
|
+
console.log('\n---\n');
|
|
24
|
+
console.log('### The Answer Key (For Verification Only)');
|
|
25
|
+
const baseCat = puzzle.categories[0];
|
|
26
|
+
for (const val of baseCat.values) {
|
|
27
|
+
let line = '';
|
|
28
|
+
for (const cat of puzzle.categories) {
|
|
29
|
+
line += `${cat.id}: ${puzzle.solution[cat.id][val]} | `;
|
|
30
|
+
}
|
|
31
|
+
console.log(line.slice(0, -3));
|
|
32
|
+
}
|
|
33
|
+
console.log('\n### Goal');
|
|
34
|
+
console.log(`What ${puzzle.targetFact.category2Id} does [Name: ${puzzle.targetFact.value1}] have?`);
|
|
35
|
+
console.log('\n---\n');
|
|
36
|
+
console.log('### 🧩 Clue-by-Clue Proof Chain\n');
|
|
37
|
+
// The new API provides the proof chain directly, so we don't need to re-run the solver.
|
|
38
|
+
// We still create a grid to show the ambiguity snapshot at each step.
|
|
39
|
+
const logicGrid = new LogicGrid_1.LogicGrid(categories);
|
|
40
|
+
const solver = new Solver_1.Solver();
|
|
41
|
+
puzzle.proofChain.forEach((step, index) => {
|
|
42
|
+
console.log(`STEP ${index + 1}: Clue: ${clueToString(step.clue)}`);
|
|
43
|
+
console.log(`Deductions: ${step.deductions} eliminations made.`);
|
|
44
|
+
// Apply the clue to our local grid to show the state *after* this clue is applied
|
|
45
|
+
solver.applyClue(logicGrid, step.clue);
|
|
46
|
+
console.log('\nAmbiguity Snapshot:');
|
|
47
|
+
// Focused snapshot for the target fact
|
|
48
|
+
const targetCat2 = categories.find(c => c.id === puzzle.targetFact.category2Id);
|
|
49
|
+
if (targetCat2) {
|
|
50
|
+
const possibleValues = targetCat2.values.filter((v) => logicGrid.isPossible(puzzle.targetFact.category1Id, puzzle.targetFact.value1, puzzle.targetFact.category2Id, v));
|
|
51
|
+
console.log(`- Target: [${puzzle.targetFact.category1Id}: ${puzzle.targetFact.value1}] vs [${puzzle.targetFact.category2Id}]: ${possibleValues.length} possibilities remain -> [${possibleValues.join(', ')}]`);
|
|
52
|
+
}
|
|
53
|
+
// Overall grid completion
|
|
54
|
+
const { totalPossible, currentPossible, solutionPossible } = logicGrid.getGridStats();
|
|
55
|
+
const totalEliminatable = totalPossible - solutionPossible;
|
|
56
|
+
const eliminatedSoFar = totalPossible - currentPossible;
|
|
57
|
+
const percentageSolved = totalEliminatable > 0 ? (eliminatedSoFar / totalEliminatable) * 100 : 0;
|
|
58
|
+
console.log(`- Grid Solved: ${percentageSolved.toFixed(1)}%`);
|
|
59
|
+
console.log('\n---\n');
|
|
60
|
+
});
|
|
61
|
+
const targetValue = puzzle.solution[puzzle.targetFact.category2Id][puzzle.targetFact.value1];
|
|
62
|
+
console.log(`\n**Target Fact Solved:** [${puzzle.targetFact.category1Id}: ${puzzle.targetFact.value1}] is correlated with [${puzzle.targetFact.category2Id}: ${targetValue}]!`);
|
|
63
|
+
function clueToString(clue) {
|
|
64
|
+
switch (clue.type) {
|
|
65
|
+
case Clue_1.ClueType.BINARY:
|
|
66
|
+
const b = clue;
|
|
67
|
+
const op = b.operator === Clue_1.BinaryOperator.IS ? 'is' : 'is not';
|
|
68
|
+
return `[${b.cat1}: ${b.val1}] ${op} [${b.cat2}: ${b.val2}]`;
|
|
69
|
+
case Clue_1.ClueType.ORDINAL:
|
|
70
|
+
const o = clue;
|
|
71
|
+
const ordOp = o.operator === Clue_1.OrdinalOperator.GREATER_THAN ? 'is greater than' : 'is less than';
|
|
72
|
+
return `The one with [${o.item1Cat}: ${o.item1Val}] ${ordOp} the one with [${o.item2Cat}: ${o.item2Val}] (by ${o.ordinalCat})`;
|
|
73
|
+
case Clue_1.ClueType.SUPERLATIVE:
|
|
74
|
+
const s = clue;
|
|
75
|
+
const supOp = s.operator === Clue_1.SuperlativeOperator.MAX ? 'is the oldest' : 'is the youngest';
|
|
76
|
+
return `[${s.targetCat}: ${s.targetVal}] ${supOp}`;
|
|
77
|
+
case Clue_1.ClueType.UNARY:
|
|
78
|
+
const u = clue;
|
|
79
|
+
const filterStr = u.filter === Clue_1.UnaryFilter.IS_EVEN ? 'even' : 'odd';
|
|
80
|
+
return `[${u.targetCat}: ${u.targetVal}] is associated with an ${filterStr} ${u.ordinalCat}`;
|
|
81
|
+
default:
|
|
82
|
+
return 'Unknown Clue Type';
|
|
83
|
+
}
|
|
84
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./types"), exports);
|
|
18
|
+
__exportStar(require("./Clue"), exports);
|
|
19
|
+
__exportStar(require("./LogicGrid"), exports);
|
|
20
|
+
__exportStar(require("./Solver"), exports);
|
|
21
|
+
__exportStar(require("./Generator"), exports);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const types_1 = require("./types");
|
|
4
|
+
const Generator_1 = require("./Generator");
|
|
5
|
+
const Solver_1 = require("./Solver");
|
|
6
|
+
const LogicGrid_1 = require("./LogicGrid");
|
|
7
|
+
const Clue_1 = require("./Clue");
|
|
8
|
+
const categories = [
|
|
9
|
+
{ id: 'Name', type: types_1.CategoryType.NOMINAL, values: ['Alice', 'Bob', 'Charlie', 'David'] },
|
|
10
|
+
{ id: 'Genre', type: types_1.CategoryType.NOMINAL, values: ['Horror', 'Sci-Fi', 'Comedy', 'Drama'] },
|
|
11
|
+
{ id: 'Snack', type: types_1.CategoryType.NOMINAL, values: ['Chips', 'Popcorn', 'Candy', 'Chocolate'] },
|
|
12
|
+
{ id: 'Age', type: types_1.CategoryType.ORDINAL, values: [20, 30, 40, 50] },
|
|
13
|
+
];
|
|
14
|
+
const targetFact = {
|
|
15
|
+
category1Id: 'Name',
|
|
16
|
+
value1: 'David',
|
|
17
|
+
category2Id: 'Snack',
|
|
18
|
+
};
|
|
19
|
+
const seed = 1234;
|
|
20
|
+
const generator = new Generator_1.Generator(seed);
|
|
21
|
+
const puzzle = generator.generatePuzzle(categories, targetFact);
|
|
22
|
+
console.log(`## ✨ Generated Puzzle (Seed: ${seed})`);
|
|
23
|
+
console.log('\n---\n');
|
|
24
|
+
console.log('### The Answer Key (For Verification Only)');
|
|
25
|
+
const baseCat = puzzle.categories[0];
|
|
26
|
+
for (const val of baseCat.values) {
|
|
27
|
+
let line = '';
|
|
28
|
+
for (const cat of puzzle.categories) {
|
|
29
|
+
line += `${cat.id}: ${puzzle.solution[cat.id][val]} | `;
|
|
30
|
+
}
|
|
31
|
+
console.log(line.slice(0, -3));
|
|
32
|
+
}
|
|
33
|
+
console.log('\n### Goal');
|
|
34
|
+
console.log(`What ${puzzle.targetFact.category2Id} does [Name: ${puzzle.targetFact.value1}] have?`);
|
|
35
|
+
console.log('\n---\n');
|
|
36
|
+
console.log('### 🧩 Clue-by-Clue Proof Chain\n');
|
|
37
|
+
// The new API provides the proof chain directly, so we don't need to re-run the solver.
|
|
38
|
+
// We still create a grid to show the ambiguity snapshot at each step.
|
|
39
|
+
const logicGrid = new LogicGrid_1.LogicGrid(categories);
|
|
40
|
+
const solver = new Solver_1.Solver();
|
|
41
|
+
puzzle.proofChain.forEach((step, index) => {
|
|
42
|
+
console.log(`STEP ${index + 1}: Clue: ${clueToString(step.clue)}`);
|
|
43
|
+
console.log(`Deductions: ${step.deductions} eliminations made.`);
|
|
44
|
+
// Apply the clue to our local grid to show the state *after* this clue is applied
|
|
45
|
+
solver.applyClue(logicGrid, step.clue);
|
|
46
|
+
console.log('\nAmbiguity Snapshot:');
|
|
47
|
+
// Focused snapshot for the target fact
|
|
48
|
+
const targetCat2 = categories.find(c => c.id === puzzle.targetFact.category2Id);
|
|
49
|
+
if (targetCat2) {
|
|
50
|
+
const possibleValues = targetCat2.values.filter(v => logicGrid.isPossible(puzzle.targetFact.category1Id, puzzle.targetFact.value1, puzzle.targetFact.category2Id, v));
|
|
51
|
+
console.log(`- Target: [${puzzle.targetFact.category1Id}: ${puzzle.targetFact.value1}] vs [${puzzle.targetFact.category2Id}]: ${possibleValues.length} possibilities remain -> [${possibleValues.join(', ')}]`);
|
|
52
|
+
}
|
|
53
|
+
// Overall grid completion
|
|
54
|
+
const { totalPossible, currentPossible, solutionPossible } = logicGrid.getGridStats();
|
|
55
|
+
const totalEliminatable = totalPossible - solutionPossible;
|
|
56
|
+
const eliminatedSoFar = totalPossible - currentPossible;
|
|
57
|
+
const percentageSolved = totalEliminatable > 0 ? (eliminatedSoFar / totalEliminatable) * 100 : 0;
|
|
58
|
+
console.log(`- Grid Solved: ${percentageSolved.toFixed(1)}%`);
|
|
59
|
+
console.log('\n---\n');
|
|
60
|
+
});
|
|
61
|
+
const targetValue = puzzle.solution[puzzle.targetFact.category2Id][puzzle.targetFact.value1];
|
|
62
|
+
console.log(`\n**Target Fact Solved:** [${puzzle.targetFact.category1Id}: ${puzzle.targetFact.value1}] is correlated with [${puzzle.targetFact.category2Id}: ${targetValue}]!`);
|
|
63
|
+
function clueToString(clue) {
|
|
64
|
+
switch (clue.type) {
|
|
65
|
+
case Clue_1.ClueType.BINARY:
|
|
66
|
+
const b = clue;
|
|
67
|
+
const op = b.operator === Clue_1.BinaryOperator.IS ? 'is' : 'is not';
|
|
68
|
+
return `[${b.cat1}: ${b.val1}] ${op} [${b.cat2}: ${b.val2}]`;
|
|
69
|
+
case Clue_1.ClueType.ORDINAL:
|
|
70
|
+
const o = clue;
|
|
71
|
+
const ordOp = o.operator === Clue_1.OrdinalOperator.GREATER_THAN ? 'is greater than' : 'is less than';
|
|
72
|
+
return `The one with [${o.item1Cat}: ${o.item1Val}] ${ordOp} the one with [${o.item2Cat}: ${o.item2Val}] (by ${o.ordinalCat})`;
|
|
73
|
+
case Clue_1.ClueType.SUPERLATIVE:
|
|
74
|
+
const s = clue;
|
|
75
|
+
const supOp = s.operator === Clue_1.SuperlativeOperator.MAX ? 'is the oldest' : 'is the youngest';
|
|
76
|
+
return `[${s.targetCat}: ${s.targetVal}] ${supOp}`;
|
|
77
|
+
case Clue_1.ClueType.UNARY:
|
|
78
|
+
const u = clue;
|
|
79
|
+
const filterStr = u.filter === Clue_1.UnaryFilter.IS_EVEN ? 'even' : 'odd';
|
|
80
|
+
return `[${u.targetCat}: ${u.targetVal}] is associated with an ${filterStr} ${u.ordinalCat}`;
|
|
81
|
+
default:
|
|
82
|
+
return 'Unknown Clue Type';
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DEFAULT_CATEGORIES = void 0;
|
|
4
|
+
const types_1 = require("./types");
|
|
5
|
+
/**
|
|
6
|
+
* A default set of categories for a standard logic puzzle (Name, Snack, Age).
|
|
7
|
+
*/
|
|
8
|
+
exports.DEFAULT_CATEGORIES = [
|
|
9
|
+
{
|
|
10
|
+
id: 'Name',
|
|
11
|
+
type: types_1.CategoryType.NOMINAL,
|
|
12
|
+
values: ['Alice', 'Bob', 'Charlie', 'David'],
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
id: 'Snack',
|
|
16
|
+
type: types_1.CategoryType.NOMINAL,
|
|
17
|
+
values: ['Chips', 'Popcorn', 'Candy', 'Chocolate'],
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
id: 'Age',
|
|
21
|
+
type: types_1.CategoryType.ORDINAL,
|
|
22
|
+
values: [20, 30, 40, 50],
|
|
23
|
+
},
|
|
24
|
+
];
|