@sudobility/sudojo_types 1.2.18 → 1.2.19

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/index.cjs CHANGED
@@ -4,7 +4,7 @@
4
4
  * TypeScript types for Sudojo API - Sudoku learning platform
5
5
  */
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
- exports.DEFAULT_SCRAMBLE_CONFIG = exports.TOTAL_CELLS = exports.BLOCK_SIZE = exports.BOARD_SIZE = exports.BELT_ICON_VIEWBOX = exports.BELT_ICON_PATHS = exports.BELT_COLORS = exports.TECHNIQUE_TITLE_TO_ID = exports.TechniqueId = exports.HINT_LEVEL_LIMITS = void 0;
7
+ exports.HELP_FILE_TO_TECHNIQUE = exports.TECHNIQUE_TO_HELP_FILE = exports.EMPTY_PENCILMARKS = exports.EMPTY_BOARD = exports.DEFAULT_SCRAMBLE_CONFIG = exports.TOTAL_CELLS = exports.BLOCK_SIZE = exports.BOARD_SIZE = exports.BELT_ICON_VIEWBOX = exports.BELT_ICON_PATHS = exports.BELT_COLORS = exports.TECHNIQUE_TITLE_TO_ID = exports.TechniqueId = exports.HINT_LEVEL_LIMITS = void 0;
8
8
  exports.successResponse = successResponse;
9
9
  exports.errorResponse = errorResponse;
10
10
  exports.techniqueToBit = techniqueToBit;
@@ -18,6 +18,26 @@ exports.parseBoardString = parseBoardString;
18
18
  exports.stringifyBoard = stringifyBoard;
19
19
  exports.scrambleBoard = scrambleBoard;
20
20
  exports.noScramble = noScramble;
21
+ exports.isBoardFilled = isBoardFilled;
22
+ exports.isBoardSolved = isBoardSolved;
23
+ exports.getMergedBoardState = getMergedBoardState;
24
+ exports.hasInvalidPencilmarksStep = hasInvalidPencilmarksStep;
25
+ exports.hasPencilmarkContent = hasPencilmarkContent;
26
+ exports.getTechniqueNameById = getTechniqueNameById;
27
+ exports.cellName = cellName;
28
+ exports.cellList = cellList;
29
+ exports.getBlockIndex = getBlockIndex;
30
+ exports.getBlockNumber = getBlockNumber;
31
+ exports.indexToRowCol = indexToRowCol;
32
+ exports.rowColToIndex = rowColToIndex;
33
+ exports.formatTime = formatTime;
34
+ exports.parseTime = parseTime;
35
+ exports.formatDigits = formatDigits;
36
+ exports.isValidUUID = isValidUUID;
37
+ exports.validateUUID = validateUUID;
38
+ exports.getTechniqueIconUrl = getTechniqueIconUrl;
39
+ exports.getHelpFileUrl = getHelpFileUrl;
40
+ exports.getTechniqueFromHelpFile = getTechniqueFromHelpFile;
21
41
  /** Create a success response */
