@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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Omer Cohen
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,122 @@
1
+ # Queens
2
+
3
+ A cross-platform Queens logic game for Paseo. Play from the global sidebar or open the compact composer mini-game while agents work in the background.
4
+
5
+ ## Screenshots
6
+
7
+ These screenshots use synthetic workspace and agent names on an isolated Paseo daemon.
8
+
9
+ ### Full game
10
+
11
+ ![Queens full game with an 8×8 puzzle](https://raw.githubusercontent.com/omercnet/paseo-plugins/main/queens/docs/images/queens-wide.png)
12
+
13
+ ### Compact 14×14 board
14
+
15
+ ![Queens compact layout with a 14×14 hard puzzle](https://raw.githubusercontent.com/omercnet/paseo-plugins/main/queens/docs/images/queens-compact.png)
16
+
17
+ ### Composer mini-game
18
+
19
+ ![Queens composer pill and playable popover](https://raw.githubusercontent.com/omercnet/paseo-plugins/main/queens/docs/images/queens-composer.png)
20
+
21
+ ## What it does
22
+
23
+ - Provides a global **Queens** sidebar surface and Command Center entry.
24
+ - Adds a **Queens** composer pill to every active agent.
25
+ - Opens a playable anchored popover on wide layouts and a compact sheet on mobile.
26
+ - Supports 5×5 through 14×14 boards at Beginner, Easy, Medium, and Hard levels.
27
+ - Stores progress, completion records, selected size, and difficulty in host-scoped settings.
28
+ - Shares progress and undo history between the full surface and composer mini-game.
29
+ - Shows the number of running agents and notifies you when they all become idle.
30
+
31
+ ## How to play
32
+
33
+ Place exactly one queen in every row, column, and colored region. Queens may not touch, including diagonally.
34
+
35
+ - Tap or click once to place or remove an X.
36
+ - Double-tap or double-click to place or remove a queen.
37
+ - Drag from an empty square to mark multiple Xs.
38
+ - Drag from an X to erase multiple Xs.
39
+ - Use **Hint**, **Undo**, and **Reset** when needed.
40
+
41
+ ## Puzzle catalog
42
+
43
+ The catalog contains 97,184 curated puzzles across 40 size and difficulty combinations. Puzzle layouts, classifications, and solutions are sourced from the public [Queens Ultimate](https://queensultimate.com/) puzzle corpus.
44
+
45
+ The compact groups live in an unlisted, revision-pinned [GitHub Gist](https://gist.github.com/omercnet/652af12e2640979ba7fb5dc5adddde42). The Paseo daemon fetches only the selected group, verifies its SHA-256 digest, and caches it for the session. Opening a size and difficulty for the first time therefore requires network access to `gist.githubusercontent.com`.
46
+
47
+ The importer validates every public index and source chunk, then writes the 40 upload payloads and a manifest to `/tmp/queens-curated-gist` by default:
48
+
49
+ ```bash
50
+ npm run import:puzzles
51
+ ```
52
+
53
+ After uploading those files to an unlisted Gist, regenerate the pinned runtime manifest with the immutable Gist revision:
54
+
55
+ ```bash
56
+ QUEENS_GIST_ID=<gist-id> \
57
+ QUEENS_GIST_REVISION=<revision-sha> \
58
+ npm run import:puzzles
59
+ ```
60
+
61
+ ## Install
62
+
63
+ ### Paseo 0.9 beta
64
+
65
+ Install the published npm package on the daemon host:
66
+
67
+ ```bash
68
+ paseo plugin install npm:@omercnet/paseo-queens
69
+ ```
70
+
71
+ Update an npm installation:
72
+
73
+ ```bash
74
+ paseo plugin update queens --check
75
+ paseo plugin update queens
76
+ ```
77
+
78
+ ### Paseo 0.8
79
+
80
+ Install from this monorepo:
81
+
82
+ ```bash
83
+ paseo plugin install omercnet/paseo-plugins:queens
84
+ ```
85
+
86
+ Or install a local checkout by absolute path on the daemon host:
87
+
88
+ ```bash
89
+ paseo plugin install /absolute/path/to/paseo-plugins/queens
90
+ ```
91
+
92
+ Open **Queens** from the sidebar or Command Center. Open any agent session to use the composer mini-game.
93
+
94
+ ## Develop
95
+
96
+ Use an isolated Paseo home and explicit host. Never reload this development checkout against your default daemon.
97
+
98
+ ```bash
99
+ npm ci
100
+ npm run check
101
+ npm run typecheck
102
+ npm test
103
+
104
+ paseo daemon config set daemon.listen 127.0.0.1:6802 --home /tmp/queens-dev
105
+ paseo daemon config set daemon.relay.enabled false --home /tmp/queens-dev
106
+ paseo daemon config set features.webUi.enabled true --home /tmp/queens-dev
107
+ paseo daemon config set pluginsEnabled true --home /tmp/queens-dev
108
+ paseo daemon start --home /tmp/queens-dev
109
+ paseo plugin install "$PWD" --host 127.0.0.1:6802
110
+ ```
111
+
112
+ Release Please owns package versions, changelog generation, component tags, and GitHub releases.
113
+
114
+ ## Requirements
115
+
116
+ - Paseo `^0.8.0` or `^0.9.0-beta.1`
117
+ - Plugins enabled on the target daemon
118
+ - React Native-compatible Paseo client on web, desktop, iOS, or Android
119
+
120
+ ## License
121
+
122
+ MIT. See [LICENSE](./LICENSE).
@@ -0,0 +1,55 @@
1
+ import type { ReactNode } from "react";
2
+ import { type StyleProp, StyleSheet, View, type ViewStyle } from "react-native";
3
+
4
+ export type CompletionFeedbackProps = {
5
+ solved: boolean;
6
+ color: string;
7
+ children: ReactNode;
8
+ style?: StyleProp<ViewStyle>;
9
+ };
10
+
11
+ /**
12
+ * Adds a static, non-interactive solved-state illumination around existing content.
13
+ * Keeping this to core View primitives makes the surface safe in every plugin runtime.
14
+ */
15
+ export function CompletionFeedback({ solved, color, children, style }: CompletionFeedbackProps) {
16
+ return (
17
+ <View style={[styles.container, style]}>
18
+ {children}
19
+ {solved ? (
20
+ <View
21
+ pointerEvents="none"
22
+ accessible={false}
23
+ accessibilityElementsHidden
24
+ importantForAccessibility="no-hide-descendants"
25
+ style={styles.overlay}
26
+ >
27
+ <View style={[styles.tint, { backgroundColor: color }]} />
28
+ <View style={[styles.outline, { borderColor: color }]} />
29
+ </View>
30
+ ) : null}
31
+ </View>
32
+ );
33
+ }
34
+
35
+ const styles = StyleSheet.create({
36
+ container: {
37
+ position: "relative",
38
+ overflow: "hidden",
39
+ borderRadius: 14,
40
+ },
41
+ overlay: {
42
+ ...StyleSheet.absoluteFillObject,
43
+ borderRadius: 14,
44
+ },
45
+ tint: {
46
+ ...StyleSheet.absoluteFillObject,
47
+ opacity: 0.055,
48
+ },
49
+ outline: {
50
+ ...StyleSheet.absoluteFillObject,
51
+ borderWidth: 2,
52
+ borderRadius: 14,
53
+ opacity: 0.34,
54
+ },
55
+ });
@@ -0,0 +1,74 @@
1
+ import type { PluginClientContext } from "@getpaseo/plugin/client";
2
+ import { QueenPillIcon, QueensPopover } from "./queens-popover";
3
+
4
+ const PAGE_LIMIT = 200;
5
+ const MAX_PAGES = 10;
6
+
7
+ type AgentTarget = {
8
+ readonly id: string;
9
+ readonly workspaceId?: string | null;
10
+ readonly status: string;
11
+ readonly archivedAt?: string | null;
12
+ };
13
+
14
+ export function registerQueensComposerPills(client: PluginClientContext) {
15
+ const pills = new Map<string, { workspaceId: string; remove(): void }>();
16
+ let stopped = false;
17
+
18
+ const removePill = (agentId: string) => {
19
+ pills.get(agentId)?.remove();
20
+ pills.delete(agentId);
21
+ };
22
+
23
+ const syncAgent = (agent: AgentTarget) => {
24
+ if (!agent.workspaceId || agent.archivedAt || agent.status === "closed") {
25
+ removePill(agent.id);
26
+ return;
27
+ }
28
+
29
+ const current = pills.get(agent.id);
30
+ if (current?.workspaceId === agent.workspaceId) return;
31
+ removePill(agent.id);
32
+ const pill = client.addComposerPill({
33
+ id: "queens",
34
+ workspaceId: agent.workspaceId,
35
+ agentId: agent.id,
36
+ button: {
37
+ title: "Queens",
38
+ label: "Queens",
39
+ icon: QueenPillIcon,
40
+ behavior: { kind: "popover", Content: QueensPopover },
41
+ },
42
+ });
43
+ pills.set(agent.id, { workspaceId: agent.workspaceId, remove: pill.remove });
44
+ };
45
+
46
+ const unsubscribe = client.paseo.agents.subscribe((update) => {
47
+ if (update.kind === "remove") {
48
+ removePill(update.agentId);
49
+ return;
50
+ }
51
+ syncAgent(update.agent);
52
+ });
53
+
54
+ void (async () => {
55
+ let cursor: string | undefined;
56
+ for (let page = 0; page < MAX_PAGES; page += 1) {
57
+ const result = await client.paseo.agents.list({
58
+ sort: [{ key: "updated_at", direction: "desc" }],
59
+ page: { limit: PAGE_LIMIT, ...(cursor ? { cursor } : {}) },
60
+ });
61
+ if (stopped) return;
62
+ for (const { agent } of result.entries) syncAgent(agent);
63
+ cursor = result.pageInfo.hasMore ? (result.pageInfo.nextCursor ?? undefined) : undefined;
64
+ if (!cursor) return;
65
+ }
66
+ })().catch(() => undefined);
67
+
68
+ return () => {
69
+ stopped = true;
70
+ unsubscribe();
71
+ for (const pill of pills.values()) pill.remove();
72
+ pills.clear();
73
+ };
74
+ }
@@ -0,0 +1,34 @@
1
+ import type { PluginClientContext } from "@getpaseo/plugin/client";
2
+ import { registerQueensComposerPills } from "./composer-pill";
3
+ import { PaseoQueensSurface } from "./queens-surface";
4
+ import { disposePersistedGames } from "./use-persisted-game";
5
+
6
+ export function registerPaseoQueensClient(client: PluginClientContext) {
7
+ const cleanups = [
8
+ disposePersistedGames,
9
+ client.addSurface("queens", PaseoQueensSurface),
10
+ registerQueensComposerPills(client),
11
+ client.addSidebarItem({
12
+ id: "queens",
13
+ title: "Queens",
14
+ icon: "Crown",
15
+ surface: "queens",
16
+ }),
17
+ client.addCommandCenterItem({
18
+ id: "open-queens",
19
+ title: "Open Queens",
20
+ icon: "Crown",
21
+ keywords: ["queens", "logic", "puzzle", "game"],
22
+ context: "global",
23
+ onSelect({ openSurface }) {
24
+ openSurface("queens");
25
+ },
26
+ }),
27
+ ];
28
+
29
+ return () => {
30
+ for (const cleanup of cleanups.reverse()) cleanup();
31
+ };
32
+ }
33
+
34
+ export default registerPaseoQueensClient;
@@ -0,0 +1,91 @@
1
+ import type { PuzzleDifficulty } from "../../shared/puzzle-catalog";
2
+ import type { Puzzle } from "./types";
3
+
4
+ export type EncodedPuzzleDeck = {
5
+ readonly size: number;
6
+ readonly difficulty: PuzzleDifficulty;
7
+ readonly count: number;
8
+ readonly recordBytes: number;
9
+ readonly data: string;
10
+ };
11
+
12
+ export type CuratedPuzzleDeck = {
13
+ readonly key: string;
14
+ readonly size: number;
15
+ readonly difficulty: PuzzleDifficulty;
16
+ readonly puzzles: readonly Puzzle[];
17
+ readonly solutions: Readonly<Record<string, readonly number[]>>;
18
+ };
19
+
20
+ const BASE64_VALUES = new Int16Array(128);
21
+ BASE64_VALUES.fill(-1);
22
+ for (let index = 0; index < 26; index += 1) {
23
+ BASE64_VALUES[65 + index] = index;
24
+ BASE64_VALUES[97 + index] = index + 26;
25
+ }
26
+ for (let index = 0; index < 10; index += 1) BASE64_VALUES[48 + index] = index + 52;
27
+ BASE64_VALUES[43] = 62;
28
+ BASE64_VALUES[47] = 63;
29
+
30
+ function decodeBase64(encoded: string): Uint8Array {
31
+ const padding = encoded.endsWith("==") ? 2 : encoded.endsWith("=") ? 1 : 0;
32
+ const output = new Uint8Array((encoded.length * 3) / 4 - padding);
33
+ let accumulator = 0;
34
+ let bits = 0;
35
+ let outputIndex = 0;
36
+ for (let index = 0; index < encoded.length; index += 1) {
37
+ const code = encoded.charCodeAt(index);
38
+ if (code === 61) break;
39
+ const value = code < BASE64_VALUES.length ? BASE64_VALUES[code] : -1;
40
+ if (value < 0) throw new RangeError("Curated puzzle data contains invalid base64.");
41
+ accumulator = (accumulator << 6) | value;
42
+ bits += 6;
43
+ if (bits >= 8) {
44
+ bits -= 8;
45
+ output[outputIndex] = (accumulator >> bits) & 255;
46
+ outputIndex += 1;
47
+ }
48
+ }
49
+ if (outputIndex !== output.length) throw new RangeError("Curated puzzle data is truncated.");
50
+ return output;
51
+ }
52
+
53
+ export function decodePuzzleDeck(encoded: EncodedPuzzleDeck): CuratedPuzzleDeck {
54
+ const key = `${encoded.size}-${encoded.difficulty}`;
55
+
56
+ const bytes = decodeBase64(encoded.data);
57
+ if (bytes.length !== encoded.count * encoded.recordBytes) {
58
+ throw new RangeError(`Curated ${key} deck has an invalid byte length.`);
59
+ }
60
+
61
+ const regionBytes = Math.ceil((encoded.size * encoded.size) / 2);
62
+ const expectedRecordBytes = 2 + regionBytes + encoded.size;
63
+ if (encoded.recordBytes !== expectedRecordBytes) {
64
+ throw new RangeError(`Curated ${key} deck has an invalid record size.`);
65
+ }
66
+
67
+ const puzzles: Puzzle[] = [];
68
+ const solutions: Record<string, readonly number[]> = {};
69
+ for (let record = 0; record < encoded.count; record += 1) {
70
+ let offset = record * encoded.recordBytes;
71
+ const sourceId = (bytes[offset] << 8) | bytes[offset + 1];
72
+ offset += 2;
73
+ const id = `queens-${encoded.size}x${encoded.size}-${encoded.difficulty}-${sourceId}`;
74
+ const regions = Array<number>(encoded.size * encoded.size);
75
+ for (let cell = 0; cell < regions.length; cell += 2) {
76
+ const packed = bytes[offset + Math.floor(cell / 2)];
77
+ regions[cell] = packed >> 4;
78
+ if (cell + 1 < regions.length) regions[cell + 1] = packed & 15;
79
+ }
80
+ offset += regionBytes;
81
+ const solution = Array<number>(encoded.size);
82
+ for (let row = 0; row < encoded.size; row += 1) {
83
+ solution[row] = row * encoded.size + bytes[offset + row];
84
+ }
85
+ puzzles.push({ id, size: encoded.size, regions });
86
+ solutions[id] = solution;
87
+ }
88
+
89
+ const deck = { key, size: encoded.size, difficulty: encoded.difficulty, puzzles, solutions };
90
+ return deck;
91
+ }
@@ -0,0 +1,199 @@
1
+ import type { CellState, Puzzle } from "./types";
2
+
3
+ function assertSize(size: number): void {
4
+ if (!Number.isInteger(size) || size < 1) {
5
+ throw new RangeError("Board size must be a positive integer.");
6
+ }
7
+ }
8
+
9
+ function assertPuzzle(puzzle: Puzzle): void {
10
+ assertSize(puzzle.size);
11
+
12
+ const expectedCellCount = puzzle.size * puzzle.size;
13
+ if (puzzle.regions.length !== expectedCellCount) {
14
+ throw new RangeError(`Puzzle must contain exactly ${expectedCellCount} region cells.`);
15
+ }
16
+
17
+ const regions = new Set<number>();
18
+ for (const region of puzzle.regions) {
19
+ if (!Number.isInteger(region) || region < 0 || region >= puzzle.size) {
20
+ throw new RangeError(`Region ids must be integers from 0 to ${puzzle.size - 1}.`);
21
+ }
22
+ regions.add(region);
23
+ }
24
+
25
+ if (regions.size !== puzzle.size) {
26
+ throw new RangeError(`Puzzle must contain all ${puzzle.size} region ids.`);
27
+ }
28
+ }
29
+
30
+ function assertCells(puzzle: Puzzle, cells: readonly CellState[]): void {
31
+ const expectedCellCount = puzzle.size * puzzle.size;
32
+ if (cells.length !== expectedCellCount) {
33
+ throw new RangeError(`Board must contain exactly ${expectedCellCount} cells.`);
34
+ }
35
+ }
36
+
37
+ export function createEmptyCells(size: number): CellState[] {
38
+ assertSize(size);
39
+ return Array.from({ length: size * size }, () => "empty" as const);
40
+ }
41
+
42
+ export function setCellStates(
43
+ cells: readonly CellState[],
44
+ indexes: readonly number[],
45
+ state: CellState,
46
+ ): readonly CellState[] {
47
+ let next: CellState[] | null = null;
48
+ for (const index of indexes) {
49
+ if (!Number.isInteger(index) || index < 0 || index >= cells.length) {
50
+ throw new RangeError("Cell index is outside the board.");
51
+ }
52
+ if (cells[index] === state) continue;
53
+
54
+ next ??= cells.slice();
55
+ next[index] = state;
56
+ }
57
+ return next ?? cells;
58
+ }
59
+
60
+ function addGroupConflicts(
61
+ groups: ReadonlyMap<number, readonly number[]>,
62
+ conflicts: Set<number>,
63
+ ): void {
64
+ for (const indexes of groups.values()) {
65
+ if (indexes.length > 1) {
66
+ for (const index of indexes) conflicts.add(index);
67
+ }
68
+ }
69
+ }
70
+ function addToGroup(groups: Map<number, number[]>, key: number, index: number): void {
71
+ const group = groups.get(key);
72
+ if (group) group.push(index);
73
+ else groups.set(key, [index]);
74
+ }
75
+
76
+ export function findConflicts(puzzle: Puzzle, cells: readonly CellState[]): Set<number> {
77
+ assertPuzzle(puzzle);
78
+ assertCells(puzzle, cells);
79
+
80
+ const { size } = puzzle;
81
+ const markedIndexes: number[] = [];
82
+ const rows = new Map<number, number[]>();
83
+ const columns = new Map<number, number[]>();
84
+ const regions = new Map<number, number[]>();
85
+
86
+ for (let index = 0; index < cells.length; index += 1) {
87
+ if (cells[index] !== "marked") continue;
88
+
89
+ markedIndexes.push(index);
90
+ const row = Math.floor(index / size);
91
+ const column = index % size;
92
+ const region = puzzle.regions[index];
93
+ addToGroup(rows, row, index);
94
+ addToGroup(columns, column, index);
95
+ addToGroup(regions, region, index);
96
+ }
97
+
98
+ const conflicts = new Set<number>();
99
+ addGroupConflicts(rows, conflicts);
100
+ addGroupConflicts(columns, conflicts);
101
+ addGroupConflicts(regions, conflicts);
102
+
103
+ const marked = new Set(markedIndexes);
104
+ for (const index of markedIndexes) {
105
+ const row = Math.floor(index / size);
106
+ const column = index % size;
107
+
108
+ for (let rowOffset = -1; rowOffset <= 1; rowOffset += 1) {
109
+ for (let columnOffset = -1; columnOffset <= 1; columnOffset += 1) {
110
+ if (rowOffset === 0 && columnOffset === 0) continue;
111
+
112
+ const adjacentRow = row + rowOffset;
113
+ const adjacentColumn = column + columnOffset;
114
+ if (
115
+ adjacentRow < 0 ||
116
+ adjacentRow >= size ||
117
+ adjacentColumn < 0 ||
118
+ adjacentColumn >= size
119
+ ) {
120
+ continue;
121
+ }
122
+
123
+ const adjacentIndex = adjacentRow * size + adjacentColumn;
124
+ if (marked.has(adjacentIndex)) {
125
+ conflicts.add(index);
126
+ conflicts.add(adjacentIndex);
127
+ }
128
+ }
129
+ }
130
+ }
131
+
132
+ return conflicts;
133
+ }
134
+
135
+ export function isSolved(puzzle: Puzzle, cells: readonly CellState[]): boolean {
136
+ const conflicts = findConflicts(puzzle, cells);
137
+
138
+ let markedCount = 0;
139
+ const markedRows = new Set<number>();
140
+ const markedColumns = new Set<number>();
141
+ const markedRegions = new Set<number>();
142
+
143
+ for (let index = 0; index < cells.length; index += 1) {
144
+ if (cells[index] !== "marked") continue;
145
+ markedCount += 1;
146
+ markedRows.add(Math.floor(index / puzzle.size));
147
+ markedColumns.add(index % puzzle.size);
148
+ markedRegions.add(puzzle.regions[index]);
149
+ }
150
+
151
+ return (
152
+ markedCount === puzzle.size &&
153
+ markedRows.size === puzzle.size &&
154
+ markedColumns.size === puzzle.size &&
155
+ markedRegions.size === puzzle.size &&
156
+ conflicts.size === 0
157
+ );
158
+ }
159
+
160
+ export function countSolutions(puzzle: Puzzle, limit = Number.POSITIVE_INFINITY): number {
161
+ assertPuzzle(puzzle);
162
+ if (limit !== Number.POSITIVE_INFINITY && (!Number.isInteger(limit) || limit < 0)) {
163
+ throw new RangeError("Solution limit must be a non-negative integer or Infinity.");
164
+ }
165
+ if (limit === 0) return 0;
166
+
167
+ const { size } = puzzle;
168
+ const usedColumns = new Uint8Array(size);
169
+ const usedRegions = new Uint8Array(size);
170
+ let solutions = 0;
171
+
172
+ function search(row: number, previousColumn: number): void {
173
+ if (solutions >= limit) return;
174
+ if (row === size) {
175
+ solutions += 1;
176
+ return;
177
+ }
178
+
179
+ for (let column = 0; column < size; column += 1) {
180
+ const region = puzzle.regions[row * size + column];
181
+ if (
182
+ usedColumns[column] === 1 ||
183
+ usedRegions[region] === 1 ||
184
+ (row > 0 && Math.abs(column - previousColumn) <= 1)
185
+ ) {
186
+ continue;
187
+ }
188
+
189
+ usedColumns[column] = 1;
190
+ usedRegions[region] = 1;
191
+ search(row + 1, column);
192
+ usedColumns[column] = 0;
193
+ usedRegions[region] = 0;
194
+ }
195
+ }
196
+
197
+ search(0, -2);
198
+ return solutions;
199
+ }
@@ -0,0 +1,5 @@
1
+ export { countSolutions, createEmptyCells, findConflicts, isSolved, setCellStates } from "./engine";
2
+ export { PUZZLES } from "./puzzles";
3
+ export type { GameAction, GameState, PuzzleProgress, PuzzleSolution, TimerState } from "./state";
4
+ export { createGameState, gameReducer } from "./state";
5
+ export type { CellState, Puzzle } from "./types";