@sudobility/sudojo_lib 0.0.101 → 0.0.103

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/README.md ADDED
@@ -0,0 +1,55 @@
1
+ # @sudobility/sudojo_lib
2
+
3
+ Business logic library for Sudojo with React hooks for game state, hints, and Sudoku operations.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ bun add @sudobility/sudojo_lib
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```typescript
14
+ import { useBoard, useHint, useGameSession } from '@sudobility/sudojo_lib';
15
+
16
+ function GameComponent() {
17
+ const { board, setCell, clearCell } = useBoard(initialPuzzle);
18
+ const { getHint, isLoading } = useHint();
19
+ const { startGame, endGame, timer } = useGameSession();
20
+ }
21
+ ```
22
+
23
+ ```typescript
24
+ import { validateBoard, isBoardComplete, getCandidates } from '@sudobility/sudojo_lib';
25
+
26
+ const isValid = validateBoard(board);
27
+ const candidates = getCandidates(board, row, col);
28
+ ```
29
+
30
+ ## API
31
+
32
+ - **Hooks**: `useBoard`, `useHint`, `useGameSession`, `useSudoku` (replaces deprecated `useGame`)
33
+ - **Utilities**: `validateBoard`, `isBoardComplete`, `getCandidates`, and more
34
+ - Shared between web (`sudojo_app`) and mobile (`sudojo_app_rn`) apps
35
+
36
+ ## Development
37
+
38
+ ```bash
39
+ bun run build # Build to dist/
40
+ bun run test # Run Vitest
41
+ bun run test:run # Run tests once
42
+ bun run typecheck # TypeScript check
43
+ bun run lint # ESLint
44
+ bun run check-all # Lint + typecheck + tests
45
+ ```
46
+
47
+ ## Related Packages
48
+
49
+ - `@sudobility/sudojo_client` -- API client hooks (peer dependency)
50
+ - `@sudobility/sudojo_types` -- Type definitions
51
+ - `sudojo_app` / `sudojo_app_rn` -- Consumer apps
52
+
53
+ ## License
54
+
55
+ BUSL-1.1
@@ -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":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AACjC,OAAO,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAU7C,MAAM,SAAS,GAAG,CAAC,IAAc,EAAa,EAAE,CAC9C,IAAI,KAAK,OAAO,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,UAAU,CAAC;AA+B9C,MAAM,CAAC,MAAM,gBAAgB,GAAG,MAAM,EAAiB,CACrD,OAAO,CACL,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE,CAAC,CAAC;IACb,SAAS,EAAE,IAAI;IACf,QAAQ,EAAE,IAAI;IAEd,SAAS,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,GAAG,EAAE,EAAE,EAAE;QACvD,MAAM,GAAG,GAAG,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;QACrC,GAAG,CAAC;YACF,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,EAAE;gBACjB,MAAM;gBACN,MAAM;gBACN,QAAQ;gBACR,IAAI;gBACJ,WAAW,EAAE,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC;gBAC3B,iBAAiB,EAAE,EAAE;gBACrB,YAAY,EAAE,KAAK;gBACnB,eAAe,EAAE,KAAK;gBACtB,WAAW,EAAE,CAAC;gBACd,SAAS,EAAE,GAAG;gBACd,SAAS,EAAE,GAAG;aACf;SACF,CAAC,CAAC;IACL,CAAC;IAED,cAAc,EAAE,CACd,IAAI,EACJ,WAAW,EACX,iBAAiB,EACjB,YAAY,EACZ,eAAe,EACf,WAAW,EACX,EAAE;QACF,MAAM,KAAK,GAAG,SAAS,CAAC,IAAI,CAAC,CAAC;QAC9B,MAAM,OAAO,GAAG,GAAG,EAAE,CAAC,KAAK,CAAC,CAAC;QAC7B,IAAI,CAAC,OAAO;YAAE,OAAO;QAErB,GAAG,CAAC;YACF,CAAC,KAAK,CAAC,EAAE;gBACP,GAAG,OAAO;gBACV,WAAW;gBACX,iBAAiB;gBACjB,YAAY;gBACZ,eAAe;gBACf,WAAW;gBACX,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;aACpC;SACF,CAAC,CAAC;IACL,CAAC;IAED,SAAS,EAAE,IAAI,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC;CACpD,CAAC,EACF;IACE,IAAI,EAAE,qBAAqB;IAC3B,OAAO,EAAE,CAAC;IACV,OAAO,EAAE,CAAC,cAAuB,EAAE,OAAe,EAAE,EAAE;QACpD,IAAI,OAAO,KAAK,CAAC,EAAE,CAAC;YAClB,kEAAkE;YAClE,MAAM,GAAG,GAAG,cAEX,CAAC;YACF,MAAM,IAAI,GAAG,GAAG,CAAC,WAAW,IAAI,IAAI,CAAC;YACrC,MAAM,aAAa,GAAG,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC;YAE/C,kDAAkD;YAClD,MAAM,EAAE,WAAW,EAAE,CAAC,EAAE,GAAG,IAAI,EAAE,GAAG,GAA8B,CAAC;YACnE,OAAO;gBACL,GAAG,IAAI;gBACP,SAAS,EAAE,aAAa,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI;gBACtC,QAAQ,EAAE,aAAa,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI;aACtC,CAAC;QACJ,CAAC;QACD,IAAI,OAAO,KAAK,CAAC,EAAE,CAAC;YAClB,MAAM,KAAK,GAAG,cAGb,CAAC;YACF,MAAM,OAAO,GAAG,CAAC,IAAoC,EAAE,EAAE,CACvD,IAAI;gBACF,CAAC,CAAC;oBACE,GAAG,IAAI;oBACP,eAAe,EACb,iBAAiB,IAAI,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC,CAAC,KAAK;iBAC3D;gBACH,CAAC,CAAC,CAAC,IAAI,IAAI,IAAI,CAAC,CAAC;YACrB,OAAO;gBACL,GAAG,KAAK;gBACR,SAAS,EAAE,OAAO,CAAC,KAAK,CAAC,SAAS,CAAC;gBACnC,QAAQ,EAAE,OAAO,CAAC,KAAK,CAAC,QAAQ,CAAC;aAClC,CAAC;QACJ,CAAC;QACD,OAAO,cAAc,CAAC;IACxB,CAAC;CACF,CACF,CACF,CAAC","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":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AACjC,OAAO,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAW7C,MAAM,SAAS,GAAG,CAAC,IAAc,EAAa,EAAE,CAC9C,IAAI,KAAK,OAAO,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,UAAU,CAAC;AA+B9C,MAAM,CAAC,MAAM,gBAAgB,GAAG,MAAM,EAAiB,CACrD,OAAO,CACL,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE,CAAC,CAAC;IACb,SAAS,EAAE,IAAI;IACf,QAAQ,EAAE,IAAI;IAEd,SAAS,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,QAAQ,EAAE,IAAI,GAAG,EAAE,EAAE,EAAE;QACtD,MAAM,GAAG,GAAG,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;QACrC,GAAG,CAAC;YACF,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,EAAE;gBACjB,MAAM;gBACN,MAAM,EAAE,KAAK,CAAC,QAAQ;gBACtB,QAAQ;gBACR,IAAI;gBACJ,WAAW,EAAE,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC;gBAC3B,iBAAiB,EAAE,KAAK,CAAC,UAAU,CAAC,OAAO;gBAC3C,YAAY,EAAE,KAAK;gBACnB,eAAe,EAAE,KAAK,CAAC,UAAU,CAAC,UAAU;gBAC5C,WAAW,EAAE,CAAC;gBACd,SAAS,EAAE,GAAG;gBACd,SAAS,EAAE,GAAG;aACf;SACF,CAAC,CAAC;IACL,CAAC;IAED,cAAc,EAAE,CACd,IAAI,EACJ,WAAW,EACX,iBAAiB,EACjB,YAAY,EACZ,eAAe,EACf,WAAW,EACX,EAAE;QACF,MAAM,KAAK,GAAG,SAAS,CAAC,IAAI,CAAC,CAAC;QAC9B,MAAM,OAAO,GAAG,GAAG,EAAE,CAAC,KAAK,CAAC,CAAC;QAC7B,IAAI,CAAC,OAAO;YAAE,OAAO;QAErB,GAAG,CAAC;YACF,CAAC,KAAK,CAAC,EAAE;gBACP,GAAG,OAAO;gBACV,WAAW;gBACX,iBAAiB;gBACjB,YAAY;gBACZ,eAAe;gBACf,WAAW;gBACX,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;aACpC;SACF,CAAC,CAAC;IACL,CAAC;IAED,SAAS,EAAE,IAAI,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC;CACpD,CAAC,EACF;IACE,IAAI,EAAE,qBAAqB;IAC3B,OAAO,EAAE,CAAC;IACV,OAAO,EAAE,CAAC,cAAuB,EAAE,OAAe,EAAE,EAAE;QACpD,IAAI,OAAO,KAAK,CAAC,EAAE,CAAC;YAClB,kEAAkE;YAClE,MAAM,GAAG,GAAG,cAEX,CAAC;YACF,MAAM,IAAI,GAAG,GAAG,CAAC,WAAW,IAAI,IAAI,CAAC;YACrC,MAAM,aAAa,GAAG,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC;YAE/C,kDAAkD;YAClD,MAAM,EAAE,WAAW,EAAE,CAAC,EAAE,GAAG,IAAI,EAAE,GAAG,GAA8B,CAAC;YACnE,OAAO;gBACL,GAAG,IAAI;gBACP,SAAS,EAAE,aAAa,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI;gBACtC,QAAQ,EAAE,aAAa,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI;aACtC,CAAC;QACJ,CAAC;QACD,IAAI,OAAO,KAAK,CAAC,EAAE,CAAC;YAClB,MAAM,KAAK,GAAG,cAGb,CAAC;YACF,MAAM,OAAO,GAAG,CAAC,IAAoC,EAAE,EAAE,CACvD,IAAI;gBACF,CAAC,CAAC;oBACE,GAAG,IAAI;oBACP,eAAe,EACb,iBAAiB,IAAI,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC,CAAC,KAAK;iBAC3D;gBACH,CAAC,CAAC,CAAC,IAAI,IAAI,IAAI,CAAC,CAAC;YACrB,OAAO;gBACL,GAAG,KAAK;gBACR,SAAS,EAAE,OAAO,CAAC,KAAK,CAAC,SAAS,CAAC;gBACnC,QAAQ,EAAE,OAAO,CAAC,KAAK,CAAC,QAAQ,CAAC;aAClC,CAAC;QACJ,CAAC;QACD,OAAO,cAAc,CAAC;IACxB,CAAC;CACF,CACF,CACF,CAAC","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.101",
3
+ "version": "0.0.103",
4
4
  "description": "Business logic library for Sudojo app",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -46,10 +46,10 @@