22
42
  function successResponse(data) {
23
43
  return {
@@ -574,4 +594,438 @@ function noScramble(puzzle, solution) {
574
594
  reverseDigitMapping: identityMapping,
575
595
  };
576
596
  }
597
+ // =============================================================================
598
+ // Solver Utilities (shared between frontend and backend)
599
+ // =============================================================================
600
+ /**
601
+ * Check if all cells are filled (no empty cells remaining).
602
+ * A cell is considered filled if either the original puzzle has a digit
603
+ * or the user has entered a digit.
604
+ *
605
+ * @param original - 81-char original puzzle string (0 = empty)
606
+ * @param user - 81-char user input string (0 = no input)
607
+ * @returns true if all 81 cells have a non-zero digit
608
+ *
609
+ * @example
610
+ * ```typescript
611
+ * const original = '530070000...'; // partial puzzle
612
+ * const user = '000000000...'; // no user input yet
613
+ * isBoardFilled(original, user); // false
614
+ * ```
615
+ */
616
+ function isBoardFilled(original, user) {
617
+ for (let i = 0; i < exports.TOTAL_CELLS; i++) {
618
+ const originalChar = original[i] || '0';
619
+ const userChar = user[i] || '0';
620
+ const actualChar = userChar !== '0' ? userChar : originalChar;
621
+ if (actualChar === '0')
622
+ return false;
623
+ }
624
+ return true;
625
+ }
626
+ /**
627
+ * Check if all cells are filled AND match the solution.
628
+ * Useful for validating that a puzzle is correctly solved.
629
+ *
630
+ * @param original - 81-char original puzzle string (0 = empty)
631
+ * @param user - 81-char user input string (0 = no input)
632
+ * @param solution - 81-char solution string
633
+ * @returns true if board is filled and all values match solution
634
+ *
635
+ * @example
636
+ * ```typescript
637
+ * isBoardSolved(original, userInput, solution); // true if correctly solved
638
+ * ```
639
+ */
640
+ function isBoardSolved(original, user, solution) {
641
+ for (let i = 0; i < exports.TOTAL_CELLS; i++) {
642
+ const originalChar = original[i] || '0';
643
+ const userChar = user[i] || '0';
644
+ const solutionChar = solution[i] || '0';
645
+ const actualChar = userChar !== '0' ? userChar : originalChar;
646
+ if (actualChar === '0')
647
+ return false;
648
+ if (actualChar !== solutionChar)
649
+ return false;
650
+ }
651
+ return true;
652
+ }
653
+ /**
654
+ * Get the current board state by merging original puzzle and user input.
655
+ * For each cell, returns the user's input if non-zero, otherwise the original value.
656
+ *
657
+ * @param original - 81-char original puzzle string (0 = empty)
658
+ * @param user - 81-char user input string (0 = no input)
659
+ * @returns 81-char string representing current board state
660
+ *
661
+ * @example
662
+ * ```typescript
663
+ * const original = '530070000...';
664
+ * const user = '006000000...';
665
+ * getMergedBoardState(original, user); // '536070000...'
666
+ * ```
667
+ */
668
+ function getMergedBoardState(original, user) {
669
+ let result = '';
670
+ for (let i = 0; i < exports.TOTAL_CELLS; i++) {
671
+ const originalChar = original[i] || '0';
672
+ const userChar = user[i] || '0';
673
+ result += userChar !== '0' ? userChar : originalChar;
674
+ }
675
+ return result;
676
+ }
677
+ /**
678
+ * Check for "Invalid Pencilmarks" error in hint steps.
679
+ * This indicates the solver detected inconsistent pencilmarks.
680
+ *
681
+ * @param steps - Array of hint steps from solver response
682
+ * @returns true if any step has title "Invalid Pencilmarks"
683
+ *
684
+ * @example
685
+ * ```typescript
686
+ * if (hasInvalidPencilmarksStep(response.data.hints.steps)) {
687
+ * throw new Error('Invalid pencilmarks detected');
688
+ * }
689
+ * ```
690
+ */
691
+ function hasInvalidPencilmarksStep(steps) {
692
+ if (!steps)
693
+ return false;
694
+ return steps.some(step => step.title === 'Invalid Pencilmarks');
695
+ }
696
+ /**
697
+ * Check if pencilmarks string has actual content (not just empty commas).
698
+ * Pencilmarks are stored as comma-separated values like "123,45,9,,...".
699
+ * An "empty" pencilmarks string would be all commas: ",,,,,...".
700
+ *
701
+ * @param pencilmarks - Comma-separated pencilmarks string
702
+ * @returns true if there are actual pencilmark values
703
+ *
704
+ * @example
705
+ * ```typescript
706
+ * hasPencilmarkContent('123,45,,9,'); // true
707
+ * hasPencilmarkContent(',,,,,,,,'); // false
708
+ * hasPencilmarkContent(''); // false
709
+ * ```
710
+ */
711
+ function hasPencilmarkContent(pencilmarks) {
712
+ return pencilmarks.replace(/,/g, '').length > 0;
713
+ }
714
+ // Reverse lookup map from TechniqueId to title (computed once)
715
+ const TECHNIQUE_ID_TO_TITLE = Object.fromEntries(Object.entries(exports.TECHNIQUE_TITLE_TO_ID).map(([title, id]) => [id, title]));
716
+ /**
717
+ * Get the technique title/name from its ID.
718
+ * Inverse of TECHNIQUE_TITLE_TO_ID lookup.
719
+ *
720
+ * @param techniqueId - The technique ID number
721
+ * @returns The technique title, or "Technique {id}" if unknown
722
+ *
723
+ * @example
724
+ * ```typescript
725
+ * getTechniqueNameById(1); // "Full House"
726
+ * getTechniqueNameById(14); // "XY-Wing"
727
+ * getTechniqueNameById(999); // "Technique 999"
728
+ * ```
729
+ */
730
+ function getTechniqueNameById(techniqueId) {
731
+ return TECHNIQUE_ID_TO_TITLE[techniqueId] ?? `Technique ${techniqueId}`;
732
+ }
733
+ // =============================================================================
734
+ // Board State Constants
735
+ // =============================================================================
736
+ /**
737
+ * Empty board string (81 zeros) representing no user input.
738
+ * Used as default value for user input in solver APIs.
739
+ */
740
+ exports.EMPTY_BOARD = '0'.repeat(81);
741
+ /**
742
+ * Empty pencilmarks string (80 commas = 81 empty elements).
743
+ * Used as default value for pencilmarks in solver APIs.
744
+ */
745
+ exports.EMPTY_PENCILMARKS = ','.repeat(80);
746
+ // =============================================================================
747
+ // Cell Notation Utilities
748
+ // =============================================================================
749
+ /**
750
+ * Format a cell position in R1C1 notation (1-indexed for human readability).
751
+ *
752
+ * @param row - Row index (0-8)
753
+ * @param col - Column index (0-8)
754
+ * @returns String like "R1C1" for top-left, "R9C9" for bottom-right
755
+ *
756
+ * @example
757
+ * ```typescript
758
+ * cellName(0, 0); // "R1C1"
759
+ * cellName(4, 4); // "R5C5"
760
+ * cellName(8, 8); // "R9C9"
761
+ * ```
762
+ */
763
+ function cellName(row, col) {
764
+ return `R${row + 1}C${col + 1}`;
765
+ }
766
+ /**
767
+ * Format multiple cell positions as a comma-separated list in R1C1 notation.
768
+ *
769
+ * @param cells - Array of [row, col] pairs
770
+ * @returns String like "R1C1, R2C3, R5C5"
771
+ *
772
+ * @example
773
+ * ```typescript
774
+ * cellList([[0, 0], [1, 2], [4, 4]]); // "R1C1, R2C3, R5C5"
775
+ * ```
776
+ */
777
+ function cellList(cells) {
778
+ return cells.map(([row, col]) => cellName(row, col)).join(', ');
779
+ }
780
+ /**
781
+ * Get the block index (0-8) for a given cell position.
782
+ * Blocks are numbered left-to-right, top-to-bottom:
783
+ * ```
784
+ * 0 1 2
785
+ * 3 4 5
786
+ * 6 7 8
787
+ * ```
788
+ *
789
+ * @param row - Row index (0-8)
790
+ * @param col - Column index (0-8)
791
+ * @returns Block index (0-8)
792
+ *
793
+ * @example
794
+ * ```typescript
795
+ * getBlockIndex(0, 0); // 0 (top-left block)
796
+ * getBlockIndex(4, 4); // 4 (center block)
797
+ * getBlockIndex(8, 8); // 8 (bottom-right block)
798
+ * ```
799
+ */
800
+ function getBlockIndex(row, col) {
801
+ return Math.floor(row / exports.BLOCK_SIZE) * exports.BLOCK_SIZE + Math.floor(col / exports.BLOCK_SIZE);
802
+ }
803
+ /**
804
+ * Get the block number (1-9) for a given cell position.
805
+ * Same as getBlockIndex but 1-indexed for human readability.
806
+ *
807
+ * @param row - Row index (0-8)
808
+ * @param col - Column index (0-8)
809
+ * @returns Block number (1-9)
810
+ */
811
+ function getBlockNumber(row, col) {
812
+ return getBlockIndex(row, col) + 1;
813
+ }
814
+ /**
815
+ * Convert a flat cell index (0-80) to row and column.
816
+ *
817
+ * @param index - Flat index (0-80)
818
+ * @returns [row, col] tuple (both 0-8)
819
+ *
820
+ * @example
821
+ * ```typescript
822
+ * indexToRowCol(0); // [0, 0]
823
+ * indexToRowCol(40); // [4, 4]
824
+ * indexToRowCol(80); // [8, 8]
825
+ * ```
826
+ */
827
+ function indexToRowCol(index) {
828
+ return [Math.floor(index / exports.BOARD_SIZE), index % exports.BOARD_SIZE];
829
+ }
830
+ /**
831
+ * Convert row and column to a flat cell index (0-80).
832
+ *
833
+ * @param row - Row index (0-8)
834
+ * @param col - Column index (0-8)
835
+ * @returns Flat index (0-80)
836
+ *
837
+ * @example
838
+ * ```typescript
839
+ * rowColToIndex(0, 0); // 0
840
+ * rowColToIndex(4, 4); // 40
841
+ * rowColToIndex(8, 8); // 80
842
+ * ```
843
+ */
844
+ function rowColToIndex(row, col) {
845
+ return row * exports.BOARD_SIZE + col;
846
+ }
847
+ // =============================================================================
848
+ // Time Formatting Utilities
849
+ // =============================================================================
850
+ /**
851
+ * Format seconds as MM:SS or HH:MM:SS.
852
+ *
853
+ * @param seconds - Total seconds to format
854
+ * @returns Formatted time string
855
+ *
856
+ * @example
857
+ * ```typescript
858
+ * formatTime(65); // "01:05"
859
+ * formatTime(3661); // "01:01:01"
860
+ * formatTime(0); // "00:00"
861
+ * ```
862
+ */
863
+ function formatTime(seconds) {
864
+ const hours = Math.floor(seconds / 3600);
865
+ const minutes = Math.floor((seconds % 3600) / 60);
866
+ const secs = seconds % 60;
867
+ if (hours > 0) {
868
+ return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
869
+ }
870
+ return `${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
871
+ }
872
+ /**
873
+ * Parse MM:SS or HH:MM:SS time string to seconds.
874
+ *
875
+ * @param timeString - Time string in MM:SS or HH:MM:SS format
876
+ * @returns Total seconds, or 0 if invalid format
877
+ *
878
+ * @example
879
+ * ```typescript
880
+ * parseTime("01:05"); // 65
881
+ * parseTime("01:01:01"); // 3661
882
+ * parseTime("invalid"); // 0
883
+ * ```
884
+ */
885
+ function parseTime(timeString) {
886
+ const parts = timeString.split(':').map(p => parseInt(p, 10));
887
+ if (parts.some(isNaN))
888
+ return 0;
889
+ if (parts.length === 2) {
890
+ return (parts[0] ?? 0) * 60 + (parts[1] ?? 0);
891
+ }
892
+ else if (parts.length === 3) {
893
+ return (parts[0] ?? 0) * 3600 + (parts[1] ?? 0) * 60 + (parts[2] ?? 0);
894
+ }
895
+ return 0;
896
+ }
897
+ /**
898
+ * Format a set of digits like {1, 2, 3} for display.
899
+ *
900
+ * @param digits - String or array of digits
901
+ * @returns Formatted string like "{1, 2, 3}"
902
+ *
903
+ * @example
904
+ * ```typescript
905
+ * formatDigits("123"); // "{1, 2, 3}"
906
+ * formatDigits([1, 2, 3]); // "{1, 2, 3}"
907
+ * ```
908
+ */
909
+ function formatDigits(digits) {
910
+ const arr = typeof digits === 'string' ? digits.split('') : digits.map(String);
911
+ return `{${arr.join(', ')}}`;
912
+ }
913
+ // =============================================================================
914
+ // UUID Validation Utilities
915
+ // =============================================================================
916
+ /** UUID regex pattern (lowercase or uppercase) */
917
+ const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
918
+ /**
919
+ * Check if a string is a valid UUID format.
920
+ *
921
+ * @param id - String to validate
922
+ * @returns true if valid UUID format
923
+ *
924
+ * @example
925
+ * ```typescript
926
+ * isValidUUID('123e4567-e89b-12d3-a456-426614174000'); // true
927
+ * isValidUUID('not-a-uuid'); // false
928
+ * ```
929
+ */
930
+ function isValidUUID(id) {
931
+ return UUID_PATTERN.test(id);
932
+ }
933
+ /**
934
+ * Validate a UUID and throw if invalid.
935
+ *
936
+ * @param uuid - UUID string to validate
937
+ * @param name - Optional name for error message (e.g., "Board UUID")
938
+ * @returns The validated UUID string
939
+ * @throws Error if UUID is missing or invalid format
940
+ *
941
+ * @example
942
+ * ```typescript
943
+ * const id = validateUUID(params.id, 'Board UUID'); // throws if invalid
944
+ * ```
945
+ */
946
+ function validateUUID(uuid, name = 'UUID') {
947
+ if (!uuid) {
948
+ throw new Error(`${name} is required`);
949
+ }
950
+ if (!isValidUUID(uuid)) {
951
+ throw new Error(`Invalid ${name} format: "${uuid}". Expected UUID format (xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)`);
952
+ }
953
+ return uuid;
954
+ }
955
+ // =============================================================================
956
+ // Technique URL Utilities
957
+ // =============================================================================
958
+ /**
959
+ * Convert technique title to icon filename/URL path.
960
+ *
961
+ * @param title - Technique title (e.g., "Full House", "X-Wing")
962
+ * @returns Icon URL path (e.g., "/technique.full.house.svg")
963
+ *
964
+ * @example
965
+ * ```typescript
966
+ * getTechniqueIconUrl("Full House"); // "/technique.full.house.svg"
967
+ * getTechniqueIconUrl("X-Wing"); // "/technique.x.wing.svg"
968
+ * getTechniqueIconUrl("XY-Wing"); // "/technique.xy.wing.svg"
969
+ * ```
970
+ */
971
+ function getTechniqueIconUrl(title) {
972
+ const normalized = title
973
+ .toLowerCase()
974
+ .replace(/\s+/g, '.') // spaces to dots
975
+ .replace(/-/g, '.'); // hyphens to dots
976
+ return `/technique.${normalized}.svg`;
977
+ }
978
+ /** Map technique titles to HTML help file paths */
979
+ exports.TECHNIQUE_TO_HELP_FILE = {
980
+ 'Full House': 'Full_House.html',
981
+ 'Naked Single': 'Naked_Single.html',
982
+ 'Hidden Single': 'Hidden_Single.html',
983
+ 'Naked Pair': 'Naked_Pair.html',
984
+ 'Hidden Pair': 'Hidden_Pair.html',
985
+ 'Locked Candidates': 'Locked_Candidates.html',
986
+ 'Naked Triple': 'Naked_Triple.html',
987
+ 'Hidden Triple': 'Hidden_Triple.html',
988
+ 'Naked Quad': 'Naked_Quad.html',
989
+ 'Hidden Quad': 'Hidden_Quad.html',
990
+ 'X-Wing': 'X-Wing.html',
991
+ 'Swordfish': 'Swordfish.html',
992
+ 'Jellyfish': 'Jellyfish.html',
993
+ 'Squirmbag': 'Squirmbag.html',
994
+ 'XY-Wing': 'XY-Wing.html',
995
+ 'XYZ-Wing': 'XYZ-Wing.html',
996
+ 'WXYZ-Wing': 'WXYZ-Wing.html',
997
+ 'Finned X-Wing': 'Finned_X-Wing.html',
998
+ 'Finned Swordfish': 'Finned_Swordfish.html',
999
+ 'Finned Jellyfish': 'Finned_Jellyfish.html',
1000
+ 'Finned Squirmbag': 'Finned_Squirmbag.html',
1001
+ 'Almost Locked Sets': 'Almost_Locked_Sets.html',
1002
+ 'ALS Chain': 'ALS-Chain.html',
1003
+ 'ALS-Chain': 'ALS-Chain.html',
1004
+ };
1005
+ /** Reverse mapping: HTML file name (lowercase) -> technique title */
1006
+ exports.HELP_FILE_TO_TECHNIQUE = Object.fromEntries(Object.entries(exports.TECHNIQUE_TO_HELP_FILE).map(([title, file]) => [file.toLowerCase(), title]));
1007
+ /**
1008
+ * Get help file URL for a technique.
1009
+ *
1010
+ * @param techniqueTitle - Technique title
1011
+ * @returns Help file URL path (e.g., "/help/Full_House.html")
1012
+ */
1013
+ function getHelpFileUrl(techniqueTitle) {
1014
+ const fileName = exports.TECHNIQUE_TO_HELP_FILE[techniqueTitle];
1015
+ if (fileName) {
1016
+ return `/help/${fileName}`;
1017
+ }
1018
+ // Generate filename from title (spaces to underscores, preserve hyphens)
1019
+ const generatedFileName = `${techniqueTitle.replace(/\s+/g, '_')}.html`;
1020
+ return `/help/${generatedFileName}`;
1021
+ }
1022
+ /**
1023
+ * Get technique title from help file name.
1024
+ *
1025
+ * @param fileName - Help file name (e.g., "Full_House.html")
1026
+ * @returns Technique title or undefined if not found
1027
+ */
1028
+ function getTechniqueFromHelpFile(fileName) {
1029
+ return exports.HELP_FILE_TO_TECHNIQUE[fileName.toLowerCase()];
1030
+ }
577
1031
  //# sourceMappingURL=index.js.map