logic-puzzle-generator 1.0.0 → 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/dist/Clue.d.ts DELETED
@@ -1,81 +0,0 @@
1
- import { ValueLabel } from './types';
2
- /**
3
- * Enumeration of all supported clue types.
4
- */
5
- export declare enum ClueType {
6
- /** Expresses a direct relationship (IS or IS NOT) between two values. */
7
- BINARY = 0,
8
- /** Expresses a comparison (GREATER THAN or LESS THAN) between two values based on an ordinal category. */
9
- ORDINAL = 1,
10
- /** Expresses an extreme value relationship (MIN or MAX) within an ordinal category. */
11
- SUPERLATIVE = 2,
12
- /** Expresses a property of a single value (e.g., IS EVEN) relative to an ordinal category. */
13
- UNARY = 3
14
- }
15
- export declare enum BinaryOperator {
16
- IS = 0,
17
- IS_NOT = 1
18
- }
19
- export declare enum OrdinalOperator {
20
- GREATER_THAN = 0,
21
- LESS_THAN = 1
22
- }
23
- export declare enum SuperlativeOperator {
24
- MIN = 0,
25
- MAX = 1
26
- }
27
- export declare enum UnaryFilter {
28
- IS_ODD = 0,
29
- IS_EVEN = 1
30
- }
31
- /**
32
- * A clue that establishes a direct link or separation between two specific values.
33
- * Example: "Alice likes Horror movies." (IS)
34
- * Example: "Bob does not like Comedy." (IS_NOT)
35
- */
36
- export interface BinaryClue {
37
- type: ClueType.BINARY;
38
- operator: BinaryOperator;
39
- cat1: string;
40
- val1: ValueLabel;
41
- cat2: string;
42
- val2: ValueLabel;
43
- }
44
- /**
45
- * A clue that compares two entities based on a third ordinal category.
46
- * Example: "The person who likes Horror is older than Alice."
47
- */
48
- export interface OrdinalClue {
49
- type: ClueType.ORDINAL;
50
- operator: OrdinalOperator;
51
- item1Cat: string;
52
- item1Val: ValueLabel;
53
- item2Cat: string;
54
- item2Val: ValueLabel;
55
- /** The ordinal category used for comparison (e.g., "Age"). */
56
- ordinalCat: string;
57
- }
58
- /**
59
- * A clue that identifies an entity as having an extreme value in an ordinal category.
60
- * Example: "The person who likes Popcorn is the oldest."
61
- */
62
- export interface SuperlativeClue {
63
- type: ClueType.SUPERLATIVE;
64
- operator: SuperlativeOperator;
65
- targetCat: string;
66
- targetVal: ValueLabel;
67
- /** The ordinal category (e.g., "Age") where the value is extreme. */
68
- ordinalCat: string;
69
- }
70
- /**
71
- * A clue that filters a value based on a property of its associated ordinal value.
72
- * Example: "The person who likes Chips has an even age."
73
- */
74
- export interface UnaryClue {
75
- type: ClueType.UNARY;
76
- filter: UnaryFilter;
77
- targetCat: string;
78
- targetVal: ValueLabel;
79
- ordinalCat: string;
80
- }
81
- export type Clue = BinaryClue | OrdinalClue | SuperlativeClue | UnaryClue;
package/dist/Clue.js DELETED
@@ -1,37 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.UnaryFilter = exports.SuperlativeOperator = exports.OrdinalOperator = exports.BinaryOperator = exports.ClueType = void 0;
4
- /**
5
- * Enumeration of all supported clue types.
6
- */
7
- var ClueType;
8
- (function (ClueType) {
9
- /** Expresses a direct relationship (IS or IS NOT) between two values. */
10
- ClueType[ClueType["BINARY"] = 0] = "BINARY";
11
- /** Expresses a comparison (GREATER THAN or LESS THAN) between two values based on an ordinal category. */
12
- ClueType[ClueType["ORDINAL"] = 1] = "ORDINAL";
13
- /** Expresses an extreme value relationship (MIN or MAX) within an ordinal category. */
14
- ClueType[ClueType["SUPERLATIVE"] = 2] = "SUPERLATIVE";
15
- /** Expresses a property of a single value (e.g., IS EVEN) relative to an ordinal category. */
16
- ClueType[ClueType["UNARY"] = 3] = "UNARY";
17
- })(ClueType || (exports.ClueType = ClueType = {}));
18
- var BinaryOperator;
19
- (function (BinaryOperator) {
20
- BinaryOperator[BinaryOperator["IS"] = 0] = "IS";
21
- BinaryOperator[BinaryOperator["IS_NOT"] = 1] = "IS_NOT";
22
- })(BinaryOperator || (exports.BinaryOperator = BinaryOperator = {}));
23
- var OrdinalOperator;
24
- (function (OrdinalOperator) {
25
- OrdinalOperator[OrdinalOperator["GREATER_THAN"] = 0] = "GREATER_THAN";
26
- OrdinalOperator[OrdinalOperator["LESS_THAN"] = 1] = "LESS_THAN";
27
- })(OrdinalOperator || (exports.OrdinalOperator = OrdinalOperator = {}));
28
- var SuperlativeOperator;
29
- (function (SuperlativeOperator) {
30
- SuperlativeOperator[SuperlativeOperator["MIN"] = 0] = "MIN";
31
- SuperlativeOperator[SuperlativeOperator["MAX"] = 1] = "MAX";
32
- })(SuperlativeOperator || (exports.SuperlativeOperator = SuperlativeOperator = {}));
33
- var UnaryFilter;
34
- (function (UnaryFilter) {
35
- UnaryFilter[UnaryFilter["IS_ODD"] = 0] = "IS_ODD";
36
- UnaryFilter[UnaryFilter["IS_EVEN"] = 1] = "IS_EVEN";
37
- })(UnaryFilter || (exports.UnaryFilter = UnaryFilter = {}));
@@ -1,58 +0,0 @@
1
- import { CategoryConfig, Solution, TargetFact } from './types';
2
- import { Clue } from './Clue';
3
- /**
4
- * Represents a single step in the logical deduction path.
5
- */
6
- export interface ProofStep {
7
- /** The clue applied at this step. */
8
- clue: Clue;
9
- /** The number of logical eliminations that resulted immediately from this clue. */
10
- deductions: number;
11
- }
12
- /**
13
- * The complete result of the puzzle generation process.
14
- */
15
- export interface Puzzle {
16
- /** The solution grid (Category -> Value -> Corresponding Value). */
17
- solution: Solution;
18
- /** The list of clues needed to solve the puzzle, in no particular order. */
19
- clues: Clue[];
20
- /** An ordered list of clues that demonstrates a step-by-step logical solution. */
21
- proofChain: ProofStep[];
22
- /** The configuration used to generate this puzzle. */
23
- categories: CategoryConfig[];
24
- /** The specific fact that the puzzle is designed to reveal at the end. */
25
- targetFact: TargetFact;
26
- }
27
- /**
28
- * The main class responsible for generating logic puzzles.
29
- *
30
- * It handles the creation of a consistent solution, the generation of all possible clues,
31
- * and the selection of an optimal set of clues to form a solvable puzzle with a specific target.
32
- */
33
- export declare class Generator {
34
- private seed;
35
- private random;
36
- private solver;
37
- private solution;
38
- private valueMap;
39
- private reverseSolution;
40
- /**
41
- * Creates a new Generator instance.
42
- *
43
- * @param seed - A numeric seed for the random number generator to ensure reproducibility.
44
- */
45
- constructor(seed: number);
46
- /**
47
- * Generates a fully solvable logic puzzle based on the provided configuration.
48
- *
49
- * @param categories - The categories and values to include in the puzzle.
50
- * @param target - The specific fact that should be the final deduction of the puzzle.
51
- * @returns A complete Puzzle object containing the solution, clues, and proof chain.
52
- */
53
- generatePuzzle(categories: CategoryConfig[], target: TargetFact): Puzzle;
54
- private createSolution;
55
- private generateAllPossibleClues;
56
- private calculateScore;
57
- private isPuzzleSolved;
58
- }
package/dist/Generator.js DELETED
@@ -1,433 +0,0 @@
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;
@@ -1,70 +0,0 @@
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
- }