@omercnet/paseo-queens 0.1.0-next.126.2

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.
@@ -0,0 +1,334 @@
1
+ import type { PuzzleDifficulty } from "../shared/puzzle-catalog";
2
+
3
+ /** Pinned, integrity-checked files in an unlisted GitHub Gist. */
4
+ export type RemotePuzzleGroup = {
5
+ readonly size: number;
6
+ readonly difficulty: PuzzleDifficulty;
7
+ readonly count: number;
8
+ readonly recordBytes: number;
9
+ readonly sha256: string;
10
+ readonly url: string;
11
+ };
12
+
13
+ export const REMOTE_PUZZLE_GROUPS: Readonly<Record<string, RemotePuzzleGroup>> = {
14
+ "5-beginner": {
15
+ size: 5,
16
+ difficulty: "beginner",
17
+ count: 237,
18
+ recordBytes: 20,
19
+ sha256: "a0eb4398895869f50cf81562b96db1703b5886e6c98c0926296eb8a0a01be101",
20
+ url: "https://gist.githubusercontent.com/omercnet/652af12e2640979ba7fb5dc5adddde42/raw/4f3e565c2805a709a5121c5617bdd6f327a2ecd8/5-beginner.b64",
21
+ },
22
+ "5-easy": {
23
+ size: 5,
24
+ difficulty: "easy",
25
+ count: 4661,
26
+ recordBytes: 20,
27
+ sha256: "98a26f23e42bb1587cb660f4a11adeccb26e8142fd33b6c5c2a7a8d39a7185e4",
28
+ url: "https://gist.githubusercontent.com/omercnet/652af12e2640979ba7fb5dc5adddde42/raw/4f3e565c2805a709a5121c5617bdd6f327a2ecd8/5-easy.b64",
29
+ },
30
+ "5-medium": {
31
+ size: 5,
32
+ difficulty: "medium",
33
+ count: 1044,
34
+ recordBytes: 20,
35
+ sha256: "2b37f37fbeff1fc91feb9219efb4b86fe7935260b003bc996c807c734cb2a28c",
36
+ url: "https://gist.githubusercontent.com/omercnet/652af12e2640979ba7fb5dc5adddde42/raw/4f3e565c2805a709a5121c5617bdd6f327a2ecd8/5-medium.b64",
37
+ },
38
+ "5-hard": {
39
+ size: 5,
40
+ difficulty: "hard",
41
+ count: 178,
42
+ recordBytes: 20,
43
+ sha256: "e620330449d1592893950b54afa341d77a4fb6b5028575e110640cf5f31a9a74",
44
+ url: "https://gist.githubusercontent.com/omercnet/652af12e2640979ba7fb5dc5adddde42/raw/4f3e565c2805a709a5121c5617bdd6f327a2ecd8/5-hard.b64",
45
+ },
46
+ "6-beginner": {
47
+ size: 6,
48
+ difficulty: "beginner",
49
+ count: 311,
50
+ recordBytes: 26,
51
+ sha256: "88423258dd66d3d88a7813c9665a26ce39a14b2357c94551009bd51e007e314e",
52
+ url: "https://gist.githubusercontent.com/omercnet/652af12e2640979ba7fb5dc5adddde42/raw/4f3e565c2805a709a5121c5617bdd6f327a2ecd8/6-beginner.b64",
53
+ },
54
+ "6-easy": {
55
+ size: 6,
56
+ difficulty: "easy",
57
+ count: 6172,
58
+ recordBytes: 26,
59
+ sha256: "c1ddd59328108831787195f9c218b3d35c0497bead6fc61d37bc4a6cb5810191",
60
+ url: "https://gist.githubusercontent.com/omercnet/652af12e2640979ba7fb5dc5adddde42/raw/4f3e565c2805a709a5121c5617bdd6f327a2ecd8/6-easy.b64",
61
+ },
62
+ "6-medium": {
63
+ size: 6,
64
+ difficulty: "medium",
65
+ count: 2059,
66
+ recordBytes: 26,
67
+ sha256: "e07837d97570709f6e32a0e4a3b38b496a1f819fe43134168ab9c01af2ba8e7c",
68
+ url: "https://gist.githubusercontent.com/omercnet/652af12e2640979ba7fb5dc5adddde42/raw/4f3e565c2805a709a5121c5617bdd6f327a2ecd8/6-medium.b64",
69
+ },
70
+ "6-hard": {
71
+ size: 6,
72
+ difficulty: "hard",
73
+ count: 181,
74
+ recordBytes: 26,
75
+ sha256: "ae71e185be0e5732b4351c1c042484731c4b7e0443b07cba4748f78f4e988859",
76
+ url: "https://gist.githubusercontent.com/omercnet/652af12e2640979ba7fb5dc5adddde42/raw/4f3e565c2805a709a5121c5617bdd6f327a2ecd8/6-hard.b64",
77
+ },
78
+ "7-beginner": {
79
+ size: 7,
80
+ difficulty: "beginner",
81
+ count: 253,
82
+ recordBytes: 34,
83
+ sha256: "83171ec81b3a347023aa1577a771627af98794b6e938857729a45e8ea1643b3c",
84
+ url: "https://gist.githubusercontent.com/omercnet/652af12e2640979ba7fb5dc5adddde42/raw/4f3e565c2805a709a5121c5617bdd6f327a2ecd8/7-beginner.b64",
85
+ },
86
+ "7-easy": {
87
+ size: 7,
88
+ difficulty: "easy",
89
+ count: 5899,
90
+ recordBytes: 34,
91
+ sha256: "d95beb7dca32b83b5639afe7e673ca10575d6499100bd6a02beaff012d291713",
92
+ url: "https://gist.githubusercontent.com/omercnet/652af12e2640979ba7fb5dc5adddde42/raw/4f3e565c2805a709a5121c5617bdd6f327a2ecd8/7-easy.b64",
93
+ },
94
+ "7-medium": {
95
+ size: 7,
96
+ difficulty: "medium",
97
+ count: 3034,
98
+ recordBytes: 34,
99
+ sha256: "dd93b0c0d4a3bfd7d2401b2aefbea81c10153adc69dd9cc512d944a876501815",
100
+ url: "https://gist.githubusercontent.com/omercnet/652af12e2640979ba7fb5dc5adddde42/raw/4f3e565c2805a709a5121c5617bdd6f327a2ecd8/7-medium.b64",
101
+ },
102
+ "7-hard": {
103
+ size: 7,
104
+ difficulty: "hard",
105
+ count: 310,
106
+ recordBytes: 34,
107
+ sha256: "c7f6e137741e6b03f2fa8626a73f63b1f1fea7c5de8f35121a388dde9c87f937",
108
+ url: "https://gist.githubusercontent.com/omercnet/652af12e2640979ba7fb5dc5adddde42/raw/4f3e565c2805a709a5121c5617bdd6f327a2ecd8/7-hard.b64",
109
+ },
110
+ "8-beginner": {
111
+ size: 8,
112
+ difficulty: "beginner",
113
+ count: 164,
114
+ recordBytes: 42,
115
+ sha256: "e8a8790dd97b54e0d0baac0c2e16b7ad01c0ba3b40575e9283a0797284dd0a8e",
116
+ url: "https://gist.githubusercontent.com/omercnet/652af12e2640979ba7fb5dc5adddde42/raw/4f3e565c2805a709a5121c5617bdd6f327a2ecd8/8-beginner.b64",
117
+ },
118
+ "8-easy": {
119
+ size: 8,
120
+ difficulty: "easy",
121
+ count: 5187,
122
+ recordBytes: 42,
123
+ sha256: "55f53bb48eb0f42d1251abe747bf720cbcf50256db98a81b015ef4f4d3de536b",
124
+ url: "https://gist.githubusercontent.com/omercnet/652af12e2640979ba7fb5dc5adddde42/raw/4f3e565c2805a709a5121c5617bdd6f327a2ecd8/8-easy.b64",
125
+ },
126
+ "8-medium": {
127
+ size: 8,
128
+ difficulty: "medium",
129
+ count: 4203,
130
+ recordBytes: 42,
131
+ sha256: "882f9f58b6b90723252c34c7e4b5c4adffe8537e637649f6705d43ed60d4a149",
132
+ url: "https://gist.githubusercontent.com/omercnet/652af12e2640979ba7fb5dc5adddde42/raw/4f3e565c2805a709a5121c5617bdd6f327a2ecd8/8-medium.b64",
133
+ },
134
+ "8-hard": {
135
+ size: 8,
136
+ difficulty: "hard",
137
+ count: 456,
138
+ recordBytes: 42,
139
+ sha256: "cf89a88a7e3aff16a311cad357982bddc21bb851c83b38d5657da1a6a90d49e1",
140
+ url: "https://gist.githubusercontent.com/omercnet/652af12e2640979ba7fb5dc5adddde42/raw/4f3e565c2805a709a5121c5617bdd6f327a2ecd8/8-hard.b64",
141
+ },
142
+ "9-beginner": {
143
+ size: 9,
144
+ difficulty: "beginner",
145
+ count: 119,
146
+ recordBytes: 52,
147
+ sha256: "c84d59f243ecef3db80b49bf629803814e47bfa16db6a53e601d234f04932d41",
148
+ url: "https://gist.githubusercontent.com/omercnet/652af12e2640979ba7fb5dc5adddde42/raw/4f3e565c2805a709a5121c5617bdd6f327a2ecd8/9-beginner.b64",
149
+ },
150
+ "9-easy": {
151
+ size: 9,
152
+ difficulty: "easy",
153
+ count: 3989,
154
+ recordBytes: 52,
155
+ sha256: "30a6832ac47bd5fe02f272e34bde499bfdcaad6830058de50f2ad137fb4780d7",
156
+ url: "https://gist.githubusercontent.com/omercnet/652af12e2640979ba7fb5dc5adddde42/raw/4f3e565c2805a709a5121c5617bdd6f327a2ecd8/9-easy.b64",
157
+ },
158
+ "9-medium": {
159
+ size: 9,
160
+ difficulty: "medium",
161
+ count: 5002,
162
+ recordBytes: 52,
163
+ sha256: "dca2c411656446d3bfd741b942cd4fded86da1438e15689549c19fa14f0d2c27",
164
+ url: "https://gist.githubusercontent.com/omercnet/652af12e2640979ba7fb5dc5adddde42/raw/4f3e565c2805a709a5121c5617bdd6f327a2ecd8/9-medium.b64",
165
+ },
166
+ "9-hard": {
167
+ size: 9,
168
+ difficulty: "hard",
169
+ count: 721,
170
+ recordBytes: 52,
171
+ sha256: "b3782fb6a4858c38f6060ca3c0e74ab62bb5c9e826c3c6bf31288c73a99506b6",
172
+ url: "https://gist.githubusercontent.com/omercnet/652af12e2640979ba7fb5dc5adddde42/raw/4f3e565c2805a709a5121c5617bdd6f327a2ecd8/9-hard.b64",
173
+ },
174
+ "10-beginner": {
175
+ size: 10,
176
+ difficulty: "beginner",
177
+ count: 61,
178
+ recordBytes: 62,
179
+ sha256: "d0b030ae157690fd8306666cd75610d9f724c6fdbf847dc3252de55c4ad97c52",
180
+ url: "https://gist.githubusercontent.com/omercnet/652af12e2640979ba7fb5dc5adddde42/raw/4f3e565c2805a709a5121c5617bdd6f327a2ecd8/10-beginner.b64",
181
+ },
182
+ "10-easy": {
183
+ size: 10,
184
+ difficulty: "easy",
185
+ count: 2974,
186
+ recordBytes: 62,
187
+ sha256: "51e3b5bd38f91cce2ae9e13942e0b1284f1a8f6489006033cf6917a908691124",
188
+ url: "https://gist.githubusercontent.com/omercnet/652af12e2640979ba7fb5dc5adddde42/raw/4f3e565c2805a709a5121c5617bdd6f327a2ecd8/10-easy.b64",
189
+ },
190
+ "10-medium": {
191
+ size: 10,
192
+ difficulty: "medium",
193
+ count: 5842,
194
+ recordBytes: 62,
195
+ sha256: "51df6bee224ea44dc58757f960a5e418f01241caddcdd23fd603fee8fb72dbd5",
196
+ url: "https://gist.githubusercontent.com/omercnet/652af12e2640979ba7fb5dc5adddde42/raw/4f3e565c2805a709a5121c5617bdd6f327a2ecd8/10-medium.b64",
197
+ },
198
+ "10-hard": {
199
+ size: 10,
200
+ difficulty: "hard",
201
+ count: 1044,
202
+ recordBytes: 62,
203
+ sha256: "2481a379ccb608e3f3e21e8068b616f63523c5d70152bb50034cb0041dc46faa",
204
+ url: "https://gist.githubusercontent.com/omercnet/652af12e2640979ba7fb5dc5adddde42/raw/4f3e565c2805a709a5121c5617bdd6f327a2ecd8/10-hard.b64",
205
+ },
206
+ "11-beginner": {
207
+ size: 11,
208
+ difficulty: "beginner",
209
+ count: 55,
210
+ recordBytes: 74,
211
+ sha256: "3b4aa704d937035969580ff8b8270e3ce672287027d590a99a9a67457b384c4a",
212
+ url: "https://gist.githubusercontent.com/omercnet/652af12e2640979ba7fb5dc5adddde42/raw/4f3e565c2805a709a5121c5617bdd6f327a2ecd8/11-beginner.b64",
213
+ },
214
+ "11-easy": {
215
+ size: 11,
216
+ difficulty: "easy",
217
+ count: 2257,
218
+ recordBytes: 74,
219
+ sha256: "1c4c5ac5a6d262babac70144024fd5829ebbca5d657cbd4534a6da9b7377e046",
220
+ url: "https://gist.githubusercontent.com/omercnet/652af12e2640979ba7fb5dc5adddde42/raw/4f3e565c2805a709a5121c5617bdd6f327a2ecd8/11-easy.b64",
221
+ },
222
+ "11-medium": {
223
+ size: 11,
224
+ difficulty: "medium",
225
+ count: 6592,
226
+ recordBytes: 74,
227
+ sha256: "6dea1159d6159ded26743accf88eee714f0ee1dc98bda8a5d998a282f2879ed0",
228
+ url: "https://gist.githubusercontent.com/omercnet/652af12e2640979ba7fb5dc5adddde42/raw/4f3e565c2805a709a5121c5617bdd6f327a2ecd8/11-medium.b64",
229
+ },
230
+ "11-hard": {
231
+ size: 11,
232
+ difficulty: "hard",
233
+ count: 1750,
234
+ recordBytes: 74,
235
+ sha256: "30c04ec600b0ae57e90d049f8194af67bcb1dfbae29df57c1b2a06a82995c356",
236
+ url: "https://gist.githubusercontent.com/omercnet/652af12e2640979ba7fb5dc5adddde42/raw/4f3e565c2805a709a5121c5617bdd6f327a2ecd8/11-hard.b64",
237
+ },
238
+ "12-beginner": {
239
+ size: 12,
240
+ difficulty: "beginner",
241
+ count: 20,
242
+ recordBytes: 86,
243
+ sha256: "e59449cc42c78b25e2a82e1fd22cf7a0e021fe9d08517e92ed24b74a7c99eb63",
244
+ url: "https://gist.githubusercontent.com/omercnet/652af12e2640979ba7fb5dc5adddde42/raw/4f3e565c2805a709a5121c5617bdd6f327a2ecd8/12-beginner.b64",
245
+ },
246
+ "12-easy": {
247
+ size: 12,
248
+ difficulty: "easy",
249
+ count: 1552,
250
+ recordBytes: 86,
251
+ sha256: "05e24ded88bd1f2ba66d2d5bae31b96659a1173e222d067e8afaf22679c38186",
252
+ url: "https://gist.githubusercontent.com/omercnet/652af12e2640979ba7fb5dc5adddde42/raw/4f3e565c2805a709a5121c5617bdd6f327a2ecd8/12-easy.b64",
253
+ },
254
+ "12-medium": {
255
+ size: 12,
256
+ difficulty: "medium",
257
+ count: 6145,
258
+ recordBytes: 86,
259
+ sha256: "ffe7226b9ed146a91696966ec3d5ef92244a3287baa3081ff8f732f1fe5ad027",
260
+ url: "https://gist.githubusercontent.com/omercnet/652af12e2640979ba7fb5dc5adddde42/raw/4f3e565c2805a709a5121c5617bdd6f327a2ecd8/12-medium.b64",
261
+ },
262
+ "12-hard": {
263
+ size: 12,
264
+ difficulty: "hard",
265
+ count: 2278,
266
+ recordBytes: 86,
267
+ sha256: "dfe7f957e895dd9f9d3a10bef34615ad1a106f9a2bfa8c54c5db5c75e5e8c679",
268
+ url: "https://gist.githubusercontent.com/omercnet/652af12e2640979ba7fb5dc5adddde42/raw/4f3e565c2805a709a5121c5617bdd6f327a2ecd8/12-hard.b64",
269
+ },
270
+ "13-beginner": {
271
+ size: 13,
272
+ difficulty: "beginner",
273
+ count: 53,
274
+ recordBytes: 100,
275
+ sha256: "d7bcc65265743fd276da25a2fa1768f38ace124930d3c44d51b54c1f7315eb47",
276
+ url: "https://gist.githubusercontent.com/omercnet/652af12e2640979ba7fb5dc5adddde42/raw/4f3e565c2805a709a5121c5617bdd6f327a2ecd8/13-beginner.b64",
277
+ },
278
+ "13-easy": {
279
+ size: 13,
280
+ difficulty: "easy",
281
+ count: 2646,
282
+ recordBytes: 100,
283
+ sha256: "8ab626bc8b7bf3362403839b2a3127a3a3eea09f484e20a931fd6515ebf4c56c",
284
+ url: "https://gist.githubusercontent.com/omercnet/652af12e2640979ba7fb5dc5adddde42/raw/4f3e565c2805a709a5121c5617bdd6f327a2ecd8/13-easy.b64",
285
+ },
286
+ "13-medium": {
287
+ size: 13,
288
+ difficulty: "medium",
289
+ count: 2949,
290
+ recordBytes: 100,
291
+ sha256: "5fd11c7100d513dd1e4a31254d1f857ca27403337b4b24d4989f3f35dffbc4b6",
292
+ url: "https://gist.githubusercontent.com/omercnet/652af12e2640979ba7fb5dc5adddde42/raw/4f3e565c2805a709a5121c5617bdd6f327a2ecd8/13-medium.b64",
293
+ },
294
+ "13-hard": {
295
+ size: 13,
296
+ difficulty: "hard",
297
+ count: 4582,
298
+ recordBytes: 100,
299
+ sha256: "928d08e0b2d272781adae11692c500629d91db89583778640618f8e969c09e0f",
300
+ url: "https://gist.githubusercontent.com/omercnet/652af12e2640979ba7fb5dc5adddde42/raw/4f3e565c2805a709a5121c5617bdd6f327a2ecd8/13-hard.b64",
301
+ },
302
+ "14-beginner": {
303
+ size: 14,
304
+ difficulty: "beginner",
305
+ count: 99,
306
+ recordBytes: 114,
307
+ sha256: "53a77886ed0156726e178aed64e558a4777801243875b3347eda87b6fea9e944",
308
+ url: "https://gist.githubusercontent.com/omercnet/652af12e2640979ba7fb5dc5adddde42/raw/4f3e565c2805a709a5121c5617bdd6f327a2ecd8/14-beginner.b64",
309
+ },
310
+ "14-easy": {
311
+ size: 14,
312
+ difficulty: "easy",
313
+ count: 3338,
314
+ recordBytes: 114,
315
+ sha256: "9959ea694895cc76dbd171c585e03ac5ef913373fb583b038387f7937e55dd9e",
316
+ url: "https://gist.githubusercontent.com/omercnet/652af12e2640979ba7fb5dc5adddde42/raw/4f3e565c2805a709a5121c5617bdd6f327a2ecd8/14-easy.b64",
317
+ },
318
+ "14-medium": {
319
+ size: 14,
320
+ difficulty: "medium",
321
+ count: 2960,
322
+ recordBytes: 114,
323
+ sha256: "c1e50bb2acd25cbbcdf45fc78dc65f6d292e6ec9af743f207ac4c9930b786090",
324
+ url: "https://gist.githubusercontent.com/omercnet/652af12e2640979ba7fb5dc5adddde42/raw/4f3e565c2805a709a5121c5617bdd6f327a2ecd8/14-medium.b64",
325
+ },
326
+ "14-hard": {
327
+ size: 14,
328
+ difficulty: "hard",
329
+ count: 5807,
330
+ recordBytes: 114,
331
+ sha256: "c01f62423bf0f8940f26e98f2fab5e276a845194b01ae3e6c5337cdff29493c5",
332
+ url: "https://gist.githubusercontent.com/omercnet/652af12e2640979ba7fb5dc5adddde42/raw/4f3e565c2805a709a5121c5617bdd6f327a2ecd8/14-hard.b64",
333
+ },
334
+ };
@@ -0,0 +1,70 @@
1
+ import { createHash } from "node:crypto";
2
+ import type { z } from "zod";
3
+ import type { loadPuzzleDeck } from "../shared/puzzle-catalog";
4
+ import { REMOTE_PUZZLE_GROUPS, type RemotePuzzleGroup } from "./curated-manifest";
5
+
6
+ const MAX_REMOTE_BYTES = 2_000_000;
7
+ const FETCH_TIMEOUT_MS = 15_000;
8
+
9
+ type PuzzleDeckInput = z.output<typeof loadPuzzleDeck.input>;
10
+ type PuzzleGroupManifest = Readonly<Record<string, RemotePuzzleGroup>>;
11
+
12
+ export function createPuzzleDeckLoader(
13
+ fetcher: typeof fetch = fetch,
14
+ groups: PuzzleGroupManifest = REMOTE_PUZZLE_GROUPS,
15
+ ) {
16
+ const cache = new Map<string, Promise<string>>();
17
+
18
+ const fetchGroup = (key: string, group: RemotePuzzleGroup) => {
19
+ const existing = cache.get(key);
20
+ if (existing) return existing;
21
+
22
+ const request = (async () => {
23
+ const controller = new AbortController();
24
+ const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
25
+ try {
26
+ const response = await fetcher(group.url, { signal: controller.signal });
27
+ if (!response.ok) throw new Error(`Curated puzzle host returned HTTP ${response.status}.`);
28
+ const declaredLength = Number(response.headers.get("content-length"));
29
+ if (Number.isFinite(declaredLength) && declaredLength > MAX_REMOTE_BYTES) {
30
+ throw new RangeError("Curated puzzle payload exceeds the download limit.");
31
+ }
32
+ const data = (await response.text()).trim();
33
+ if (data.length === 0 || data.length > MAX_REMOTE_BYTES) {
34
+ throw new RangeError("Curated puzzle payload has an invalid size.");
35
+ }
36
+ const digest = createHash("sha256").update(data).digest("hex");
37
+ if (digest !== group.sha256) {
38
+ throw new Error("Curated puzzle payload failed its integrity check.");
39
+ }
40
+ return data;
41
+ } finally {
42
+ clearTimeout(timeout);
43
+ }
44
+ })().catch((error: unknown) => {
45
+ cache.delete(key);
46
+ throw error;
47
+ });
48
+
49
+ cache.set(key, request);
50
+ return request;
51
+ };
52
+
53
+ return async (input: PuzzleDeckInput) => {
54
+ const key = `${input.size}-${input.difficulty}`;
55
+ const group = groups[key];
56
+ if (!group) {
57
+ throw new RangeError(`No curated ${input.size}×${input.size} ${input.difficulty} deck.`);
58
+ }
59
+ const data = await fetchGroup(key, group);
60
+ return {
61
+ size: group.size,
62
+ difficulty: group.difficulty,
63
+ count: group.count,
64
+ recordBytes: group.recordBytes,
65
+ data,
66
+ };
67
+ };
68
+ }
69
+
70
+ export const getPuzzleDeck = createPuzzleDeckLoader();
@@ -0,0 +1,114 @@
1
+ import { defineSettings } from "@getpaseo/plugin";
2
+ import { z } from "zod";
3
+ import { PuzzleDifficultySchema, PuzzleSizeSchema } from "./puzzle-catalog";
4
+
5
+ export const DEFAULT_PUZZLE_ID = "paseo-queens-01";
6
+ export const DEFAULT_BOARD_SIZE = 6;
7
+ export const DEFAULT_DIFFICULTY = "easy" as const;
8
+
9
+ export const GAME_SETTINGS_LIMITS = {
10
+ puzzleIdLength: 128,
11
+ puzzles: 256,
12
+ cellsPerPuzzle: 400,
13
+ completionsPerPuzzle: 1_000_000,
14
+ } as const;
15
+
16
+ export const PersistedCellStateSchema = z.enum(["empty", "excluded", "marked"]);
17
+
18
+ export const PuzzleIdSchema = z
19
+ .string()
20
+ .min(1)
21
+ .max(GAME_SETTINGS_LIMITS.puzzleIdLength)
22
+ .regex(/^[A-Za-z0-9][A-Za-z0-9._-]*$/);
23
+
24
+ const TimestampMsSchema = z.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER);
25
+
26
+ export const PuzzleStateSchema = z
27
+ .object({
28
+ cells: z.array(PersistedCellStateSchema).min(1).max(GAME_SETTINGS_LIMITS.cellsPerPuzzle),
29
+ startedAtMs: TimestampMsSchema.nullable(),
30
+ completedAtMs: TimestampMsSchema.nullable(),
31
+ hintUsed: z.boolean(),
32
+ })
33
+ .strict()
34
+ .superRefine((state, context) => {
35
+ if (state.completedAtMs !== null && state.startedAtMs === null) {
36
+ context.addIssue({
37
+ code: "custom",
38
+ path: ["completedAtMs"],
39
+ message: "A completed puzzle must have a start timestamp",
40
+ });
41
+ return;
42
+ }
43
+
44
+ if (
45
+ state.completedAtMs !== null &&
46
+ state.startedAtMs !== null &&
47
+ state.completedAtMs < state.startedAtMs
48
+ ) {
49
+ context.addIssue({
50
+ code: "custom",
51
+ path: ["completedAtMs"],
52
+ message: "Completion timestamp cannot precede start timestamp",
53
+ });
54
+ }
55
+ });
56
+
57
+ export const CompletionRecordSchema = z
58
+ .object({
59
+ completions: z.number().int().min(1).max(GAME_SETTINGS_LIMITS.completionsPerPuzzle),
60
+ bestTimeMs: TimestampMsSchema,
61
+ })
62
+ .strict();
63
+
64
+ export const PuzzleStatesSchema = z
65
+ .record(PuzzleIdSchema, PuzzleStateSchema)
66
+ .superRefine((states, context) => {
67
+ if (Object.keys(states).length > GAME_SETTINGS_LIMITS.puzzles) {
68
+ context.addIssue({
69
+ code: "custom",
70
+ message: `Puzzle state collection cannot exceed ${GAME_SETTINGS_LIMITS.puzzles} entries`,
71
+ });
72
+ }
73
+ });
74
+
75
+ export const CompletionRecordsSchema = z
76
+ .record(PuzzleIdSchema, CompletionRecordSchema)
77
+ .superRefine((records, context) => {
78
+ if (Object.keys(records).length > GAME_SETTINGS_LIMITS.puzzles) {
79
+ context.addIssue({
80
+ code: "custom",
81
+ message: `Completion record collection cannot exceed ${GAME_SETTINGS_LIMITS.puzzles} entries`,
82
+ });
83
+ }
84
+ });
85
+
86
+ export const GameSettingsSchema = z
87
+ .object({
88
+ boardSize: PuzzleSizeSchema.default(DEFAULT_BOARD_SIZE),
89
+ difficulty: PuzzleDifficultySchema.default(DEFAULT_DIFFICULTY),
90
+ currentPuzzleId: PuzzleIdSchema.default(DEFAULT_PUZZLE_ID),
91
+ puzzleStates: PuzzleStatesSchema.default({}),
92
+ records: CompletionRecordsSchema.default({}),
93
+ })
94
+ .strict();
95
+
96
+ export type PersistedCellState = z.infer<typeof PersistedCellStateSchema>;
97
+ export type PuzzleState = z.infer<typeof PuzzleStateSchema>;
98
+ export type CompletionRecord = z.infer<typeof CompletionRecordSchema>;
99
+ export type GameSettings = z.infer<typeof GameSettingsSchema>;
100
+
101
+ export const DEFAULT_GAME_SETTINGS = {
102
+ boardSize: DEFAULT_BOARD_SIZE,
103
+ difficulty: DEFAULT_DIFFICULTY,
104
+ currentPuzzleId: DEFAULT_PUZZLE_ID,
105
+ puzzleStates: {},
106
+ records: {},
107
+ } satisfies GameSettings;
108
+
109
+ export const gameSettings = defineSettings({
110
+ id: "paseo-queens-game",
111
+ scope: "host",
112
+ version: 1,
113
+ schema: GameSettingsSchema,
114
+ });
@@ -0,0 +1,29 @@
1
+ import { defineRpc } from "@getpaseo/plugin";
2
+ import { z } from "zod";
3
+
4
+ export const PUZZLE_SIZES = [5, 6, 7, 8, 9, 10, 11, 12, 13, 14] as const;
5
+ export const PUZZLE_DIFFICULTIES = ["beginner", "easy", "medium", "hard"] as const;
6
+
7
+ export const PuzzleSizeSchema = z.number().int().min(5).max(14);
8
+ export const PuzzleDifficultySchema = z.enum(PUZZLE_DIFFICULTIES);
9
+
10
+ export type PuzzleDifficulty = z.infer<typeof PuzzleDifficultySchema>;
11
+
12
+ export const loadPuzzleDeck = defineRpc({
13
+ name: "queens.load-puzzle-deck",
14
+ input: z
15
+ .object({
16
+ size: PuzzleSizeSchema,
17
+ difficulty: PuzzleDifficultySchema,
18
+ })
19
+ .strict(),
20
+ output: z
21
+ .object({
22
+ size: PuzzleSizeSchema,
23
+ difficulty: PuzzleDifficultySchema,
24
+ count: z.number().int().positive().max(20_000),
25
+ recordBytes: z.number().int().positive().max(256),
26
+ data: z.string().min(1).max(2_000_000),
27
+ })
28
+ .strict(),
29
+ });