logic-puzzle-generator 1.2.4 → 1.3.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.
@@ -82,5 +82,6 @@ export type Clue = BinaryClue | OrdinalClue | SuperlativeClue | UnaryClue | Cros
82
82
  export type ClueWithMetadata = Clue & {
83
83
  deductions?: number;
84
84
  updates?: number;
85
+ reasons?: import('../types').DeductionReason[];
85
86
  percentComplete?: number;
86
87
  };
@@ -128,6 +128,7 @@ class GenerativeSession {
128
128
  // Apply it
129
129
  const result = this.solver.applyClue(this.grid, clue);
130
130
  clue.deductions = result.deductions;
131
+ clue.reasons = result.reasons;
131
132
  // Calculate % Complete
132
133
  // Grid starts with 'totalPossible' and ends with 'solutionPossible'.
133
134
  // % = (Total - Current) / (Total - Solution)
@@ -459,6 +459,8 @@ class Generator {
459
459
  else {
460
460
  step.clue.percentComplete = 100;
461
461
  }
462
+ // Capture Deduction Reasons (XAI)
463
+ step.clue.reasons = result.reasons;
462
464
  }
463
465
  return {
464
466
  solution: this.solution,
@@ -20,6 +20,7 @@ export declare class Solver {
20
20
  applyClue(grid: LogicGrid, clue: Clue): {
21
21
  grid: LogicGrid;
22
22
  deductions: number;
23
+ reasons: import('../types').DeductionReason[];
23
24
  };
24
25
  private applyCrossOrdinalClue;
25
26
  private applyUnaryClue;
@@ -21,31 +21,32 @@ class Solver {
21
21
  */
22
22
  applyClue(grid, clue) {
23
23
  let deductions = 0;
24
+ const reasons = [];
24
25
  switch (clue.type) {
25
26
  case types_1.ClueType.BINARY:
26
- deductions += this.applyBinaryClue(grid, clue);
27
+ deductions += this.applyBinaryClue(grid, clue, reasons);
27
28
  break;
28
29
  case types_1.ClueType.SUPERLATIVE:
29
- deductions += this.applySuperlativeClue(grid, clue);
30
+ deductions += this.applySuperlativeClue(grid, clue, reasons);
30
31
  break;
31
32
  case types_1.ClueType.ORDINAL:
32
- deductions += this.applyOrdinalClue(grid, clue);
33
+ deductions += this.applyOrdinalClue(grid, clue, reasons);
33
34
  break;
34
35
  case types_1.ClueType.UNARY:
35
- deductions += this.applyUnaryClue(grid, clue);
36
+ deductions += this.applyUnaryClue(grid, clue, reasons);
36
37
  break;
37
38
  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
+ deductions += this.applyCrossOrdinalClue(grid, clue, reasons);
39
40
  break;
40
41
  }
41
42
  let newDeductions;
42
43
  do {
43
- newDeductions = this.runDeductionLoop(grid);
44
+ newDeductions = this.runDeductionLoop(grid, reasons);
44
45
  deductions += newDeductions;
45
46
  } while (newDeductions > 0);
46
- return { grid, deductions };
47
+ return { grid, deductions, reasons };
47
48
  }
48
- applyCrossOrdinalClue(grid, clue) {
49
+ applyCrossOrdinalClue(grid, clue, reasons) {
49
50
  let deductions = 0;
50
51
  const categories = grid.categories;
51
52
  const ord1Config = categories.find(c => c.id === clue.ordinal1);
@@ -71,6 +72,11 @@ class Solver {
71
72
  if (targetVal1 === undefined) {
72
73
  grid.setPossibility(clue.item1Cat, clue.item1Val, clue.ordinal1, cand1.val, false);
73
74
  deductions++;
75
+ reasons.push({
76
+ type: 'cross_ordinal',
77
+ description: `Cross-Ordinal: ${clue.item1Val} cannot be ${cand1.val} because offset ${clue.offset1} goes out of bounds.`,
78
+ cells: [{ cat: clue.item1Cat, val: clue.item1Val }, { cat: clue.ordinal1, val: cand1.val }]
79
+ });
74
80
  continue;
75
81
  }
76
82
  // Compatibility check
@@ -90,6 +96,11 @@ class Solver {
90
96
  if (!supported) {
91
97
  grid.setPossibility(clue.item1Cat, clue.item1Val, clue.ordinal1, cand1.val, false);
92
98
  deductions++;
99
+ reasons.push({
100
+ type: 'cross_ordinal',
101
+ description: `Cross-Ordinal: ${clue.item1Val} as ${cand1.val} finds no compatible ${clue.item2Val} at offset pair.`,
102
+ cells: [{ cat: clue.item1Cat, val: clue.item1Val }, { cat: clue.ordinal1, val: cand1.val }]
103
+ });
93
104
  }
94
105
  }
95
106
  // Filter 2 based on 1 (Symmetric)
@@ -99,6 +110,11 @@ class Solver {
99
110
  if (targetVal2 === undefined) {
100
111
  grid.setPossibility(clue.item2Cat, clue.item2Val, clue.ordinal2, cand2.val, false);
101
112
  deductions++;
113
+ reasons.push({
114
+ type: 'cross_ordinal',
115
+ description: `Cross-Ordinal: ${clue.item2Val} cannot be ${cand2.val} because offset ${clue.offset2} goes out of bounds.`,
116
+ cells: [{ cat: clue.item2Cat, val: clue.item2Val }, { cat: clue.ordinal1, val: cand2.val }]
117
+ });
102
118
  continue;
103
119
  }
104
120
  let supported = false;
@@ -115,6 +131,11 @@ class Solver {
115
131
  if (!supported) {
116
132
  grid.setPossibility(clue.item2Cat, clue.item2Val, clue.ordinal2, cand2.val, false);
117
133
  deductions++;
134
+ reasons.push({
135
+ type: 'cross_ordinal',
136
+ description: `Cross-Ordinal: ${clue.item2Val} as ${cand2.val} finds no compatible ${clue.item1Val} at offset pair.`,
137
+ cells: [{ cat: clue.item2Cat, val: clue.item2Val }, { cat: clue.ordinal2, val: cand2.val }]
138
+ });
118
139
  }
119
140
  }
120
141
  // Lock linkage if unique
@@ -130,6 +151,11 @@ class Solver {
130
151
  if (grid.getPossibilitiesCount(clue.ordinal1, v1, clue.ordinal2) > 1) {
131
152
  grid.setPossibility(clue.ordinal1, v1, clue.ordinal2, v2, true);
132
153
  deductions++;
154
+ reasons.push({
155
+ type: 'cross_ordinal',
156
+ description: `Cross-Ordinal Link: ${v1} must be ${v2} based on forced offsets.`,
157
+ cells: [{ cat: clue.ordinal1, val: v1 }, { cat: clue.ordinal2, val: v2 }]
158
+ });
133
159
  }
134
160
  }
135
161
  }
@@ -160,6 +186,11 @@ class Solver {
160
186
  if (grid.isPossible(clue.ordinal1, v1, clue.ordinal2, v2)) {
161
187
  grid.setPossibility(clue.ordinal1, v1, clue.ordinal2, v2, false);
162
188
  deductions++;
189
+ reasons.push({
190
+ type: 'cross_ordinal',
191
+ description: `Cross-Ordinal NOT: ${v1} cannot be ${v2} because positions are forbidden.`,
192
+ cells: [{ cat: clue.ordinal1, val: v1 }, { cat: clue.ordinal2, val: v2 }]
193
+ });
163
194
  }
164
195
  }
165
196
  }
@@ -328,6 +359,11 @@ class Solver {
328
359
  if (grid.isPossible(clue.ordinal1, v1, clue.ordinal2, v2)) {
329
360
  grid.setPossibility(clue.item2Cat, clue.item2Val, clue.ordinal2, cand2.val, false);
330
361
  deductions++;
362
+ reasons.push({
363
+ type: 'cross_ordinal',
364
+ description: `Cross-Ordinal NOT: ${clue.item2Val} cannot be ${cand2.val} because it implies forbidden link to ${v1}.`,
365
+ cells: [{ cat: clue.item2Cat, val: clue.item2Val }, { cat: clue.ordinal2, val: cand2.val }]
366
+ });
331
367
  }
332
368
  }
333
369
  }
@@ -345,6 +381,11 @@ class Solver {
345
381
  if (grid.isPossible(clue.ordinal1, v1, clue.ordinal2, v2)) {
346
382
  grid.setPossibility(clue.item1Cat, clue.item1Val, clue.ordinal1, cand1.val, false);
347
383
  deductions++;
384
+ reasons.push({
385
+ type: 'cross_ordinal',
386
+ description: `Cross-Ordinal NOT: ${clue.item1Val} cannot be ${cand1.val} because it implies forbidden link to ${v2}.`,
387
+ cells: [{ cat: clue.item1Cat, val: clue.item1Val }, { cat: clue.ordinal1, val: cand1.val }]
388
+ });
348
389
  }
349
390
  }
350
391
  }
@@ -353,7 +394,7 @@ class Solver {
353
394
  }
354
395
  return deductions;
355
396
  }
356
- applyUnaryClue(grid, clue) {
397
+ applyUnaryClue(grid, clue, reasons) {
357
398
  let deductions = 0;
358
399
  const categories = grid.categories;
359
400
  const ordinalCatConfig = categories.find(c => c.id === clue.ordinalCat);
@@ -369,12 +410,17 @@ class Solver {
369
410
  if (grid.isPossible(clue.targetCat, clue.targetVal, clue.ordinalCat, ordVal)) {
370
411
  grid.setPossibility(clue.targetCat, clue.targetVal, clue.ordinalCat, ordVal, false);
371
412
  deductions++;
413
+ reasons.push({
414
+ type: 'unary',
415
+ description: `Unary Rule: ${ordVal} eliminated for ${clue.targetVal} because it is ${isEven ? 'not even' : 'not odd'}.`,
416
+ cells: [{ cat: clue.targetCat, val: clue.targetVal }, { cat: clue.ordinalCat, val: String(ordVal) }]
417
+ });
372
418
  }
373
419
  }
374
420
  }
375
421
  return deductions;
376
422
  }
377
- applyBinaryClue(grid, clue) {
423
+ applyBinaryClue(grid, clue, reasons) {
378
424
  let deductions = 0;
379
425
  const categories = grid.categories;
380
426
  const cat1Config = categories.find(c => c.id === clue.cat1);
@@ -385,6 +431,11 @@ class Solver {
385
431
  // Count the "Positive Confirmation" (Green Check) as a deduction if it wasn't already known
386
432
  if (grid.getPossibilitiesCount(clue.cat1, clue.val1, clue.cat2) > 1) {
387
433
  deductions++;
434
+ reasons.push({
435
+ type: 'confirmation',
436
+ description: `Directly from clue: ${clue.val1} is ${clue.val2}.`,
437
+ cells: [{ cat: clue.cat1, val: clue.val1 }, { cat: clue.cat2, val: clue.val2 }]
438
+ });
388
439
  }
389
440
  if (grid.isPossible(clue.cat1, clue.val1, clue.cat2, clue.val2)) {
390
441
  // This is not a deduction, but a fact application. Still, we need to eliminate other possibilities.
@@ -395,6 +446,11 @@ class Solver {
395
446
  if (grid.isPossible(clue.cat1, clue.val1, clue.cat2, val)) {
396
447
  grid.setPossibility(clue.cat1, clue.val1, clue.cat2, val, false);
397
448
  deductions++;
449
+ reasons.push({
450
+ type: 'elimination',
451
+ description: `Since ${clue.val1} is ${clue.val2}, it cannot be ${val}.`,
452
+ cells: [{ cat: clue.cat1, val: clue.val1 }, { cat: clue.cat2, val: val }]
453
+ });
398
454
  }
399
455
  }
400
456
  }
@@ -403,6 +459,11 @@ class Solver {
403
459
  if (grid.isPossible(clue.cat1, val, clue.cat2, clue.val2)) {
404
460
  grid.setPossibility(clue.cat1, val, clue.cat2, clue.val2, false);
405
461
  deductions++;
462
+ reasons.push({
463
+ type: 'elimination',
464
+ description: `Since ${clue.val2} is ${clue.val1}, it cannot be ${val}.`,
465
+ cells: [{ cat: clue.cat1, val: val }, { cat: clue.cat2, val: clue.val2 }]
466
+ });
406
467
  }
407
468
  }
408
469
  }
@@ -411,11 +472,16 @@ class Solver {
411
472
  if (grid.isPossible(clue.cat1, clue.val1, clue.cat2, clue.val2)) {
412
473
  grid.setPossibility(clue.cat1, clue.val1, clue.cat2, clue.val2, false);
413
474
  deductions++;
475
+ reasons.push({
476
+ type: 'elimination',
477
+ description: `Directly from clue: ${clue.val1} is NOT ${clue.val2}.`,
478
+ cells: [{ cat: clue.cat1, val: clue.val1 }, { cat: clue.cat2, val: clue.val2 }]
479
+ });
414
480
  }
415
481
  }
416
482
  return deductions;
417
483
  }
418
- runDeductionLoop(grid) {
484
+ runDeductionLoop(grid, reasons) {
419
485
  let deductions = 0;
420
486
  const categories = grid.categories;
421
487
  for (const cat1 of categories) {
@@ -433,6 +499,11 @@ class Solver {
433
499
  if (grid.isPossible(cat1.id, otherVal1, cat2.id, val2)) {
434
500
  grid.setPossibility(cat1.id, otherVal1, cat2.id, val2, false);
435
501
  deductions++;
502
+ reasons.push({
503
+ type: 'uniqueness',
504
+ description: `Since ${val2} is uniquely ${val1} in ${cat1.id}, it cannot be ${otherVal1}.`,
505
+ cells: [{ cat: cat1.id, val: val1 }, { cat: cat2.id, val: val2 }, { cat: cat1.id, val: otherVal1 }]
506
+ });
436
507
  }
437
508
  }
438
509
  }
@@ -453,6 +524,11 @@ class Solver {
453
524
  else if (grid.getPossibilitiesCount(cat1.id, val1, cat3.id) > 1) {
454
525
  grid.setPossibility(cat1.id, val1, cat3.id, definiteVal3, true);
455
526
  deductions++;
527
+ reasons.push({
528
+ type: 'transitivity',
529
+ description: `Since ${cat1.id}:${val1} is ${cat2.id}:${definiteVal2}, and that is ${cat3.id}:${definiteVal3}, ${val1} must be ${definiteVal3}.`,
530
+ cells: [{ cat: cat1.id, val: val1 }, { cat: cat2.id, val: definiteVal2 }, { cat: cat3.id, val: definiteVal3 }]
531
+ });
456
532
  }
457
533
  }
458
534
  }
@@ -463,6 +539,11 @@ class Solver {
463
539
  if (!isPathPossible) {
464
540
  grid.setPossibility(cat1.id, val1, cat3.id, val3, false);
465
541
  deductions++;
542
+ reasons.push({
543
+ type: 'transitivity',
544
+ description: `Negative Transitivity: No path exists between ${val1} and ${val3} via ${cat2.id}.`,
545
+ cells: [{ cat: cat1.id, val: val1 }, { cat: cat3.id, val: val3 }]
546
+ });
466
547
  }
467
548
  }
468
549
  }
@@ -472,7 +553,7 @@ class Solver {
472
553
  }
473
554
  return deductions;
474
555
  }
475
- applyOrdinalClue(grid, constraint) {
556
+ applyOrdinalClue(grid, constraint, reasons) {
476
557
  let deductions = 0;
477
558
  const categories = grid.categories;
478
559
  const ordCatConfig = categories.find(c => c.id === constraint.ordinalCat);
@@ -494,6 +575,11 @@ class Solver {
494
575
  if (grid.isPossible(constraint.item1Cat, constraint.item1Val, constraint.ordinalCat, pval1.val)) {
495
576
  grid.setPossibility(constraint.item1Cat, constraint.item1Val, constraint.ordinalCat, pval1.val, false);
496
577
  deductions++;
578
+ reasons.push({
579
+ type: 'ordinal',
580
+ description: `${constraint.item1Val} cannot be ${pval1.val} (idx ${pval1.idx}) because it must be > ${constraint.item2Val}.`,
581
+ cells: [{ cat: constraint.item1Cat, val: constraint.item1Val }, { cat: constraint.ordinalCat, val: pval1.val }]
582
+ });
497
583
  }
498
584
  }
499
585
  }
@@ -505,6 +591,11 @@ class Solver {
505
591
  if (grid.isPossible(constraint.item1Cat, constraint.item1Val, constraint.ordinalCat, pval1.val)) {
506
592
  grid.setPossibility(constraint.item1Cat, constraint.item1Val, constraint.ordinalCat, pval1.val, false);
507
593
  deductions++;
594
+ reasons.push({
595
+ type: 'ordinal',
596
+ description: `${constraint.item1Val} cannot be ${pval1.val} (idx ${pval1.idx}) because it must be < ${constraint.item2Val}.`,
597
+ cells: [{ cat: constraint.item1Cat, val: constraint.item1Val }, { cat: constraint.ordinalCat, val: pval1.val }]
598
+ });
508
599
  }
509
600
  }
510
601
  }
@@ -516,6 +607,11 @@ class Solver {
516
607
  if (grid.isPossible(constraint.item1Cat, constraint.item1Val, constraint.ordinalCat, pval1.val)) {
517
608
  grid.setPossibility(constraint.item1Cat, constraint.item1Val, constraint.ordinalCat, pval1.val, false);
518
609
  deductions++;
610
+ reasons.push({
611
+ type: 'ordinal',
612
+ description: `${constraint.item1Val} cannot be ${pval1.val} (idx ${pval1.idx}) because it must be <= ${constraint.item2Val}.`,
613
+ cells: [{ cat: constraint.item1Cat, val: constraint.item1Val }, { cat: constraint.ordinalCat, val: pval1.val }]
614
+ });
519
615
  }
520
616
  }
521
617
  }
@@ -527,6 +623,11 @@ class Solver {
527
623
  if (grid.isPossible(constraint.item1Cat, constraint.item1Val, constraint.ordinalCat, pval1.val)) {
528
624
  grid.setPossibility(constraint.item1Cat, constraint.item1Val, constraint.ordinalCat, pval1.val, false);
529
625
  deductions++;
626
+ reasons.push({
627
+ type: 'ordinal',
628
+ description: `${constraint.item1Val} cannot be ${pval1.val} (idx ${pval1.idx}) because it must be >= ${constraint.item2Val}.`,
629
+ cells: [{ cat: constraint.item1Cat, val: constraint.item1Val }, { cat: constraint.ordinalCat, val: pval1.val }]
630
+ });
530
631
  }
531
632
  }
532
633
  }
@@ -539,6 +640,11 @@ class Solver {
539
640
  if (grid.isPossible(constraint.item2Cat, constraint.item2Val, constraint.ordinalCat, pval2.val)) {
540
641
  grid.setPossibility(constraint.item2Cat, constraint.item2Val, constraint.ordinalCat, pval2.val, false);
541
642
  deductions++;
643
+ reasons.push({
644
+ type: 'ordinal',
645
+ description: `${constraint.item2Val} cannot be ${pval2.val} (idx ${pval2.idx}) because it must be < ${constraint.item1Val}.`,
646
+ cells: [{ cat: constraint.item2Cat, val: constraint.item2Val }, { cat: constraint.ordinalCat, val: pval2.val }]
647
+ });
542
648
  }
543
649
  }
544
650
  }
