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
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Generator = void 0;
|
|
4
|
+
const types_1 = require("./types");
|
|
5
|
+
const Clue_1 = require("./Clue");
|
|
6
|
+
const LogicGrid_1 = require("./LogicGrid");
|
|
7
|
+
const Solver_1 = require("./Solver");
|
|
8
|
+
// A simple seeded PRNG (mulberry32)
|
|
9
|
+
function mulberry32(a) {
|
|
10
|
+
return function () {
|
|
11
|
+
a |= 0;
|
|
12
|
+
a = a + 0x6D2B79F5 | 0;
|
|
13
|
+
var t = Math.imul(a ^ a >>> 15, 1 | a);
|
|
14
|
+
t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
|
|
15
|
+
return ((t ^ t >>> 14) >>> 0) / 4294967296;
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* The main class responsible for generating logic puzzles.
|
|
20
|
+
*
|
|
21
|
+
* It handles the creation of a consistent solution, the generation of all possible clues,
|
|
22
|
+
* and the selection of an optimal set of clues to form a solvable puzzle with a specific target.
|
|
23
|
+
*/
|
|
24
|
+
class Generator {
|
|
25
|
+
/**
|
|
26
|
+
* Creates a new Generator instance.
|
|
27
|
+
*
|
|
28
|
+
* @param seed - A numeric seed for the random number generator to ensure reproducibility.
|
|
29
|
+
*/
|
|
30
|
+
constructor(seed) {
|
|
31
|
+
this.solution = {};
|
|
32
|
+
this.valueMap = new Map();
|
|
33
|
+
this.reverseSolution = new Map();
|
|
34
|
+
this.seed = seed;
|
|
35
|
+
this.random = mulberry32(seed);
|
|
36
|
+
this.solver = new Solver_1.Solver();
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Generates a fully solvable logic puzzle based on the provided configuration.
|
|
40
|
+
*
|
|
41
|
+
* @param categories - The categories and values to include in the puzzle.
|
|
42
|
+
* @param target - The specific fact that should be the final deduction of the puzzle.
|
|
43
|
+
* @returns A complete Puzzle object containing the solution, clues, and proof chain.
|
|
44
|
+
*/
|
|
45
|
+
generatePuzzle(categories, target) {
|
|
46
|
+
this.createSolution(categories);
|
|
47
|
+
let availableClues = this.generateAllPossibleClues(categories);
|
|
48
|
+
const logicGrid = new LogicGrid_1.LogicGrid(categories);
|
|
49
|
+
const proofChain = [];
|
|
50
|
+
while (proofChain.length < 100) { // Safety break
|
|
51
|
+
let bestCandidate = null;
|
|
52
|
+
let finalCandidate = null;
|
|
53
|
+
// Iterate backwards to safely remove clues
|
|
54
|
+
for (let i = availableClues.length - 1; i >= 0; i--) {
|
|
55
|
+
const clue = availableClues[i];
|
|
56
|
+
const tempGrid = logicGrid.clone();
|
|
57
|
+
const { deductions } = this.solver.applyClue(tempGrid, clue);
|
|
58
|
+
if (deductions === 0) {
|
|
59
|
+
availableClues.splice(i, 1); // Permanently remove redundant clue
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
const score = this.calculateScore(tempGrid, target, deductions, clue, proofChain.map(p => p.clue));
|
|
63
|
+
if (score > -1000000) { // Not a premature solve
|
|
64
|
+
if (!bestCandidate || score > bestCandidate.score) {
|
|
65
|
+
bestCandidate = { clue, score };
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
finalCandidate = { clue, score }; // It solves the target, keep as a final option
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
const chosenCandidate = bestCandidate || finalCandidate;
|
|
73
|
+
if (!chosenCandidate) {
|
|
74
|
+
// No more useful clues found
|
|
75
|
+
break;
|
|
76
|
+
}
|
|
77
|
+
const chosenClue = chosenCandidate.clue;
|
|
78
|
+
const { deductions } = this.solver.applyClue(logicGrid, chosenClue);
|
|
79
|
+
proofChain.push({ clue: chosenClue, deductions });
|
|
80
|
+
// Remove the chosen clue from the available list
|
|
81
|
+
const chosenIndex = availableClues.findIndex(c => JSON.stringify(c) === JSON.stringify(chosenClue));
|
|
82
|
+
if (chosenIndex > -1) {
|
|
83
|
+
availableClues.splice(chosenIndex, 1);
|
|
84
|
+
}
|
|
85
|
+
if (this.isPuzzleSolved(logicGrid)) {
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return {
|
|
90
|
+
solution: this.solution,
|
|
91
|
+
clues: proofChain.map(p => p.clue),
|
|
92
|
+
proofChain,
|
|
93
|
+
categories,
|
|
94
|
+
targetFact: target,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
createSolution(categories) {
|
|
98
|
+
const baseCategory = categories[0];
|
|
99
|
+
baseCategory.values.forEach(val => {
|
|
100
|
+
this.valueMap.set(val, { [baseCategory.id]: val });
|
|
101
|
+
});
|
|
102
|
+
for (let i = 1; i < categories.length; i++) {
|
|
103
|
+
const currentCategory = categories[i];
|
|
104
|
+
const shuffledValues = [...currentCategory.values].sort(() => this.random() - 0.5);
|
|
105
|
+
let i_shuffled = 0;
|
|
106
|
+
for (const val of baseCategory.values) {
|
|
107
|
+
const record = this.valueMap.get(val);
|
|
108
|
+
if (record)
|
|
109
|
+
record[currentCategory.id] = shuffledValues[i_shuffled++];
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
for (const cat of categories) {
|
|
113
|
+
this.solution[cat.id] = {};
|
|
114
|
+
this.reverseSolution.set(cat.id, new Map());
|
|
115
|
+
}
|
|
116
|
+
for (const baseVal of baseCategory.values) {
|
|
117
|
+
const mappings = this.valueMap.get(baseVal);
|
|
118
|
+
if (mappings) {
|
|
119
|
+
for (const catId in mappings) {
|
|
120
|
+
this.solution[catId][baseVal] = mappings[catId];
|
|
121
|
+
this.reverseSolution.get(catId)?.set(mappings[catId], baseVal);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
generateAllPossibleClues(categories) {
|
|
127
|
+
const clues = [];
|
|
128
|
+
const baseCategory = categories[0];
|
|
129
|
+
// Generate BinaryClues
|
|
130
|
+
for (const cat1 of categories) {
|
|
131
|
+
for (const val1 of cat1.values) {
|
|
132
|
+
for (const cat2 of categories) {
|
|
133
|
+
if (cat1.id >= cat2.id)
|
|
134
|
+
continue;
|
|
135
|
+
const baseVal = this.reverseSolution.get(cat1.id)?.get(val1);
|
|
136
|
+
if (!baseVal)
|
|
137
|
+
continue;
|
|
138
|
+
const mappings = this.valueMap.get(baseVal);
|
|
139
|
+
if (!mappings)
|
|
140
|
+
continue;
|
|
141
|
+
for (const val2 of cat2.values) {
|
|
142
|
+
const correctVal2 = mappings[cat2.id];
|
|
143
|
+
if (val2 === correctVal2) {
|
|
144
|
+
clues.push({
|
|
145
|
+
type: Clue_1.ClueType.BINARY,
|
|
146
|
+
operator: Clue_1.BinaryOperator.IS,
|
|
147
|
+
cat1: cat1.id,
|
|
148
|
+
val1: val1,
|
|
149
|
+
cat2: cat2.id,
|
|
150
|
+
val2: val2,
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
clues.push({
|
|
155
|
+
type: Clue_1.ClueType.BINARY,
|
|
156
|
+
operator: Clue_1.BinaryOperator.IS_NOT,
|
|
157
|
+
cat1: cat1.id,
|
|
158
|
+
val1: val1,
|
|
159
|
+
cat2: cat2.id,
|
|
160
|
+
val2: val2,
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
// Generate Ordinal and SuperlativeClues
|
|
168
|
+
for (const ordCategory of categories.filter(c => c.type === types_1.CategoryType.ORDINAL)) {
|
|
169
|
+
const sortedValues = [...ordCategory.values].sort((a, b) => a - b);
|
|
170
|
+
const minVal = sortedValues[0];
|
|
171
|
+
const maxVal = sortedValues[sortedValues.length - 1];
|
|
172
|
+
// Generate SuperlativeClues for all categories
|
|
173
|
+
for (const targetCat of categories) {
|
|
174
|
+
if (targetCat.id === ordCategory.id)
|
|
175
|
+
continue;
|
|
176
|
+
const baseValForMin = this.reverseSolution.get(ordCategory.id).get(minVal);
|
|
177
|
+
const itemValForMin = this.valueMap.get(baseValForMin)[targetCat.id];
|
|
178
|
+
clues.push({
|
|
179
|
+
type: Clue_1.ClueType.SUPERLATIVE,
|
|
180
|
+
operator: Clue_1.SuperlativeOperator.MIN,
|
|
181
|
+
targetCat: targetCat.id,
|
|
182
|
+
targetVal: itemValForMin,
|
|
183
|
+
ordinalCat: ordCategory.id,
|
|
184
|
+
});
|
|
185
|
+
const baseValForMax = this.reverseSolution.get(ordCategory.id).get(maxVal);
|
|
186
|
+
const itemValForMax = this.valueMap.get(baseValForMax)[targetCat.id];
|
|
187
|
+
clues.push({
|
|
188
|
+
type: Clue_1.ClueType.SUPERLATIVE,
|
|
189
|
+
operator: Clue_1.SuperlativeOperator.MAX,
|
|
190
|
+
targetCat: targetCat.id,
|
|
191
|
+
targetVal: itemValForMax,
|
|
192
|
+
ordinalCat: ordCategory.id,
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
// Generate OrdinalClues for all pairs of categories
|
|
196
|
+
for (const item1Cat of categories) {
|
|
197
|
+
if (item1Cat.id === ordCategory.id)
|
|
198
|
+
continue;
|
|
199
|
+
for (const item2Cat of categories) {
|
|
200
|
+
if (item2Cat.id === ordCategory.id)
|
|
201
|
+
continue;
|
|
202
|
+
for (const item1Val of item1Cat.values) {
|
|
203
|
+
for (const item2Val of item2Cat.values) {
|
|
204
|
+
if (item1Cat.id === item2Cat.id && item1Val === item2Val)
|
|
205
|
+
continue;
|
|
206
|
+
const baseVal1 = this.reverseSolution.get(item1Cat.id)?.get(item1Val);
|
|
207
|
+
const baseVal2 = this.reverseSolution.get(item2Cat.id)?.get(item2Val);
|
|
208
|
+
if (!baseVal1 || !baseVal2)
|
|
209
|
+
continue;
|
|
210
|
+
// if they are the same entity, don't compare
|
|
211
|
+
if (baseVal1 === baseVal2)
|
|
212
|
+
continue;
|
|
213
|
+
const mappings1 = this.valueMap.get(baseVal1);
|
|
214
|
+
const mappings2 = this.valueMap.get(baseVal2);
|
|
215
|
+
if (!mappings1 || !mappings2)
|
|
216
|
+
continue;
|
|
217
|
+
const ordVal1 = mappings1[ordCategory.id];
|
|
218
|
+
const ordVal2 = mappings2[ordCategory.id];
|
|
219
|
+
if (ordVal1 > ordVal2) {
|
|
220
|
+
clues.push({
|
|
221
|
+
type: Clue_1.ClueType.ORDINAL,
|
|
222
|
+
operator: Clue_1.OrdinalOperator.GREATER_THAN,
|
|
223
|
+
item1Cat: item1Cat.id,
|
|
224
|
+
item1Val: item1Val,
|
|
225
|
+
item2Cat: item2Cat.id,
|
|
226
|
+
item2Val: item2Val,
|
|
227
|
+
ordinalCat: ordCategory.id,
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
else if (ordVal1 < ordVal2) {
|
|
231
|
+
clues.push({
|
|
232
|
+
type: Clue_1.ClueType.ORDINAL,
|
|
233
|
+
operator: Clue_1.OrdinalOperator.LESS_THAN,
|
|
234
|
+
item1Cat: item1Cat.id,
|
|
235
|
+
item1Val: item1Val,
|
|
236
|
+
item2Cat: item2Cat.id,
|
|
237
|
+
item2Val: item2Val,
|
|
238
|
+
ordinalCat: ordCategory.id,
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
// Generate UnaryClues
|
|
247
|
+
for (const ordCategory of categories) {
|
|
248
|
+
if (ordCategory.type !== types_1.CategoryType.ORDINAL)
|
|
249
|
+
continue;
|
|
250
|
+
// Check if all values are numbers
|
|
251
|
+
if (!ordCategory.values.every(v => typeof v === 'number'))
|
|
252
|
+
continue;
|
|
253
|
+
for (const targetCategory of categories) {
|
|
254
|
+
if (targetCategory.id === ordCategory.id)
|
|
255
|
+
continue;
|
|
256
|
+
for (const targetVal of targetCategory.values) {
|
|
257
|
+
const baseVal = this.reverseSolution.get(targetCategory.id)?.get(targetVal);
|
|
258
|
+
if (!baseVal)
|
|
259
|
+
continue;
|
|
260
|
+
const mappings = this.valueMap.get(baseVal);
|
|
261
|
+
if (!mappings)
|
|
262
|
+
continue;
|
|
263
|
+
const ordValue = mappings[ordCategory.id];
|
|
264
|
+
if (ordValue % 2 === 0) {
|
|
265
|
+
clues.push({
|
|
266
|
+
type: Clue_1.ClueType.UNARY,
|
|
267
|
+
filter: Clue_1.UnaryFilter.IS_EVEN,
|
|
268
|
+
targetCat: targetCategory.id,
|
|
269
|
+
targetVal: targetVal,
|
|
270
|
+
ordinalCat: ordCategory.id,
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
else {
|
|
274
|
+
clues.push({
|
|
275
|
+
type: Clue_1.ClueType.UNARY,
|
|
276
|
+
filter: Clue_1.UnaryFilter.IS_ODD,
|
|
277
|
+
targetCat: targetCategory.id,
|
|
278
|
+
targetVal: targetVal,
|
|
279
|
+
ordinalCat: ordCategory.id,
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
return clues;
|
|
286
|
+
}
|
|
287
|
+
calculateScore(grid, target, deductions, clue, previouslySelectedClues) {
|
|
288
|
+
const clueType = clue.type;
|
|
289
|
+
const targetValue = this.solution[target.category2Id][target.value1];
|
|
290
|
+
const isTargetSolved = grid.isPossible(target.category1Id, target.value1, target.category2Id, targetValue) &&
|
|
291
|
+
grid.getPossibilitiesCount(target.category1Id, target.value1, target.category2Id) === 1;
|
|
292
|
+
const puzzleSolved = this.isPuzzleSolved(grid);
|
|
293
|
+
if (isTargetSolved && puzzleSolved) {
|
|
294
|
+
return 1000000; // This is the winning clue
|
|
295
|
+
}
|
|
296
|
+
if (isTargetSolved && !puzzleSolved) {
|
|
297
|
+
return -1000000; // This clue solves the target too early
|
|
298
|
+
}
|
|
299
|
+
const synergyScore = deductions;
|
|
300
|
+
const { totalPossible, currentPossible, solutionPossible } = grid.getGridStats();
|
|
301
|
+
const totalEliminatable = totalPossible - solutionPossible;
|
|
302
|
+
const eliminatedSoFar = totalPossible - currentPossible;
|
|
303
|
+
const completenessScore = totalEliminatable > 0 ? (eliminatedSoFar / totalEliminatable) : 0;
|
|
304
|
+
let complexityBonus = 0;
|
|
305
|
+
switch (clueType) {
|
|
306
|
+
case Clue_1.ClueType.ORDINAL:
|
|
307
|
+
complexityBonus = 1.5;
|
|
308
|
+
break;
|
|
309
|
+
case Clue_1.ClueType.SUPERLATIVE:
|
|
310
|
+
complexityBonus = 1.2;
|
|
311
|
+
break;
|
|
312
|
+
case Clue_1.ClueType.UNARY:
|
|
313
|
+
complexityBonus = 1.2;
|
|
314
|
+
break;
|
|
315
|
+
case Clue_1.ClueType.BINARY:
|
|
316
|
+
complexityBonus = 1.0;
|
|
317
|
+
// Boost IS_NOT to encourage variety, as they are weaker deduction-wise
|
|
318
|
+
if (clue.operator === Clue_1.BinaryOperator.IS_NOT) {
|
|
319
|
+
complexityBonus = 5.0;
|
|
320
|
+
}
|
|
321
|
+
break;
|
|
322
|
+
}
|
|
323
|
+
// --- Combined Three-Part Penalty Logic ---
|
|
324
|
+
let repetitionScore = 0;
|
|
325
|
+
// 1. Subject Penalty
|
|
326
|
+
const getEntities = (c) => {
|
|
327
|
+
const safeGet = (cat, val) => this.reverseSolution.get(cat)?.get(val);
|
|
328
|
+
let primary = [];
|
|
329
|
+
let secondary = [];
|
|
330
|
+
switch (c.type) {
|
|
331
|
+
case Clue_1.ClueType.BINARY:
|
|
332
|
+
const b = c;
|
|
333
|
+
primary.push(safeGet(b.cat1, b.val1));
|
|
334
|
+
if (b.operator === Clue_1.BinaryOperator.IS_NOT) {
|
|
335
|
+
secondary.push(safeGet(b.cat2, b.val2));
|
|
336
|
+
}
|
|
337
|
+
break;
|
|
338
|
+
case Clue_1.ClueType.SUPERLATIVE:
|
|
339
|
+
const s = c;
|
|
340
|
+
primary.push(safeGet(s.targetCat, s.targetVal));
|
|
341
|
+
break;
|
|
342
|
+
case Clue_1.ClueType.ORDINAL:
|
|
343
|
+
const o = c;
|
|
344
|
+
primary.push(safeGet(o.item1Cat, o.item1Val));
|
|
345
|
+
secondary.push(safeGet(o.item2Cat, o.item2Val));
|
|
346
|
+
break;
|
|
347
|
+
case Clue_1.ClueType.UNARY:
|
|
348
|
+
const u = c;
|
|
349
|
+
primary.push(safeGet(u.targetCat, u.targetVal));
|
|
350
|
+
break;
|
|
351
|
+
}
|
|
352
|
+
return {
|
|
353
|
+
primary: primary.filter((e) => !!e),
|
|
354
|
+
secondary: secondary.filter((e) => !!e)
|
|
355
|
+
};
|
|
356
|
+
};
|
|
357
|
+
const mentionedEntities = new Set();
|
|
358
|
+
for (const pClue of previouslySelectedClues) {
|
|
359
|
+
const { primary, secondary } = getEntities(pClue);
|
|
360
|
+
primary.forEach(e => mentionedEntities.add(e));
|
|
361
|
+
secondary.forEach(e => mentionedEntities.add(e));
|
|
362
|
+
}
|
|
363
|
+
const { primary: currentPrimary, secondary: currentSecondary } = getEntities(clue);
|
|
364
|
+
currentPrimary.forEach(e => {
|
|
365
|
+
if (mentionedEntities.has(e)) {
|
|
366
|
+
repetitionScore += 1.0; // Full penalty for primary subject
|
|
367
|
+
}
|
|
368
|
+
});
|
|
369
|
+
currentSecondary.forEach(e => {
|
|
370
|
+
if (mentionedEntities.has(e)) {
|
|
371
|
+
repetitionScore += 0.5; // Half penalty for secondary subject
|
|
372
|
+
}
|
|
373
|
+
});
|
|
374
|
+
// 2. Dimension (Ordinal Category) Penalty
|
|
375
|
+
const currentOrdinalCat = clue.ordinalCat;
|
|
376
|
+
if (currentOrdinalCat) {
|
|
377
|
+
for (const pClue of previouslySelectedClues) {
|
|
378
|
+
const prevOrdinalCat = pClue.ordinalCat;
|
|
379
|
+
if (currentOrdinalCat === prevOrdinalCat) {
|
|
380
|
+
repetitionScore += 0.5; // Penalize reuse of 'Age'
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
// 3. Structure (Clue Type) Penalty
|
|
385
|
+
if (previouslySelectedClues.length > 0) {
|
|
386
|
+
const lastClue = previouslySelectedClues[previouslySelectedClues.length - 1];
|
|
387
|
+
// Immediate Repetition Penalty
|
|
388
|
+
if (clue.type === lastClue.type) {
|
|
389
|
+
repetitionScore += 2.0; // Strong penalty for same type
|
|
390
|
+
// Double "IS" Penalty - Binary IS clues are very powerful but boring if repeated
|
|
391
|
+
if (clue.type === Clue_1.ClueType.BINARY &&
|
|
392
|
+
clue.operator === Clue_1.BinaryOperator.IS &&
|
|
393
|
+
lastClue.operator === Clue_1.BinaryOperator.IS) {
|
|
394
|
+
repetitionScore += 2.0;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
// Streak Penalty (3 in a row)
|
|
398
|
+
if (previouslySelectedClues.length > 1) {
|
|
399
|
+
const secondLastClue = previouslySelectedClues[previouslySelectedClues.length - 2];
|
|
400
|
+
if (clue.type === lastClue.type && clue.type === secondLastClue.type) {
|
|
401
|
+
repetitionScore += 5.0; // Massive penalty for 3-streak
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
const repetitionPenalty = Math.pow(0.4, repetitionScore);
|
|
406
|
+
const score = ((synergyScore * complexityBonus) + (completenessScore * 5)) * repetitionPenalty;
|
|
407
|
+
return score;
|
|
408
|
+
}
|
|
409
|
+
isPuzzleSolved(grid) {
|
|
410
|
+
const categories = grid.categories;
|
|
411
|
+
const baseCategory = categories[0];
|
|
412
|
+
for (const cat1 of categories) {
|
|
413
|
+
for (const val1 of cat1.values) {
|
|
414
|
+
for (const cat2 of categories) {
|
|
415
|
+
if (cat1.id >= cat2.id)
|
|
416
|
+
continue;
|
|
417
|
+
const baseVal = this.reverseSolution.get(cat1.id)?.get(val1);
|
|
418
|
+
if (!baseVal)
|
|
419
|
+
return false; // Should not happen
|
|
420
|
+
const correctVal2 = this.solution[cat2.id][baseVal];
|
|
421
|
+
if (grid.getPossibilitiesCount(cat1.id, val1, cat2.id) > 1) {
|
|
422
|
+
return false;
|
|
423
|
+
}
|
|
424
|
+
if (!grid.isPossible(cat1.id, val1, cat2.id, correctVal2)) {
|
|
425
|
+
return false;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
return true;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
exports.Generator = Generator;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { CategoryConfig, ValueLabel } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* Represents the state of the logic puzzle grid.
|
|
4
|
+
*
|
|
5
|
+
* It manages the possibilities between every pair of values across different categories.
|
|
6
|
+
* The grid is initialized where all connections are possible (true).
|
|
7
|
+
* As clues are applied, possibilities are eliminated (set to false).
|
|
8
|
+
*/
|
|
9
|
+
export declare class LogicGrid {
|
|
10
|
+
private grid;
|
|
11
|
+
private categories;
|
|
12
|
+
private valueMap;
|
|
13
|
+
/**
|
|
14
|
+
* Creates a new LogicGrid instance.
|
|
15
|
+
*
|
|
16
|
+
* @param categories - The configuration of categories and their values for the puzzle.
|
|
17
|
+
* @throws {Error} If the configuration is invalid (duplicate IDs, duplicate values, or mismatched value counts).
|
|
18
|
+
*/
|
|
19
|
+
constructor(categories: CategoryConfig[]);
|
|
20
|
+
private _validateCategories;
|
|
21
|
+
/**
|
|
22
|
+
* Sets the possibility state between two values from different categories.
|
|
23
|
+
*
|
|
24
|
+
* @param cat1Id - The ID of the first category.
|
|
25
|
+
* @param val1 - The value from the first category.
|
|
26
|
+
* @param cat2Id - The ID of the second category.
|
|
27
|
+
* @param val2 - The value from the second category.
|
|
28
|
+
* @param state - true if the connection is possible, false if eliminated.
|
|
29
|
+
*/
|
|
30
|
+
setPossibility(cat1Id: string, val1: ValueLabel, cat2Id: string, val2: ValueLabel, state: boolean): void;
|
|
31
|
+
/**
|
|
32
|
+
* Checks if a connection between two values is currently possible.
|
|
33
|
+
*
|
|
34
|
+
* @param cat1Id - The ID of the first category.
|
|
35
|
+
* @param val1 - The value from the first category.
|
|
36
|
+
* @param cat2Id - The ID of the second category.
|
|
37
|
+
* @param val2 - The value from the second category.
|
|
38
|
+
* @returns true if the connection is possible, false otherwise.
|
|
39
|
+
*/
|
|
40
|
+
isPossible(cat1Id: string, val1: ValueLabel, cat2Id: string, val2: ValueLabel): boolean;
|
|
41
|
+
/**
|
|
42
|
+
* Gets the number of possible connections for a specific value in one category
|
|
43
|
+
* relative to another category.
|
|
44
|
+
*
|
|
45
|
+
* @param cat1Id - The ID of the starting category.
|
|
46
|
+
* @param val1 - The value from the starting category.
|
|
47
|
+
* @param cat2Id - The target category ID.
|
|
48
|
+
* @returns The number of values in cat2 that are still possible for val1.
|
|
49
|
+
*/
|
|
50
|
+
getPossibilitiesCount(cat1Id: string, val1: ValueLabel, cat2Id: string): number;
|
|
51
|
+
/**
|
|
52
|
+
* Calculates statistics about the current state of the grid.
|
|
53
|
+
*
|
|
54
|
+
* @returns An object containing:
|
|
55
|
+
* - totalPossible: The initial total logical connections.
|
|
56
|
+
* - currentPossible: The number of remaining possible connections.
|
|
57
|
+
* - solutionPossible: The target number of connections for a solved grid.
|
|
58
|
+
*/
|
|
59
|
+
getGridStats(): {
|
|
60
|
+
totalPossible: number;
|
|
61
|
+
currentPossible: number;
|
|
62
|
+
solutionPossible: number;
|
|
63
|
+
};
|
|
64
|
+
/**
|
|
65
|
+
* Creates a deep copy of the current LogicGrid.
|
|
66
|
+
*
|
|
67
|
+
* @returns A new LogicGrid instance with the exact same state.
|
|
68
|
+
*/
|
|
69
|
+
clone(): LogicGrid;
|
|
70
|
+
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.LogicGrid = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Represents the state of the logic puzzle grid.
|
|
6
|
+
*
|
|
7
|
+
* It manages the possibilities between every pair of values across different categories.
|
|
8
|
+
* The grid is initialized where all connections are possible (true).
|
|
9
|
+
* As clues are applied, possibilities are eliminated (set to false).
|
|
10
|
+
*/
|
|
11
|
+
class LogicGrid {
|
|
12
|
+
/**
|
|
13
|
+
* Creates a new LogicGrid instance.
|
|
14
|
+
*
|
|
15
|
+
* @param categories - The configuration of categories and their values for the puzzle.
|
|
16
|
+
* @throws {Error} If the configuration is invalid (duplicate IDs, duplicate values, or mismatched value counts).
|
|
17
|
+
*/
|
|
18
|
+
constructor(categories) {
|
|
19
|
+
this._validateCategories(categories);
|
|
20
|
+
this.categories = categories;
|
|
21
|
+
this.valueMap = new Map(categories.map(c => [c.id, new Map(c.values.map((v, i) => [v, i]))]));
|
|
22
|
+
this.grid = new Map();
|
|
23
|
+
for (const cat1 of categories) {
|
|
24
|
+
const cat1Map = new Map();
|
|
25
|
+
for (const val1 of cat1.values) {
|
|
26
|
+
const cat2Map = new Map();
|
|
27
|
+
for (const cat2 of categories) {
|
|
28
|
+
if (cat1.id === cat2.id)
|
|
29
|
+
continue;
|
|
30
|
+
cat2Map.set(cat2.id, Array(cat2.values.length).fill(true));
|
|
31
|
+
}
|
|
32
|
+
cat1Map.set(val1, cat2Map);
|
|
33
|
+
}
|
|
34
|
+
this.grid.set(cat1.id, cat1Map);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
_validateCategories(categories) {
|
|
38
|
+
if (categories.length === 0) {
|
|
39
|
+
return; // Or throw an error, for now we allow it.
|
|
40
|
+
}
|
|
41
|
+
const firstCategoryValueCount = categories[0].values.length;
|
|
42
|
+
const categoryIds = new Set();
|
|
43
|
+
for (const category of categories) {
|
|
44
|
+
if (categoryIds.has(category.id)) {
|
|
45
|
+
throw new Error(`Invalid configuration: Duplicate category ID found: "${category.id}".`);
|
|
46
|
+
}
|
|
47
|
+
categoryIds.add(category.id);
|
|
48
|
+
if (category.values.length !== firstCategoryValueCount) {
|
|
49
|
+
throw new Error(`Invalid configuration: All categories must have the same number of values. Category "${category.id}" has ${category.values.length} values, but category "${categories[0].id}" has ${firstCategoryValueCount}.`);
|
|
50
|
+
}
|
|
51
|
+
const categoryValues = new Set();
|
|
52
|
+
for (const value of category.values) {
|
|
53
|
+
if (categoryValues.has(value)) {
|
|
54
|
+
throw new Error(`Invalid configuration: Duplicate value "${value}" found in category "${category.id}".`);
|
|
55
|
+
}
|
|
56
|
+
categoryValues.add(value);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Sets the possibility state between two values from different categories.
|
|
62
|
+
*
|
|
63
|
+
* @param cat1Id - The ID of the first category.
|
|
64
|
+
* @param val1 - The value from the first category.
|
|
65
|
+
* @param cat2Id - The ID of the second category.
|
|
66
|
+
* @param val2 - The value from the second category.
|
|
67
|
+
* @param state - true if the connection is possible, false if eliminated.
|
|
68
|
+
*/
|
|
69
|
+
setPossibility(cat1Id, val1, cat2Id, val2, state) {
|
|
70
|
+
const val2Index = this.valueMap.get(cat2Id)?.get(val2);
|
|
71
|
+
if (val2Index !== undefined) {
|
|
72
|
+
const cat1Map = this.grid.get(cat1Id);
|
|
73
|
+
if (cat1Map) {
|
|
74
|
+
const val1Map = cat1Map.get(val1);
|
|
75
|
+
if (val1Map) {
|
|
76
|
+
const cat2Arr = val1Map.get(cat2Id);
|
|
77
|
+
if (cat2Arr) {
|
|
78
|
+
cat2Arr[val2Index] = state;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
const val1Index = this.valueMap.get(cat1Id)?.get(val1);
|
|
84
|
+
if (val1Index !== undefined) {
|
|
85
|
+
const cat2Map = this.grid.get(cat2Id);
|
|
86
|
+
if (cat2Map) {
|
|
87
|
+
const val2Map = cat2Map.get(val2);
|
|
88
|
+
if (val2Map) {
|
|
89
|
+
const cat1Arr = val2Map.get(cat1Id);
|
|
90
|
+
if (cat1Arr) {
|
|
91
|
+
cat1Arr[val1Index] = state;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Checks if a connection between two values is currently possible.
|
|
99
|
+
*
|
|
100
|
+
* @param cat1Id - The ID of the first category.
|
|
101
|
+
* @param val1 - The value from the first category.
|
|
102
|
+
* @param cat2Id - The ID of the second category.
|
|
103
|
+
* @param val2 - The value from the second category.
|
|
104
|
+
* @returns true if the connection is possible, false otherwise.
|
|
105
|
+
*/
|
|
106
|
+
isPossible(cat1Id, val1, cat2Id, val2) {
|
|
107
|
+
const val2Index = this.valueMap.get(cat2Id)?.get(val2);
|
|
108
|
+
if (val2Index === undefined)
|
|
109
|
+
return false;
|
|
110
|
+
const cat1Grid = this.grid.get(cat1Id);
|
|
111
|
+
if (!cat1Grid)
|
|
112
|
+
return false;
|
|
113
|
+
const val1Grid = cat1Grid.get(val1);
|
|
114
|
+
if (!val1Grid)
|
|
115
|
+
return false;
|
|
116
|
+
const cat2Arr = val1Grid.get(cat2Id);
|
|
117
|
+
if (!cat2Arr)
|
|
118
|
+
return false;
|
|
119
|
+
return cat2Arr[val2Index];
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Gets the number of possible connections for a specific value in one category
|
|
123
|
+
* relative to another category.
|
|
124
|
+
*
|
|
125
|
+
* @param cat1Id - The ID of the starting category.
|
|
126
|
+
* @param val1 - The value from the starting category.
|
|
127
|
+
* @param cat2Id - The target category ID.
|
|
128
|
+
* @returns The number of values in cat2 that are still possible for val1.
|
|
129
|
+
*/
|
|
130
|
+
getPossibilitiesCount(cat1Id, val1, cat2Id) {
|
|
131
|
+
const possibilities = this.grid.get(cat1Id)?.get(val1)?.get(cat2Id);
|
|
132
|
+
if (!possibilities)
|
|
133
|
+
return 0;
|
|
134
|
+
return possibilities.filter(p => p).length;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Calculates statistics about the current state of the grid.
|
|
138
|
+
*
|
|
139
|
+
* @returns An object containing:
|
|
140
|
+
* - totalPossible: The initial total logical connections.
|
|
141
|
+
* - currentPossible: The number of remaining possible connections.
|
|
142
|
+
* - solutionPossible: The target number of connections for a solved grid.
|
|
143
|
+
*/
|
|
144
|
+
getGridStats() {
|
|
145
|
+
let currentPossible = 0;
|
|
146
|
+
const catCount = this.categories.length;
|
|
147
|
+
const valCount = this.categories[0]?.values.length || 0;
|
|
148
|
+
if (valCount === 0 || catCount < 2) {
|
|
149
|
+
return { totalPossible: 0, currentPossible: 0, solutionPossible: 0 };
|
|
150
|
+
}
|
|
151
|
+
const numPairs = catCount * (catCount - 1) / 2;
|
|
152
|
+
const totalPossible = numPairs * valCount * valCount;
|
|
153
|
+
const solutionPossible = numPairs * valCount;
|
|
154
|
+
for (const cat1 of this.categories) {
|
|
155
|
+
for (const val1 of cat1.values) {
|
|
156
|
+
for (const cat2 of this.categories) {
|
|
157
|
+
if (cat1.id >= cat2.id)
|
|
158
|
+
continue;
|
|
159
|
+
const possibilities = this.grid.get(cat1.id)?.get(val1)?.get(cat2.id);
|
|
160
|
+
if (possibilities) {
|
|
161
|
+
currentPossible += possibilities.filter(p => p).length;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return { totalPossible, currentPossible, solutionPossible };
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Creates a deep copy of the current LogicGrid.
|
|
170
|
+
*
|
|
171
|
+
* @returns A new LogicGrid instance with the exact same state.
|
|
172
|
+
*/
|
|
173
|
+
clone() {
|
|
174
|
+
const newGrid = new LogicGrid(this.categories);
|
|
175
|
+
newGrid.grid = new Map([...this.grid.entries()].map(([cat1Id, cat1Map]) => [
|
|
176
|
+
cat1Id,
|
|
177
|
+
new Map([...cat1Map.entries()].map(([val1, val1Map]) => [
|
|
178
|
+
val1,
|
|
179
|
+
new Map([...val1Map.entries()].map(([cat2Id, cat2Arr]) => [
|
|
180
|
+
cat2Id,
|
|
181
|
+
[...cat2Arr],
|
|
182
|
+
])),
|
|
183
|
+
])),
|
|
184
|
+
]));
|
|
185
|
+
return newGrid;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
exports.LogicGrid = LogicGrid;
|