46
46
  "author": "Sudobility team",
47
47
  "license": "BUSL-1.1",
48
48
  "devDependencies": {
49
- "@sudobility/di": "^1.5.43",
50
- "@sudobility/sudojo_client": "^0.0.85",
51
- "@sudobility/sudojo_types": "^1.2.36",
52
- "@sudobility/types": "^1.9.55",
49
+ "@sudobility/di": "^1.5.45",
50
+ "@sudobility/sudojo_client": "^0.0.86",
51
+ "@sudobility/sudojo_types": "^1.2.37",
52
+ "@sudobility/types": "^1.9.57",
53
53
  "@tanstack/react-query": "^5.90.11",
54
54
  "@testing-library/dom": "^10.4.1",
55
55
  "@testing-library/react": "^16.3.0",
@@ -72,10 +72,10 @@
72
72
  "zustand": "^5.0.9"
73
73
  },
74
74
  "peerDependencies": {
75
- "@sudobility/di": "^1.5.43",
76
- "@sudobility/sudojo_client": "^0.0.85",
77
- "@sudobility/sudojo_types": "^1.2.36",
78
- "@sudobility/types": "^1.9.55",
75
+ "@sudobility/di": "^1.5.45",
76
+ "@sudobility/sudojo_client": "^0.0.86",
77
+ "@sudobility/sudojo_types": "^1.2.37",
78
+ "@sudobility/types": "^1.9.57",
79
79
  "@tanstack/react-query": ">=5.0.0",
80
80
  "react": ">=18.0.0",
81
81
  "zustand": ">=5.0.0"