@sudobility/sudojo_lib 0.0.201 → 0.0.203
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/admin/adminApi.d.ts +34 -0
- package/dist/admin/adminApi.d.ts.map +1 -0
- package/dist/admin/adminApi.js +59 -0
- package/dist/admin/adminApi.js.map +1 -0
- package/dist/admin/boardRules.d.ts +75 -0
- package/dist/admin/boardRules.d.ts.map +1 -0
- package/dist/admin/boardRules.js +127 -0
- package/dist/admin/boardRules.js.map +1 -0
- package/dist/admin/boardStrings.d.ts +55 -0
- package/dist/admin/boardStrings.d.ts.map +1 -0
- package/dist/admin/boardStrings.js +99 -0
- package/dist/admin/boardStrings.js.map +1 -0
- package/dist/admin/examples.d.ts +58 -0
- package/dist/admin/examples.d.ts.map +1 -0
- package/dist/admin/examples.js +104 -0
- package/dist/admin/examples.js.map +1 -0
- package/dist/admin/extraction.d.ts +87 -0
- package/dist/admin/extraction.d.ts.map +1 -0
- package/dist/admin/extraction.js +141 -0
- package/dist/admin/extraction.js.map +1 -0
- package/dist/admin/index.d.ts +32 -0
- package/dist/admin/index.d.ts.map +1 -0
- package/dist/admin/index.js +22 -0
- package/dist/admin/index.js.map +1 -0
- package/dist/admin/jobs.d.ts +108 -0
- package/dist/admin/jobs.d.ts.map +1 -0
- package/dist/admin/jobs.js +435 -0
- package/dist/admin/jobs.js.map +1 -0
- package/dist/admin/regenerateHints.d.ts +16 -0
- package/dist/admin/regenerateHints.d.ts.map +1 -0
- package/dist/admin/regenerateHints.js +43 -0
- package/dist/admin/regenerateHints.js.map +1 -0
- package/dist/admin/types.d.ts +42 -0
- package/dist/admin/types.d.ts.map +1 -0
- package/dist/admin/types.js +21 -0
- package/dist/admin/types.js.map +1 -0
- package/dist/admin/useAdminJob.d.ts +28 -0
- package/dist/admin/useAdminJob.d.ts.map +1 -0
- package/dist/admin/useAdminJob.js +99 -0
- package/dist/admin/useAdminJob.js.map +1 -0
- package/dist/admin/useAdminStats.d.ts +46 -0
- package/dist/admin/useAdminStats.d.ts.map +1 -0
- package/dist/admin/useAdminStats.js +91 -0
- package/dist/admin/useAdminStats.js.map +1 -0
- package/dist/admin/useBoardGenerator.d.ts +29 -0
- package/dist/admin/useBoardGenerator.d.ts.map +1 -0
- package/dist/admin/useBoardGenerator.js +38 -0
- package/dist/admin/useBoardGenerator.js.map +1 -0
- package/dist/admin/useBoardTechniquesUpdater.d.ts +14 -0
- package/dist/admin/useBoardTechniquesUpdater.d.ts.map +1 -0
- package/dist/admin/useBoardTechniquesUpdater.js +28 -0
- package/dist/admin/useBoardTechniquesUpdater.js.map +1 -0
- package/dist/admin/useExampleCreator.d.ts +29 -0
- package/dist/admin/useExampleCreator.d.ts.map +1 -0
- package/dist/admin/useExampleCreator.js +58 -0
- package/dist/admin/useExampleCreator.js.map +1 -0
- package/dist/admin/useSingleBoardExtractor.d.ts +18 -0
- package/dist/admin/useSingleBoardExtractor.d.ts.map +1 -0
- package/dist/admin/useSingleBoardExtractor.js +19 -0
- package/dist/admin/useSingleBoardExtractor.js.map +1 -0
- package/dist/admin/useTechniqueExtractor.d.ts +26 -0
- package/dist/admin/useTechniqueExtractor.d.ts.map +1 -0
- package/dist/admin/useTechniqueExtractor.js +37 -0
- package/dist/admin/useTechniqueExtractor.js.map +1 -0
- package/dist/context/SudojoApiContext.d.ts +58 -0
- package/dist/context/SudojoApiContext.d.ts.map +1 -0
- package/dist/context/SudojoApiContext.js +52 -0
- package/dist/context/SudojoApiContext.js.map +1 -0
- package/dist/context/SudojoApiProvider.d.ts +21 -0
- package/dist/context/SudojoApiProvider.d.ts.map +1 -0
- package/dist/context/SudojoApiProvider.js +15 -0
- package/dist/context/SudojoApiProvider.js.map +1 -0
- package/dist/context/index.d.ts +3 -0
- package/dist/context/index.d.ts.map +1 -1
- package/dist/context/index.js +2 -0
- package/dist/context/index.js.map +1 -1
- package/dist/hooks/index.d.ts +24 -1
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/index.js +22 -0
- package/dist/hooks/index.js.map +1 -1
- package/dist/hooks/useBoardEntry.d.ts +65 -5
- package/dist/hooks/useBoardEntry.d.ts.map +1 -1
- package/dist/hooks/useBoardEntry.js +124 -27
- package/dist/hooks/useBoardEntry.js.map +1 -1
- package/dist/hooks/useBoardScan.d.ts +27 -0
- package/dist/hooks/useBoardScan.d.ts.map +1 -0
- package/dist/hooks/useBoardScan.js +27 -0
- package/dist/hooks/useBoardScan.js.map +1 -0
- package/dist/hooks/useCommunities.d.ts +6 -4
- package/dist/hooks/useCommunities.d.ts.map +1 -1
- package/dist/hooks/useCommunities.js +6 -1
- package/dist/hooks/useCommunities.js.map +1 -1
- package/dist/hooks/useCompletionTrigger.d.ts +23 -0
- package/dist/hooks/useCompletionTrigger.d.ts.map +1 -0
- package/dist/hooks/useCompletionTrigger.js +39 -0
- package/dist/hooks/useCompletionTrigger.js.map +1 -0
- package/dist/hooks/useDailyGame.d.ts +11 -6
- package/dist/hooks/useDailyGame.d.ts.map +1 -1
- package/dist/hooks/useDailyGame.js +20 -61
- package/dist/hooks/useDailyGame.js.map +1 -1
- package/dist/hooks/useDeleteAccount.d.ts +72 -0
- package/dist/hooks/useDeleteAccount.d.ts.map +1 -0
- package/dist/hooks/useDeleteAccount.js +71 -0
- package/dist/hooks/useDeleteAccount.js.map +1 -0
- package/dist/hooks/useEnteredGameSession.d.ts +73 -0
- package/dist/hooks/useEnteredGameSession.d.ts.map +1 -0
- package/dist/hooks/useEnteredGameSession.js +155 -0
- package/dist/hooks/useEnteredGameSession.js.map +1 -0
- package/dist/hooks/useGameSession.d.ts +8 -4
- package/dist/hooks/useGameSession.d.ts.map +1 -1
- package/dist/hooks/useGameSession.js +4 -1
- package/dist/hooks/useGameSession.js.map +1 -1
- package/dist/hooks/useGameTeaching.d.ts.map +1 -1
- package/dist/hooks/useGameTeaching.js +11 -13
- package/dist/hooks/useGameTeaching.js.map +1 -1
- package/dist/hooks/useHint.d.ts +7 -7
- package/dist/hooks/useHint.d.ts.map +1 -1
- package/dist/hooks/useHint.js +18 -9
- package/dist/hooks/useHint.js.map +1 -1
- package/dist/hooks/useIsSiteAdmin.d.ts +39 -0
- package/dist/hooks/useIsSiteAdmin.d.ts.map +1 -0
- package/dist/hooks/useIsSiteAdmin.js +31 -0
- package/dist/hooks/useIsSiteAdmin.js.map +1 -0
- package/dist/hooks/useLearning.d.ts +8 -8
- package/dist/hooks/useLearning.d.ts.map +1 -1
- package/dist/hooks/useLearning.js +5 -2
- package/dist/hooks/useLearning.js.map +1 -1
- package/dist/hooks/useLevelGame.d.ts +7 -7
- package/dist/hooks/useLevelGame.d.ts.map +1 -1
- package/dist/hooks/useLevelGame.js +13 -49
- package/dist/hooks/useLevelGame.js.map +1 -1
- package/dist/hooks/useLevels.d.ts +8 -8
- package/dist/hooks/useLevels.d.ts.map +1 -1
- package/dist/hooks/useLevels.js +7 -3
- package/dist/hooks/useLevels.js.map +1 -1
- package/dist/hooks/usePracticeGame.d.ts +53 -0
- package/dist/hooks/usePracticeGame.d.ts.map +1 -0
- package/dist/hooks/usePracticeGame.js +72 -0
- package/dist/hooks/usePracticeGame.js.map +1 -0
- package/dist/hooks/usePuzzleDistribution.d.ts +2 -0
- package/dist/hooks/usePuzzleDistribution.d.ts.map +1 -1
- package/dist/hooks/usePuzzleDistribution.js +4 -14
- package/dist/hooks/usePuzzleDistribution.js.map +1 -1
- package/dist/hooks/usePuzzleProgress.d.ts +37 -0
- package/dist/hooks/usePuzzleProgress.d.ts.map +1 -0
- package/dist/hooks/usePuzzleProgress.js +48 -0
- package/dist/hooks/usePuzzleProgress.js.map +1 -0
- package/dist/hooks/usePuzzleSession.d.ts +215 -0
- package/dist/hooks/usePuzzleSession.d.ts.map +1 -0
- package/dist/hooks/usePuzzleSession.js +358 -0
- package/dist/hooks/usePuzzleSession.js.map +1 -0
- package/dist/hooks/useResumeGame.d.ts +70 -0
- package/dist/hooks/useResumeGame.d.ts.map +1 -0
- package/dist/hooks/useResumeGame.js +123 -0
- package/dist/hooks/useResumeGame.js.map +1 -0
- package/dist/hooks/useStrategies.d.ts +39 -0
- package/dist/hooks/useStrategies.d.ts.map +1 -0
- package/dist/hooks/useStrategies.js +34 -0
- package/dist/hooks/useStrategies.js.map +1 -0
- package/dist/hooks/useSudoku.d.ts +5 -0
- package/dist/hooks/useSudoku.d.ts.map +1 -1
- package/dist/hooks/useSudoku.js +7 -1
- package/dist/hooks/useSudoku.js.map +1 -1
- package/dist/hooks/useTechniqueByPath.d.ts +46 -0
- package/dist/hooks/useTechniqueByPath.d.ts.map +1 -0
- package/dist/hooks/useTechniqueByPath.js +47 -0
- package/dist/hooks/useTechniqueByPath.js.map +1 -0
- package/dist/hooks/useTechniqueExample.d.ts +60 -0
- package/dist/hooks/useTechniqueExample.d.ts.map +1 -0
- package/dist/hooks/useTechniqueExample.js +63 -0
- package/dist/hooks/useTechniqueExample.js.map +1 -0
- package/dist/hooks/useTechniques.d.ts +8 -8
- package/dist/hooks/useTechniques.d.ts.map +1 -1
- package/dist/hooks/useTechniques.js +9 -21
- package/dist/hooks/useTechniques.js.map +1 -1
- package/dist/index.d.ts +418 -11
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +276 -10
- package/dist/index.js.map +1 -1
- package/dist/types/currentGame.d.ts +9 -1
- package/dist/types/currentGame.d.ts.map +1 -1
- package/dist/types/currentGame.js.map +1 -1
- package/dist/utils/auth.d.ts +6 -0
- package/dist/utils/auth.d.ts.map +1 -1
- package/dist/utils/auth.js +8 -0
- package/dist/utils/auth.js.map +1 -1
- package/dist/utils/community.d.ts +64 -0
- package/dist/utils/community.d.ts.map +1 -0
- package/dist/utils/community.js +66 -0
- package/dist/utils/community.js.map +1 -0
- package/dist/utils/date.d.ts +37 -0
- package/dist/utils/date.d.ts.map +1 -0
- package/dist/utils/date.js +68 -0
- package/dist/utils/date.js.map +1 -0
- package/dist/utils/entityTranslate.d.ts +120 -0
- package/dist/utils/entityTranslate.d.ts.map +1 -0
- package/dist/utils/entityTranslate.js +146 -0
- package/dist/utils/entityTranslate.js.map +1 -0
- package/dist/utils/gameFetchStatus.d.ts +74 -0
- package/dist/utils/gameFetchStatus.d.ts.map +1 -0
- package/dist/utils/gameFetchStatus.js +118 -0
- package/dist/utils/gameFetchStatus.js.map +1 -0
- package/dist/utils/gamification.d.ts +36 -0
- package/dist/utils/gamification.d.ts.map +1 -0
- package/dist/utils/gamification.js +33 -0
- package/dist/utils/gamification.js.map +1 -0
- package/dist/utils/hintAccess.d.ts +64 -0
- package/dist/utils/hintAccess.d.ts.map +1 -0
- package/dist/utils/hintAccess.js +57 -0
- package/dist/utils/hintAccess.js.map +1 -0
- package/dist/utils/index.d.ts +31 -10
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +20 -9
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/language.d.ts +123 -0
- package/dist/utils/language.d.ts.map +1 -0
- package/dist/utils/language.js +95 -0
- package/dist/utils/language.js.map +1 -0
- package/dist/utils/localizedHint.d.ts +60 -0
- package/dist/utils/localizedHint.d.ts.map +1 -1
- package/dist/utils/localizedHint.js +90 -0
- package/dist/utils/localizedHint.js.map +1 -1
- package/dist/utils/progress.d.ts +10 -1
- package/dist/utils/progress.d.ts.map +1 -1
- package/dist/utils/progress.js +24 -0
- package/dist/utils/progress.js.map +1 -1
- package/dist/utils/scannedBoard.d.ts +79 -0
- package/dist/utils/scannedBoard.d.ts.map +1 -0
- package/dist/utils/scannedBoard.js +96 -0
- package/dist/utils/scannedBoard.js.map +1 -0
- package/dist/utils/shareUrl.d.ts +38 -15
- package/dist/utils/shareUrl.d.ts.map +1 -1
- package/dist/utils/shareUrl.js +75 -26
- package/dist/utils/shareUrl.js.map +1 -1
- package/dist/utils/subscription.d.ts +118 -0
- package/dist/utils/subscription.d.ts.map +1 -1
- package/dist/utils/subscription.js +212 -0
- package/dist/utils/subscription.js.map +1 -1
- package/dist/utils/sudokuPresenter.d.ts +42 -0
- package/dist/utils/sudokuPresenter.d.ts.map +1 -1
- package/dist/utils/sudokuPresenter.js +114 -1
- package/dist/utils/sudokuPresenter.js.map +1 -1
- package/dist/utils/sudokuScrambler.d.ts +8 -0
- package/dist/utils/sudokuScrambler.d.ts.map +1 -1
- package/dist/utils/sudokuScrambler.js +12 -0
- package/dist/utils/sudokuScrambler.js.map +1 -1
- package/dist/utils/technique.d.ts +112 -1
- package/dist/utils/technique.d.ts.map +1 -1
- package/dist/utils/technique.js +293 -1
- package/dist/utils/technique.js.map +1 -1
- package/dist/utils/techniqueBitmask.d.ts +31 -0
- package/dist/utils/techniqueBitmask.d.ts.map +1 -0
- package/dist/utils/techniqueBitmask.js +53 -0
- package/dist/utils/techniqueBitmask.js.map +1 -0
- package/dist/utils/techniqueExample.d.ts +42 -0
- package/dist/utils/techniqueExample.d.ts.map +1 -0
- package/dist/utils/techniqueExample.js +57 -0
- package/dist/utils/techniqueExample.js.map +1 -0
- package/dist/utils/theme.d.ts +2 -0
- package/dist/utils/theme.d.ts.map +1 -1
- package/dist/utils/theme.js +2 -0
- package/dist/utils/theme.js.map +1 -1
- package/package.json +7 -7
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hook for the player's puzzle progress (completed puzzles, streak, stats),
|
|
3
|
+
* persisted in localStorage under PROGRESS_STORAGE_KEY.
|
|
4
|
+
*
|
|
5
|
+
* The web ProgressProvider's logic. RN does not track progress today; where
|
|
6
|
+
* it shims localStorage (as it does for the game store) the hook works there
|
|
7
|
+
* too.
|
|
8
|
+
*/
|
|
9
|
+
import { useCallback, useMemo } from 'react';
|
|
10
|
+
import { DEFAULT_PROGRESS, PROGRESS_STORAGE_KEY } from '../types/progress';
|
|
11
|
+
import { applyPuzzleCompletion, getCompletedDailyDates, getCompletedLevelIds, isPuzzleCompleted, } from '../utils/progress';
|
|
12
|
+
import { useLocalStorage } from './useLocalStorage';
|
|
13
|
+
/**
|
|
14
|
+
* @example
|
|
15
|
+
* ```tsx
|
|
16
|
+
* const { markCompleted, isCompleted } = usePuzzleProgress();
|
|
17
|
+
* markCompleted({ type: 'daily', id: '2026-10-05', timeSeconds: 312 });
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
export function usePuzzleProgress(options = {}) {
|
|
21
|
+
const { storageKey = PROGRESS_STORAGE_KEY } = options;
|
|
22
|
+
const [progress, setProgress] = useLocalStorage(storageKey, DEFAULT_PROGRESS);
|
|
23
|
+
const markCompleted = useCallback((puzzle) => {
|
|
24
|
+
setProgress(prev => applyPuzzleCompletion(prev, puzzle));
|
|
25
|
+
}, [setProgress]);
|
|
26
|
+
const isCompleted = useCallback((type, id) => isPuzzleCompleted(progress.completedPuzzles, type, id), [progress.completedPuzzles]);
|
|
27
|
+
const getLevelIds = useCallback(() => getCompletedLevelIds(progress.completedPuzzles), [progress.completedPuzzles]);
|
|
28
|
+
const getDailyDates = useCallback(() => getCompletedDailyDates(progress.completedPuzzles), [progress.completedPuzzles]);
|
|
29
|
+
const resetProgress = useCallback(() => {
|
|
30
|
+
setProgress(DEFAULT_PROGRESS);
|
|
31
|
+
}, [setProgress]);
|
|
32
|
+
return useMemo(() => ({
|
|
33
|
+
progress,
|
|
34
|
+
markCompleted,
|
|
35
|
+
isCompleted,
|
|
36
|
+
getCompletedLevelIds: getLevelIds,
|
|
37
|
+
getCompletedDailyDates: getDailyDates,
|
|
38
|
+
resetProgress,
|
|
39
|
+
}), [
|
|
40
|
+
progress,
|
|
41
|
+
markCompleted,
|
|
42
|
+
isCompleted,
|
|
43
|
+
getLevelIds,
|
|
44
|
+
getDailyDates,
|
|
45
|
+
resetProgress,
|
|
46
|
+
]);
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=usePuzzleProgress.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"usePuzzleProgress.js","sourceRoot":"","sources":["../../src/hooks/usePuzzleProgress.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,WAAW,EAAE,OAAO,EAAE,MAAM,OAAO,CAAC;AAE7C,OAAO,EAAE,gBAAgB,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAC;AAC3E,OAAO,EACL,qBAAqB,EACrB,sBAAsB,EACtB,oBAAoB,EACpB,iBAAiB,GAElB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAsBpD;;;;;;GAMG;AACH,MAAM,UAAU,iBAAiB,CAC/B,UAAoC,EAAE;IAEtC,MAAM,EAAE,UAAU,GAAG,oBAAoB,EAAE,GAAG,OAAO,CAAC;IACtD,MAAM,CAAC,QAAQ,EAAE,WAAW,CAAC,GAAG,eAAe,CAC7C,UAAU,EACV,gBAAgB,CACjB,CAAC;IAEF,MAAM,aAAa,GAAG,WAAW,CAC/B,CAAC,MAAwB,EAAE,EAAE;QAC3B,WAAW,CAAC,IAAI,CAAC,EAAE,CAAC,qBAAqB,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC;IAC3D,CAAC,EACD,CAAC,WAAW,CAAC,CACd,CAAC;IAEF,MAAM,WAAW,GAAG,WAAW,CAC7B,CAAC,IAAuB,EAAE,EAAU,EAAE,EAAE,CACtC,iBAAiB,CAAC,QAAQ,CAAC,gBAAgB,EAAE,IAAI,EAAE,EAAE,CAAC,EACxD,CAAC,QAAQ,CAAC,gBAAgB,CAAC,CAC5B,CAAC;IAEF,MAAM,WAAW,GAAG,WAAW,CAC7B,GAAG,EAAE,CAAC,oBAAoB,CAAC,QAAQ,CAAC,gBAAgB,CAAC,EACrD,CAAC,QAAQ,CAAC,gBAAgB,CAAC,CAC5B,CAAC;IAEF,MAAM,aAAa,GAAG,WAAW,CAC/B,GAAG,EAAE,CAAC,sBAAsB,CAAC,QAAQ,CAAC,gBAAgB,CAAC,EACvD,CAAC,QAAQ,CAAC,gBAAgB,CAAC,CAC5B,CAAC;IAEF,MAAM,aAAa,GAAG,WAAW,CAAC,GAAG,EAAE;QACrC,WAAW,CAAC,gBAAgB,CAAC,CAAC;IAChC,CAAC,EAAE,CAAC,WAAW,CAAC,CAAC,CAAC;IAElB,OAAO,OAAO,CACZ,GAAG,EAAE,CAAC,CAAC;QACL,QAAQ;QACR,aAAa;QACb,WAAW;QACX,oBAAoB,EAAE,WAAW;QACjC,sBAAsB,EAAE,aAAa;QACrC,aAAa;KACd,CAAC,EACF;QACE,QAAQ;QACR,aAAa;QACb,WAAW;QACX,WAAW;QACX,aAAa;QACb,aAAa;KACd,CACF,CAAC;AACJ,CAAC","sourcesContent":["/**\n * Hook for the player's puzzle progress (completed puzzles, streak, stats),\n * persisted in localStorage under PROGRESS_STORAGE_KEY.\n *\n * The web ProgressProvider's logic. RN does not track progress today; where\n * it shims localStorage (as it does for the game store) the hook works there\n * too.\n */\n\nimport { useCallback, useMemo } from 'react';\nimport type { UserProgress } from '../types/progress';\nimport { DEFAULT_PROGRESS, PROGRESS_STORAGE_KEY } from '../types/progress';\nimport {\n applyPuzzleCompletion,\n getCompletedDailyDates,\n getCompletedLevelIds,\n isPuzzleCompleted,\n type PuzzleCompletion,\n} from '../utils/progress';\nimport { useLocalStorage } from './useLocalStorage';\n\nexport interface UsePuzzleProgressOptions {\n /** localStorage key (default PROGRESS_STORAGE_KEY, 'sudojo-progress') */\n storageKey?: string;\n}\n\nexport interface UsePuzzleProgressResult {\n /** The stored progress */\n progress: UserProgress;\n /** Record a completion (no-op when the puzzle is already completed) */\n markCompleted: (puzzle: PuzzleCompletion) => void;\n /** Whether a puzzle has been completed */\n isCompleted: (type: 'daily' | 'level', id: string) => boolean;\n /** Ids of completed level puzzles */\n getCompletedLevelIds: () => string[];\n /** Dates (YYYY-MM-DD) of completed dailies */\n getCompletedDailyDates: () => string[];\n /** Reset to DEFAULT_PROGRESS */\n resetProgress: () => void;\n}\n\n/**\n * @example\n * ```tsx\n * const { markCompleted, isCompleted } = usePuzzleProgress();\n * markCompleted({ type: 'daily', id: '2026-10-05', timeSeconds: 312 });\n * ```\n */\nexport function usePuzzleProgress(\n options: UsePuzzleProgressOptions = {}\n): UsePuzzleProgressResult {\n const { storageKey = PROGRESS_STORAGE_KEY } = options;\n const [progress, setProgress] = useLocalStorage<UserProgress>(\n storageKey,\n DEFAULT_PROGRESS\n );\n\n const markCompleted = useCallback(\n (puzzle: PuzzleCompletion) => {\n setProgress(prev => applyPuzzleCompletion(prev, puzzle));\n },\n [setProgress]\n );\n\n const isCompleted = useCallback(\n (type: 'daily' | 'level', id: string) =>\n isPuzzleCompleted(progress.completedPuzzles, type, id),\n [progress.completedPuzzles]\n );\n\n const getLevelIds = useCallback(\n () => getCompletedLevelIds(progress.completedPuzzles),\n [progress.completedPuzzles]\n );\n\n const getDailyDates = useCallback(\n () => getCompletedDailyDates(progress.completedPuzzles),\n [progress.completedPuzzles]\n );\n\n const resetProgress = useCallback(() => {\n setProgress(DEFAULT_PROGRESS);\n }, [setProgress]);\n\n return useMemo(\n () => ({\n progress,\n markCompleted,\n isCompleted,\n getCompletedLevelIds: getLevelIds,\n getCompletedDailyDates: getDailyDates,\n resetProgress,\n }),\n [\n progress,\n markCompleted,\n isCompleted,\n getLevelIds,\n getDailyDates,\n resetProgress,\n ]\n );\n}\n"]}
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Game session orchestration for the Daily and Level play screens.
|
|
3
|
+
*
|
|
4
|
+
* Combines the saved-game store (useGamePlay), the resume decision
|
|
5
|
+
* (useResumeGame) and server gamification (useGameSession) into what a play
|
|
6
|
+
* screen renders: which puzzle to show and how (fresh and scrambled, or
|
|
7
|
+
* resumed with its saved input), when to save the game and start the server
|
|
8
|
+
* session, and what to do on completion (clear the save, finish the session,
|
|
9
|
+
* show achievements, record progress). Platform-neutral: no router, no
|
|
10
|
+
* dialogs.
|
|
11
|
+
*/
|
|
12
|
+
import type { GameFetchStatus } from '../utils/gameFetchStatus';
|
|
13
|
+
import type { SudojoApiOptions } from '../context/SudojoApiContext';
|
|
14
|
+
import type { CurrentGame } from '../types/currentGame';
|
|
15
|
+
import { type AuthUser } from '../utils/auth';
|
|
16
|
+
import type { PuzzleCompletion } from '../utils/progress';
|
|
17
|
+
import { type GameFinishResult } from './useGameSession';
|
|
18
|
+
import { type PuzzleSessionSource, type UseResumeGameResult } from './useResumeGame';
|
|
19
|
+
/** The fetched puzzle a session plays: a Daily or a Board from the API. */
|
|
20
|
+
export interface SessionPuzzleInput {
|
|
21
|
+
uuid: string;
|
|
22
|
+
/** 81-char puzzle */
|
|
23
|
+
board: string;
|
|
24
|
+
/** 81-char solution */
|
|
25
|
+
solution: string;
|
|
26
|
+
level?: number | null;
|
|
27
|
+
techniques?: number | null;
|
|
28
|
+
techniques_bitmask?: string | null;
|
|
29
|
+
difficulty_score?: number | null;
|
|
30
|
+
/** A daily's date (YYYY-MM-DD) */
|
|
31
|
+
date?: string | null;
|
|
32
|
+
}
|
|
33
|
+
/** The puzzle the screen shows (fetched, or rebuilt from the saved game). */
|
|
34
|
+
export interface ActivePuzzle {
|
|
35
|
+
/** Board/daily uuid (for a resumed game: its boardUuid, else startedAt) */
|
|
36
|
+
uuid: string;
|
|
37
|
+
/** 81-char puzzle (a resumed game's is already scrambled) */
|
|
38
|
+
puzzle: string;
|
|
39
|
+
/** 81-char solution */
|
|
40
|
+
solution: string;
|
|
41
|
+
/** Difficulty level (1-12), when known */
|
|
42
|
+
level: number | null;
|
|
43
|
+
/** Solver difficulty score, when known */
|
|
44
|
+
difficultyScore: number | null;
|
|
45
|
+
/** A daily's date (YYYY-MM-DD) */
|
|
46
|
+
dailyDate: string | null;
|
|
47
|
+
/** Lossy techniques bitmask */
|
|
48
|
+
techniques: number | null;
|
|
49
|
+
/** Exact techniques bitmask (base-10 string) */
|
|
50
|
+
techniques_bitmask: string | null;
|
|
51
|
+
}
|
|
52
|
+
/** Props for the game component (SudokuGame) of either app. */
|
|
53
|
+
export interface PuzzleGameProps {
|
|
54
|
+
puzzle: string;
|
|
55
|
+
solution: string;
|
|
56
|
+
/** false when resuming (the saved puzzle is already scrambled) */
|
|
57
|
+
scramble: boolean;
|
|
58
|
+
initialInput?: string | undefined;
|
|
59
|
+
initialPencilmarks?: string | undefined;
|
|
60
|
+
initialElapsedTime?: number | undefined;
|
|
61
|
+
initialAutoPencilmarks?: boolean | undefined;
|
|
62
|
+
}
|
|
63
|
+
export interface UsePuzzleSessionOptions extends SudojoApiOptions {
|
|
64
|
+
/** 'daily' or 'level' */
|
|
65
|
+
source: PuzzleSessionSource;
|
|
66
|
+
/** Level screens: the level number (1-12) */
|
|
67
|
+
levelNumber?: number | null | undefined;
|
|
68
|
+
/** Level screens: the level's title (saved in meta for "Continue") */
|
|
69
|
+
levelTitle?: string | null | undefined;
|
|
70
|
+
/**
|
|
71
|
+
* The fetched puzzle (useDailyGame's `daily` / useLevelGame's `board`), or
|
|
72
|
+
* null while loading. Ignored while resuming.
|
|
73
|
+
*/
|
|
74
|
+
puzzle: SessionPuzzleInput | null | undefined;
|
|
75
|
+
/** Dailies: useDailyGame's `dailyDate` (default: the puzzle's `date`) */
|
|
76
|
+
dailyDate?: string | null | undefined;
|
|
77
|
+
/** The fetch hook's status (default 'loading' until a puzzle arrives) */
|
|
78
|
+
fetchStatus?: GameFetchStatus | undefined;
|
|
79
|
+
/** The signed-in user: server sessions run only for a real (non-anonymous) user */
|
|
80
|
+
user?: AuthUser | null | undefined;
|
|
81
|
+
/** The resume decision from useResumeGame (default: computed here) */
|
|
82
|
+
resume?: UseResumeGameResult | undefined;
|
|
83
|
+
/** Forwarded to the internal useResumeGame when `resume` is not passed */
|
|
84
|
+
isStoreHydrated?: boolean | undefined;
|
|
85
|
+
/** Forwarded to the internal useResumeGame (default true) */
|
|
86
|
+
clearStaleDaily?: boolean | undefined;
|
|
87
|
+
/**
|
|
88
|
+
* Whether a puzzle was already completed (progress). A completed daily is
|
|
89
|
+
* not saved, started on the server, or recorded again (web DailyPage).
|
|
90
|
+
*/
|
|
91
|
+
isPuzzleCompleted?: (type: 'daily' | 'level', id: string) => boolean;
|
|
92
|
+
/** Record a completion in the player's progress (web markCompleted) */
|
|
93
|
+
onPuzzleCompleted?: (completion: PuzzleCompletion) => void;
|
|
94
|
+
/**
|
|
95
|
+
* Start a server session for a resumed game too (RN DailyScreen did; the web
|
|
96
|
+
* does not). Default false: the session from when the game was started is
|
|
97
|
+
* still the server's active one, and restarting it resets its clock.
|
|
98
|
+
*/
|
|
99
|
+
startSessionOnResume?: boolean;
|
|
100
|
+
/**
|
|
101
|
+
* Show the achievement modal even when nothing was earned (RN did).
|
|
102
|
+
* Default false (web: only on level-up, new badges or points).
|
|
103
|
+
*/
|
|
104
|
+
showEmptyAchievement?: boolean;
|
|
105
|
+
}
|
|
106
|
+
export interface UsePuzzleSessionResult {
|
|
107
|
+
/**
|
|
108
|
+
* 'success' once a puzzle is pinned or resumed; otherwise the fetch status
|
|
109
|
+
* ('loading' until the store is ready and while a fetched puzzle is not
|
|
110
|
+
* pinned yet)
|
|
111
|
+
*/
|
|
112
|
+
status: GameFetchStatus;
|
|
113
|
+
/** Whether the resume decision is made (store hydrated) */
|
|
114
|
+
isReady: boolean;
|
|
115
|
+
/** Whether the screen resumes a saved game */
|
|
116
|
+
isResuming: boolean;
|
|
117
|
+
/** The saved game being resumed, or null */
|
|
118
|
+
resumeGame: CurrentGame | null;
|
|
119
|
+
/** The puzzle to show (pinned once loaded), or null */
|
|
120
|
+
activePuzzle: ActivePuzzle | null;
|
|
121
|
+
/**
|
|
122
|
+
* Props for the game component, or null while there is no puzzle. Render
|
|
123
|
+
* it with `key={gameKey}` so a new puzzle remounts the game.
|
|
124
|
+
*/
|
|
125
|
+
gameProps: PuzzleGameProps | null;
|
|
126
|
+
/** Remount key for the game component (the puzzle's uuid), or null */
|
|
127
|
+
gameKey: string | null;
|
|
128
|
+
/** The puzzle's progress id: the daily date, or the board uuid */
|
|
129
|
+
puzzleId: string | null;
|
|
130
|
+
/** Whether this puzzle was already completed (isPuzzleCompleted) */
|
|
131
|
+
alreadyCompleted: boolean;
|
|
132
|
+
/** Whether server gamification is active (real user with a token) */
|
|
133
|
+
isAuthenticated: boolean;
|
|
134
|
+
/** Web SudokuGame's onScrambledReady: saves the fresh game and starts the session */
|
|
135
|
+
onScrambledReady: (scrambled: {
|
|
136
|
+
puzzle: string;
|
|
137
|
+
solution: string;
|
|
138
|
+
}) => void;
|
|
139
|
+
/** RN SudokuGame's onBoardReady: same as onScrambledReady */
|
|
140
|
+
onBoardReady: (scrambledPuzzle: string, scrambledSolution: string) => void;
|
|
141
|
+
/** The game's onProgressUpdate: saves progress (throttled) */
|
|
142
|
+
onProgress: (inputString: string, pencilmarksString: string, isPencilMode: boolean, autoPencilmarks: boolean, elapsedTime: number) => void;
|
|
143
|
+
/**
|
|
144
|
+
* The game's onComplete: clears the save, finishes the server session
|
|
145
|
+
* (sets the achievement state), records progress. Resolves to the finish
|
|
146
|
+
* result (null when not authenticated).
|
|
147
|
+
*/
|
|
148
|
+
onComplete: (timeSeconds: number) => Promise<GameFinishResult | null>;
|
|
149
|
+
/**
|
|
150
|
+
* Abandon the current puzzle: clear the save, stop resuming, unpin. Call the
|
|
151
|
+
* fetch hook's nextPuzzle()/refetch() too; the old puzzle is not pinned again.
|
|
152
|
+
*/
|
|
153
|
+
newGame: () => void;
|
|
154
|
+
/** Achievement result to show, or null */
|
|
155
|
+
achievementResult: GameFinishResult | null;
|
|
156
|
+
/** Whether the achievement modal is open */
|
|
157
|
+
showAchievement: boolean;
|
|
158
|
+
/** Close the achievement modal */
|
|
159
|
+
closeAchievement: () => void;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Orchestrate a Daily or Level play screen.
|
|
163
|
+
*
|
|
164
|
+
* Behavior (web DailyPage / LevelPlayPage, with RN's stale-daily rule):
|
|
165
|
+
* - Resume: a saved game for this screen (see getResumeGame) is shown with
|
|
166
|
+
* `scramble: false` and its saved input, pencilmarks, time and auto-pencil.
|
|
167
|
+
* A saved daily from another (UTC) day is cleared, not resumed.
|
|
168
|
+
* - Fresh puzzle: pinned once loaded (refetches cannot unmount the game).
|
|
169
|
+
* When the game reports its scrambled board (onScrambledReady /
|
|
170
|
+
* onBoardReady) the game is saved once per puzzle uuid with meta (dailyDate
|
|
171
|
+
* or levelId/levelTitle, boardUuid, level, difficultyScore, techniques +
|
|
172
|
+
* exact techniques_bitmask), and a server session is started once per uuid
|
|
173
|
+
* for a real user (a daily only when it has a level). A daily already in
|
|
174
|
+
* progress is skipped.
|
|
175
|
+
* - Completion: clear the save, finish the server session (authenticated),
|
|
176
|
+
* open the achievement modal when something was earned, then record
|
|
177
|
+
* progress (`onPuzzleCompleted`, not again for a completed daily).
|
|
178
|
+
*
|
|
179
|
+
* @example Daily
|
|
180
|
+
* ```tsx
|
|
181
|
+
* const resume = useResumeGame({ source: 'daily' });
|
|
182
|
+
* const { daily, dailyDate, status, refetch } = useDailyGame({ enabled: resume.isReady && !resume.resumeGame });
|
|
183
|
+
* const { markCompleted, isCompleted } = usePuzzleProgress();
|
|
184
|
+
* const session = usePuzzleSession({
|
|
185
|
+
* source: 'daily', resume, puzzle: daily, dailyDate, fetchStatus: status, user,
|
|
186
|
+
* isPuzzleCompleted: isCompleted, onPuzzleCompleted: markCompleted,
|
|
187
|
+
* });
|
|
188
|
+
* if (session.status === 'auth_required') return <SignInGate />;
|
|
189
|
+
* return session.gameProps && (
|
|
190
|
+
* <>
|
|
191
|
+
* <SudokuGame key={session.gameKey} {...session.gameProps} onScrambledReady={session.onScrambledReady}
|
|
192
|
+
* onProgressUpdate={session.onProgress} onComplete={session.onComplete} />
|
|
193
|
+
* <AchievementModal show={session.showAchievement} onClose={session.closeAchievement}
|
|
194
|
+
* {...session.achievementResult} />
|
|
195
|
+
* </>
|
|
196
|
+
* );
|
|
197
|
+
* ```
|
|
198
|
+
*
|
|
199
|
+
* @example LevelPlay
|
|
200
|
+
* ```tsx
|
|
201
|
+
* const resume = useResumeGame({ source: 'level', levelNumber });
|
|
202
|
+
* const { board, status, nextPuzzle } = useLevelGame({
|
|
203
|
+
* level: levelNumber, symmetrical, userEntitlements, levelEntitlement,
|
|
204
|
+
* enabled: resume.isReady && !resume.resumeGame,
|
|
205
|
+
* });
|
|
206
|
+
* const session = usePuzzleSession({
|
|
207
|
+
* source: 'level', levelNumber, levelTitle: level?.title, resume,
|
|
208
|
+
* puzzle: board, fetchStatus: status, user, onPuzzleCompleted: markCompleted,
|
|
209
|
+
* });
|
|
210
|
+
* const onNewGame = () => { session.newGame(); nextPuzzle(); };
|
|
211
|
+
* // RN: <SudokuGame key={session.gameKey} {...session.gameProps} onBoardReady={session.onBoardReady} ... />
|
|
212
|
+
* ```
|
|
213
|
+
*/
|
|
214
|
+
export declare function usePuzzleSession(options: UsePuzzleSessionOptions): UsePuzzleSessionResult;
|
|
215
|
+
//# sourceMappingURL=usePuzzleSession.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"usePuzzleSession.d.ts","sourceRoot":"","sources":["../../src/hooks/usePuzzleSession.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Game session orchestration for the Daily and Level play screens.
|
|
3
|
+
*
|
|
4
|
+
* Combines the saved-game store (useGamePlay), the resume decision
|
|
5
|
+
* (useResumeGame) and server gamification (useGameSession) into what a play
|
|
6
|
+
* screen renders: which puzzle to show and how (fresh and scrambled, or
|
|
7
|
+
* resumed with its saved input), when to save the game and start the server
|
|
8
|
+
* session, and what to do on completion (clear the save, finish the session,
|
|
9
|
+
* show achievements, record progress). Platform-neutral: no router, no
|
|
10
|
+
* dialogs.
|
|
11
|
+
*/
|
|
12
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
13
|
+
import { useResolvedSudojoApi } from '../context/SudojoApiContext';
|
|
14
|
+
import { isRealUser } from '../utils/auth';
|
|
15
|
+
import { techniqueBitmaskString, techniqueFieldsOf, } from '../utils/techniqueBitmask';
|
|
16
|
+
import { useGameSession } from './useGameSession';
|
|
17
|
+
import { useGamePlay } from './useGamePlay';
|
|
18
|
+
import { useResumeGame, } from './useResumeGame';
|
|
19
|
+
function hasAchievement(result) {
|
|
20
|
+
return (result.leveledUp ||
|
|
21
|
+
result.newBadges.length > 0 ||
|
|
22
|
+
result.totalPointsEarned > 0);
|
|
23
|
+
}
|
|
24
|
+
function puzzleFromResume(game, source, levelNumber) {
|
|
25
|
+
const meta = game.meta;
|
|
26
|
+
return {
|
|
27
|
+
uuid: meta.boardUuid ?? game.startedAt ?? game.updatedAt ?? 'resume',
|
|
28
|
+
puzzle: game.puzzle,
|
|
29
|
+
solution: game.solution,
|
|
30
|
+
level: meta.level ?? (source === 'level' ? (levelNumber ?? null) : null),
|
|
31
|
+
difficultyScore: meta.difficultyScore ?? null,
|
|
32
|
+
dailyDate: meta.dailyDate ?? null,
|
|
33
|
+
...techniqueFieldsOf(meta),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
function puzzleFromFetch(input, source, levelNumber, dailyDate) {
|
|
37
|
+
return {
|
|
38
|
+
uuid: input.uuid,
|
|
39
|
+
puzzle: input.board,
|
|
40
|
+
solution: input.solution,
|
|
41
|
+
level: input.level ?? (source === 'level' ? (levelNumber ?? null) : null),
|
|
42
|
+
difficultyScore: input.difficulty_score ?? null,
|
|
43
|
+
dailyDate: source === 'daily' ? (dailyDate ?? input.date ?? null) : null,
|
|
44
|
+
...techniqueFieldsOf(input),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Orchestrate a Daily or Level play screen.
|
|
49
|
+
*
|
|
50
|
+
* Behavior (web DailyPage / LevelPlayPage, with RN's stale-daily rule):
|
|
51
|
+
* - Resume: a saved game for this screen (see getResumeGame) is shown with
|
|
52
|
+
* `scramble: false` and its saved input, pencilmarks, time and auto-pencil.
|
|
53
|
+
* A saved daily from another (UTC) day is cleared, not resumed.
|
|
54
|
+
* - Fresh puzzle: pinned once loaded (refetches cannot unmount the game).
|
|
55
|
+
* When the game reports its scrambled board (onScrambledReady /
|
|
56
|
+
* onBoardReady) the game is saved once per puzzle uuid with meta (dailyDate
|
|
57
|
+
* or levelId/levelTitle, boardUuid, level, difficultyScore, techniques +
|
|
58
|
+
* exact techniques_bitmask), and a server session is started once per uuid
|
|
59
|
+
* for a real user (a daily only when it has a level). A daily already in
|
|
60
|
+
* progress is skipped.
|
|
61
|
+
* - Completion: clear the save, finish the server session (authenticated),
|
|
62
|
+
* open the achievement modal when something was earned, then record
|
|
63
|
+
* progress (`onPuzzleCompleted`, not again for a completed daily).
|
|
64
|
+
*
|
|
65
|
+
* @example Daily
|
|
66
|
+
* ```tsx
|
|
67
|
+
* const resume = useResumeGame({ source: 'daily' });
|
|
68
|
+
* const { daily, dailyDate, status, refetch } = useDailyGame({ enabled: resume.isReady && !resume.resumeGame });
|
|
69
|
+
* const { markCompleted, isCompleted } = usePuzzleProgress();
|
|
70
|
+
* const session = usePuzzleSession({
|
|
71
|
+
* source: 'daily', resume, puzzle: daily, dailyDate, fetchStatus: status, user,
|
|
72
|
+
* isPuzzleCompleted: isCompleted, onPuzzleCompleted: markCompleted,
|
|
73
|
+
* });
|
|
74
|
+
* if (session.status === 'auth_required') return <SignInGate />;
|
|
75
|
+
* return session.gameProps && (
|
|
76
|
+
* <>
|
|
77
|
+
* <SudokuGame key={session.gameKey} {...session.gameProps} onScrambledReady={session.onScrambledReady}
|
|
78
|
+
* onProgressUpdate={session.onProgress} onComplete={session.onComplete} />
|
|
79
|
+
* <AchievementModal show={session.showAchievement} onClose={session.closeAchievement}
|
|
80
|
+
* {...session.achievementResult} />
|
|
81
|
+
* </>
|
|
82
|
+
* );
|
|
83
|
+
* ```
|
|
84
|
+
*
|
|
85
|
+
* @example LevelPlay
|
|
86
|
+
* ```tsx
|
|
87
|
+
* const resume = useResumeGame({ source: 'level', levelNumber });
|
|
88
|
+
* const { board, status, nextPuzzle } = useLevelGame({
|
|
89
|
+
* level: levelNumber, symmetrical, userEntitlements, levelEntitlement,
|
|
90
|
+
* enabled: resume.isReady && !resume.resumeGame,
|
|
91
|
+
* });
|
|
92
|
+
* const session = usePuzzleSession({
|
|
93
|
+
* source: 'level', levelNumber, levelTitle: level?.title, resume,
|
|
94
|
+
* puzzle: board, fetchStatus: status, user, onPuzzleCompleted: markCompleted,
|
|
95
|
+
* });
|
|
96
|
+
* const onNewGame = () => { session.newGame(); nextPuzzle(); };
|
|
97
|
+
* // RN: <SudokuGame key={session.gameKey} {...session.gameProps} onBoardReady={session.onBoardReady} ... />
|
|
98
|
+
* ```
|
|
99
|
+
*/
|
|
100
|
+
export function usePuzzleSession(options) {
|
|
101
|
+
const { source, levelNumber, levelTitle, puzzle: fetched, dailyDate: fetchedDailyDate, fetchStatus, user, isPuzzleCompleted, onPuzzleCompleted, startSessionOnResume = false, showEmptyAchievement = false, } = options;
|
|
102
|
+
const api = useResolvedSudojoApi(options, 'usePuzzleSession');
|
|
103
|
+
const slot = source === 'daily' ? 'daily' : 'play';
|
|
104
|
+
const internalResume = useResumeGame({
|
|
105
|
+
source,
|
|
106
|
+
levelNumber,
|
|
107
|
+
slot,
|
|
108
|
+
...(options.clearStaleDaily !== undefined && {
|
|
109
|
+
clearStaleDaily: options.clearStaleDaily,
|
|
110
|
+
}),
|
|
111
|
+
...(options.isStoreHydrated !== undefined && {
|
|
112
|
+
isStoreHydrated: options.isStoreHydrated,
|
|
113
|
+
}),
|
|
114
|
+
});
|
|
115
|
+
const resume = options.resume ?? internalResume;
|
|
116
|
+
const resumeGame = resume.resumeGame;
|
|
117
|
+
const isResuming = resumeGame !== null;
|
|
118
|
+
const { startGame, updateProgress, clearGame } = useGamePlay({ slot });
|
|
119
|
+
const { startSession, finishSession, isAuthenticated } = useGameSession({
|
|
120
|
+
networkClient: api.networkClient,
|
|
121
|
+
baseUrl: api.baseUrl,
|
|
122
|
+
token: api.token,
|
|
123
|
+
userId: isRealUser(user) && user ? user.uid : null,
|
|
124
|
+
});
|
|
125
|
+
// --- Pinned fresh puzzle -------------------------------------------------
|
|
126
|
+
const screenKey = `${source}|${levelNumber ?? ''}`;
|
|
127
|
+
const [pinned, setPinned] = useState(null);
|
|
128
|
+
const excludedUuidRef = useRef(null);
|
|
129
|
+
const fetchedPuzzle = useMemo(() => fetched && fetched.board?.length === 81 && fetched.solution?.length === 81
|
|
130
|
+
? puzzleFromFetch(fetched, source, levelNumber, fetchedDailyDate)
|
|
131
|
+
: null, [fetched, source, levelNumber, fetchedDailyDate]);
|
|
132
|
+
useEffect(() => {
|
|
133
|
+
if (isResuming || !fetchedPuzzle)
|
|
134
|
+
return;
|
|
135
|
+
if (fetchStatus !== undefined && fetchStatus !== 'success')
|
|
136
|
+
return;
|
|
137
|
+
if (pinned?.key === screenKey)
|
|
138
|
+
return;
|
|
139
|
+
if (excludedUuidRef.current === fetchedPuzzle.uuid)
|
|
140
|
+
return;
|
|
141
|
+
excludedUuidRef.current = null;
|
|
142
|
+
setPinned({ key: screenKey, puzzle: fetchedPuzzle });
|
|
143
|
+
}, [isResuming, fetchedPuzzle, fetchStatus, pinned, screenKey]);
|
|
144
|
+
const resumedPuzzle = useMemo(() => resumeGame ? puzzleFromResume(resumeGame, source, levelNumber) : null, [resumeGame, source, levelNumber]);
|
|
145
|
+
const activePuzzle = resumedPuzzle ?? (pinned?.key === screenKey ? pinned.puzzle : null);
|
|
146
|
+
// 'success' only with a puzzle to show: a fetched puzzle that is not
|
|
147
|
+
// pinned yet (or was abandoned by newGame) still reads as loading.
|
|
148
|
+
const status = !resume.isReady || (!activePuzzle && fetchStatus === 'success')
|
|
149
|
+
? 'loading'
|
|
150
|
+
: activePuzzle
|
|
151
|
+
? 'success'
|
|
152
|
+
: (fetchStatus ?? 'loading');
|
|
153
|
+
const puzzleId = source === 'daily'
|
|
154
|
+
? (activePuzzle?.dailyDate ?? null)
|
|
155
|
+
: (activePuzzle?.uuid ?? null);
|
|
156
|
+
const alreadyCompleted = source === 'daily' && puzzleId !== null
|
|
157
|
+
? (isPuzzleCompleted?.('daily', puzzleId) ?? false)
|
|
158
|
+
: false;
|
|
159
|
+
// --- Save the fresh game and start the server session -------------------
|
|
160
|
+
const [scrambled, setScrambled] = useState(null);
|
|
161
|
+
const activeUuid = activePuzzle?.uuid ?? null;
|
|
162
|
+
const onScrambledReady = useCallback((data) => {
|
|
163
|
+
if (!activeUuid)
|
|
164
|
+
return;
|
|
165
|
+
if (data.puzzle.length !== 81 || data.solution.length !== 81)
|
|
166
|
+
return;
|
|
167
|
+
setScrambled(prev => prev?.uuid === activeUuid &&
|
|
168
|
+
prev.puzzle === data.puzzle &&
|
|
169
|
+
prev.solution === data.solution
|
|
170
|
+
? prev
|
|
171
|
+
: { uuid: activeUuid, ...data });
|
|
172
|
+
}, [activeUuid]);
|
|
173
|
+
const onBoardReady = useCallback((scrambledPuzzle, scrambledSolution) => {
|
|
174
|
+
onScrambledReady({
|
|
175
|
+
puzzle: scrambledPuzzle,
|
|
176
|
+
solution: scrambledSolution,
|
|
177
|
+
});
|
|
178
|
+
}, [onScrambledReady]);
|
|
179
|
+
const startedGameRef = useRef('');
|
|
180
|
+
const startedSessionRef = useRef('');
|
|
181
|
+
useEffect(() => {
|
|
182
|
+
startedGameRef.current = '';
|
|
183
|
+
startedSessionRef.current = '';
|
|
184
|
+
}, [activeUuid, screenKey]);
|
|
185
|
+
const sessionRequest = useCallback((p, board, solution) => {
|
|
186
|
+
const level = source === 'level' ? (levelNumber ?? p.level) : p.level;
|
|
187
|
+
if (!level)
|
|
188
|
+
return null;
|
|
189
|
+
const request = {
|
|
190
|
+
board,
|
|
191
|
+
solution,
|
|
192
|
+
level,
|
|
193
|
+
techniques: techniqueBitmaskString(p),
|
|
194
|
+
puzzleType: source,
|
|
195
|
+
};
|
|
196
|
+
const id = source === 'daily' ? p.dailyDate : p.uuid;
|
|
197
|
+
if (id)
|
|
198
|
+
request.puzzleId = id;
|
|
199
|
+
return request;
|
|
200
|
+
}, [source, levelNumber]);
|
|
201
|
+
useEffect(() => {
|
|
202
|
+
if (!activePuzzle || status !== 'success')
|
|
203
|
+
return;
|
|
204
|
+
const uuid = activePuzzle.uuid;
|
|
205
|
+
if (isResuming) {
|
|
206
|
+
// Optional (RN): restart the server session for a resumed game.
|
|
207
|
+
if (startSessionOnResume &&
|
|
208
|
+
isAuthenticated &&
|
|
209
|
+
startedSessionRef.current !== uuid) {
|
|
210
|
+
const request = sessionRequest(activePuzzle, activePuzzle.puzzle, activePuzzle.solution);
|
|
211
|
+
if (request) {
|
|
212
|
+
startedSessionRef.current = uuid;
|
|
213
|
+
void startSession(request);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
if (!scrambled || scrambled.uuid !== uuid)
|
|
219
|
+
return;
|
|
220
|
+
if (source === 'daily' && (!activePuzzle.dailyDate || alreadyCompleted)) {
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
if (source === 'level' && levelNumber == null)
|
|
224
|
+
return;
|
|
225
|
+
if (startedGameRef.current === uuid)
|
|
226
|
+
return;
|
|
227
|
+
startedGameRef.current = uuid;
|
|
228
|
+
const meta = {
|
|
229
|
+
boardUuid: uuid,
|
|
230
|
+
...techniqueFieldsOf(activePuzzle),
|
|
231
|
+
};
|
|
232
|
+
if (source === 'daily' && activePuzzle.dailyDate) {
|
|
233
|
+
meta.dailyDate = activePuzzle.dailyDate;
|
|
234
|
+
}
|
|
235
|
+
if (source === 'level') {
|
|
236
|
+
meta.levelId = String(levelNumber);
|
|
237
|
+
if (levelTitle)
|
|
238
|
+
meta.levelTitle = levelTitle;
|
|
239
|
+
}
|
|
240
|
+
if (activePuzzle.level !== null)
|
|
241
|
+
meta.level = activePuzzle.level;
|
|
242
|
+
if (activePuzzle.difficultyScore !== null) {
|
|
243
|
+
meta.difficultyScore = activePuzzle.difficultyScore;
|
|
244
|
+
}
|
|
245
|
+
startGame(source, {
|
|
246
|
+
original: scrambled.puzzle,
|
|
247
|
+
user: scrambled.puzzle,
|
|
248
|
+
pencilmark: { numbers: '', autopencil: false },
|
|
249
|
+
}, scrambled.solution, meta);
|
|
250
|
+
if (isAuthenticated && startedSessionRef.current !== uuid) {
|
|
251
|
+
// The server session gets the board as played (scrambled): hints send
|
|
252
|
+
// that board as `original`, and the API credits a hint to the session
|
|
253
|
+
// only when the two match exactly.
|
|
254
|
+
const request = sessionRequest(activePuzzle, scrambled.puzzle, scrambled.solution);
|
|
255
|
+
if (request) {
|
|
256
|
+
startedSessionRef.current = uuid;
|
|
257
|
+
void startSession(request);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}, [
|
|
261
|
+
activePuzzle,
|
|
262
|
+
status,
|
|
263
|
+
isResuming,
|
|
264
|
+
scrambled,
|
|
265
|
+
source,
|
|
266
|
+
levelNumber,
|
|
267
|
+
levelTitle,
|
|
268
|
+
alreadyCompleted,
|
|
269
|
+
isAuthenticated,
|
|
270
|
+
startSessionOnResume,
|
|
271
|
+
startGame,
|
|
272
|
+
startSession,
|
|
273
|
+
sessionRequest,
|
|
274
|
+
]);
|
|
275
|
+
// --- Completion ----------------------------------------------------------
|
|
276
|
+
const [achievementResult, setAchievementResult] = useState(null);
|
|
277
|
+
const [showAchievement, setShowAchievement] = useState(false);
|
|
278
|
+
const onComplete = useCallback(async (timeSeconds) => {
|
|
279
|
+
clearGame();
|
|
280
|
+
let result = null;
|
|
281
|
+
if (isAuthenticated) {
|
|
282
|
+
result = await finishSession(timeSeconds);
|
|
283
|
+
if (showEmptyAchievement || hasAchievement(result)) {
|
|
284
|
+
setAchievementResult(result);
|
|
285
|
+
setShowAchievement(true);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
if (puzzleId && onPuzzleCompleted && !alreadyCompleted) {
|
|
289
|
+
onPuzzleCompleted({ type: source, id: puzzleId, timeSeconds });
|
|
290
|
+
}
|
|
291
|
+
return result;
|
|
292
|
+
}, [
|
|
293
|
+
clearGame,
|
|
294
|
+
isAuthenticated,
|
|
295
|
+
finishSession,
|
|
296
|
+
showEmptyAchievement,
|
|
297
|
+
puzzleId,
|
|
298
|
+
onPuzzleCompleted,
|
|
299
|
+
alreadyCompleted,
|
|
300
|
+
source,
|
|
301
|
+
]);
|
|
302
|
+
const closeAchievement = useCallback(() => {
|
|
303
|
+
setShowAchievement(false);
|
|
304
|
+
setAchievementResult(null);
|
|
305
|
+
}, []);
|
|
306
|
+
const discardResume = resume.discard;
|
|
307
|
+
const newGame = useCallback(() => {
|
|
308
|
+
clearGame();
|
|
309
|
+
discardResume();
|
|
310
|
+
excludedUuidRef.current = activeUuid;
|
|
311
|
+
setPinned(null);
|
|
312
|
+
setScrambled(null);
|
|
313
|
+
startedGameRef.current = '';
|
|
314
|
+
startedSessionRef.current = '';
|
|
315
|
+
}, [clearGame, discardResume, activeUuid]);
|
|
316
|
+
// --- Game props ----------------------------------------------------------
|
|
317
|
+
const gameProps = useMemo(() => {
|
|
318
|
+
if (!activePuzzle)
|
|
319
|
+
return null;
|
|
320
|
+
if (resumeGame) {
|
|
321
|
+
return {
|
|
322
|
+
puzzle: activePuzzle.puzzle,
|
|
323
|
+
solution: activePuzzle.solution,
|
|
324
|
+
scramble: false,
|
|
325
|
+
initialInput: resumeGame.inputString,
|
|
326
|
+
initialPencilmarks: resumeGame.pencilmarksString,
|
|
327
|
+
initialElapsedTime: resumeGame.elapsedTime,
|
|
328
|
+
initialAutoPencilmarks: resumeGame.autoPencilmarks,
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
return {
|
|
332
|
+
puzzle: activePuzzle.puzzle,
|
|
333
|
+
solution: activePuzzle.solution,
|
|
334
|
+
scramble: true,
|
|
335
|
+
};
|
|
336
|
+
}, [activePuzzle, resumeGame]);
|
|
337
|
+
return {
|
|
338
|
+
status,
|
|
339
|
+
isReady: resume.isReady,
|
|
340
|
+
isResuming,
|
|
341
|
+
resumeGame,
|
|
342
|
+
activePuzzle,
|
|
343
|
+
gameProps,
|
|
344
|
+
gameKey: activeUuid,
|
|
345
|
+
puzzleId,
|
|
346
|
+
alreadyCompleted,
|
|
347
|
+
isAuthenticated,
|
|
348
|
+
onScrambledReady,
|
|
349
|
+
onBoardReady,
|
|
350
|
+
onProgress: updateProgress,
|
|
351
|
+
onComplete,
|
|
352
|
+
newGame,
|
|
353
|
+
achievementResult,
|
|
354
|
+
showAchievement,
|
|
355
|
+
closeAchievement,
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
//# sourceMappingURL=usePuzzleSession.js.map
|