@sudobility/sudojo_lib 0.0.102 → 0.0.104

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.
@@ -1 +1 @@
1
- {"version":3,"file":"useBoardEntry.d.ts","sourceRoot":"","sources":["../../src/hooks/useBoardEntry.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAE3C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAEvD,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,oBAAoB;IACnC,aAAa,EAAE,aAAa,CAAC;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,mBAAmB;IAClC,sCAAsC;IACtC,KAAK,EAAE,UAAU,EAAE,CAAC;IACpB,oCAAoC;IACpC,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,wCAAwC;IACxC,YAAY,EAAE,OAAO,CAAC;IACtB,mCAAmC;IACnC,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,gDAAgD;IAChD,eAAe,EAAE,eAAe,GAAG,IAAI,CAAC;IACxC,6BAA6B;IAC7B,UAAU,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACpC,6CAA6C;IAC7C,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,8BAA8B;IAC9B,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB,+CAA+C;IAC/C,eAAe,EAAE,MAAM,MAAM,CAAC;IAC9B,yBAAyB;IACzB,QAAQ,EAAE,MAAM,IAAI,CAAC;IACrB,2BAA2B;IAC3B,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB,8BAA8B;IAC9B,SAAS,EAAE,MAAM,CAAC;IAClB,mDAAmD;IACnD,kBAAkB,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;CAC9C;AAsDD,wBAAgB,aAAa,CAAC,EAC5B,aAAa,EACb,OAAO,EACP,KAAK,GACN,EAAE,oBAAoB,GAAG,mBAAmB,CAoK5C"}
1
+ {"version":3,"file":"useBoardEntry.d.ts","sourceRoot":"","sources":["../../src/hooks/useBoardEntry.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAE3C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAEvD,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,oBAAoB;IACnC,aAAa,EAAE,aAAa,CAAC;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,mBAAmB;IAClC,sCAAsC;IACtC,KAAK,EAAE,UAAU,EAAE,CAAC;IACpB,oCAAoC;IACpC,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,wCAAwC;IACxC,YAAY,EAAE,OAAO,CAAC;IACtB,mCAAmC;IACnC,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,gDAAgD;IAChD,eAAe,EAAE,eAAe,GAAG,IAAI,CAAC;IACxC,6BAA6B;IAC7B,UAAU,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACpC,6CAA6C;IAC7C,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,8BAA8B;IAC9B,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB,+CAA+C;IAC/C,eAAe,EAAE,MAAM,MAAM,CAAC;IAC9B,yBAAyB;IACzB,QAAQ,EAAE,MAAM,IAAI,CAAC;IACrB,2BAA2B;IAC3B,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB,8BAA8B;IAC9B,SAAS,EAAE,MAAM,CAAC;IAClB,mDAAmD;IACnD,kBAAkB,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;CAC9C;AAoED,wBAAgB,aAAa,CAAC,EAC5B,aAAa,EACb,OAAO,EACP,KAAK,GACN,EAAE,oBAAoB,GAAG,mBAAmB,CAiL5C"}
@@ -50,6 +50,19 @@ function puzzleStringToCells(puzzle) {
50
50
  };
51
51
  });
52
52
  }