@@ -550,6 +656,11 @@ class Solver {
550
656
  if (grid.isPossible(constraint.item2Cat, constraint.item2Val, constraint.ordinalCat, pval2.val)) {
551
657
  grid.setPossibility(constraint.item2Cat, constraint.item2Val, constraint.ordinalCat, pval2.val, false);
552
658
  deductions++;
659
+ reasons.push({
660
+ type: 'ordinal',
661
+ description: `${constraint.item2Val} cannot be ${pval2.val} (idx ${pval2.idx}) because it must be > ${constraint.item1Val}.`,
662
+ cells: [{ cat: constraint.item2Cat, val: constraint.item2Val }, { cat: constraint.ordinalCat, val: pval2.val }]
663
+ });
553
664
  }
554
665
  }
555
666
  }
@@ -561,6 +672,11 @@ class Solver {
561
672
  if (grid.isPossible(constraint.item2Cat, constraint.item2Val, constraint.ordinalCat, pval2.val)) {
562
673
  grid.setPossibility(constraint.item2Cat, constraint.item2Val, constraint.ordinalCat, pval2.val, false);
563
674
  deductions++;
675
+ reasons.push({
676
+ type: 'ordinal',
677
+ description: `${constraint.item2Val} cannot be ${pval2.val} (idx ${pval2.idx}) because it must be >= ${constraint.item1Val}.`,
678
+ cells: [{ cat: constraint.item2Cat, val: constraint.item2Val }, { cat: constraint.ordinalCat, val: pval2.val }]
679
+ });
564
680
  }
565
681
  }
