@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
|
@@ -80,4 +80,94 @@ export function getLocalizedHintTitle(t, step, keyPrefix = '') {
|
|
|
80
80
|
const loc = step.localization;
|
|
81
81
|
return resolveLocalization(t, loc?.title, step.title, keyPrefix);
|
|
82
82
|
}
|
|
83
|
+
// =============================================================================
|
|
84
|
+
// Per-step hint headings
|
|
85
|
+
// =============================================================================
|
|
86
|
+
const HINT_STEP_KEY_PREFIX = 'hints.';
|
|
87
|
+
/**
|
|
88
|
+
* Top-level tree in the apps' `hints.json` that mirrors `hints`, with one
|
|
89
|
+
* heading per step: `hints.hiddenSingle.row.scan` → `headings.hiddenSingle.row.scan`.
|
|
90
|
+
*/
|
|
91
|
+
export const HINT_HEADING_KEY_PREFIX = 'headings.';
|
|
92
|
+
/**
|
|
93
|
+
* Heading lookup for ONE hint step.
|
|
94
|
+
*
|
|
95
|
+
* Every step key the solver emits (`hints.hiddenSingle.row.scan`) has a heading
|
|
96
|
+
* at the same path under the `headings` tree (`headings.hiddenSingle.row.scan`),
|
|
97
|
+
* interpolated with that step's own values, so `{{valueN}}` means the same in
|
|
98
|
+
* the heading as in the body. Injected `hints.conflict.*` steps get headings too.
|
|
99
|
+
*
|
|
100
|
+
* Accepts the legacy flat localization shape. Returns undefined when the step
|
|
101
|
+
* carries no `hints.*` localization; callers then render no heading.
|
|
102
|
+
*/
|
|
103
|
+
export function getStepHeadingLocalization(step) {
|
|
104
|
+
if (!step)
|
|
105
|
+
return undefined;
|
|
106
|
+
const loc = getTextLocalization(step);
|
|
107
|
+
if (!loc?.stringKey.startsWith(HINT_STEP_KEY_PREFIX))
|
|
108
|
+
return undefined;
|
|
109
|
+
return {
|
|
110
|
+
stringKey: HINT_HEADING_KEY_PREFIX +
|
|
111
|
+
loc.stringKey.slice(HINT_STEP_KEY_PREFIX.length),
|
|
112
|
+
values: loc.values ?? [],
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Translated heading for one hint step, or '' when it has none (no `hints.*`
|
|
117
|
+
* key, or the heading is missing), so the UI renders nothing instead of a raw key.
|
|
118
|
+
*
|
|
119
|
+
* @param t - Translation function for the `hints` namespace
|
|
120
|
+
*/
|
|
121
|
+
export function getLocalizedHintHeading(t, step) {
|
|
122
|
+
const loc = getStepHeadingLocalization(step);
|
|
123
|
+
return loc ? localizedField(t, loc, '') : '';
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Fill `{{valueN}}` placeholders from a values array (values[0] → `{{value1}}`).
|
|
127
|
+
* Missing values become ''.
|
|
128
|
+
*/
|
|
129
|
+
export function interpolateHintValues(template, values) {
|
|
130
|
+
return template.replace(/\{\{\s*value(\d+)\s*\}\}/g, (_match, n) => values[Number(n) - 1] ?? '');
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Heading for one hint step looked up directly in a heading tree, without an
|
|
134
|
+
* i18n library (the Chrome extension bundles the English tree as JSON).
|
|
135
|
+
*
|
|
136
|
+
* The step's `hints.<path>` key maps to `<path>` in the tree and the template
|
|
137
|
+
* is interpolated with the step's values. Returns '' when there is no heading.
|
|
138
|
+
*/
|
|
139
|
+
export function getStepHeadingFromTree(tree, step) {
|
|
140
|
+
const loc = getStepHeadingLocalization(step);
|
|
141
|
+
if (!loc)
|
|
142
|
+
return '';
|
|
143
|
+
let node = tree;
|
|
144
|
+
for (const segment of loc.stringKey
|
|
145
|
+
.slice(HINT_HEADING_KEY_PREFIX.length)
|
|
146
|
+
.split('.')) {
|
|
147
|
+
if (!node || typeof node === 'string')
|
|
148
|
+
return '';
|
|
149
|
+
node = Object.prototype.hasOwnProperty.call(node, segment)
|
|
150
|
+
? node[segment]
|
|
151
|
+
: undefined;
|
|
152
|
+
}
|
|
153
|
+
return typeof node === 'string'
|
|
154
|
+
? interpolateHintValues(node, loc.values)
|
|
155
|
+
: '';
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Build the hint text/title/heading resolvers the apps' `useLocalizedHint`
|
|
159
|
+
* hooks return. The hooks stay in the apps because they read react-i18next;
|
|
160
|
+
* they should wrap this.
|
|
161
|
+
*
|
|
162
|
+
* @param tHints - Translate function for the `hints` namespace (text + headings)
|
|
163
|
+
* @param tTitle - Entity translate function for titles
|
|
164
|
+
* (`techniques.<path>.title`), e.g. from createNamespacedTranslate
|
|
165
|
+
*/
|
|
166
|
+
export function createLocalizedHintHelpers(tHints, tTitle) {
|
|
167
|
+
return {
|
|
168
|
+
getLocalizedText: step => getLocalizedHintText(tHints, step),
|
|
169
|
+
getLocalizedTitle: step => getLocalizedHintTitle(tTitle, step),
|
|
170
|
+
getLocalizedHeading: step => getLocalizedHintHeading(tHints, step),
|
|
171
|
+
};
|
|
172
|
+
}
|
|
83
173
|
//# sourceMappingURL=localizedHint.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"localizedHint.js","sourceRoot":"","sources":["../../src/utils/localizedHint.ts"],"names":[],"mappings":"AAWA;;;GAGG;AACH,SAAS,qBAAqB,CAAC,MAAgB;IAC7C,MAAM,MAAM,GAA2B,EAAE,CAAC;IAC1C,MAAM,CAAC,OAAO,CAAC,CAAC,KAAK,EAAE,KAAK,EAAE,EAAE;QAC9B,MAAM,CAAC,QAAQ,KAAK,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC;IACtC,CAAC,CAAC,CAAC;IACH,OAAO,MAAM,CAAC;AAChB,CAAC;AAED;;GAEG;AACH,SAAS,mBAAmB,CAC1B,CAAoB,EACpB,GAA8B,EAC9B,QAAgB,EAChB,YAAoB,EAAE;IAEtB,IAAI,CAAC,GAAG,EAAE,SAAS;QAAE,OAAO,QAAQ,CAAC;IAErC,MAAM,aAAa,GAAG,qBAAqB,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;IACxD,MAAM,OAAO,GAAG,SAAS,GAAG,GAAG,CAAC,SAAS,CAAC;IAC1C,MAAM,UAAU,GAAG,CAAC,CAAC,OAAO,EAAE,aAAa,CAAC,CAAC;IAE7C,4EAA4E;IAC5E,IAAI,UAAU,KAAK,OAAO;QAAE,OAAO,QAAQ,CAAC;IAC5C,OAAO,UAAU,CAAC;AACpB,CAAC;AAED;;;;GAIG;AACH,SAAS,mBAAmB,CAAC,IAAoB;IAC/C,MAAM,GAAG,GAAG,IAAI,CAAC,YAGJ,CAAC;IAEd,IAAI,CAAC,GAAG;QAAE,OAAO,SAAS,CAAC;IAE3B,yDAAyD;IACzD,IAAI,GAAG,CAAC,IAAI,EAAE,SAAS;QAAE,OAAO,GAAG,CAAC,IAAI,CAAC;IAEzC,kDAAkD;IAClD,IAAI,WAAW,IAAI,GAAG,IAAK,GAAqB,CAAC,SAAS,EAAE,CAAC;QAC3D,OAAO,GAA+B,CAAC;IACzC,CAAC;IAED,OAAO,SAAS,CAAC;AACnB,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,oBAAoB,CAClC,CAAoB,EACpB,IAAoB,EACpB,YAAoB,EAAE;IAEtB,MAAM,OAAO,GAAG,mBAAmB,CAAC,IAAI,CAAC,CAAC;IAC1C,OAAO,mBAAmB,CAAC,CAAC,EAAE,OAAO,EAAE,IAAI,CAAC,IAAI,EAAE,SAAS,CAAC,CAAC;AAC/D,CAAC;AAED;;;;;;;GAOG;AACH;;;;;;;GAOG;AACH,MAAM,UAAU,cAAc,CAC5B,CAAoB,EACpB,GAA8B,EAC9B,QAAmC;IAEnC,OAAO,mBAAmB,CAAC,CAAC,EAAE,GAAG,EAAE,QAAQ,IAAI,EAAE,CAAC,CAAC;AACrD,CAAC;AAED,MAAM,UAAU,qBAAqB,CACnC,CAAoB,EACpB,IAAoB,EACpB,YAAoB,EAAE;IAEtB,MAAM,GAAG,GAAG,IAAI,CAAC,YAAqD,CAAC;IACvE,OAAO,mBAAmB,CAAC,CAAC,EAAE,GAAG,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,SAAS,CAAC,CAAC;AACnE,CAAC","sourcesContent":["import type { LocalizedHint, SolverHintStep } from '@sudobility/sudojo_types';\n\n/**\n * A translation function that takes a key and interpolation values.\n * Compatible with i18next's `t` function.\n */\nexport type TranslateFunction = (\n key: string,\n values?: Record<string, string>\n) => string;\n\n/**\n * Convert localization values array to i18next interpolation object.\n * values: [\"3\", \"R1C5\"] → { value1: \"3\", value2: \"R1C5\" }\n */\nfunction valuesToInterpolation(values: string[]): Record<string, string> {\n const result: Record<string, string> = {};\n values.forEach((value, index) => {\n result[`value${index + 1}`] = value;\n });\n return result;\n}\n\n/**\n * Resolve a LocalizedHint to translated text, falling back to the raw value.\n */\nfunction resolveLocalization(\n t: TranslateFunction,\n loc: LocalizedHint | undefined,\n fallback: string,\n keyPrefix: string = ''\n): string {\n if (!loc?.stringKey) return fallback;\n\n const interpolation = valuesToInterpolation(loc.values);\n const fullKey = keyPrefix + loc.stringKey;\n const translated = t(fullKey, interpolation);\n\n // If translation returns the key itself (missing translation), use fallback\n if (translated === fullKey) return fallback;\n return translated;\n}\n\n/**\n * Extract the text LocalizedHint from a hint step's localization field.\n * Handles both the new structured format { text?: LocalizedHint }\n * and the old flat format (LocalizedHint directly) for backward compatibility.\n */\nfunction getTextLocalization(step: SolverHintStep): LocalizedHint | undefined {\n const loc = step.localization as\n | { text?: LocalizedHint; title?: LocalizedHint }\n | (LocalizedHint & { text?: never })\n | undefined;\n\n if (!loc) return undefined;\n\n // New structured format: { text: { stringKey, values } }\n if (loc.text?.stringKey) return loc.text;\n\n // Old flat format: { stringKey, values } directly\n if ('stringKey' in loc && (loc as LocalizedHint).stringKey) {\n return loc as unknown as LocalizedHint;\n }\n\n return undefined;\n}\n\n/**\n * Get localized text for a hint step using a provided translation function.\n * Returns the translated text with interpolated values,\n * or falls back to step.text if localization is unavailable.\n *\n * Handles both new structured format { text?: LocalizedHint, title?: LocalizedHint }\n * and old flat format (LocalizedHint directly) for backward compatibility.\n *\n * @param t - Translation function (e.g. from `useTranslation('hints')`)\n * @param step - The hint step containing localization data\n * @param keyPrefix - Optional prefix to prepend to the string key (e.g. 'hints.')\n */\nexport function getLocalizedHintText(\n t: TranslateFunction,\n step: SolverHintStep,\n keyPrefix: string = ''\n): string {\n const textLoc = getTextLocalization(step);\n return resolveLocalization(t, textLoc, step.text, keyPrefix);\n}\n\n/**\n * Get localized title for a hint step (technique name).\n * Returns the translated title or falls back to step.title.\n *\n * @param t - Translation function (e.g. from `useTranslation()` for common namespace)\n * @param step - The hint step containing localization data\n * @param keyPrefix - Optional prefix to prepend to the string key\n */\n/**\n * Resolve a localized field value using the API-provided string key.\n * Falls back to the raw value if translation is missing.\n *\n * @param t - Translation function (e.g. from `useTranslation()`)\n * @param loc - LocalizedHint from API response (stringKey + values)\n * @param fallback - Raw value from API to use if translation is missing\n */\nexport function localizedField(\n t: TranslateFunction,\n loc: LocalizedHint | undefined,\n fallback: string | null | undefined\n): string {\n return resolveLocalization(t, loc, fallback ?? '');\n}\n\nexport function getLocalizedHintTitle(\n t: TranslateFunction,\n step: SolverHintStep,\n keyPrefix: string = ''\n): string {\n const loc = step.localization as { title?: LocalizedHint } | undefined;\n return resolveLocalization(t, loc?.title, step.title, keyPrefix);\n}\n"]}
|
|
1
|
+
{"version":3,"file":"localizedHint.js","sourceRoot":"","sources":["../../src/utils/localizedHint.ts"],"names":[],"mappings":"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","sourcesContent":["import type { LocalizedHint, SolverHintStep } from '@sudobility/sudojo_types';\n\n/**\n * A translation function that takes a key and interpolation values.\n * Compatible with i18next's `t` function.\n */\nexport type TranslateFunction = (\n key: string,\n values?: Record<string, string>\n) => string;\n\n/**\n * Convert localization values array to i18next interpolation object.\n * values: [\"3\", \"R1C5\"] → { value1: \"3\", value2: \"R1C5\" }\n */\nfunction valuesToInterpolation(values: string[]): Record<string, string> {\n const result: Record<string, string> = {};\n values.forEach((value, index) => {\n result[`value${index + 1}`] = value;\n });\n return result;\n}\n\n/**\n * Resolve a LocalizedHint to translated text, falling back to the raw value.\n */\nfunction resolveLocalization(\n t: TranslateFunction,\n loc: LocalizedHint | undefined,\n fallback: string,\n keyPrefix: string = ''\n): string {\n if (!loc?.stringKey) return fallback;\n\n const interpolation = valuesToInterpolation(loc.values);\n const fullKey = keyPrefix + loc.stringKey;\n const translated = t(fullKey, interpolation);\n\n // If translation returns the key itself (missing translation), use fallback\n if (translated === fullKey) return fallback;\n return translated;\n}\n\n/**\n * Extract the text LocalizedHint from a hint step's localization field.\n * Handles both the new structured format { text?: LocalizedHint }\n * and the old flat format (LocalizedHint directly) for backward compatibility.\n */\nfunction getTextLocalization(step: SolverHintStep): LocalizedHint | undefined {\n const loc = step.localization as\n | { text?: LocalizedHint; title?: LocalizedHint }\n | (LocalizedHint & { text?: never })\n | undefined;\n\n if (!loc) return undefined;\n\n // New structured format: { text: { stringKey, values } }\n if (loc.text?.stringKey) return loc.text;\n\n // Old flat format: { stringKey, values } directly\n if ('stringKey' in loc && (loc as LocalizedHint).stringKey) {\n return loc as unknown as LocalizedHint;\n }\n\n return undefined;\n}\n\n/**\n * Get localized text for a hint step using a provided translation function.\n * Returns the translated text with interpolated values,\n * or falls back to step.text if localization is unavailable.\n *\n * Handles both new structured format { text?: LocalizedHint, title?: LocalizedHint }\n * and old flat format (LocalizedHint directly) for backward compatibility.\n *\n * @param t - Translation function (e.g. from `useTranslation('hints')`)\n * @param step - The hint step containing localization data\n * @param keyPrefix - Optional prefix to prepend to the string key (e.g. 'hints.')\n */\nexport function getLocalizedHintText(\n t: TranslateFunction,\n step: SolverHintStep,\n keyPrefix: string = ''\n): string {\n const textLoc = getTextLocalization(step);\n return resolveLocalization(t, textLoc, step.text, keyPrefix);\n}\n\n/**\n * Get localized title for a hint step (technique name).\n * Returns the translated title or falls back to step.title.\n *\n * @param t - Translation function (e.g. from `useTranslation()` for common namespace)\n * @param step - The hint step containing localization data\n * @param keyPrefix - Optional prefix to prepend to the string key\n */\n/**\n * Resolve a localized field value using the API-provided string key.\n * Falls back to the raw value if translation is missing.\n *\n * @param t - Translation function (e.g. from `useTranslation()`)\n * @param loc - LocalizedHint from API response (stringKey + values)\n * @param fallback - Raw value from API to use if translation is missing\n */\nexport function localizedField(\n t: TranslateFunction,\n loc: LocalizedHint | undefined,\n fallback: string | null | undefined\n): string {\n return resolveLocalization(t, loc, fallback ?? '');\n}\n\nexport function getLocalizedHintTitle(\n t: TranslateFunction,\n step: SolverHintStep,\n keyPrefix: string = ''\n): string {\n const loc = step.localization as { title?: LocalizedHint } | undefined;\n return resolveLocalization(t, loc?.title, step.title, keyPrefix);\n}\n\n// =============================================================================\n// Per-step hint headings\n// =============================================================================\n\nconst HINT_STEP_KEY_PREFIX = 'hints.';\n\n/**\n * Top-level tree in the apps' `hints.json` that mirrors `hints`, with one\n * heading per step: `hints.hiddenSingle.row.scan` → `headings.hiddenSingle.row.scan`.\n */\nexport const HINT_HEADING_KEY_PREFIX = 'headings.';\n\n/**\n * Heading lookup for ONE hint step.\n *\n * Every step key the solver emits (`hints.hiddenSingle.row.scan`) has a heading\n * at the same path under the `headings` tree (`headings.hiddenSingle.row.scan`),\n * interpolated with that step's own values, so `{{valueN}}` means the same in\n * the heading as in the body. Injected `hints.conflict.*` steps get headings too.\n *\n * Accepts the legacy flat localization shape. Returns undefined when the step\n * carries no `hints.*` localization; callers then render no heading.\n */\nexport function getStepHeadingLocalization(\n step: SolverHintStep | null | undefined\n): LocalizedHint | undefined {\n if (!step) return undefined;\n const loc = getTextLocalization(step);\n if (!loc?.stringKey.startsWith(HINT_STEP_KEY_PREFIX)) return undefined;\n return {\n stringKey:\n HINT_HEADING_KEY_PREFIX +\n loc.stringKey.slice(HINT_STEP_KEY_PREFIX.length),\n values: loc.values ?? [],\n };\n}\n\n/**\n * Translated heading for one hint step, or '' when it has none (no `hints.*`\n * key, or the heading is missing), so the UI renders nothing instead of a raw key.\n *\n * @param t - Translation function for the `hints` namespace\n */\nexport function getLocalizedHintHeading(\n t: TranslateFunction,\n step: SolverHintStep | null | undefined\n): string {\n const loc = getStepHeadingLocalization(step);\n return loc ? localizedField(t, loc, '') : '';\n}\n\n/** A nested heading tree, e.g. the `headings` object of `hints.json`. */\nexport interface HintHeadingTree {\n [key: string]: string | HintHeadingTree;\n}\n\n/**\n * Fill `{{valueN}}` placeholders from a values array (values[0] → `{{value1}}`).\n * Missing values become ''.\n */\nexport function interpolateHintValues(\n template: string,\n values: readonly string[]\n): string {\n return template.replace(\n /\\{\\{\\s*value(\\d+)\\s*\\}\\}/g,\n (_match, n: string) => values[Number(n) - 1] ?? ''\n );\n}\n\n/**\n * Heading for one hint step looked up directly in a heading tree, without an\n * i18n library (the Chrome extension bundles the English tree as JSON).\n *\n * The step's `hints.<path>` key maps to `<path>` in the tree and the template\n * is interpolated with the step's values. Returns '' when there is no heading.\n */\nexport function getStepHeadingFromTree(\n tree: HintHeadingTree,\n step: SolverHintStep | null | undefined\n): string {\n const loc = getStepHeadingLocalization(step);\n if (!loc) return '';\n let node: string | HintHeadingTree | undefined = tree;\n for (const segment of loc.stringKey\n .slice(HINT_HEADING_KEY_PREFIX.length)\n .split('.')) {\n if (!node || typeof node === 'string') return '';\n node = Object.prototype.hasOwnProperty.call(node, segment)\n ? node[segment]\n : undefined;\n }\n return typeof node === 'string'\n ? interpolateHintValues(node, loc.values)\n : '';\n}\n\n/** Localized text, title and heading resolvers for hint steps. */\nexport interface LocalizedHintHelpers {\n /** Localized body text (falls back to `step.text`). */\n getLocalizedText: (step: SolverHintStep) => string;\n /** Localized technique title (falls back to `step.title`). */\n getLocalizedTitle: (step: SolverHintStep) => string;\n /** Localized per-step heading, or ''. */\n getLocalizedHeading: (step: SolverHintStep | null | undefined) => string;\n}\n\n/**\n * Build the hint text/title/heading resolvers the apps' `useLocalizedHint`\n * hooks return. The hooks stay in the apps because they read react-i18next;\n * they should wrap this.\n *\n * @param tHints - Translate function for the `hints` namespace (text + headings)\n * @param tTitle - Entity translate function for titles\n * (`techniques.<path>.title`), e.g. from createNamespacedTranslate\n */\nexport function createLocalizedHintHelpers(\n tHints: TranslateFunction,\n tTitle: TranslateFunction\n): LocalizedHintHelpers {\n return {\n getLocalizedText: step => getLocalizedHintText(tHints, step),\n getLocalizedTitle: step => getLocalizedHintTitle(tTitle, step),\n getLocalizedHeading: step => getLocalizedHintHeading(tHints, step),\n };\n}\n"]}
|
package/dist/utils/progress.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Progress calculation utilities
|
|
3
3
|
*/
|
|
4
|
-
import type { CompletedPuzzle, GameStats } from '../types/progress';
|
|
4
|
+
import type { CompletedPuzzle, GameStats, UserProgress } from '../types/progress';
|
|
5
5
|
/**
|
|
6
6
|
* Calculate game statistics from completed puzzles
|
|
7
7
|
*
|
|
@@ -41,4 +41,13 @@ export declare function getCompletedLevelIds(completedPuzzles: CompletedPuzzle[]
|
|
|
41
41
|
* @returns Array of completed daily dates (YYYY-MM-DD)
|
|
42
42
|
*/
|
|
43
43
|
export declare function getCompletedDailyDates(completedPuzzles: CompletedPuzzle[]): string[];
|
|
44
|
+
/** A puzzle completion to record (completedAt is stamped on apply). */
|
|
45
|
+
export type PuzzleCompletion = Omit<CompletedPuzzle, 'completedAt'>;
|
|
46
|
+
/**
|
|
47
|
+
* Record a puzzle completion (the web ProgressProvider's markCompleted
|
|
48
|
+
* reducer). Returns `progress` unchanged when the puzzle was already
|
|
49
|
+
* completed; otherwise appends it with `completedAt`, sets lastDailyDate for a
|
|
50
|
+
* daily (its id is the date), and recomputes the streak, total and stats.
|
|
51
|
+
*/
|
|
52
|
+
export declare function applyPuzzleCompletion(progress: UserProgress, puzzle: PuzzleCompletion, now?: Date): UserProgress;
|
|
44
53
|
//# sourceMappingURL=progress.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"progress.d.ts","sourceRoot":"","sources":["../../src/utils/progress.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"progress.d.ts","sourceRoot":"","sources":["../../src/utils/progress.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EACV,eAAe,EACf,SAAS,EACT,YAAY,EACb,MAAM,mBAAmB,CAAC;AAE3B;;;;;;GAMG;AACH,wBAAgB,cAAc,CAC5B,gBAAgB,EAAE,eAAe,EAAE,EACnC,OAAO,CAAC,EAAE,MAAM,GACf,SAAS,CAqCX;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAC7B,gBAAgB,EAAE,eAAe,EAAE,EACnC,aAAa,EAAE,MAAM,GAAG,IAAI,GAC3B,MAAM,CAoCR;AAED;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAC/B,gBAAgB,EAAE,eAAe,EAAE,EACnC,IAAI,EAAE,OAAO,GAAG,OAAO,EACvB,EAAE,EAAE,MAAM,GACT,OAAO,CAET;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAClC,gBAAgB,EAAE,eAAe,EAAE,GAClC,MAAM,EAAE,CAEV;AAED;;;;;GAKG;AACH,wBAAgB,sBAAsB,CACpC,gBAAgB,EAAE,eAAe,EAAE,GAClC,MAAM,EAAE,CAEV;AAED,uEAAuE;AACvE,MAAM,MAAM,gBAAgB,GAAG,IAAI,CAAC,eAAe,EAAE,aAAa,CAAC,CAAC;AAEpE;;;;;GAKG;AACH,wBAAgB,qBAAqB,CACnC,QAAQ,EAAE,YAAY,EACtB,MAAM,EAAE,gBAAgB,EACxB,GAAG,GAAE,IAAiB,GACrB,YAAY,CAoBd"}
|
package/dist/utils/progress.js
CHANGED
|
@@ -101,4 +101,28 @@ export function getCompletedLevelIds(completedPuzzles) {
|
|
|
101
101
|
export function getCompletedDailyDates(completedPuzzles) {
|
|
102
102
|
return completedPuzzles.filter(p => p.type === 'daily').map(p => p.id);
|
|
103
103
|
}
|
|
104
|
+
/**
|
|
105
|
+
* Record a puzzle completion (the web ProgressProvider's markCompleted
|
|
106
|
+
* reducer). Returns `progress` unchanged when the puzzle was already
|
|
107
|
+
* completed; otherwise appends it with `completedAt`, sets lastDailyDate for a
|
|
108
|
+
* daily (its id is the date), and recomputes the streak, total and stats.
|
|
109
|
+
*/
|
|
110
|
+
export function applyPuzzleCompletion(progress, puzzle, now = new Date()) {
|
|
111
|
+
if (isPuzzleCompleted(progress.completedPuzzles, puzzle.type, puzzle.id)) {
|
|
112
|
+
return progress;
|
|
113
|
+
}
|
|
114
|
+
const completedPuzzle = {
|
|
115
|
+
...puzzle,
|
|
116
|
+
completedAt: now.toISOString(),
|
|
117
|
+
};
|
|
118
|
+
const completedPuzzles = [...progress.completedPuzzles, completedPuzzle];
|
|
119
|
+
const lastDailyDate = puzzle.type === 'daily' ? puzzle.id : progress.lastDailyDate;
|
|
120
|
+
return {
|
|
121
|
+
completedPuzzles,
|
|
122
|
+
dailyStreak: calculateStreak(completedPuzzles, lastDailyDate),
|
|
123
|
+
lastDailyDate,
|
|
124
|
+
totalCompleted: progress.totalCompleted + 1,
|
|
125
|
+
stats: calculateStats(completedPuzzles, puzzle.timeSeconds),
|
|
126
|
+
};
|
|
127
|
+
}
|
|
104
128
|
//# sourceMappingURL=progress.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"progress.js","sourceRoot":"","sources":["../../src/utils/progress.ts"],"names":[],"mappings":"AAAA;;GAEG;
|
|
1
|
+
{"version":3,"file":"progress.js","sourceRoot":"","sources":["../../src/utils/progress.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Progress calculation utilities\n */\n\nimport type {\n CompletedPuzzle,\n GameStats,\n UserProgress,\n} from '../types/progress';\n\n/**\n * Calculate game statistics from completed puzzles\n *\n * @param completedPuzzles - Array of completed puzzle records\n * @param newTime - Optional new time to include in calculations\n * @returns Computed game statistics\n */\nexport function calculateStats(\n completedPuzzles: CompletedPuzzle[],\n newTime?: number\n): GameStats {\n const puzzlesWithTime = completedPuzzles.filter(\n p => p.timeSeconds !== undefined\n );\n\n // Calculate total time\n const totalTimePlayed =\n puzzlesWithTime.reduce((sum, p) => sum + (p.timeSeconds ?? 0), 0) +\n (newTime ?? 0);\n\n // Calculate average time\n const count = puzzlesWithTime.length + (newTime !== undefined ? 1 : 0);\n const averageTime = count > 0 ? Math.round(totalTimePlayed / count) : null;\n\n // Calculate best times by type\n const dailyTimes = completedPuzzles\n .filter(\n (p): p is CompletedPuzzle & { timeSeconds: number } =>\n p.type === 'daily' && p.timeSeconds !== undefined\n )\n .map(p => p.timeSeconds);\n const levelTimes = completedPuzzles\n .filter(\n (p): p is CompletedPuzzle & { timeSeconds: number } =>\n p.type === 'level' && p.timeSeconds !== undefined\n )\n .map(p => p.timeSeconds);\n\n const bestDailyTime = dailyTimes.length > 0 ? Math.min(...dailyTimes) : null;\n const bestLevelTime = levelTimes.length > 0 ? Math.min(...levelTimes) : null;\n\n return {\n bestDailyTime,\n bestLevelTime,\n averageTime,\n totalTimePlayed,\n };\n}\n\n/**\n * Calculate daily streak based on completions\n *\n * @param completedPuzzles - Array of completed puzzle records\n * @param lastDailyDate - Date of last daily completion (YYYY-MM-DD)\n * @returns Current streak count\n */\nexport function calculateStreak(\n completedPuzzles: CompletedPuzzle[],\n lastDailyDate: string | null\n): number {\n if (!lastDailyDate) return 0;\n\n const today = new Date().toISOString().split('T')[0];\n const yesterday = new Date(Date.now() - 86400000).toISOString().split('T')[0];\n\n // If last daily wasn't today or yesterday, streak is broken\n if (lastDailyDate !== today && lastDailyDate !== yesterday) {\n return 0;\n }\n\n // Count consecutive days\n const dailyDates = completedPuzzles\n .filter(p => p.type === 'daily')\n .map(p => p.id)\n .sort()\n .reverse();\n\n let streak = 0;\n let currentDate: string | undefined =\n lastDailyDate === today ? today : yesterday;\n\n for (const date of dailyDates) {\n if (currentDate === undefined) break;\n if (date === currentDate) {\n streak++;\n // Go back one day\n const previousDay: Date = new Date(currentDate);\n previousDay.setDate(previousDay.getDate() - 1);\n currentDate = previousDay.toISOString().split('T')[0];\n } else if (date < currentDate) {\n break;\n }\n }\n\n return streak;\n}\n\n/**\n * Check if a specific puzzle is completed\n *\n * @param completedPuzzles - Array of completed puzzle records\n * @param type - Puzzle type to check\n * @param id - Puzzle ID to check\n * @returns Whether the puzzle is completed\n */\nexport function isPuzzleCompleted(\n completedPuzzles: CompletedPuzzle[],\n type: 'daily' | 'level',\n id: string\n): boolean {\n return completedPuzzles.some(p => p.type === type && p.id === id);\n}\n\n/**\n * Get completed level IDs\n *\n * @param completedPuzzles - Array of completed puzzle records\n * @returns Array of completed level IDs\n */\nexport function getCompletedLevelIds(\n completedPuzzles: CompletedPuzzle[]\n): string[] {\n return completedPuzzles.filter(p => p.type === 'level').map(p => p.id);\n}\n\n/**\n * Get completed daily dates\n *\n * @param completedPuzzles - Array of completed puzzle records\n * @returns Array of completed daily dates (YYYY-MM-DD)\n */\nexport function getCompletedDailyDates(\n completedPuzzles: CompletedPuzzle[]\n): string[] {\n return completedPuzzles.filter(p => p.type === 'daily').map(p => p.id);\n}\n\n/** A puzzle completion to record (completedAt is stamped on apply). */\nexport type PuzzleCompletion = Omit<CompletedPuzzle, 'completedAt'>;\n\n/**\n * Record a puzzle completion (the web ProgressProvider's markCompleted\n * reducer). Returns `progress` unchanged when the puzzle was already\n * completed; otherwise appends it with `completedAt`, sets lastDailyDate for a\n * daily (its id is the date), and recomputes the streak, total and stats.\n */\nexport function applyPuzzleCompletion(\n progress: UserProgress,\n puzzle: PuzzleCompletion,\n now: Date = new Date()\n): UserProgress {\n if (isPuzzleCompleted(progress.completedPuzzles, puzzle.type, puzzle.id)) {\n return progress;\n }\n\n const completedPuzzle: CompletedPuzzle = {\n ...puzzle,\n completedAt: now.toISOString(),\n };\n const completedPuzzles = [...progress.completedPuzzles, completedPuzzle];\n const lastDailyDate =\n puzzle.type === 'daily' ? puzzle.id : progress.lastDailyDate;\n\n return {\n completedPuzzles,\n dailyStreak: calculateStreak(completedPuzzles, lastDailyDate),\n lastDailyDate,\n totalCompleted: progress.totalCompleted + 1,\n stats: calculateStats(completedPuzzles, puzzle.timeSeconds),\n };\n}\n"]}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scanned board - turn an OCR result from sudojo_api into game state.
|
|
3
|
+
*
|
|
4
|
+
* A photo of a game in progress shows three things: the puzzle's givens, the
|
|
5
|
+
* digits a player had already entered (drawn in another color), and
|
|
6
|
+
* pencilmarks. `POST /api/v1/ocr/extract` returns them as a SolverBoard
|
|
7
|
+
* (`original`, `user`, `pencilmark`). Every client that scans a board goes
|
|
8
|
+
* through here, so they all keep the same parts of it.
|
|
9
|
+
*/
|
|
10
|
+
import type { OCRExtractData } from '@sudobility/sudojo_types';
|
|
11
|
+
import type { BaseResponse } from '@sudobility/types';
|
|
12
|
+
/**
|
|
13
|
+
* Fewest givens a scanned board needs: MIN_CLUES (17) from sudojo_types, the
|
|
14
|
+
* fewest a Sudoku can have and still be solvable to a unique answer. Kept as an
|
|
15
|
+
* alias so existing imports keep working.
|
|
16
|
+
*/
|
|
17
|
+
export declare const MIN_SCAN_CLUES = 17;
|
|
18
|
+
/** A scanned board, normalized and ready to load into entry or play. */
|
|
19
|
+
export interface ScannedBoard {
|
|
20
|
+
/** The givens, 81 chars, '0' = empty. Goes into the entry grid. */
|
|
21
|
+
original: string;
|
|
22
|
+
/**
|
|
23
|
+
* Digits the player had entered, 81 chars, '0' = no input. Given cells are
|
|
24
|
+
* always '0', the same shape as the game's input string, so it can be passed
|
|
25
|
+
* straight to `initialInput` / `applyHintData`.
|
|
26
|
+
*/
|
|
27
|
+
user: string;
|
|
28
|
+
/** 81 comma-separated pencilmark entries (e.g. "126"), or '' when none. */
|
|
29
|
+
pencilmarks: string;
|
|
30
|
+
/** Whether the pencilmarks were auto-filled in the scanned game. */
|
|
31
|
+
autopencil: boolean;
|
|
32
|
+
/** Number of givens in `original`. */
|
|
33
|
+
clueCount: number;
|
|
34
|
+
/** OCR confidence, 0-100. */
|
|
35
|
+
confidence: number;
|
|
36
|
+
/** Which backend read the image, when the API says. */
|
|
37
|
+
engine?: OCRExtractData['engine'];
|
|
38
|
+
}
|
|
39
|
+
export type ScanBoardErrorCode =
|
|
40
|
+
/** The request failed or the API reported an error. */
|
|
41
|
+
'REQUEST_FAILED'
|
|
42
|
+
/** The API answered without a usable 81-cell board. */
|
|
43
|
+
| 'INVALID_BOARD'
|
|
44
|
+
/** The board has fewer than MIN_SCAN_CLUES givens. */
|
|
45
|
+
| 'TOO_FEW_CLUES';
|
|
46
|
+
export declare class ScanBoardError extends Error {
|
|
47
|
+
readonly code: ScanBoardErrorCode;
|
|
48
|
+
/** The board as read, when there was one (set for TOO_FEW_CLUES). */
|
|
49
|
+
readonly board?: ScannedBoard;
|
|
50
|
+
constructor(code: ScanBoardErrorCode, message: string, options?: {
|
|
51
|
+
board?: ScannedBoard;
|
|
52
|
+
cause?: unknown;
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Normalize an OCR result into a ScannedBoard.
|
|
57
|
+
*
|
|
58
|
+
* - `user` keeps only the player's digits: given cells are blanked, so a given
|
|
59
|
+
* can never be overwritten by a misread input digit.
|
|
60
|
+
* - Pencilmarks are dropped from cells that hold a digit, and kept only when
|
|
61
|
+
* there are 81 entries.
|
|
62
|
+
*
|
|
63
|
+
* @throws ScanBoardError `INVALID_BOARD` when `original` is not 81 digits
|
|
64
|
+
*/
|
|
65
|
+
export declare function toScannedBoard(data: OCRExtractData | null | undefined): ScannedBoard;
|
|
66
|
+
/** Whether the scan recorded any player digits. */
|
|
67
|
+
export declare function hasScannedInput(board: ScannedBoard): boolean;
|
|
68
|
+
/**
|
|
69
|
+
* Turn the OCR endpoint's response into a playable ScannedBoard.
|
|
70
|
+
*
|
|
71
|
+
* The request itself belongs to sudojo_client (`useSudojoOcrExtract`); this
|
|
72
|
+
* only interprets what came back.
|
|
73
|
+
*
|
|
74
|
+
* @throws ScanBoardError `REQUEST_FAILED` when the API reported an error,
|
|
75
|
+
* `INVALID_BOARD` when there is no 81-cell board, `TOO_FEW_CLUES` when the
|
|
76
|
+
* board has fewer than MIN_SCAN_CLUES givens (the error carries the board)
|
|
77
|
+
*/
|
|
78
|
+
export declare function scannedBoardFromResponse(response: BaseResponse<OCRExtractData>): ScannedBoard;
|
|
79
|
+
//# sourceMappingURL=scannedBoard.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scannedBoard.d.ts","sourceRoot":"","sources":["../../src/utils/scannedBoard.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAE/D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAEtD;;;;GAIG;AACH,eAAO,MAAM,cAAc,KAAY,CAAC;AAIxC,wEAAwE;AACxE,MAAM,WAAW,YAAY;IAC3B,mEAAmE;IACnE,QAAQ,EAAE,MAAM,CAAC;IACjB;;;;OAIG;IACH,IAAI,EAAE,MAAM,CAAC;IACb,2EAA2E;IAC3E,WAAW,EAAE,MAAM,CAAC;IACpB,oEAAoE;IACpE,UAAU,EAAE,OAAO,CAAC;IACpB,sCAAsC;IACtC,SAAS,EAAE,MAAM,CAAC;IAClB,6BAA6B;IAC7B,UAAU,EAAE,MAAM,CAAC;IACnB,uDAAuD;IACvD,MAAM,CAAC,EAAE,cAAc,CAAC,QAAQ,CAAC,CAAC;CACnC;AAED,MAAM,MAAM,kBAAkB;AAC5B,uDAAuD;AACrD,gBAAgB;AAClB,uDAAuD;GACrD,eAAe;AACjB,sDAAsD;GACpD,eAAe,CAAC;AAEpB,qBAAa,cAAe,SAAQ,KAAK;IACvC,QAAQ,CAAC,IAAI,EAAE,kBAAkB,CAAC;IAClC,qEAAqE;IACrE,QAAQ,CAAC,KAAK,CAAC,EAAE,YAAY,CAAC;gBAG5B,IAAI,EAAE,kBAAkB,EACxB,OAAO,EAAE,MAAM,EACf,OAAO,GAAE;QAAE,KAAK,CAAC,EAAE,YAAY,CAAC;QAAC,KAAK,CAAC,EAAE,OAAO,CAAA;KAAO;CAU1D;AAED;;;;;;;;;GASG;AACH,wBAAgB,cAAc,CAC5B,IAAI,EAAE,cAAc,GAAG,IAAI,GAAG,SAAS,GACtC,YAAY,CAyCd;AAED,mDAAmD;AACnD,wBAAgB,eAAe,CAAC,KAAK,EAAE,YAAY,GAAG,OAAO,CAE5D;AAED;;;;;;;;;GASG;AACH,wBAAgB,wBAAwB,CACtC,QAAQ,EAAE,YAAY,CAAC,cAAc,CAAC,GACrC,YAAY,CAiBd"}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scanned board - turn an OCR result from sudojo_api into game state.
|
|
3
|
+
*
|
|
4
|
+
* A photo of a game in progress shows three things: the puzzle's givens, the
|
|
5
|
+
* digits a player had already entered (drawn in another color), and
|
|
6
|
+
* pencilmarks. `POST /api/v1/ocr/extract` returns them as a SolverBoard
|
|
7
|
+
* (`original`, `user`, `pencilmark`). Every client that scans a board goes
|
|
8
|
+
* through here, so they all keep the same parts of it.
|
|
9
|
+
*/
|
|
10
|
+
import { MIN_CLUES } from '@sudobility/sudojo_types';
|
|
11
|
+
/**
|
|
12
|
+
* Fewest givens a scanned board needs: MIN_CLUES (17) from sudojo_types, the
|
|
13
|
+
* fewest a Sudoku can have and still be solvable to a unique answer. Kept as an
|
|
14
|
+
* alias so existing imports keep working.
|
|
15
|
+
*/
|
|
16
|
+
export const MIN_SCAN_CLUES = MIN_CLUES;
|
|
17
|
+
const BOARD_PATTERN = /^[0-9]{81}$/;
|
|
18
|
+
export class ScanBoardError extends Error {
|
|
19
|
+
constructor(code, message, options = {}) {
|
|
20
|
+
super(message);
|
|
21
|
+
this.name = 'ScanBoardError';
|
|
22
|
+
this.code = code;
|
|
23
|
+
if (options.board)
|
|
24
|
+
this.board = options.board;
|
|
25
|
+
if (options.cause !== undefined) {
|
|
26
|
+
this.cause = options.cause;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Normalize an OCR result into a ScannedBoard.
|
|
32
|
+
*
|
|
33
|
+
* - `user` keeps only the player's digits: given cells are blanked, so a given
|
|
34
|
+
* can never be overwritten by a misread input digit.
|
|
35
|
+
* - Pencilmarks are dropped from cells that hold a digit, and kept only when
|
|
36
|
+
* there are 81 entries.
|
|
37
|
+
*
|
|
38
|
+
* @throws ScanBoardError `INVALID_BOARD` when `original` is not 81 digits
|
|
39
|
+
*/
|
|
40
|
+
export function toScannedBoard(data) {
|
|
41
|
+
const board = data?.board;
|
|
42
|
+
const original = board?.original;
|
|
43
|
+
if (!original || !BOARD_PATTERN.test(original)) {
|
|
44
|
+
throw new ScanBoardError('INVALID_BOARD', 'OCR did not return an 81-cell board');
|
|
45
|
+
}
|
|
46
|
+
const rawUser = board.user && BOARD_PATTERN.test(board.user) ? board.user : null;
|
|
47
|
+
const user = Array.from({ length: 81 }, (_, i) => rawUser && original[i] === '0' ? rawUser[i] : '0').join('');
|
|
48
|
+
const entries = board.pencilmark?.numbers?.split(',') ?? [];
|
|
49
|
+
let pencilmarks = '';
|
|
50
|
+
if (entries.length === 81) {
|
|
51
|
+
const cleaned = entries.map((entry, i) => original[i] !== '0' || user[i] !== '0'
|
|
52
|
+
? ''
|
|
53
|
+
: Array.from(new Set(entry.replace(/[^1-9]/g, '')))
|
|
54
|
+
.sort()
|
|
55
|
+
.join(''));
|
|
56
|
+
if (cleaned.some(entry => entry !== '')) {
|
|
57
|
+
pencilmarks = cleaned.join(',');
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
const result = {
|
|
61
|
+
original,
|
|
62
|
+
user,
|
|
63
|
+
pencilmarks,
|
|
64
|
+
autopencil: pencilmarks !== '' && board.pencilmark?.autopencil === true,
|
|
65
|
+
clueCount: original.replace(/0/g, '').length,
|
|
66
|
+
confidence: data?.confidence ?? 0,
|
|
67
|
+
};
|
|
68
|
+
if (data?.engine)
|
|
69
|
+
result.engine = data.engine;
|
|
70
|
+
return result;
|
|
71
|
+
}
|
|
72
|
+
/** Whether the scan recorded any player digits. */
|
|
73
|
+
export function hasScannedInput(board) {
|
|
74
|
+
return /[1-9]/.test(board.user);
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Turn the OCR endpoint's response into a playable ScannedBoard.
|
|
78
|
+
*
|
|
79
|
+
* The request itself belongs to sudojo_client (`useSudojoOcrExtract`); this
|
|
80
|
+
* only interprets what came back.
|
|
81
|
+
*
|
|
82
|
+
* @throws ScanBoardError `REQUEST_FAILED` when the API reported an error,
|
|
83
|
+
* `INVALID_BOARD` when there is no 81-cell board, `TOO_FEW_CLUES` when the
|
|
84
|
+
* board has fewer than MIN_SCAN_CLUES givens (the error carries the board)
|
|
85
|
+
*/
|
|
86
|
+
export function scannedBoardFromResponse(response) {
|
|
87
|
+
if (!response.success) {
|
|
88
|
+
throw new ScanBoardError('REQUEST_FAILED', response.error || 'OCR request failed');
|
|
89
|
+
}
|
|
90
|
+
const board = toScannedBoard(response.data);
|
|
91
|
+
if (board.clueCount < MIN_SCAN_CLUES) {
|
|
92
|
+
throw new ScanBoardError('TOO_FEW_CLUES', `Too few clues detected (${board.clueCount}/${MIN_SCAN_CLUES} minimum)`, { board });
|
|
93
|
+
}
|
|
94
|
+
return board;
|
|
95
|
+
}
|
|
96
|
+
//# sourceMappingURL=scannedBoard.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scannedBoard.js","sourceRoot":"","sources":["../../src/utils/scannedBoard.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Scanned board - turn an OCR result from sudojo_api into game state.\n *\n * A photo of a game in progress shows three things: the puzzle's givens, the\n * digits a player had already entered (drawn in another color), and\n * pencilmarks. `POST /api/v1/ocr/extract` returns them as a SolverBoard\n * (`original`, `user`, `pencilmark`). Every client that scans a board goes\n * through here, so they all keep the same parts of it.\n */\n\nimport type { OCRExtractData } from '@sudobility/sudojo_types';\nimport { MIN_CLUES } from '@sudobility/sudojo_types';\nimport type { BaseResponse } from '@sudobility/types';\n\n/**\n * Fewest givens a scanned board needs: MIN_CLUES (17) from sudojo_types, the\n * fewest a Sudoku can have and still be solvable to a unique answer. Kept as an\n * alias so existing imports keep working.\n */\nexport const MIN_SCAN_CLUES = MIN_CLUES;\n\nconst BOARD_PATTERN = /^[0-9]{81}$/;\n\n/** A scanned board, normalized and ready to load into entry or play. */\nexport interface ScannedBoard {\n /** The givens, 81 chars, '0' = empty. Goes into the entry grid. */\n original: string;\n /**\n * Digits the player had entered, 81 chars, '0' = no input. Given cells are\n * always '0', the same shape as the game's input string, so it can be passed\n * straight to `initialInput` / `applyHintData`.\n */\n user: string;\n /** 81 comma-separated pencilmark entries (e.g. \"126\"), or '' when none. */\n pencilmarks: string;\n /** Whether the pencilmarks were auto-filled in the scanned game. */\n autopencil: boolean;\n /** Number of givens in `original`. */\n clueCount: number;\n /** OCR confidence, 0-100. */\n confidence: number;\n /** Which backend read the image, when the API says. */\n engine?: OCRExtractData['engine'];\n}\n\nexport type ScanBoardErrorCode =\n /** The request failed or the API reported an error. */\n | 'REQUEST_FAILED'\n /** The API answered without a usable 81-cell board. */\n | 'INVALID_BOARD'\n /** The board has fewer than MIN_SCAN_CLUES givens. */\n | 'TOO_FEW_CLUES';\n\nexport class ScanBoardError extends Error {\n readonly code: ScanBoardErrorCode;\n /** The board as read, when there was one (set for TOO_FEW_CLUES). */\n readonly board?: ScannedBoard;\n\n constructor(\n code: ScanBoardErrorCode,\n message: string,\n options: { board?: ScannedBoard; cause?: unknown } = {}\n ) {\n super(message);\n this.name = 'ScanBoardError';\n this.code = code;\n if (options.board) this.board = options.board;\n if (options.cause !== undefined) {\n (this as { cause?: unknown }).cause = options.cause;\n }\n }\n}\n\n/**\n * Normalize an OCR result into a ScannedBoard.\n *\n * - `user` keeps only the player's digits: given cells are blanked, so a given\n * can never be overwritten by a misread input digit.\n * - Pencilmarks are dropped from cells that hold a digit, and kept only when\n * there are 81 entries.\n *\n * @throws ScanBoardError `INVALID_BOARD` when `original` is not 81 digits\n */\nexport function toScannedBoard(\n data: OCRExtractData | null | undefined\n): ScannedBoard {\n const board = data?.board;\n const original = board?.original;\n if (!original || !BOARD_PATTERN.test(original)) {\n throw new ScanBoardError(\n 'INVALID_BOARD',\n 'OCR did not return an 81-cell board'\n );\n }\n\n const rawUser =\n board.user && BOARD_PATTERN.test(board.user) ? board.user : null;\n const user = Array.from({ length: 81 }, (_, i) =>\n rawUser && original[i] === '0' ? rawUser[i] : '0'\n ).join('');\n\n const entries = board.pencilmark?.numbers?.split(',') ?? [];\n let pencilmarks = '';\n if (entries.length === 81) {\n const cleaned = entries.map((entry, i) =>\n original[i] !== '0' || user[i] !== '0'\n ? ''\n : Array.from(new Set(entry.replace(/[^1-9]/g, '')))\n .sort()\n .join('')\n );\n if (cleaned.some(entry => entry !== '')) {\n pencilmarks = cleaned.join(',');\n }\n }\n\n const result: ScannedBoard = {\n original,\n user,\n pencilmarks,\n autopencil: pencilmarks !== '' && board.pencilmark?.autopencil === true,\n clueCount: original.replace(/0/g, '').length,\n confidence: data?.confidence ?? 0,\n };\n if (data?.engine) result.engine = data.engine;\n return result;\n}\n\n/** Whether the scan recorded any player digits. */\nexport function hasScannedInput(board: ScannedBoard): boolean {\n return /[1-9]/.test(board.user);\n}\n\n/**\n * Turn the OCR endpoint's response into a playable ScannedBoard.\n *\n * The request itself belongs to sudojo_client (`useSudojoOcrExtract`); this\n * only interprets what came back.\n *\n * @throws ScanBoardError `REQUEST_FAILED` when the API reported an error,\n * `INVALID_BOARD` when there is no 81-cell board, `TOO_FEW_CLUES` when the\n * board has fewer than MIN_SCAN_CLUES givens (the error carries the board)\n */\nexport function scannedBoardFromResponse(\n response: BaseResponse<OCRExtractData>\n): ScannedBoard {\n if (!response.success) {\n throw new ScanBoardError(\n 'REQUEST_FAILED',\n response.error || 'OCR request failed'\n );\n }\n\n const board = toScannedBoard(response.data);\n if (board.clueCount < MIN_SCAN_CLUES) {\n throw new ScanBoardError(\n 'TOO_FEW_CLUES',\n `Too few clues detected (${board.clueCount}/${MIN_SCAN_CLUES} minimum)`,\n { board }\n );\n }\n return board;\n}\n"]}
|
package/dist/utils/shareUrl.d.ts
CHANGED
|
@@ -1,49 +1,72 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Share URL builder and parser for Sudojo
|
|
2
|
+
* Share URL builder and parser for Sudojo sharing.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Paths match the web app's routes (the RN app's deep-link config accepts the
|
|
5
|
+
* same paths):
|
|
6
|
+
*
|
|
7
|
+
* | type | URL |
|
|
8
|
+
* |--------------|---------------------------------------------------------|
|
|
9
|
+
* | `daily` | `/play/daily` (bare `/daily` has no web route) |
|
|
10
|
+
* | `puzzle` | `/play/puzzle?level=&original=&user=&autopencilmarks=&pencilmarks=&hint=` |
|
|
11
|
+
* | `levels` | `/play` (the level list) |
|
|
12
|
+
* | `play` | `/play/<level>`, or `/play` without a level |
|
|
13
|
+
* | `enter` | `/play/enter` |
|
|
14
|
+
* | `techniques` | `/techniques` |
|
|
15
|
+
* | `technique` | `/techniques/<canonical path>[?hint=<step>]` |
|
|
6
16
|
*
|
|
7
17
|
* Pencilmarks are appended as a raw comma-separated string (not URL-encoded)
|
|
8
18
|
* to keep URLs readable.
|
|
9
19
|
*/
|
|
20
|
+
/** What a share URL points at. */
|
|
21
|
+
export type ShareUrlType = 'daily' | 'puzzle' | 'levels' | 'play' | 'enter' | 'techniques' | 'technique';
|
|
10
22
|
export interface ShareUrlParams {
|
|
11
|
-
type:
|
|
12
|
-
/** 81-char original puzzle string */
|
|
23
|
+
type: ShareUrlType;
|
|
24
|
+
/** 81-char original puzzle string (`puzzle`) */
|
|
13
25
|
original?: string;
|
|
14
|
-
/** 81-char user input string */
|
|
26
|
+
/** 81-char user input string (`puzzle`) */
|
|
15
27
|
user?: string;
|
|
16
|
-
/** Comma-separated pencilmarks string
|
|
28
|
+
/** Comma-separated pencilmarks string, 81 cells, 80 commas (`puzzle`) */
|
|
17
29
|
pencilmarks?: string;
|
|
18
|
-
/** Whether auto-pencilmarks are enabled */
|
|
30
|
+
/** Whether auto-pencilmarks are enabled (`puzzle`) */
|
|
19
31
|
autopencilmarks?: boolean;
|
|
20
|
-
/** Difficulty level
|
|
32
|
+
/** Difficulty level, 1-based (`puzzle`, `play`) */
|
|
21
33
|
level?: number;
|
|
22
|
-
/** Hint step index
|
|
34
|
+
/** Hint step index, 0-based. Only included when a hint is active (`puzzle`, `technique`). */
|
|
23
35
|
hint?: number;
|
|
36
|
+
/** Technique path, API or canonical; shared as the canonical slug (`technique`) */
|
|
37
|
+
path?: string;
|
|
24
38
|
/** Base domain (default: https://sudojo.com) */
|
|
25
39
|
domain?: string;
|
|
26
40
|
}
|
|
27
41
|
export interface ParsedShareParams {
|
|
28
42
|
original: string;
|
|
43
|
+
/** Player input; 81 zeros when the URL has none (a bare puzzle share). */
|
|
29
44
|
user: string;
|
|
30
45
|
pencilmarks: string;
|
|
31
46
|
autopencilmarks: boolean;
|
|
32
47
|
level?: number;
|
|
33
48
|
hint?: number;
|
|
34
49
|
}
|
|
50
|
+
/** Query values from URLSearchParams or a plain object (RN route params). */
|
|
51
|
+
export type ShareParamsSource = URLSearchParams | Record<string, string | null | undefined>;
|
|
35
52
|
/**
|
|
36
|
-
* Build a share URL
|
|
53
|
+
* Build a share URL. See the table at the top of this file for each type.
|
|
37
54
|
*
|
|
38
|
-
*
|
|
39
|
-
* encoding) so commas remain as literal commas.
|
|
55
|
+
* For `puzzle`, pencilmarks are appended directly to the URL (without
|
|
56
|
+
* URLSearchParams encoding) so commas remain as literal commas.
|
|
40
57
|
*/
|
|
41
58
|
export declare function buildShareUrl(params: ShareUrlParams): string;
|
|
42
59
|
/**
|
|
43
60
|
* Parse share URL query parameters into game state.
|
|
44
|
-
*
|
|
61
|
+
*
|
|
62
|
+
* Accepts URLSearchParams (web) or a plain object of strings (RN route
|
|
63
|
+
* params). Returns null if `original` is missing. A missing `user` (a bare
|
|
64
|
+
* puzzle share, e.g. a freshly entered puzzle) becomes 81 zeros.
|
|
65
|
+
* `level` is kept only when it is a valid level (1-12); `hint` only when it
|
|
66
|
+
* is a non-negative integer. When `level` is absent, callers fall back to the
|
|
67
|
+
* level the solver rates the puzzle at (`parsed.level ?? validatedLevel`).
|
|
45
68
|
*/
|
|
46
|
-
export declare function parseShareParams(params:
|
|
69
|
+
export declare function parseShareParams(params: ShareParamsSource): ParsedShareParams | null;
|
|
47
70
|
/**
|
|
48
71
|
* Derive the web app URL from an API base URL.
|
|
49
72
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"shareUrl.d.ts","sourceRoot":"","sources":["../../src/utils/shareUrl.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"shareUrl.d.ts","sourceRoot":"","sources":["../../src/utils/shareUrl.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAUH,kCAAkC;AAClC,MAAM,MAAM,YAAY,GACpB,OAAO,GACP,QAAQ,GACR,QAAQ,GACR,MAAM,GACN,OAAO,GACP,YAAY,GACZ,WAAW,CAAC;AAEhB,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,YAAY,CAAC;IACnB,gDAAgD;IAChD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,2CAA2C;IAC3C,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,yEAAyE;IACzE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,sDAAsD;IACtD,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,mDAAmD;IACnD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,6FAA6F;IAC7F,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,mFAAmF;IACnF,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,gDAAgD;IAChD,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,MAAM,CAAC;IACjB,0EAA0E;IAC1E,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,eAAe,EAAE,OAAO,CAAC;IACzB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,6EAA6E;AAC7E,MAAM,MAAM,iBAAiB,GACzB,eAAe,GACf,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC,CAAC;AA+B9C;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,cAAc,GAAG,MAAM,CAyB5D;AAOD;;;;;;;;;GASG;AACH,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,iBAAiB,GACxB,iBAAiB,GAAG,IAAI,CA6B1B;AAED;;;;;;GAMG;AACH,wBAAgB,SAAS,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAUpD"}
|