53
+ /**
54
+ * Classify a validation error message into a translation key
55
+ */
56
+ function classifyValidationError(errorMsg) {
57
+ const lower = errorMsg.toLowerCase();
58
+ if (lower.includes('multiple') || lower.includes('not unique')) {
59
+ return 'enter.errors.multipleSolutions';
60
+ }
61
+ if (lower.includes('cannot solve') || lower.includes('no solution')) {
62
+ return 'enter.errors.noSolution';
63
+ }
64
+ return 'enter.errors.validationFailed';
65
+ }
53
66
  export function useBoardEntry({ networkClient, baseUrl, token, }) {
54
67
  const [cells, setCells] = useState(createEmptyBoard);
55
68
  const [selectedIndex, setSelectedIndex] = useState(null);
@@ -60,12 +73,13 @@ export function useBoardEntry({ networkClient, baseUrl, token, }) {
60
73
  const puzzleString = useMemo(() => cellsToPuzzleString(cells), [cells]);
61
74
  const clueCount = useMemo(() => countClues(cells), [cells]);
62
75
  // Use the solver validate hook
63
- const { data: validateData, isLoading: isValidating } = useSolverValidate(networkClient, baseUrl, token, { original: puzzleString }, {
76
+ const { data: validateData, error: validateError, isLoading: isValidating, } = useSolverValidate(networkClient, baseUrl, token, { original: puzzleString }, {
64
77
  enabled: shouldValidate && clueCount >= 17,
65
78
  retry: false,
66
79
  });
67
80
  // Track whether we've already processed this validation result
68
81
  const processedDataRef = useRef(null);
82
+ const processedErrorRef = useRef(null);
69
83
  // Handle validation result
70
84
  useEffect(() => {
71
85
  if (!shouldValidate || isValidating)
@@ -87,17 +101,7 @@ export function useBoardEntry({ networkClient, baseUrl, token, }) {
87
101
  }
88
102
  else if (validateData.error) {
89
103
  // Validation failed with specific error
90
- const errorMsg = validateData.error.toLowerCase();
91
- if (errorMsg.includes('multiple') || errorMsg.includes('not unique')) {
92
- setValidationError('enter.errors.multipleSolutions');
93
- }
94
- else if (errorMsg.includes('cannot solve') ||
95
- errorMsg.includes('no solution')) {
96
- setValidationError('enter.errors.noSolution');
97
- }
98
- else {
99
- setValidationError('enter.errors.validationFailed');
100
- }
104
+ setValidationError(classifyValidationError(validateData.error));
101
105
  setValidatedPuzzle(null);
102
106
  }
103
107
  else {
@@ -108,6 +112,22 @@ export function useBoardEntry({ networkClient, baseUrl, token, }) {
108
112
  }
109
113
  setShouldValidate(false);
110
114
  }, [validateData, shouldValidate, isValidating, puzzleString]);
115
+ // Handle network/query errors (e.g. 400 responses throw NetworkError)
116
+ useEffect(() => {
117
+ if (!shouldValidate || isValidating)
118
+ return;
119
+ if (!validateError)
120
+ return;
121
+ if (processedErrorRef.current === validateError)
122
+ return;
123
+ processedErrorRef.current = validateError;
124
+ // Extract error message from NetworkError response body or message
125
+ const networkErr = validateError;
126
+ const errorMsg = networkErr.response?.error ?? networkErr.message ?? '';
127
+ setValidationError(classifyValidationError(errorMsg));
128
+ setValidatedPuzzle(null);
129
+ setShouldValidate(false);
130
+ }, [validateError, shouldValidate, isValidating]);
111
131
  // Select a cell
112
132
  const selectCell = useCallback((index) => {
113
133
  if (index >= 0 && index < 81) {
@@ -1 +1 @@
1
- {"version":3,"file":"useBoardEntry.js","sourceRoot":"","sources":["../../src/hooks/useBoardEntry.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Hook for managing Sudoku board entry mode state\n * Allows users to enter given clues for a custom puzzle\n */\n\nimport { useCallback, useEffect, useMemo, useRef, useState } from 'react';\nimport type { SudokuCell } from '../types';\nimport { useSolverValidate } from '@sudobility/sudojo_client';\nimport type { NetworkClient } from '@sudobility/types';\n\nexport interface ValidatedPuzzle {\n puzzle: string;\n solution: string;\n}\n\nexport interface UseBoardEntryOptions {\n networkClient: NetworkClient;\n baseUrl: string;\n token: string;\n}\n\nexport interface UseBoardEntryReturn {\n /** 81 cells with given values only */\n cells: SudokuCell[];\n /** Currently selected cell index */\n selectedIndex: number | null;\n /** Whether validation is in progress */\n isValidating: boolean;\n /** Validation error message key */\n validationError: string | null;\n /** Validated puzzle data (puzzle + solution) */\n validatedPuzzle: ValidatedPuzzle | null;\n /** Select a cell by index */\n selectCell: (index: number) => void;\n /** Set a given value at the selected cell */\n setGiven: (value: number) => void;\n /** Erase the selected cell */\n erase: () => void;\n /** Get the puzzle as an 81-character string */\n getPuzzleString: () => string;\n /** Trigger validation */\n validate: () => void;\n /** Reset to empty board */\n reset: () => void;\n /** Number of clues entered */\n clueCount: number;\n /** Set cells from an 81-character puzzle string */\n setCellsFromPuzzle: (puzzle: string) => void;\n}\n\n/**\n * Create an empty 81-cell board for entry mode\n */\nfunction createEmptyBoard(): SudokuCell[] {\n return Array.from({ length: 81 }, (_, index) => ({\n index,\n solution: null,\n given: null,\n input: null,\n pencilmarks: null,\n }));\n}\n\n/**\n * Convert cells to 81-character puzzle string\n * '0' for empty, '1'-'9' for given values\n */\nfunction cellsToPuzzleString(cells: SudokuCell[]): string {\n return cells.map(cell => cell.given?.toString() ?? '0').join('');\n}\n\n/**\n * Count the number of non-empty cells (clues)\n */\nfunction countClues(cells: SudokuCell[]): number {\n return cells.filter(cell => cell.given !== null).length;\n}\n\n/**\n * Create a board from an 81-character puzzle string\n * '0' or '.' for empty, '1'-'9' for given values\n */\nfunction puzzleStringToCells(puzzle: string): SudokuCell[] {\n if (puzzle.length !== 81) {\n return createEmptyBoard();\n }\n\n return Array.from({ length: 81 }, (_, index) => {\n const char = puzzle.charAt(index);\n const value = parseInt(char, 10);\n const given = value >= 1 && value <= 9 ? value : null;\n\n return {\n index,\n solution: null,\n given,\n input: null,\n pencilmarks: null,\n };\n });\n}\n\nexport function useBoardEntry({\n networkClient,\n baseUrl,\n token,\n}: UseBoardEntryOptions): UseBoardEntryReturn {\n const [cells, setCells] = useState<SudokuCell[]>(createEmptyBoard);\n const [selectedIndex, setSelectedIndex] = useState<number | null>(null);\n const [validationError, setValidationError] = useState<string | null>(null);\n const [validatedPuzzle, setValidatedPuzzle] =\n useState<ValidatedPuzzle | null>(null);\n const [shouldValidate, setShouldValidate] = useState(false);\n\n // Compute puzzle string for validation\n const puzzleString = useMemo(() => cellsToPuzzleString(cells), [cells]);\n const clueCount = useMemo(() => countClues(cells), [cells]);\n\n // Use the solver validate hook\n const { data: validateData, isLoading: isValidating } = useSolverValidate(\n networkClient,\n baseUrl,\n token,\n { original: puzzleString },\n {\n enabled: shouldValidate && clueCount >= 17,\n retry: false,\n }\n );\n\n // Track whether we've already processed this validation result\n const processedDataRef = useRef<typeof validateData | null>(null);\n\n // Handle validation result\n useEffect(() => {\n if (!shouldValidate || isValidating) return;\n if (!validateData) return;\n // Avoid re-processing the same result\n if (processedDataRef.current === validateData) return;\n processedDataRef.current = validateData;\n\n // ValidateData has board.solution\n const solution = validateData.data?.board?.solution;\n if (validateData.success && solution) {\n setValidatedPuzzle({\n puzzle: puzzleString,\n solution,\n });\n setValidationError(null);\n } else if (validateData.error) {\n // Validation failed with specific error\n const errorMsg = validateData.error.toLowerCase();\n if (errorMsg.includes('multiple') || errorMsg.includes('not unique')) {\n setValidationError('enter.errors.multipleSolutions');\n } else if (\n errorMsg.includes('cannot solve') ||\n errorMsg.includes('no solution')\n ) {\n setValidationError('enter.errors.noSolution');\n } else {\n setValidationError('enter.errors.validationFailed');\n }\n setValidatedPuzzle(null);\n } else {\n // Unexpected response - success but no solution\n console.error('Unexpected validation response:', validateData);\n setValidationError('enter.errors.validationFailed');\n setValidatedPuzzle(null);\n }\n setShouldValidate(false);\n }, [validateData, shouldValidate, isValidating, puzzleString]);\n\n // Select a cell\n const selectCell = useCallback((index: number) => {\n if (index >= 0 && index < 81) {\n setSelectedIndex(index);\n }\n }, []);\n\n // Set a given value at the selected cell\n const setGiven = useCallback(\n (value: number) => {\n if (selectedIndex === null || value < 1 || value > 9) return;\n\n setCells(prevCells => {\n const newCells = [...prevCells];\n const existing = newCells[selectedIndex];\n if (existing) {\n newCells[selectedIndex] = { ...existing, given: value };\n }\n return newCells;\n });\n\n // Clear validation state when board changes\n setValidationError(null);\n setValidatedPuzzle(null);\n },\n [selectedIndex]\n );\n\n // Erase the selected cell\n const erase = useCallback(() => {\n if (selectedIndex === null) return;\n\n setCells(prevCells => {\n const newCells = [...prevCells];\n const existing = newCells[selectedIndex];\n if (existing) {\n newCells[selectedIndex] = { ...existing, given: null };\n }\n return newCells;\n });\n\n // Clear validation state when board changes\n setValidationError(null);\n setValidatedPuzzle(null);\n }, [selectedIndex]);\n\n // Get puzzle string\n const getPuzzleString = useCallback(() => {\n return puzzleString;\n }, [puzzleString]);\n\n // Trigger validation\n const validate = useCallback(() => {\n // Check minimum clues\n if (clueCount < 17) {\n setValidationError('enter.errors.notEnoughClues');\n return;\n }\n\n // Clear previous state and trigger validation\n setValidationError(null);\n setValidatedPuzzle(null);\n setShouldValidate(true);\n }, [clueCount]);\n\n // Reset to empty board\n const reset = useCallback(() => {\n setCells(createEmptyBoard());\n setSelectedIndex(null);\n setValidationError(null);\n setValidatedPuzzle(null);\n setShouldValidate(false);\n }, []);\n\n // Set cells from puzzle string\n const setCellsFromPuzzle = useCallback((puzzle: string) => {\n setCells(puzzleStringToCells(puzzle));\n setSelectedIndex(null);\n setValidationError(null);\n setValidatedPuzzle(null);\n setShouldValidate(false);\n }, []);\n\n return {\n cells,\n selectedIndex,\n isValidating,\n validationError,\n validatedPuzzle,\n selectCell,\n setGiven,\n erase,\n getPuzzleString,\n validate,\n reset,\n clueCount,\n setCellsFromPuzzle,\n };\n}\n"]}
1
+ {"version":3,"file":"useBoardEntry.js","sourceRoot":"","sources":["../../src/hooks/useBoardEntry.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AAE1E,OAAO,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAC;AA2C9D;;GAEG;AACH,SAAS,gBAAgB;IACvB,OAAO,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE,KAAK,EAAE,EAAE,CAAC,CAAC;QAC/C,KAAK;QACL,QAAQ,EAAE,IAAI;QACd,KAAK,EAAE,IAAI;QACX,KAAK,EAAE,IAAI;QACX,WAAW,EAAE,IAAI;KAClB,CAAC,CAAC,CAAC;AACN,CAAC;AAED;;;GAGG;AACH,SAAS,mBAAmB,CAAC,KAAmB;IAC9C,OAAO,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,KAAK,EAAE,QAAQ,EAAE,IAAI,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;AACnE,CAAC;AAED;;GAEG;AACH,SAAS,UAAU,CAAC,KAAmB;IACrC,OAAO,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,KAAK,KAAK,IAAI,CAAC,CAAC,MAAM,CAAC;AAC1D,CAAC;AAED;;;GAGG;AACH,SAAS,mBAAmB,CAAC,MAAc;IACzC,IAAI,MAAM,CAAC,MAAM,KAAK,EAAE,EAAE,CAAC;QACzB,OAAO,gBAAgB,EAAE,CAAC;IAC5B,CAAC;IAED,OAAO,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE,KAAK,EAAE,EAAE;QAC7C,MAAM,IAAI,GAAG,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QAClC,MAAM,KAAK,GAAG,QAAQ,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;QACjC,MAAM,KAAK,GAAG,KAAK,IAAI,CAAC,IAAI,KAAK,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC;QAEtD,OAAO;YACL,KAAK;YACL,QAAQ,EAAE,IAAI;YACd,KAAK;YACL,KAAK,EAAE,IAAI;YACX,WAAW,EAAE,IAAI;SAClB,CAAC;IACJ,CAAC,CAAC,CAAC;AACL,CAAC;AAED;;GAEG;AACH,SAAS,uBAAuB,CAAC,QAAgB;IAC/C,MAAM,KAAK,GAAG,QAAQ,CAAC,WAAW,EAAE,CAAC;IACrC,IAAI,KAAK,CAAC,QAAQ,CAAC,UAAU,CAAC,IAAI,KAAK,CAAC,QAAQ,CAAC,YAAY,CAAC,EAAE,CAAC;QAC/D,OAAO,gCAAgC,CAAC;IAC1C,CAAC;IACD,IAAI,KAAK,CAAC,QAAQ,CAAC,cAAc,CAAC,IAAI,KAAK,CAAC,QAAQ,CAAC,aAAa,CAAC,EAAE,CAAC;QACpE,OAAO,yBAAyB,CAAC;IACnC,CAAC;IACD,OAAO,+BAA+B,CAAC;AACzC,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,EAC5B,aAAa,EACb,OAAO,EACP,KAAK,GACgB;IACrB,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,GAAG,QAAQ,CAAe,gBAAgB,CAAC,CAAC;IACnE,MAAM,CAAC,aAAa,EAAE,gBAAgB,CAAC,GAAG,QAAQ,CAAgB,IAAI,CAAC,CAAC;IACxE,MAAM,CAAC,eAAe,EAAE,kBAAkB,CAAC,GAAG,QAAQ,CAAgB,IAAI,CAAC,CAAC;IAC5E,MAAM,CAAC,eAAe,EAAE,kBAAkB,CAAC,GACzC,QAAQ,CAAyB,IAAI,CAAC,CAAC;IACzC,MAAM,CAAC,cAAc,EAAE,iBAAiB,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC;IAE5D,uCAAuC;IACvC,MAAM,YAAY,GAAG,OAAO,CAAC,GAAG,EAAE,CAAC,mBAAmB,CAAC,KAAK,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC;IACxE,MAAM,SAAS,GAAG,OAAO,CAAC,GAAG,EAAE,CAAC,UAAU,CAAC,KAAK,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC;IAE5D,+BAA+B;IAC/B,MAAM,EACJ,IAAI,EAAE,YAAY,EAClB,KAAK,EAAE,aAAa,EACpB,SAAS,EAAE,YAAY,GACxB,GAAG,iBAAiB,CACnB,aAAa,EACb,OAAO,EACP,KAAK,EACL,EAAE,QAAQ,EAAE,YAAY,EAAE,EAC1B;QACE,OAAO,EAAE,cAAc,IAAI,SAAS,IAAI,EAAE;QAC1C,KAAK,EAAE,KAAK;KACb,CACF,CAAC;IAEF,+DAA+D;IAC/D,MAAM,gBAAgB,GAAG,MAAM,CAA6B,IAAI,CAAC,CAAC;IAClE,MAAM,iBAAiB,GAAG,MAAM,CAA8B,IAAI,CAAC,CAAC;IAEpE,2BAA2B;IAC3B,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,CAAC,cAAc,IAAI,YAAY;YAAE,OAAO;QAC5C,IAAI,CAAC,YAAY;YAAE,OAAO;QAC1B,sCAAsC;QACtC,IAAI,gBAAgB,CAAC,OAAO,KAAK,YAAY;YAAE,OAAO;QACtD,gBAAgB,CAAC,OAAO,GAAG,YAAY,CAAC;QAExC,kCAAkC;QAClC,MAAM,QAAQ,GAAG,YAAY,CAAC,IAAI,EAAE,KAAK,EAAE,QAAQ,CAAC;QACpD,IAAI,YAAY,CAAC,OAAO,IAAI,QAAQ,EAAE,CAAC;YACrC,kBAAkB,CAAC;gBACjB,MAAM,EAAE,YAAY;gBACpB,QAAQ;aACT,CAAC,CAAC;YACH,kBAAkB,CAAC,IAAI,CAAC,CAAC;QAC3B,CAAC;aAAM,IAAI,YAAY,CAAC,KAAK,EAAE,CAAC;YAC9B,wCAAwC;YACxC,kBAAkB,CAAC,uBAAuB,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC,CAAC;YAChE,kBAAkB,CAAC,IAAI,CAAC,CAAC;QAC3B,CAAC;aAAM,CAAC;YACN,gDAAgD;YAChD,OAAO,CAAC,KAAK,CAAC,iCAAiC,EAAE,YAAY,CAAC,CAAC;YAC/D,kBAAkB,CAAC,+BAA+B,CAAC,CAAC;YACpD,kBAAkB,CAAC,IAAI,CAAC,CAAC;QAC3B,CAAC;QACD,iBAAiB,CAAC,KAAK,CAAC,CAAC;IAC3B,CAAC,EAAE,CAAC,YAAY,EAAE,cAAc,EAAE,YAAY,EAAE,YAAY,CAAC,CAAC,CAAC;IAE/D,sEAAsE;IACtE,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,CAAC,cAAc,IAAI,YAAY;YAAE,OAAO;QAC5C,IAAI,CAAC,aAAa;YAAE,OAAO;QAC3B,IAAI,iBAAiB,CAAC,OAAO,KAAK,aAAa;YAAE,OAAO;QACxD,iBAAiB,CAAC,OAAO,GAAG,aAAa,CAAC;QAE1C,mEAAmE;QACnE,MAAM,UAAU,GAAG,aAGlB,CAAC;QACF,MAAM,QAAQ,GAAG,UAAU,CAAC,QAAQ,EAAE,KAAK,IAAI,UAAU,CAAC,OAAO,IAAI,EAAE,CAAC;QACxE,kBAAkB,CAAC,uBAAuB,CAAC,QAAQ,CAAC,CAAC,CAAC;QACtD,kBAAkB,CAAC,IAAI,CAAC,CAAC;QACzB,iBAAiB,CAAC,KAAK,CAAC,CAAC;IAC3B,CAAC,EAAE,CAAC,aAAa,EAAE,cAAc,EAAE,YAAY,CAAC,CAAC,CAAC;IAElD,gBAAgB;IAChB,MAAM,UAAU,GAAG,WAAW,CAAC,CAAC,KAAa,EAAE,EAAE;QAC/C,IAAI,KAAK,IAAI,CAAC,IAAI,KAAK,GAAG,EAAE,EAAE,CAAC;YAC7B,gBAAgB,CAAC,KAAK,CAAC,CAAC;QAC1B,CAAC;IACH,CAAC,EAAE,EAAE,CAAC,CAAC;IAEP,yCAAyC;IACzC,MAAM,QAAQ,GAAG,WAAW,CAC1B,CAAC,KAAa,EAAE,EAAE;QAChB,IAAI,aAAa,KAAK,IAAI,IAAI,KAAK,GAAG,CAAC,IAAI,KAAK,GAAG,CAAC;YAAE,OAAO;QAE7D,QAAQ,CAAC,SAAS,CAAC,EAAE;YACnB,MAAM,QAAQ,GAAG,CAAC,GAAG,SAAS,CAAC,CAAC;YAChC,MAAM,QAAQ,GAAG,QAAQ,CAAC,aAAa,CAAC,CAAC;YACzC,IAAI,QAAQ,EAAE,CAAC;gBACb,QAAQ,CAAC,aAAa,CAAC,GAAG,EAAE,GAAG,QAAQ,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC;YAC1D,CAAC;YACD,OAAO,QAAQ,CAAC;QAClB,CAAC,CAAC,CAAC;QAEH,4CAA4C;QAC5C,kBAAkB,CAAC,IAAI,CAAC,CAAC;QACzB,kBAAkB,CAAC,IAAI,CAAC,CAAC;IAC3B,CAAC,EACD,CAAC,aAAa,CAAC,CAChB,CAAC;IAEF,0BAA0B;IAC1B,MAAM,KAAK,GAAG,WAAW,CAAC,GAAG,EAAE;QAC7B,IAAI,aAAa,KAAK,IAAI;YAAE,OAAO;QAEnC,QAAQ,CAAC,SAAS,CAAC,EAAE;YACnB,MAAM,QAAQ,GAAG,CAAC,GAAG,SAAS,CAAC,CAAC;YAChC,MAAM,QAAQ,GAAG,QAAQ,CAAC,aAAa,CAAC,CAAC;YACzC,IAAI,QAAQ,EAAE,CAAC;gBACb,QAAQ,CAAC,aAAa,CAAC,GAAG,EAAE,GAAG,QAAQ,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC;YACzD,CAAC;YACD,OAAO,QAAQ,CAAC;QAClB,CAAC,CAAC,CAAC;QAEH,4CAA4C;QAC5C,kBAAkB,CAAC,IAAI,CAAC,CAAC;QACzB,kBAAkB,CAAC,IAAI,CAAC,CAAC;IAC3B,CAAC,EAAE,CAAC,aAAa,CAAC,CAAC,CAAC;IAEpB,oBAAoB;IACpB,MAAM,eAAe,GAAG,WAAW,CAAC,GAAG,EAAE;QACvC,OAAO,YAAY,CAAC;IACtB,CAAC,EAAE,CAAC,YAAY,CAAC,CAAC,CAAC;IAEnB,qBAAqB;IACrB,MAAM,QAAQ,GAAG,WAAW,CAAC,GAAG,EAAE;QAChC,sBAAsB;QACtB,IAAI,SAAS,GAAG,EAAE,EAAE,CAAC;YACnB,kBAAkB,CAAC,6BAA6B,CAAC,CAAC;YAClD,OAAO;QACT,CAAC;QAED,8CAA8C;QAC9C,kBAAkB,CAAC,IAAI,CAAC,CAAC;QACzB,kBAAkB,CAAC,IAAI,CAAC,CAAC;QACzB,iBAAiB,CAAC,IAAI,CAAC,CAAC;IAC1B,CAAC,EAAE,CAAC,SAAS,CAAC,CAAC,CAAC;IAEhB,uBAAuB;IACvB,MAAM,KAAK,GAAG,WAAW,CAAC,GAAG,EAAE;QAC7B,QAAQ,CAAC,gBAAgB,EAAE,CAAC,CAAC;QAC7B,gBAAgB,CAAC,IAAI,CAAC,CAAC;QACvB,kBAAkB,CAAC,IAAI,CAAC,CAAC;QACzB,kBAAkB,CAAC,IAAI,CAAC,CAAC;QACzB,iBAAiB,CAAC,KAAK,CAAC,CAAC;IAC3B,CAAC,EAAE,EAAE,CAAC,CAAC;IAEP,+BAA+B;IAC/B,MAAM,kBAAkB,GAAG,WAAW,CAAC,CAAC,MAAc,EAAE,EAAE;QACxD,QAAQ,CAAC,mBAAmB,CAAC,MAAM,CAAC,CAAC,CAAC;QACtC,gBAAgB,CAAC,IAAI,CAAC,CAAC;QACvB,kBAAkB,CAAC,IAAI,CAAC,CAAC;QACzB,kBAAkB,CAAC,IAAI,CAAC,CAAC;QACzB,iBAAiB,CAAC,KAAK,CAAC,CAAC;IAC3B,CAAC,EAAE,EAAE,CAAC,CAAC;IAEP,OAAO;QACL,KAAK;QACL,aAAa;QACb,YAAY;QACZ,eAAe;QACf,eAAe;QACf,UAAU;QACV,QAAQ;QACR,KAAK;QACL,eAAe;QACf,QAAQ;QACR,KAAK;QACL,SAAS;QACT,kBAAkB;KACnB,CAAC;AACJ,CAAC","sourcesContent":["/**\n * Hook for managing Sudoku board entry mode state\n * Allows users to enter given clues for a custom puzzle\n */\n\nimport { useCallback, useEffect, useMemo, useRef, useState } from 'react';\nimport type { SudokuCell } from '../types';\nimport { useSolverValidate } from '@sudobility/sudojo_client';\nimport type { NetworkClient } from '@sudobility/types';\n\nexport interface ValidatedPuzzle {\n puzzle: string;\n solution: string;\n}\n\nexport interface UseBoardEntryOptions {\n networkClient: NetworkClient;\n baseUrl: string;\n token: string;\n}\n\nexport interface UseBoardEntryReturn {\n /** 81 cells with given values only */\n cells: SudokuCell[];\n /** Currently selected cell index */\n selectedIndex: number | null;\n /** Whether validation is in progress */\n isValidating: boolean;\n /** Validation error message key */\n validationError: string | null;\n /** Validated puzzle data (puzzle + solution) */\n validatedPuzzle: ValidatedPuzzle | null;\n /** Select a cell by index */\n selectCell: (index: number) => void;\n /** Set a given value at the selected cell */\n setGiven: (value: number) => void;\n /** Erase the selected cell */\n erase: () => void;\n /** Get the puzzle as an 81-character string */\n getPuzzleString: () => string;\n /** Trigger validation */\n validate: () => void;\n /** Reset to empty board */\n reset: () => void;\n /** Number of clues entered */\n clueCount: number;\n /** Set cells from an 81-character puzzle string */\n setCellsFromPuzzle: (puzzle: string) => void;\n}\n\n/**\n * Create an empty 81-cell board for entry mode\n */\nfunction createEmptyBoard(): SudokuCell[] {\n return Array.from({ length: 81 }, (_, index) => ({\n index,\n solution: null,\n given: null,\n input: null,\n pencilmarks: null,\n }));\n}\n\n/**\n * Convert cells to 81-character puzzle string\n * '0' for empty, '1'-'9' for given values\n */\nfunction cellsToPuzzleString(cells: SudokuCell[]): string {\n return cells.map(cell => cell.given?.toString() ?? '0').join('');\n}\n\n/**\n * Count the number of non-empty cells (clues)\n */\nfunction countClues(cells: SudokuCell[]): number {\n return cells.filter(cell => cell.given !== null).length;\n}\n\n/**\n * Create a board from an 81-character puzzle string\n * '0' or '.' for empty, '1'-'9' for given values\n */\nfunction puzzleStringToCells(puzzle: string): SudokuCell[] {\n if (puzzle.length !== 81) {\n return createEmptyBoard();\n }\n\n return Array.from({ length: 81 }, (_, index) => {\n const char = puzzle.charAt(index);\n const value = parseInt(char, 10);\n const given = value >= 1 && value <= 9 ? value : null;\n\n return {\n index,\n solution: null,\n given,\n input: null,\n pencilmarks: null,\n };\n });\n}\n\n/**\n * Classify a validation error message into a translation key\n */\nfunction classifyValidationError(errorMsg: string): string {\n const lower = errorMsg.toLowerCase();\n if (lower.includes('multiple') || lower.includes('not unique')) {\n return 'enter.errors.multipleSolutions';\n }\n if (lower.includes('cannot solve') || lower.includes('no solution')) {\n return 'enter.errors.noSolution';\n }\n return 'enter.errors.validationFailed';\n}\n\nexport function useBoardEntry({\n networkClient,\n baseUrl,\n token,\n}: UseBoardEntryOptions): UseBoardEntryReturn {\n const [cells, setCells] = useState<SudokuCell[]>(createEmptyBoard);\n const [selectedIndex, setSelectedIndex] = useState<number | null>(null);\n const [validationError, setValidationError] = useState<string | null>(null);\n const [validatedPuzzle, setValidatedPuzzle] =\n useState<ValidatedPuzzle | null>(null);\n const [shouldValidate, setShouldValidate] = useState(false);\n\n // Compute puzzle string for validation\n const puzzleString = useMemo(() => cellsToPuzzleString(cells), [cells]);\n const clueCount = useMemo(() => countClues(cells), [cells]);\n\n // Use the solver validate hook\n const {\n data: validateData,\n error: validateError,\n isLoading: isValidating,\n } = useSolverValidate(\n networkClient,\n baseUrl,\n token,\n { original: puzzleString },\n {\n enabled: shouldValidate && clueCount >= 17,\n retry: false,\n }\n );\n\n // Track whether we've already processed this validation result\n const processedDataRef = useRef<typeof validateData | null>(null);\n const processedErrorRef = useRef<typeof validateError | null>(null);\n\n // Handle validation result\n useEffect(() => {\n if (!shouldValidate || isValidating) return;\n if (!validateData) return;\n // Avoid re-processing the same result\n if (processedDataRef.current === validateData) return;\n processedDataRef.current = validateData;\n\n // ValidateData has board.solution\n const solution = validateData.data?.board?.solution;\n if (validateData.success && solution) {\n setValidatedPuzzle({\n puzzle: puzzleString,\n solution,\n });\n setValidationError(null);\n } else if (validateData.error) {\n // Validation failed with specific error\n setValidationError(classifyValidationError(validateData.error));\n setValidatedPuzzle(null);\n } else {\n // Unexpected response - success but no solution\n console.error('Unexpected validation response:', validateData);\n setValidationError('enter.errors.validationFailed');\n setValidatedPuzzle(null);\n }\n setShouldValidate(false);\n }, [validateData, shouldValidate, isValidating, puzzleString]);\n\n // Handle network/query errors (e.g. 400 responses throw NetworkError)\n useEffect(() => {\n if (!shouldValidate || isValidating) return;\n if (!validateError) return;\n if (processedErrorRef.current === validateError) return;\n processedErrorRef.current = validateError;\n\n // Extract error message from NetworkError response body or message\n const networkErr = validateError as {\n response?: { error?: string };\n message?: string;\n };\n const errorMsg = networkErr.response?.error ?? networkErr.message ?? '';\n setValidationError(classifyValidationError(errorMsg));\n setValidatedPuzzle(null);\n setShouldValidate(false);\n }, [validateError, shouldValidate, isValidating]);\n\n // Select a cell\n const selectCell = useCallback((index: number) => {\n if (index >= 0 && index < 81) {\n setSelectedIndex(index);\n }\n }, []);\n\n // Set a given value at the selected cell\n const setGiven = useCallback(\n (value: number) => {\n if (selectedIndex === null || value < 1 || value > 9) return;\n\n setCells(prevCells => {\n const newCells = [...prevCells];\n const existing = newCells[selectedIndex];\n if (existing) {\n newCells[selectedIndex] = { ...existing, given: value };\n }\n return newCells;\n });\n\n // Clear validation state when board changes\n setValidationError(null);\n setValidatedPuzzle(null);\n },\n [selectedIndex]\n );\n\n // Erase the selected cell\n const erase = useCallback(() => {\n if (selectedIndex === null) return;\n\n setCells(prevCells => {\n const newCells = [...prevCells];\n const existing = newCells[selectedIndex];\n if (existing) {\n newCells[selectedIndex] = { ...existing, given: null };\n }\n return newCells;\n });\n\n // Clear validation state when board changes\n setValidationError(null);\n setValidatedPuzzle(null);\n }, [selectedIndex]);\n\n // Get puzzle string\n const getPuzzleString = useCallback(() => {\n return puzzleString;\n }, [puzzleString]);\n\n // Trigger validation\n const validate = useCallback(() => {\n // Check minimum clues\n if (clueCount < 17) {\n setValidationError('enter.errors.notEnoughClues');\n return;\n }\n\n // Clear previous state and trigger validation\n setValidationError(null);\n setValidatedPuzzle(null);\n setShouldValidate(true);\n }, [clueCount]);\n\n // Reset to empty board\n const reset = useCallback(() => {\n setCells(createEmptyBoard());\n setSelectedIndex(null);\n setValidationError(null);\n setValidatedPuzzle(null);\n setShouldValidate(false);\n }, []);\n\n // Set cells from puzzle string\n const setCellsFromPuzzle = useCallback((puzzle: string) => {\n setCells(puzzleStringToCells(puzzle));\n setSelectedIndex(null);\n setValidationError(null);\n setValidatedPuzzle(null);\n setShouldValidate(false);\n }, []);\n\n return {\n cells,\n selectedIndex,\n isValidating,\n validationError,\n validatedPuzzle,\n selectCell,\n setGiven,\n erase,\n getPuzzleString,\n validate,\n reset,\n clueCount,\n setCellsFromPuzzle,\n };\n}\n"]}
@@ -10,7 +10,7 @@
10
10
  * const { currentGame, startGame, updateProgress, clearGame } = useGamePlay({ slot: 'daily' });
11
11
  *
12
12
  * // Start a new game
13
- * startGame('daily', puzzle, solution, { dailyDate: '2024-01-15' });
13
+ * startGame('daily', board, solution, { dailyDate: '2024-01-15' });
14
14
  *
15
15
  * // Update progress (debounced)
16
16
  * updateProgress(inputString, pencilmarksString, isPencilMode, autoPencilmarks, elapsedTime);
@@ -19,6 +19,7 @@
19
19
  * clearGame();
20
20
  * ```
21
21
  */
22
+ import type { SolverBoard } from '@sudobility/sudojo_types';
22
23
  import type { CurrentGame, CurrentGameMeta, GameSlot, GameSource } from '../types/currentGame';
23
24
  export interface UseGamePlayOptions {
24
25
  /** Which game slot to use (default: 'play') */
@@ -32,7 +33,7 @@ export interface UseGamePlayResult {
32
33
  /** Whether there's an active game */
33
34
  hasCurrentGame: boolean;
34
35
  /** Start a new game */
35
- startGame: (source: GameSource, puzzle: string, solution: string, meta?: CurrentGameMeta) => void;
36
+ startGame: (source: GameSource, board: SolverBoard, solution: string, meta?: CurrentGameMeta) => void;
36
37
  /** Update progress (debounced internally) */
37
38
  updateProgress: (inputString: string, pencilmarksString: string, isPencilMode: boolean, autoPencilmarks: boolean, elapsedTime: number) => void;
38
39
  /** Clear current game (call on completion) */
@@ -1 +1 @@
1
- {"version":3,"file":"useGamePlay.d.ts","sourceRoot":"","sources":["../../src/hooks/useGamePlay.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAIH,OAAO,KAAK,EACV,WAAW,EACX,eAAe,EACf,QAAQ,EACR,UAAU,EACX,MAAM,sBAAsB,CAAC;AAE9B,MAAM,WAAW,kBAAkB;IACjC,+CAA+C;IAC/C,IAAI,CAAC,EAAE,QAAQ,CAAC;IAChB,yDAAyD;IACzD,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,iBAAiB;IAChC,wCAAwC;IACxC,WAAW,EAAE,WAAW,GAAG,IAAI,CAAC;IAChC,qCAAqC;IACrC,cAAc,EAAE,OAAO,CAAC;IACxB,uBAAuB;IACvB,SAAS,EAAE,CACT,MAAM,EAAE,UAAU,EAClB,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,EAChB,IAAI,CAAC,EAAE,eAAe,KACnB,IAAI,CAAC;IACV,6CAA6C;IAC7C,cAAc,EAAE,CACd,WAAW,EAAE,MAAM,EACnB,iBAAiB,EAAE,MAAM,EACzB,YAAY,EAAE,OAAO,EACrB,eAAe,EAAE,OAAO,EACxB,WAAW,EAAE,MAAM,KAChB,IAAI,CAAC;IACV,8CAA8C;IAC9C,SAAS,EAAE,MAAM,IAAI,CAAC;CACvB;AAED,wBAAgB,WAAW,CACzB,OAAO,GAAE,kBAAuB,GAC/B,iBAAiB,CA2GnB"}
1
+ {"version":3,"file":"useGamePlay.d.ts","sourceRoot":"","sources":["../../src/hooks/useGamePlay.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAGH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAE5D,OAAO,KAAK,EACV,WAAW,EACX,eAAe,EACf,QAAQ,EACR,UAAU,EACX,MAAM,sBAAsB,CAAC;AAE9B,MAAM,WAAW,kBAAkB;IACjC,+CAA+C;IAC/C,IAAI,CAAC,EAAE,QAAQ,CAAC;IAChB,yDAAyD;IACzD,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,iBAAiB;IAChC,wCAAwC;IACxC,WAAW,EAAE,WAAW,GAAG,IAAI,CAAC;IAChC,qCAAqC;IACrC,cAAc,EAAE,OAAO,CAAC;IACxB,uBAAuB;IACvB,SAAS,EAAE,CACT,MAAM,EAAE,UAAU,EAClB,KAAK,EAAE,WAAW,EAClB,QAAQ,EAAE,MAAM,EAChB,IAAI,CAAC,EAAE,eAAe,KACnB,IAAI,CAAC;IACV,6CAA6C;IAC7C,cAAc,EAAE,CACd,WAAW,EAAE,MAAM,EACnB,iBAAiB,EAAE,MAAM,EACzB,YAAY,EAAE,OAAO,EACrB,eAAe,EAAE,OAAO,EACxB,WAAW,EAAE,MAAM,KAChB,IAAI,CAAC;IACV,8CAA8C;IAC9C,SAAS,EAAE,MAAM,IAAI,CAAC;CACvB;AAED,wBAAgB,WAAW,CACzB,OAAO,GAAE,kBAAuB,GAC/B,iBAAiB,CA2GnB"}
@@ -10,7 +10,7 @@
10
10
  * const { currentGame, startGame, updateProgress, clearGame } = useGamePlay({ slot: 'daily' });
11
11
  *
12
12
  * // Start a new game
13
- * startGame('daily', puzzle, solution, { dailyDate: '2024-01-15' });
13
+ * startGame('daily', board, solution, { dailyDate: '2024-01-15' });
14
14
  *
15
15
  * // Update progress (debounced)
16
16
  * updateProgress(inputString, pencilmarksString, isPencilMode, autoPencilmarks, elapsedTime);
@@ -28,10 +28,10 @@ export function useGamePlay(options = {}) {
28
28
  const timeoutRef = useRef(null);
29
29
  const pendingUpdate = useRef(null);
30
30
  // Curry slot into startGame so callers keep the same signature
31
- const startGame = useCallback((source, puzzle, solution, meta) => {
31
+ const startGame = useCallback((source, board, solution, meta) => {
32
32
  useGamePlayStore
33
33
  .getState()
34
- .startGame(slot, source, puzzle, solution, meta);
34
+ .startGame(slot, source, board, solution, meta);
35
35
  }, [slot]);
36
36
  // Curry slot into clearGame
37
37
  const clearGame = useCallback(() => {
@@ -1 +1 @@
1
- {"version":3,"file":"useGamePlay.js","sourceRoot":"","sources":["../../src/hooks/useGamePlay.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Hook for managing the current game\n *\n * Provides a clean API for starting, updating, and clearing the current game.\n * The `slot` option selects which independent store slot to use ('daily' or 'play'),\n * keeping the returned function signatures identical to before.\n *\n * @example\n * ```tsx\n * const { currentGame, startGame, updateProgress, clearGame } = useGamePlay({ slot: 'daily' });\n *\n * // Start a new game\n * startGame('daily', puzzle, solution, { dailyDate: '2024-01-15' });\n *\n * // Update progress (debounced)\n * updateProgress(inputString, pencilmarksString, isPencilMode, autoPencilmarks, elapsedTime);\n *\n * // Clear on completion\n * clearGame();\n * ```\n */\n\nimport { useCallback, useEffect, useRef } from 'react';\nimport { useGamePlayStore } from '../stores/gamePlayStore';\nimport type {\n CurrentGame,\n CurrentGameMeta,\n GameSlot,\n GameSource,\n} from '../types/currentGame';\n\nexport interface UseGamePlayOptions {\n /** Which game slot to use (default: 'play') */\n slot?: GameSlot;\n /** Debounce delay for auto-save in ms (default: 2000) */\n autoSaveDelay?: number;\n}\n\nexport interface UseGamePlayResult {\n /** Current game state (null if none) */\n currentGame: CurrentGame | null;\n /** Whether there's an active game */\n hasCurrentGame: boolean;\n /** Start a new game */\n startGame: (\n source: GameSource,\n puzzle: string,\n solution: string,\n meta?: CurrentGameMeta\n ) => void;\n /** Update progress (debounced internally) */\n updateProgress: (\n inputString: string,\n pencilmarksString: string,\n isPencilMode: boolean,\n autoPencilmarks: boolean,\n elapsedTime: number\n ) => void;\n /** Clear current game (call on completion) */\n clearGame: () => void;\n}\n\nexport function useGamePlay(\n options: UseGamePlayOptions = {}\n): UseGamePlayResult {\n const { slot = 'play', autoSaveDelay = 2000 } = options;\n\n // Select the correct slot from the store\n const currentGame = useGamePlayStore(s =>\n slot === 'daily' ? s.dailyGame : s.playGame\n );\n\n const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n const pendingUpdate = useRef<\n [string, string, boolean, boolean, number] | null\n >(null);\n\n // Curry slot into startGame so callers keep the same signature\n const startGame = useCallback(\n (\n source: GameSource,\n puzzle: string,\n solution: string,\n meta?: CurrentGameMeta\n ) => {\n useGamePlayStore\n .getState()\n .startGame(slot, source, puzzle, solution, meta);\n },\n [slot]\n );\n\n // Curry slot into clearGame\n const clearGame = useCallback(() => {\n useGamePlayStore.getState().clearGame(slot);\n }, [slot]);\n\n // Throttled update — saves at most every autoSaveDelay ms.\n // Unlike debounce, throttle ensures periodic saves even when called\n // continuously (e.g. every second from the game timer).\n const updateProgress = useCallback(\n (\n inputString: string,\n pencilmarksString: string,\n isPencilMode: boolean,\n autoPencilmarks: boolean,\n elapsedTime: number\n ) => {\n pendingUpdate.current = [\n inputString,\n pencilmarksString,\n isPencilMode,\n autoPencilmarks,\n elapsedTime,\n ];\n\n // Only start a new timer if one isn't already running (throttle)\n if (!timeoutRef.current) {\n timeoutRef.current = setTimeout(() => {\n if (pendingUpdate.current) {\n useGamePlayStore\n .getState()\n .updateProgress(slot, ...pendingUpdate.current);\n pendingUpdate.current = null;\n }\n timeoutRef.current = null;\n }, autoSaveDelay);\n }\n },\n [slot, autoSaveDelay]\n );\n\n // Flush pending updates on unmount and beforeunload\n useEffect(() => {\n const flush = () => {\n if (timeoutRef.current) {\n clearTimeout(timeoutRef.current);\n timeoutRef.current = null;\n }\n if (pendingUpdate.current) {\n useGamePlayStore\n .getState()\n .updateProgress(slot, ...pendingUpdate.current);\n pendingUpdate.current = null;\n }\n };\n\n if (\n typeof window !== 'undefined' &&\n typeof window.addEventListener === 'function'\n ) {\n window.addEventListener('beforeunload', flush);\n }\n return () => {\n if (\n typeof window !== 'undefined' &&\n typeof window.removeEventListener === 'function'\n ) {\n window.removeEventListener('beforeunload', flush);\n }\n flush();\n };\n }, [slot]);\n\n return {\n currentGame,\n hasCurrentGame: currentGame !== null,\n startGame,\n updateProgress,\n clearGame,\n };\n}\n"]}
1
+ {"version":3,"file":"useGamePlay.js","sourceRoot":"","sources":["../../src/hooks/useGamePlay.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Hook for managing the current game\n *\n * Provides a clean API for starting, updating, and clearing the current game.\n * The `slot` option selects which independent store slot to use ('daily' or 'play'),\n * keeping the returned function signatures identical to before.\n *\n * @example\n * ```tsx\n * const { currentGame, startGame, updateProgress, clearGame } = useGamePlay({ slot: 'daily' });\n *\n * // Start a new game\n * startGame('daily', board, solution, { dailyDate: '2024-01-15' });\n *\n * // Update progress (debounced)\n * updateProgress(inputString, pencilmarksString, isPencilMode, autoPencilmarks, elapsedTime);\n *\n * // Clear on completion\n * clearGame();\n * ```\n */\n\nimport { useCallback, useEffect, useRef } from 'react';\nimport type { SolverBoard } from '@sudobility/sudojo_types';\nimport { useGamePlayStore } from '../stores/gamePlayStore';\nimport type {\n CurrentGame,\n CurrentGameMeta,\n GameSlot,\n GameSource,\n} from '../types/currentGame';\n\nexport interface UseGamePlayOptions {\n /** Which game slot to use (default: 'play') */\n slot?: GameSlot;\n /** Debounce delay for auto-save in ms (default: 2000) */\n autoSaveDelay?: number;\n}\n\nexport interface UseGamePlayResult {\n /** Current game state (null if none) */\n currentGame: CurrentGame | null;\n /** Whether there's an active game */\n hasCurrentGame: boolean;\n /** Start a new game */\n startGame: (\n source: GameSource,\n board: SolverBoard,\n solution: string,\n meta?: CurrentGameMeta\n ) => void;\n /** Update progress (debounced internally) */\n updateProgress: (\n inputString: string,\n pencilmarksString: string,\n isPencilMode: boolean,\n autoPencilmarks: boolean,\n elapsedTime: number\n ) => void;\n /** Clear current game (call on completion) */\n clearGame: () => void;\n}\n\nexport function useGamePlay(\n options: UseGamePlayOptions = {}\n): UseGamePlayResult {\n const { slot = 'play', autoSaveDelay = 2000 } = options;\n\n // Select the correct slot from the store\n const currentGame = useGamePlayStore(s =>\n slot === 'daily' ? s.dailyGame : s.playGame\n );\n\n const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n const pendingUpdate = useRef<\n [string, string, boolean, boolean, number] | null\n >(null);\n\n // Curry slot into startGame so callers keep the same signature\n const startGame = useCallback(\n (\n source: GameSource,\n board: SolverBoard,\n solution: string,\n meta?: CurrentGameMeta\n ) => {\n useGamePlayStore\n .getState()\n .startGame(slot, source, board, solution, meta);\n },\n [slot]\n );\n\n // Curry slot into clearGame\n const clearGame = useCallback(() => {\n useGamePlayStore.getState().clearGame(slot);\n }, [slot]);\n\n // Throttled update — saves at most every autoSaveDelay ms.\n // Unlike debounce, throttle ensures periodic saves even when called\n // continuously (e.g. every second from the game timer).\n const updateProgress = useCallback(\n (\n inputString: string,\n pencilmarksString: string,\n isPencilMode: boolean,\n autoPencilmarks: boolean,\n elapsedTime: number\n ) => {\n pendingUpdate.current = [\n inputString,\n pencilmarksString,\n isPencilMode,\n autoPencilmarks,\n elapsedTime,\n ];\n\n // Only start a new timer if one isn't already running (throttle)\n if (!timeoutRef.current) {\n timeoutRef.current = setTimeout(() => {\n if (pendingUpdate.current) {\n useGamePlayStore\n .getState()\n .updateProgress(slot, ...pendingUpdate.current);\n pendingUpdate.current = null;\n }\n timeoutRef.current = null;\n }, autoSaveDelay);\n }\n },\n [slot, autoSaveDelay]\n );\n\n // Flush pending updates on unmount and beforeunload\n useEffect(() => {\n const flush = () => {\n if (timeoutRef.current) {\n clearTimeout(timeoutRef.current);\n timeoutRef.current = null;\n }\n if (pendingUpdate.current) {\n useGamePlayStore\n .getState()\n .updateProgress(slot, ...pendingUpdate.current);\n pendingUpdate.current = null;\n }\n };\n\n if (\n typeof window !== 'undefined' &&\n typeof window.addEventListener === 'function'\n ) {\n window.addEventListener('beforeunload', flush);\n }\n return () => {\n if (\n typeof window !== 'undefined' &&\n typeof window.removeEventListener === 'function'\n ) {\n window.removeEventListener('beforeunload', flush);\n }\n flush();\n };\n }, [slot]);\n\n return {\n currentGame,\n hasCurrentGame: currentGame !== null,\n startGame,\n updateProgress,\n clearGame,\n };\n}\n"]}
@@ -4,6 +4,7 @@
4
4
  * Persists two independent game slots (daily / play) to localStorage
5
5
  * so users can resume where they left off without one overwriting the other.
6
6
  */
7
+ import type { SolverBoard } from '@sudobility/sudojo_types';
7
8
  import type { CurrentGame, CurrentGameMeta, GameSlot, GameSource } from '../types/currentGame';
8
9
  export interface GamePlayState {
9
10
  /** Daily game slot (null if none) */
@@ -11,7 +12,7 @@ export interface GamePlayState {
11
12
  /** Play game slot — levels & entered puzzles (null if none) */
12
13
  playGame: CurrentGame | null;
13
14
  /** Start a new game in the given slot */
14
- startGame: (slot: GameSlot, source: GameSource, puzzle: string, solution: string, meta?: CurrentGameMeta) => void;
15
+ startGame: (slot: GameSlot, source: GameSource, board: SolverBoard, solution: string, meta?: CurrentGameMeta) => void;
15
16
  /** Update game progress for the given slot */
16
17
  updateProgress: (slot: GameSlot, inputString: string, pencilmarksString: string, isPencilMode: boolean, autoPencilmarks: boolean, elapsedTime: number) => void;
17
18
  /** Clear the game in the given slot */
@@ -1 +1 @@
1
- {"version":3,"file":"gamePlayStore.d.ts","sourceRoot":"","sources":["../../src/stores/gamePlayStore.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH,OAAO,KAAK,EACV,WAAW,EACX,eAAe,EACf,QAAQ,EACR,UAAU,EACX,MAAM,sBAAsB,CAAC;AAO9B,MAAM,WAAW,aAAa;IAC5B,qCAAqC;IACrC,SAAS,EAAE,WAAW,GAAG,IAAI,CAAC;IAC9B,+DAA+D;IAC/D,QAAQ,EAAE,WAAW,GAAG,IAAI,CAAC;IAE7B,yCAAyC;IACzC,SAAS,EAAE,CACT,IAAI,EAAE,QAAQ,EACd,MAAM,EAAE,UAAU,EAClB,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,EAChB,IAAI,CAAC,EAAE,eAAe,KACnB,IAAI,CAAC;IAEV,8CAA8C;IAC9C,cAAc,EAAE,CACd,IAAI,EAAE,QAAQ,EACd,WAAW,EAAE,MAAM,EACnB,iBAAiB,EAAE,MAAM,EACzB,YAAY,EAAE,OAAO,EACrB,eAAe,EAAE,OAAO,EACxB,WAAW,EAAE,MAAM,KAChB,IAAI,CAAC;IAEV,uCAAuC;IACvC,SAAS,EAAE,CAAC,IAAI,EAAE,QAAQ,KAAK,IAAI,CAAC;CACrC;AAED,eAAO,MAAM,gBAAgB;;;;;;;;;;;;EA+F5B,CAAC"}
1
+ {"version":3,"file":"gamePlayStore.d.ts","sourceRoot":"","sources":["../../src/stores/gamePlayStore.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAC5D,OAAO,KAAK,EACV,WAAW,EACX,eAAe,EACf,QAAQ,EACR,UAAU,EACX,MAAM,sBAAsB,CAAC;AAO9B,MAAM,WAAW,aAAa;IAC5B,qCAAqC;IACrC,SAAS,EAAE,WAAW,GAAG,IAAI,CAAC;IAC9B,+DAA+D;IAC/D,QAAQ,EAAE,WAAW,GAAG,IAAI,CAAC;IAE7B,yCAAyC;IACzC,SAAS,EAAE,CACT,IAAI,EAAE,QAAQ,EACd,MAAM,EAAE,UAAU,EAClB,KAAK,EAAE,WAAW,EAClB,QAAQ,EAAE,MAAM,EAChB,IAAI,CAAC,EAAE,eAAe,KACnB,IAAI,CAAC;IAEV,8CAA8C;IAC9C,cAAc,EAAE,CACd,IAAI,EAAE,QAAQ,EACd,WAAW,EAAE,MAAM,EACnB,iBAAiB,EAAE,MAAM,EACzB,YAAY,EAAE,OAAO,EACrB,eAAe,EAAE,OAAO,EACxB,WAAW,EAAE,MAAM,KAChB,IAAI,CAAC;IAEV,uCAAuC;IACvC,SAAS,EAAE,CAAC,IAAI,EAAE,QAAQ,KAAK,IAAI,CAAC;CACrC;AAED,eAAO,MAAM,gBAAgB;;;;;;;;;;;;EA+F5B,CAAC"}
@@ -10,18 +10,18 @@ const slotField = (slot) => slot === 'daily' ? 'dailyGame' : 'playGame';
10
10
  export const useGamePlayStore = create()(persist((set, get) => ({
11
11
  dailyGame: null,
12
12
  playGame: null,
13
- startGame: (slot, source, puzzle, solution, meta = {}) => {
13
+ startGame: (slot, source, board, solution, meta = {}) => {
14
14
  const now = new Date().toISOString();
15
15
  set({
16
16
  [slotField(slot)]: {
17
17
  source,
18
- puzzle,
18
+ puzzle: board.original,
19
19
  solution,
20
20
  meta,
21
21
  inputString: '0'.repeat(81),
22
- pencilmarksString: '',
22
+ pencilmarksString: board.pencilmark.numbers,
23
23
  isPencilMode: false,
24
- autoPencilmarks: false,
24
+ autoPencilmarks: board.pencilmark.autopencil,
25
25
  elapsedTime: 0,
26
26
  startedAt: now,
27
27
  updatedAt: now,
@@ -1 +1 @@
1
- {"version":3,"file":"gamePlayStore.js","sourceRoot":"","sources":["../../src/stores/gamePlayStore.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Zustand store for current game state\n *\n * Persists two independent game slots (daily / play) to localStorage\n * so users can resume where they left off without one overwriting the other.\n */\n\nimport { create } from 'zustand';\nimport { persist } from 'zustand/middleware';\nimport type {\n CurrentGame,\n CurrentGameMeta,\n GameSlot,\n GameSource,\n} from '../types/currentGame';\n\ntype SlotField = 'dailyGame' | 'playGame';\n\nconst slotField = (slot: GameSlot): SlotField =>\n slot === 'daily' ? 'dailyGame' : 'playGame';\n\nexport interface GamePlayState {\n /** Daily game slot (null if none) */\n dailyGame: CurrentGame | null;\n /** Play game slot — levels & entered puzzles (null if none) */\n playGame: CurrentGame | null;\n\n /** Start a new game in the given slot */\n startGame: (\n slot: GameSlot,\n source: GameSource,\n puzzle: string,\n solution: string,\n meta?: CurrentGameMeta\n ) => void;\n\n /** Update game progress for the given slot */\n updateProgress: (\n slot: GameSlot,\n inputString: string,\n pencilmarksString: string,\n isPencilMode: boolean,\n autoPencilmarks: boolean,\n elapsedTime: number\n ) => void;\n\n /** Clear the game in the given slot */\n clearGame: (slot: GameSlot) => void;\n}\n\nexport const useGamePlayStore = create<GamePlayState>()(\n persist(\n (set, get) => ({\n dailyGame: null,\n playGame: null,\n\n startGame: (slot, source, puzzle, solution, meta = {}) => {\n const now = new Date().toISOString();\n set({\n [slotField(slot)]: {\n source,\n puzzle,\n solution,\n meta,\n inputString: '0'.repeat(81),\n pencilmarksString: '',\n isPencilMode: false,\n autoPencilmarks: false,\n elapsedTime: 0,\n startedAt: now,\n updatedAt: now,\n },\n });\n },\n\n updateProgress: (\n slot,\n inputString,\n pencilmarksString,\n isPencilMode,\n autoPencilmarks,\n elapsedTime\n ) => {\n const field = slotField(slot);\n const current = get()[field];\n if (!current) return;\n\n set({\n [field]: {\n ...current,\n inputString,\n pencilmarksString,\n isPencilMode,\n autoPencilmarks,\n elapsedTime,\n updatedAt: new Date().toISOString(),\n },\n });\n },\n\n clearGame: slot => set({ [slotField(slot)]: null }),\n }),\n {\n name: 'sudojo-current-game',\n version: 2,\n migrate: (persistedState: unknown, version: number) => {\n if (version === 0) {\n // v0 had a single `currentGame` field — move it to the right slot\n const old = persistedState as {\n currentGame?: CurrentGame | null;\n };\n const game = old.currentGame ?? null;\n const isDailySource = game?.source === 'daily';\n\n // Remove the old field and populate the new slots\n const { currentGame: _, ...rest } = old as Record<string, unknown>;\n return {\n ...rest,\n dailyGame: isDailySource ? game : null,\n playGame: isDailySource ? null : game,\n };\n }\n if (version === 1) {\n const state = persistedState as {\n dailyGame?: CurrentGame | null;\n playGame?: CurrentGame | null;\n };\n const addAuto = (game: CurrentGame | null | undefined) =>\n game\n ? {\n ...game,\n autoPencilmarks:\n 'autoPencilmarks' in game ? game.autoPencilmarks : false,\n }\n : (game ?? null);\n return {\n ...state,\n dailyGame: addAuto(state.dailyGame),\n playGame: addAuto(state.playGame),\n };\n }\n return persistedState;\n },\n }\n )\n);\n"]}
1
+ {"version":3,"file":"gamePlayStore.js","sourceRoot":"","sources":["../../src/stores/gamePlayStore.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Zustand store for current game state\n *\n * Persists two independent game slots (daily / play) to localStorage\n * so users can resume where they left off without one overwriting the other.\n */\n\nimport { create } from 'zustand';\nimport { persist } from 'zustand/middleware';\nimport type { SolverBoard } from '@sudobility/sudojo_types';\nimport type {\n CurrentGame,\n CurrentGameMeta,\n GameSlot,\n GameSource,\n} from '../types/currentGame';\n\ntype SlotField = 'dailyGame' | 'playGame';\n\nconst slotField = (slot: GameSlot): SlotField =>\n slot === 'daily' ? 'dailyGame' : 'playGame';\n\nexport interface GamePlayState {\n /** Daily game slot (null if none) */\n dailyGame: CurrentGame | null;\n /** Play game slot — levels & entered puzzles (null if none) */\n playGame: CurrentGame | null;\n\n /** Start a new game in the given slot */\n startGame: (\n slot: GameSlot,\n source: GameSource,\n board: SolverBoard,\n solution: string,\n meta?: CurrentGameMeta\n ) => void;\n\n /** Update game progress for the given slot */\n updateProgress: (\n slot: GameSlot,\n inputString: string,\n pencilmarksString: string,\n isPencilMode: boolean,\n autoPencilmarks: boolean,\n elapsedTime: number\n ) => void;\n\n /** Clear the game in the given slot */\n clearGame: (slot: GameSlot) => void;\n}\n\nexport const useGamePlayStore = create<GamePlayState>()(\n persist(\n (set, get) => ({\n dailyGame: null,\n playGame: null,\n\n startGame: (slot, source, board, solution, meta = {}) => {\n const now = new Date().toISOString();\n set({\n [slotField(slot)]: {\n source,\n puzzle: board.original,\n solution,\n meta,\n inputString: '0'.repeat(81),\n pencilmarksString: board.pencilmark.numbers,\n isPencilMode: false,\n autoPencilmarks: board.pencilmark.autopencil,\n elapsedTime: 0,\n startedAt: now,\n updatedAt: now,\n },\n });\n },\n\n updateProgress: (\n slot,\n inputString,\n pencilmarksString,\n isPencilMode,\n autoPencilmarks,\n elapsedTime\n ) => {\n const field = slotField(slot);\n const current = get()[field];\n if (!current) return;\n\n set({\n [field]: {\n ...current,\n inputString,\n pencilmarksString,\n isPencilMode,\n autoPencilmarks,\n elapsedTime,\n updatedAt: new Date().toISOString(),\n },\n });\n },\n\n clearGame: slot => set({ [slotField(slot)]: null }),\n }),\n {\n name: 'sudojo-current-game',\n version: 2,\n migrate: (persistedState: unknown, version: number) => {\n if (version === 0) {\n // v0 had a single `currentGame` field — move it to the right slot\n const old = persistedState as {\n currentGame?: CurrentGame | null;\n };\n const game = old.currentGame ?? null;\n const isDailySource = game?.source === 'daily';\n\n // Remove the old field and populate the new slots\n const { currentGame: _, ...rest } = old as Record<string, unknown>;\n return {\n ...rest,\n dailyGame: isDailySource ? game : null,\n playGame: isDailySource ? null : game,\n };\n }\n if (version === 1) {\n const state = persistedState as {\n dailyGame?: CurrentGame | null;\n playGame?: CurrentGame | null;\n };\n const addAuto = (game: CurrentGame | null | undefined) =>\n game\n ? {\n ...game,\n autoPencilmarks:\n 'autoPencilmarks' in game ? game.autoPencilmarks : false,\n }\n : (game ?? null);\n return {\n ...state,\n dailyGame: addAuto(state.dailyGame),\n playGame: addAuto(state.playGame),\n };\n }\n return persistedState;\n },\n }\n )\n);\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sudobility/sudojo_lib",
3
- "version": "0.0.102",
3
+ "version": "0.0.104",
4
4
  "description": "Business logic library for Sudojo app",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",