566
682
  }
@@ -572,13 +688,18 @@ class Solver {
572
688
  if (grid.isPossible(constraint.item2Cat, constraint.item2Val, constraint.ordinalCat, pval2.val)) {
573
689
  grid.setPossibility(constraint.item2Cat, constraint.item2Val, constraint.ordinalCat, pval2.val, false);
574
690
  deductions++;
691
+ reasons.push({
692
+ type: 'ordinal',
693
+ description: `${constraint.item2Val} cannot be ${pval2.val} (idx ${pval2.idx}) because it must be <= ${constraint.item1Val}.`,
694
+ cells: [{ cat: constraint.item2Cat, val: constraint.item2Val }, { cat: constraint.ordinalCat, val: pval2.val }]
695
+ });
575
696
  }
576
697
  }
577
698
  }
578
699
  }
579
700
  return deductions;
580
701
  }
581
- applySuperlativeClue(grid, clue) {
702
+ applySuperlativeClue(grid, clue, reasons) {
582
703
  const categories = grid.categories;
583
704
  const ordinalCatConfig = categories.find(c => c.id === clue.ordinalCat);
584
705
  if (!ordinalCatConfig || ordinalCatConfig.type !== types_1.CategoryType.ORDINAL)
@@ -611,7 +732,7 @@ class Solver {
611
732
  val2: extremeValue,
612
733
  operator: isNot ? types_1.BinaryOperator.IS_NOT : types_1.BinaryOperator.IS,
613
734
  };
614
- return this.applyBinaryClue(grid, binaryClue);
735
+ return this.applyBinaryClue(grid, binaryClue, reasons);
615
736
  }
616
737
  }
617
738
  exports.Solver = Solver;
@@ -103,3 +103,11 @@ export interface ClueGenerationConstraints {
103
103
  */
104
104
  maxDeductions?: number;
105
105
  }
106
+ export interface DeductionReason {
107
+ type: 'elimination' | 'confirmation' | 'uniqueness' | 'transitivity' | 'clue' | 'unary' | 'ordinal' | 'cross_ordinal';
108
+ description: string;
109
+ cells?: {
110
+ cat: string;
111
+ val: ValueLabel;
112
+ }[];
113
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "logic-puzzle-generator",
3
- "version": "1.2.4",
3
+ "version": "1.3.0",
4
4
  "publishConfig": {
5
5
  "access": "public",
6
6
  "registry": "https://registry.npmjs.org/"
@@ -45,4 +45,4 @@
45
45
  "ts-jest": "^29.1.2",
46
46
  "typescript": "^5.4.5"
47
47
  }
48
- }
48
+ }