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.
Files changed (47) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +221 -0
  3. package/dist/Clue.d.ts +81 -0
  4. package/dist/Clue.js +37 -0
  5. package/dist/Generator.d.ts +58 -0
  6. package/dist/Generator.js +433 -0
  7. package/dist/LogicGrid.d.ts +70 -0
  8. package/dist/LogicGrid.js +188 -0
  9. package/dist/Solver.d.ts +29 -0
  10. package/dist/Solver.js +242 -0
  11. package/dist/examples/benchmark.d.ts +1 -0
  12. package/dist/examples/benchmark.js +129 -0
  13. package/dist/examples/cli.d.ts +1 -0
  14. package/dist/examples/cli.js +84 -0
  15. package/dist/index.d.ts +5 -0
  16. package/dist/index.js +21 -0
  17. package/dist/run_generator.d.ts +1 -0
  18. package/dist/run_generator.js +84 -0
  19. package/dist/src/defaults.d.ts +5 -0
  20. package/dist/src/defaults.js +24 -0
  21. package/dist/src/engine/BoundsCalculator.d.ts +6 -0
  22. package/dist/src/engine/BoundsCalculator.js +40 -0
  23. package/dist/src/engine/Clue.d.ts +75 -0
  24. package/dist/src/engine/Clue.js +10 -0
  25. package/dist/src/engine/DifficultyBounds.d.ts +12 -0
  26. package/dist/src/engine/DifficultyBounds.js +67 -0
  27. package/dist/src/engine/GenerativeSession.d.ts +32 -0
  28. package/dist/src/engine/GenerativeSession.js +109 -0
  29. package/dist/src/engine/Generator.d.ts +119 -0
  30. package/dist/src/engine/Generator.js +1058 -0
  31. package/dist/src/engine/LogicGrid.d.ts +70 -0
  32. package/dist/src/engine/LogicGrid.js +190 -0
  33. package/dist/src/engine/Solver.d.ts +30 -0
  34. package/dist/src/engine/Solver.js +613 -0
  35. package/dist/src/errors.d.ts +12 -0
  36. package/dist/src/errors.js +23 -0
  37. package/dist/src/index.d.ts +8 -0
  38. package/dist/src/index.js +24 -0
  39. package/dist/src/scripts/GenerateBoundsDeprecated.d.ts +1 -0
  40. package/dist/src/scripts/GenerateBoundsDeprecated.js +27 -0
  41. package/dist/src/scripts/StressTestBacktracking.d.ts +1 -0
  42. package/dist/src/scripts/StressTestBacktracking.js +49 -0
  43. package/dist/src/types.d.ts +86 -0
  44. package/dist/src/types.js +58 -0
  45. package/dist/types.d.ts +37 -0
  46. package/dist/types.js +13 -0
  47. package/package.json +40 -0
