@sudobility/sudojo_types 1.2.17 → 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 +459 -4
- package/dist/index.d.ts +328 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +438 -3
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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 {
|
|
@@ -132,19 +152,20 @@ exports.TECHNIQUE_TITLE_TO_ID = {
|
|
|
132
152
|
/** Convert a TechniqueId to its bit position in the bitfield */
|
|
133
153
|
// Uses BigInt internally to support techniques >= 32, then converts to Number
|
|
134
154
|
// (safe since we have < 52 techniques, well within Number.MAX_SAFE_INTEGER)
|
|
155
|
+
// TechniqueId N maps to bit N (1 << N)
|
|
135
156
|
function techniqueToBit(techniqueId) {
|
|
136
|
-
return Number(BigInt(1) << BigInt(techniqueId
|
|
157
|
+
return Number(BigInt(1) << BigInt(techniqueId));
|
|
137
158
|
}
|
|
138
159
|
/** Check if a technique is present in a bitfield */
|
|
139
160
|
// Uses BigInt internally to support techniques >= 32
|
|
140
161
|
function hasTechnique(bitfield, techniqueId) {
|
|
141
|
-
const bit = BigInt(1) << BigInt(techniqueId
|
|
162
|
+
const bit = BigInt(1) << BigInt(techniqueId);
|
|
142
163
|
return (BigInt(bitfield) & bit) !== BigInt(0);
|
|
143
164
|
}
|
|
144
165
|
/** Add a technique to a bitfield */
|
|
145
166
|
// Uses BigInt internally to support techniques >= 32
|
|
146
167
|
function addTechnique(bitfield, techniqueId) {
|
|
147
|
-
const bit = BigInt(1) << BigInt(techniqueId
|
|
168
|
+
const bit = BigInt(1) << BigInt(techniqueId);
|
|
148
169
|
return Number(BigInt(bitfield) | bit);
|
|
149
170
|
}
|
|
150
171
|
/** Belt colors mapped to level index (1-12) */
|
|
@@ -573,4 +594,438 @@ function noScramble(puzzle, solution) {
|
|
|
573
594
|
reverseDigitMapping: identityMapping,
|
|
574
595
|
};
|
|
575
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
|
+
}
|
|
576
1031
|
//# sourceMappingURL=index.js.map
|