@@ -0,0 +1,613 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Solver = void 0;
4
+ const types_1 = require("../types");
5
+ /**
6
+ * The logical engine responsible for applying clues to a LogicGrid and deducing consequences.
7
+ *
8
+ * It implements various deduction strategies including basic elimination, uniqueness checks,
9
+ * and transitive logic logic (if A=B and B=C, then A=C).
10
+ */
11
+ class Solver {
12
+ /**
13
+ * Applies a single clue to the grid and propagates logical deductions.
14
+ *
15
+ * This method runs a loop that explicitly applies the clue and then repeatedly
16
+ * triggers the internal deduction engine until no further eliminations can be made.
17
+ *
18
+ * @param grid - The LogicGrid to modify.
19
+ * @param clue - The Clue to apply.
20
+ * @returns An object containing the modified grid and the total count of eliminations made.
21
+ */
22
+ applyClue(grid, clue) {
23
+ let deductions = 0;
24
+ switch (clue.type) {
25
+ case types_1.ClueType.BINARY:
26
+ deductions += this.applyBinaryClue(grid, clue);
27
+ break;
28
+ case types_1.ClueType.SUPERLATIVE:
29
+ deductions += this.applySuperlativeClue(grid, clue);
30
+ break;
31
+ case types_1.ClueType.ORDINAL:
32
+ deductions += this.applyOrdinalClue(grid, clue);
33
+ break;
34
+ case types_1.ClueType.UNARY:
35
+ deductions += this.applyUnaryClue(grid, clue);
36
+ break;
37
+ case types_1.ClueType.CROSS_ORDINAL:
38
+ deductions += this.applyCrossOrdinalClue(grid, clue); // Cast as any because import might lag or circular deps? no, just strict TS.
39
+ break;
40
+ }
41
+ let newDeductions;
42
+ do {
43
+ newDeductions = this.runDeductionLoop(grid);
44
+ deductions += newDeductions;
45
+ } while (newDeductions > 0);
46
+ return { grid, deductions };
47
+ }
48
+ applyCrossOrdinalClue(grid, clue) {
49
+ let deductions = 0;
50
+ const categories = grid.categories;
51
+ const ord1Config = categories.find(c => c.id === clue.ordinal1);
52
+ const ord2Config = categories.find(c => c.id === clue.ordinal2);
53
+ if (!ord1Config || !ord2Config)
54
+ return 0;
55
+ const isNot = clue.operator === types_1.CrossOrdinalOperator.NOT_MATCH;
56
+ // Helper to get possible indices for an item in its ordinal category
57
+ const getPossibleIndices = (itemCat, itemVal, ordCat, ordConfig) => {
58
+ return ordConfig.values
59
+ .map((v, i) => ({ val: v, idx: i }))
60
+ .filter(v => grid.isPossible(itemCat, itemVal, ordCat, v.val));
61
+ };
62
+ const eligible1 = getPossibleIndices(clue.item1Cat, clue.item1Val, clue.ordinal1, ord1Config);
63
+ const eligible2 = getPossibleIndices(clue.item2Cat, clue.item2Val, clue.ordinal2, ord2Config);
64
+ if (!isNot) {
65
+ // --- POSITIVE LOGIC (MATCH) ---
66
+ // Filter 1 based on 2
67
+ for (const cand1 of eligible1) {
68
+ const targetIdx1 = cand1.idx + clue.offset1;
69
+ const targetVal1 = ord1Config.values[targetIdx1];
70
+ // Bounds check
71
+ if (targetVal1 === undefined) {
72
+ grid.setPossibility(clue.item1Cat, clue.item1Val, clue.ordinal1, cand1.val, false);
73
+ deductions++;
74
+ continue;
75
+ }
76
+ // Compatibility check
77
+ let supported = false;
78
+ for (const cand2 of eligible2) {
79
+ const targetIdx2 = cand2.idx + clue.offset2;
80
+ const targetVal2 = ord2Config.values[targetIdx2];
81
+ if (targetVal2 !== undefined) {
82
+ // Check if (Ord1=TargetVal1) is compatible with (Ord2=TargetVal2)
83
+ // i.e., can they be the same entity?
84
+ if (grid.isPossible(clue.ordinal1, targetVal1, clue.ordinal2, targetVal2)) {
85
+ supported = true;
86
+ break;
87
+ }
88
+ }
89
+ }
90
+ if (!supported) {
91
+ grid.setPossibility(clue.item1Cat, clue.item1Val, clue.ordinal1, cand1.val, false);
92
+ deductions++;
93
+ }
94
+ }
95
+ // Filter 2 based on 1 (Symmetric)
96
+ for (const cand2 of eligible2) {
97
+ const targetIdx2 = cand2.idx + clue.offset2;
98
+ const targetVal2 = ord2Config.values[targetIdx2];
99
+ if (targetVal2 === undefined) {
100
+ grid.setPossibility(clue.item2Cat, clue.item2Val, clue.ordinal2, cand2.val, false);
101
+ deductions++;
102
+ continue;
103
+ }
104
+ let supported = false;
105
+ for (const cand1 of eligible1) {
106
+ const targetIdx1 = cand1.idx + clue.offset1;
107
+ const targetVal1 = ord1Config.values[targetIdx1];
108
+ if (targetVal1 !== undefined) {
109
+ if (grid.isPossible(clue.ordinal1, targetVal1, clue.ordinal2, targetVal2)) {
110
+ supported = true;
111
+ break;
112
+ }
113
+ }
114
+ }
115
+ if (!supported) {
116
+ grid.setPossibility(clue.item2Cat, clue.item2Val, clue.ordinal2, cand2.val, false);
117
+ deductions++;
118
+ }
119
+ }
120
+ // Lock linkage if unique
121
+ // We re-query indices as they might have been reduced above
122
+ const finalEligible1 = getPossibleIndices(clue.item1Cat, clue.item1Val, clue.ordinal1, ord1Config);
123
+ const finalEligible2 = getPossibleIndices(clue.item2Cat, clue.item2Val, clue.ordinal2, ord2Config);
124
+ if (finalEligible1.length === 1 && finalEligible2.length === 1) {
125
+ const t1 = finalEligible1[0].idx + clue.offset1;
126
+ const t2 = finalEligible2[0].idx + clue.offset2;
127
+ const v1 = ord1Config.values[t1];
128
+ const v2 = ord2Config.values[t2];
129
+ if (v1 !== undefined && v2 !== undefined) {
130
+ if (grid.getPossibilitiesCount(clue.ordinal1, v1, clue.ordinal2) > 1) {
131
+ grid.setPossibility(clue.ordinal1, v1, clue.ordinal2, v2, true);
132
+ deductions++;
133
+ }
134
+ }
135
+ }
136
+ }
137
+ else {
138
+ // --- NEGATIVE LOGIC (NOT_MATCH) ---
139
+ // "The entity at (Pos1 + Off1) in Ord1 is NOT the entity at (Pos2 + Off2) in Ord2"
140
+ // If we have determined the specific Ordinal Values for both sides, we can eliminate their link.
141
+ // Re-query eligible indices (optimization: use passed in ones? No, better safe)
142
+ // (Using local consts `eligible1` etc is fine as we haven't mutated grid yet in this block)
143
+ // Can we eliminate a candidate for Item1?
144
+ // If putting Item1 at `cand1` FORCES the derived Ord1 Value to be `V1`
145
+ // AND
146
+ // The derived Ord2 Value `V2` (from Item2) is DEFINITELY determined...
147
+ // AND `V1` MUST be `V2` (i.e. they are the same entity)...
148
+ // THEN we force a contradiction?
149
+ // Simpler: If we know the derived values V1 and V2, we say V1 IS_NOT V2.
150
+ // Case 1: Both anchors fixed relative to their ordinals.
151
+ // If Item1 can ONLY be at `idx1` (so derived `targetVal1` is fixed)
152
+ // AND Item2 can ONLY be at `idx2` (so derived `targetVal2` is fixed)
153
+ // THEN `targetVal1` IS NOT `targetVal2`.
154
+ if (eligible1.length === 1 && eligible2.length === 1) {
155
+ const t1 = eligible1[0].idx + clue.offset1;
156
+ const t2 = eligible2[0].idx + clue.offset2;
157
+ const v1 = ord1Config.values[t1];
158
+ const v2 = ord2Config.values[t2];
159
+ if (v1 !== undefined && v2 !== undefined) {
160
+ if (grid.isPossible(clue.ordinal1, v1, clue.ordinal2, v2)) {
161
+ grid.setPossibility(clue.ordinal1, v1, clue.ordinal2, v2, false);
162
+ deductions++;
163
+ }
164
+ }
165
+ }
166
+ // Is that it? What about partial information?
167
+ // If `targetVal1` is fixed, and it IS connected to `targetVal2`,
168
+ // then we know Item2 CANNOT be at any position that yields `targetVal2`.
169
+ // Iterate all candidates for Item1
170
+ for (const cand1 of eligible1) {
171
+ const targetIdx1 = cand1.idx + clue.offset1;
172
+ const targetVal1 = ord1Config.values[targetIdx1];
173
+ if (targetVal1 === undefined)
174
+ continue; // Out of bounds, invalid path (should have been eliminated elsewhere? Or maybe just this path is invalid)
175
+ // If `targetVal1` is strictly linked to some `targetVal2`...
176
+ // (We need to check possibilities between Ord1 and Ord2)
177
+ // Check against all candidates for Item2
178
+ for (const cand2 of eligible2) {
179
+ const targetIdx2 = cand2.idx + clue.offset2;
180
+ const targetVal2 = ord2Config.values[targetIdx2];
181
+ if (targetVal2 === undefined)
182
+ continue;
183
+ // If we hypothetically choose both candidates:
184
+ // We assert that (Ord1=v1) IS NOT (Ord2=v2).
185
+ // If the grid says (Ord1=v1) MUST BE (Ord2=v2), then this combination is invalid.
186
+ // "MUST BE" means isPossible is true AND it's the ONLY possibility?
187
+ // Or if (clue.ordinal1, v1, clue.ordinal2, v2) is the only link?
188
+ // Actually, if grid.isPossible(Ord1, v1, Ord2, v2) is FALSE, then they are already not the same.
189
+ // If it is TRUE, they MIGHT be the same.
190
+ // The clue says they are NOT the same.
191
+ // Wait. "A is NOT B".
192
+ // Means the cell (A, B) is FALSE.
193
+ // Here A is (Ord1, v1) and B is (Ord2, v2).
194
+ // So we effectively add a "IS_NOT" boolean constraint between v1 and v2.
195
+ // BUT we only know this constraint applies IF Item1 is at cand1 and Item2 is at cand2.
196
+ // So we can't write to the grid unless we are sure about location?
197
+ // UNLESS:
198
+ // If Item1 IS defined (eligible1.length === 1).
199
+ // Then for every candidate of Item2 (cand2 -> v2),
200
+ // We know v1 IS_NOT v2.
201
+ // So we can eliminate v2 from being equal to v1.
202
+ // i.e., setPossibility(Ord1, v1, Ord2, v2, false)?
203
+ // YES.
204
+ }
205
+ }
206
+ // Refined Negative Logic:
207
+ // If Item1's derived ordinal value is fixed to V1:
208
+ // Then Item2's derived ordinal value CANNOT be any V2 that is "Equal" to V1.
209
+ // (Where "Equal" means the cell (V1, V2) is true/possible).
210
+ // Actually, "Equal" means referring to the same entity.
211
+ // If we force V1 != V2, we are setting (V1, V2) to false.
212
+ if (eligible1.length === 1) {
213
+ const targetIdx1 = eligible1[0].idx + clue.offset1;
214
+ const v1 = ord1Config.values[targetIdx1];
215
+ if (v1 !== undefined) {
216
+ // For every candidate of Item2
217
+ for (const cand2 of eligible2) {
218
+ const targetIdx2 = cand2.idx + clue.offset2;
219
+ const v2 = ord2Config.values[targetIdx2];
220
+ if (v2 !== undefined) {
221
+ // Link (v1, v2) is FORBIDDEN by this clue.
222
+ // If this link is the ONLY connection between v1 and v2?
223
+ // Wait. The clue says: The entity at V1 is NOT the entity at V2.
224
+ // So grid.setPossibility(Ord1, v1, Ord2, v2, false).
225
+ // BUT we can only do this if we are SURE Item2 uses v2?
226
+ // No. If Item2 uses v2, then V1!=V2.
227
+ // If V1 MUST be V2 (i.e. grid says so), then Item2 CANNOT uses V2.
228
+ // Logic:
229
+ // We know EntityA = Entity(Ord1, V1).
230
+ // We know EntityB = Entity(Ord2, derived from Item2).
231
+ // Clue: EntityA != EntityB.
232
+ // If EntityA is fixed (V1).
233
+ // For a candidate valid for Item2 yielding V2:
234
+ // If grid says Entity(Ord1,V1) === Entity(Ord2,V2) (Possible relationship),
235
+ // WE are adding the constraint that EntityA != EntityB.
236
+ // So if Item2 picks V2, then EntityB is Entity(Ord2,V2).
237
+ // Is Entity(Ord2,V2) same as Entity(Ord1,V1)?
238
+ // If they are physically the same slot in the "Person" hidden category?
239
+ // Our grid models pairwise relationships.
240
+ // isPossible(Ord1, V1, Ord2, V2) means "Can V1 and V2 belong to the same person?".
241
+ // The clue says "Person A (V1) is NOT Person B (derived V2)".
242
+ // So:
243
+ // If Item2=cand2 (implying Person B has V2),
244
+ // AND Person A has V1.
245
+ // AND the clue says Person A != Person B.
246
+ // DO V1 and V2 refer to the same person?
247
+ // If `isPossible(V1, V2)` is true, they MIGHT.
248
+ // Providing the clue says "They are different people",
249
+ // does that mean V1 and V2 must be incompatible?
250
+ // NO. "Person A is NOT Person B".
251
+ // It allows Person A to have Age 20 and Person B to have Height 180 (even if Age 20 usually goes with Height 180? NO).
252
+ // If Age 20 goes with Height 180, then that's ONE person.
253
+ // If Person A is Age 20, and Person B is Height 180.
254
+ // And clue says Person A != Person B.
255
+ // Then (since Age 20 <-> Height 180 defines ONE person), this state is impossible?
256
+ // YES.
257
+ // Therefore, if V1 and V2 correspond to the SAME person (are 'linked'), then Person A and Person B would be the same.
258
+ // Since they are NOT the same, then if V1 is linked to V2, we have a contradiction for this candidate.
259
+ // OR rather: One of the assumptions is wrong.
260
+ // So:
261
+ // If (V1, V2) represents the SAME entity (i.e. they are fully linked, or just possibly linked?),
262
+ // If they are possibly linked, they COULD be the same person.
263
+ // If the clue says they are DIFFERENT people,
264
+ // Then we cannot have (Item2 at cand2) AND (Item1 at cand1) both be true IF V1 and V2 are "the same person".
265
+ // But how do we know if V1 and V2 are the same person?
266
+ // In our model, V1 and V2 are "Same" if the cell (V1, V2) is TRUE.
267
+ // Actually, (V1, V2)=TRUE means they exist together on one entity.
268
+ // If (V1, V2)=FALSE, they cannot be on one entity.
269
+ // So if Clue says: StartEntity != EndEntity.
270
+ // And we select V1 for Start and V2 for End.
271
+ // If (V1, V2) is TRUE, then V1 and V2 coexist on one entity.
272
+ // Does that mean StartEntity == EndEntity?
273
+ // YES. Because V1 and V2 belong to the "same row".
274
+ // So "StartEntity != EndEntity" implies that we cannot pick V1 and V2 such that they coexist.
275
+ // i.e. We generally expect V1 and V2 to NOT coexist?
276
+ // Wait.
277
+ // If I say "The person who is 20 is NOT the person who eats Cake".
278
+ // Values: 20 (Age), Cake (Snack).
279
+ // If (20, Cake) is TRUE (Possible), then there is a person who is 20 and eats Cake.
280
+ // The clue says: Person(20) != Person(Cake).
281
+ // This means the Person(20) CANNOT be the Person(Cake).
282
+ // So the person who is 20 must NOT eat Cake.
283
+ // So (20, Cake) must be FALSE.
284
+ // CORRECT.
285
+ // "Person A != Person B" translates to "Properties of A are not Properties of B"?
286
+ // Specifically, if A is defined by Property P1, and B by P2.
287
+ // Then P1 is incompatible with P2.
288
+ // So `isPossible(P1, P2)` must be set to `false`.
289
+ // So, back to the implementation:
290
+ // IF we know P1 (Item1's derived val) and P2 (Item2's derived val),
291
+ // THEN `grid.setPossibility(P1, P2, false)`.
292
+ // If we DON'T know P1 uniquely?
293
+ // If P1 could be {A, B} and P2 is {C}.
294
+ // Then (A, C) is false AND (B, C) is false.
295
+ // i.e. ALL potential pairings of P1 and P2 are false.
296
+ // So for every pair of candidates:
297
+ // If Item1=cand1 implies Val1, and Item2=cand2 implies Val2.
298
+ // Then (Val1, Val2) must be FALSE.
299
+ // BUT we only enforce this if Item1=cand1 AND Item2=cand2 are actually true.
300
+ // We don't know they are true.
301
+ // However, if for ALL candidates of Item1, the derived Val1 is the SAME (fixed),
302
+ // Then we know P1 is fixed.
303
+ // Then for ALL candidates of Item2, the derived Val2 must be incompatible with P1.
304
+ // So `setPossibility(Ord1, fixedV1, Ord2, candVal2, false)`.
305
+ // Wait, if Item2 has multiple candidates, we can eliminate the ones that are compatible with P1?
306
+ // Yes.
307
+ // If Item2=candX implies ValX, and (ValX is compatible with P1), then Item2 CANNOT be candX.
308
+ // Because if Item2=candX, Person B has ValX. Person A has P1.
309
+ // If ValX and P1 are compatible, they COULD be the same person.
310
+ // Wait. "Person A != Person B".
311
+ // If ValX and P1 are compatible (i.e. theoretically same person allowed),
312
+ // but we are imposing they are DIFFERENT.
313
+ // Does that mean ValX cannot be P1?
314
+ // If ValX and P1 are just values in different categories...
315
+ // If they are compatible, they describe the same entity.
316
+ // If the clue says "Entity A != Entity B", can Entity A have ValX and Entity B have P1?
317
+ // Only if ValX and P1 map to DIFFERENT entities?
318
+ // But (ValX, P1) = TRUE means they map to SAME entity.
319
+ // So "Entity A != Entity B" means we cannot have a situation where Entity A has P1 and Entity B has ValX IF (P1, ValX) implies Same Entity (which it does in our grid).
320
+ // So YES. (P1, ValX) must be FALSE.
321
+ // THEREFORE:
322
+ // If P1 is fixed.
323
+ // For each candidate cand2 of Item2 (yielding Val2):
324
+ // We enforce (P1, Val2) = FALSE.
325
+ // If (P1, Val2) WAS currently TRUE (possible), then this candidate forces a contradiction with the Clue.
326
+ // So Item2 CANNOT be cand2.
327
+ // Eliminate cand2.
328
+ if (grid.isPossible(clue.ordinal1, v1, clue.ordinal2, v2)) {
329
+ grid.setPossibility(clue.item2Cat, clue.item2Val, clue.ordinal2, cand2.val, false);
330
+ deductions++;
331
+ }
332
+ }
333
+ }
334
+ }
335
+ }
336
+ // Symmetric for Eligible2 fixed
337
+ if (eligible2.length === 1) {
338
+ const targetIdx2 = eligible2[0].idx + clue.offset2;
339
+ const v2 = ord2Config.values[targetIdx2];
340
+ if (v2 !== undefined) {
341
+ for (const cand1 of eligible1) {
342
+ const targetIdx1 = cand1.idx + clue.offset1;
343
+ const v1 = ord1Config.values[targetIdx1];
344
+ if (v1 !== undefined) {
345
+ if (grid.isPossible(clue.ordinal1, v1, clue.ordinal2, v2)) {
346
+ grid.setPossibility(clue.item1Cat, clue.item1Val, clue.ordinal1, cand1.val, false);
347
+ deductions++;
348
+ }
349
+ }
350
+ }
351
+ }
352
+ }
353
+ }
354
+ return deductions;
355
+ }
356
+ applyUnaryClue(grid, clue) {
357
+ let deductions = 0;
358
+ const categories = grid.categories;
359
+ const ordinalCatConfig = categories.find(c => c.id === clue.ordinalCat);
360
+ if (!ordinalCatConfig || ordinalCatConfig.type !== types_1.CategoryType.ORDINAL)
361
+ return 0;
362
+ if (!ordinalCatConfig.values.every(v => typeof v === 'number'))
363
+ return 0;
364
+ const isEven = clue.filter === types_1.UnaryFilter.IS_EVEN;
365
+ for (const ordVal of ordinalCatConfig.values) {
366
+ const ordNum = ordVal;
367
+ const shouldEliminate = isEven ? ordNum % 2 !== 0 : ordNum % 2 === 0;
368
+ if (shouldEliminate) {
369
+ if (grid.isPossible(clue.targetCat, clue.targetVal, clue.ordinalCat, ordVal)) {
370
+ grid.setPossibility(clue.targetCat, clue.targetVal, clue.ordinalCat, ordVal, false);
371
+ deductions++;
372
+ }
373
+ }
374
+ }
375
+ return deductions;
376
+ }
377
+ applyBinaryClue(grid, clue) {
378
+ let deductions = 0;
379
+ const categories = grid.categories;
380
+ const cat1Config = categories.find(c => c.id === clue.cat1);
381
+ const cat2Config = categories.find(c => c.id === clue.cat2);
382
+ if (!cat1Config || !cat2Config)
383
+ return 0;
384
+ if (clue.operator === types_1.BinaryOperator.IS) {
385
+ if (grid.isPossible(clue.cat1, clue.val1, clue.cat2, clue.val2)) {
386
+ // This is not a deduction, but a fact application. Still, we need to eliminate other possibilities.
387
+ }
388
+ grid.setPossibility(clue.cat1, clue.val1, clue.cat2, clue.val2, true);
389
+ for (const val of cat2Config.values) {
390
+ if (val !== clue.val2) {
391
+ if (grid.isPossible(clue.cat1, clue.val1, clue.cat2, val)) {
392
+ grid.setPossibility(clue.cat1, clue.val1, clue.cat2, val, false);
393
+ deductions++;
394
+ }
395
+ }
396
+ }
397
+ for (const val of cat1Config.values) {
398
+ if (val !== clue.val1) {
399
+ if (grid.isPossible(clue.cat1, val, clue.cat2, clue.val2)) {
400
+ grid.setPossibility(clue.cat1, val, clue.cat2, clue.val2, false);
401
+ deductions++;
402
+ }
403
+ }
404
+ }
405
+ }
406
+ else { // IS_NOT
407
+ if (grid.isPossible(clue.cat1, clue.val1, clue.cat2, clue.val2)) {
408
+ grid.setPossibility(clue.cat1, clue.val1, clue.cat2, clue.val2, false);
409
+ deductions++;
410
+ }
411
+ }
412
+ return deductions;
413
+ }
414
+ runDeductionLoop(grid) {
415
+ let deductions = 0;
416
+ const categories = grid.categories;
417
+ for (const cat1 of categories) {
418
+ for (const val1 of cat1.values) {
419
+ for (const cat2 of categories) {
420
+ if (cat1.id === cat2.id)
421
+ continue;
422
+ // Uniqueness Check
423
+ const possibleValues = cat2.values.filter(val2 => grid.isPossible(cat1.id, val1, cat2.id, val2));
424
+ if (possibleValues.length === 1) {
425
+ const val2 = possibleValues[0];
426
+ // If val1 is uniquely associated with val2, then no other val from cat1 can be associated with val2
427
+ for (const otherVal1 of cat1.values) {
428
+ if (otherVal1 !== val1) {
429
+ if (grid.isPossible(cat1.id, otherVal1, cat2.id, val2)) {
430
+ grid.setPossibility(cat1.id, otherVal1, cat2.id, val2, false);
431
+ deductions++;
432
+ }
433
+ }
434
+ }
435
+ }
436
+ // Transitivity
437
+ for (const cat3 of categories) {
438
+ if (cat1.id === cat3.id || cat2.id === cat3.id)
439
+ continue;
440
+ // Positive Transitivity
441
+ const definiteVal2 = possibleValues.length === 1 ? possibleValues[0] : null;
442
+ if (definiteVal2) {
443
+ const possibleVal3s = cat3.values.filter(val3 => grid.isPossible(cat2.id, definiteVal2, cat3.id, val3));
444
+ if (possibleVal3s.length === 1) {
445
+ const definiteVal3 = possibleVal3s[0];
446
+ if (grid.isPossible(cat1.id, val1, cat3.id, definiteVal3) === false) {
447
+ // This indicates a contradiction, but for now we are just making deductions.
448
+ }
449
+ else if (grid.getPossibilitiesCount(cat1.id, val1, cat3.id) > 1) {
450
+ grid.setPossibility(cat1.id, val1, cat3.id, definiteVal3, true);
451
+ deductions++;
452
+ }
453
+ }
454
+ }
455
+ // Negative Transitivity
456
+ for (const val3 of cat3.values) {
457
+ if (grid.isPossible(cat1.id, val1, cat3.id, val3)) {
458
+ const isPathPossible = cat2.values.some(val2 => grid.isPossible(cat1.id, val1, cat2.id, val2) && grid.isPossible(cat2.id, val2, cat3.id, val3));
459
+ if (!isPathPossible) {
460
+ grid.setPossibility(cat1.id, val1, cat3.id, val3, false);
461
+ deductions++;
462
+ }
463
+ }
464
+ }
465
+ }
466
+ }
467
+ }
468
+ }
469
+ return deductions;
470
+ }
471
+ applyOrdinalClue(grid, constraint) {
472
+ let deductions = 0;
473
+ const categories = grid.categories;
474
+ const ordCatConfig = categories.find(c => c.id === constraint.ordinalCat);
475
+ if (!ordCatConfig || ordCatConfig.type !== types_1.CategoryType.ORDINAL)
476
+ return 0;
477
+ const possibleVals1 = ordCatConfig.values
478
+ .map((v, i) => ({ val: v, idx: i }))
479
+ .filter(v => grid.isPossible(constraint.item1Cat, constraint.item1Val, constraint.ordinalCat, v.val));
480
+ const possibleVals2 = ordCatConfig.values
481
+ .map((v, i) => ({ val: v, idx: i }))
482
+ .filter(v => grid.isPossible(constraint.item2Cat, constraint.item2Val, constraint.ordinalCat, v.val));
483
+ if (possibleVals1.length === 0 || possibleVals2.length === 0)
484
+ return 0;
485
+ // --- Item 1 Pruning ---
486
+ if (constraint.operator === types_1.OrdinalOperator.GREATER_THAN) { // item1 > item2
487
+ for (const pval1 of possibleVals1) {
488
+ const canBeGreaterThan = possibleVals2.some(pval2 => pval1.idx > pval2.idx);
489
+ if (!canBeGreaterThan) {
490
+ if (grid.isPossible(constraint.item1Cat, constraint.item1Val, constraint.ordinalCat, pval1.val)) {
491
+ grid.setPossibility(constraint.item1Cat, constraint.item1Val, constraint.ordinalCat, pval1.val, false);
492
+ deductions++;
493
+ }
494
+ }
495
+ }
496
+ }
497
+ else if (constraint.operator === types_1.OrdinalOperator.LESS_THAN) { // item1 < item2
498
+ for (const pval1 of possibleVals1) {
499
+ const canBeLessThan = possibleVals2.some(pval2 => pval1.idx < pval2.idx);
500
+ if (!canBeLessThan) {
501
+ if (grid.isPossible(constraint.item1Cat, constraint.item1Val, constraint.ordinalCat, pval1.val)) {
502
+ grid.setPossibility(constraint.item1Cat, constraint.item1Val, constraint.ordinalCat, pval1.val, false);
503
+ deductions++;
504
+ }
505
+ }
506
+ }
507
+ }
508
+ else if (constraint.operator === types_1.OrdinalOperator.NOT_GREATER_THAN) { // item1 <= item2
509
+ for (const pval1 of possibleVals1) {
510
+ const canBeLessOrEqual = possibleVals2.some(pval2 => pval1.idx <= pval2.idx);
511
+ if (!canBeLessOrEqual) {
512
+ if (grid.isPossible(constraint.item1Cat, constraint.item1Val, constraint.ordinalCat, pval1.val)) {
513
+ grid.setPossibility(constraint.item1Cat, constraint.item1Val, constraint.ordinalCat, pval1.val, false);
514
+ deductions++;
515
+ }
516
+ }
517
+ }
518
+ }
519
+ else if (constraint.operator === types_1.OrdinalOperator.NOT_LESS_THAN) { // item1 >= item2
520
+ for (const pval1 of possibleVals1) {
521
+ const canBeGreaterOrEqual = possibleVals2.some(pval2 => pval1.idx >= pval2.idx);
522
+ if (!canBeGreaterOrEqual) {
523
+ if (grid.isPossible(constraint.item1Cat, constraint.item1Val, constraint.ordinalCat, pval1.val)) {
524
+ grid.setPossibility(constraint.item1Cat, constraint.item1Val, constraint.ordinalCat, pval1.val, false);
525
+ deductions++;
526
+ }
527
+ }
528
+ }
529
+ }
530
+ // --- Item 2 Pruning ---
531
+ if (constraint.operator === types_1.OrdinalOperator.GREATER_THAN) { // item2 < item1
532
+ for (const pval2 of possibleVals2) {
533
+ const canBeLessThan = possibleVals1.some(pval1 => pval2.idx < pval1.idx);
534
+ if (!canBeLessThan) {
535
+ if (grid.isPossible(constraint.item2Cat, constraint.item2Val, constraint.ordinalCat, pval2.val)) {
536
+ grid.setPossibility(constraint.item2Cat, constraint.item2Val, constraint.ordinalCat, pval2.val, false);
537
+ deductions++;
538
+ }
539
+ }
540
+ }
541
+ }
542
+ else if (constraint.operator === types_1.OrdinalOperator.LESS_THAN) { // item2 > item1
543
+ for (const pval2 of possibleVals2) {
544
+ const canBeGreaterThan = possibleVals1.some(pval1 => pval2.idx > pval1.idx);
545
+ if (!canBeGreaterThan) {
546
+ if (grid.isPossible(constraint.item2Cat, constraint.item2Val, constraint.ordinalCat, pval2.val)) {
547
+ grid.setPossibility(constraint.item2Cat, constraint.item2Val, constraint.ordinalCat, pval2.val, false);
548
+ deductions++;
549
+ }
550
+ }
551
+ }
552
+ }
553
+ else if (constraint.operator === types_1.OrdinalOperator.NOT_GREATER_THAN) { // item2 >= item1
554
+ for (const pval2 of possibleVals2) {
555
+ const canBeGreaterOrEqual = possibleVals1.some(pval1 => pval2.idx >= pval1.idx);
556
+ if (!canBeGreaterOrEqual) {
557
+ if (grid.isPossible(constraint.item2Cat, constraint.item2Val, constraint.ordinalCat, pval2.val)) {
558
+ grid.setPossibility(constraint.item2Cat, constraint.item2Val, constraint.ordinalCat, pval2.val, false);
559
+ deductions++;
560
+ }
561
+ }
562
+ }
563
+ }
564
+ else if (constraint.operator === types_1.OrdinalOperator.NOT_LESS_THAN) { // item2 <= item1
565
+ for (const pval2 of possibleVals2) {
566
+ const canBeLessOrEqual = possibleVals1.some(pval1 => pval2.idx <= pval1.idx);
567
+ if (!canBeLessOrEqual) {
568
+ if (grid.isPossible(constraint.item2Cat, constraint.item2Val, constraint.ordinalCat, pval2.val)) {
569
+ grid.setPossibility(constraint.item2Cat, constraint.item2Val, constraint.ordinalCat, pval2.val, false);
570
+ deductions++;
571
+ }
572
+ }
573
+ }
574
+ }
575
+ return deductions;
576
+ }
577
+ applySuperlativeClue(grid, clue) {
578
+ const categories = grid.categories;
579
+ const ordinalCatConfig = categories.find(c => c.id === clue.ordinalCat);
580
+ if (!ordinalCatConfig || ordinalCatConfig.type !== types_1.CategoryType.ORDINAL)
581
+ return 0;
582
+ let extremeValue;
583
+ let isNot = false;
584
+ switch (clue.operator) {
585
+ case types_1.SuperlativeOperator.MAX:
586
+ extremeValue = ordinalCatConfig.values[ordinalCatConfig.values.length - 1];
587
+ break;
588
+ case types_1.SuperlativeOperator.MIN:
589
+ extremeValue = ordinalCatConfig.values[0];
590
+ break;
591
+ case types_1.SuperlativeOperator.NOT_MAX:
592
+ extremeValue = ordinalCatConfig.values[ordinalCatConfig.values.length - 1];
593
+ isNot = true;
594
+ break;
595
+ case types_1.SuperlativeOperator.NOT_MIN:
596
+ extremeValue = ordinalCatConfig.values[0];
597
+ isNot = true;
598
+ break;
599
+ default: return 0;
600
+ }
601
+ // This clue is essentially a binary IS clue.
602
+ const binaryClue = {
603
+ type: types_1.ClueType.BINARY,
604
+ cat1: clue.targetCat,
605
+ val1: clue.targetVal,
606
+ cat2: clue.ordinalCat,
607
+ val2: extremeValue,
608
+ operator: isNot ? types_1.BinaryOperator.IS_NOT : types_1.BinaryOperator.IS,
609
+ };
610
+ return this.applyBinaryClue(grid, binaryClue);
611
+ }
612
+ }
613
+ exports.Solver = Solver;
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Base error class for Logic Puzzle Generator library.
3
+ */
4
+ export declare class LogicPuzzleError extends Error {
5
+ constructor(message: string);
6
+ }
7
+ /**
8
+ * Thrown when the provided configuration is invalid (e.g., duplicate IDs, mismatched sizes).
9
+ */
10
+ export declare class ConfigurationError extends LogicPuzzleError {
11
+ constructor(message: string);
12
+ }
@@ -0,0 +1,23 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ConfigurationError = exports.LogicPuzzleError = void 0;
4
+ /**
5
+ * Base error class for Logic Puzzle Generator library.
6
+ */
7
+ class LogicPuzzleError extends Error {
8
+ constructor(message) {
9
+ super(message);
10
+ this.name = 'LogicPuzzleError';
11
+ }
12
+ }
13
+ exports.LogicPuzzleError = LogicPuzzleError;
14
+ /**
15
+ * Thrown when the provided configuration is invalid (e.g., duplicate IDs, mismatched sizes).
16
+ */
17
+ class ConfigurationError extends LogicPuzzleError {
18
+ constructor(message) {
19
+ super(message);
20
+ this.name = 'ConfigurationError';
21
+ }
22
+ }
23
+ exports.ConfigurationError = ConfigurationError;
@@ -0,0 +1,8 @@
1
+ export * from './types';
2
+ export * from './engine/Clue';
3
+ export * from './engine/LogicGrid';
4
+ export * from './engine/Solver';
5
+ export * from './engine/Generator';
6
+ export * from './engine/GenerativeSession';
7
+ export * from './defaults';
8
+ export * from './errors';