@puzzmo/cli 1.0.37 → 1.0.39

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.
Files changed (46) hide show
  1. package/lib/commands/game/create.js +187 -71
  2. package/lib/commands/game/create.js.map +1 -1
  3. package/lib/commands/upload.d.ts +1 -1
  4. package/lib/commands/upload.js +143 -64
  5. package/lib/commands/upload.js.map +1 -1
  6. package/lib/commands/validate.d.ts +1 -1
  7. package/lib/commands/validate.js +24 -31
  8. package/lib/commands/validate.js.map +1 -1
  9. package/lib/index.js +4 -8
  10. package/lib/index.js.map +1 -1
  11. package/lib/skills/runner.d.ts +2 -0
  12. package/lib/skills/runner.js +33 -0
  13. package/lib/skills/runner.js.map +1 -1
  14. package/lib/util/api.d.ts +8 -0
  15. package/lib/util/api.js +22 -3
  16. package/lib/util/api.js.map +1 -1
  17. package/lib/util/createGame.d.ts +13 -0
  18. package/lib/util/createGame.js +36 -0
  19. package/lib/util/createGame.js.map +1 -0
  20. package/lib/util/discoverGames.d.ts +25 -0
  21. package/lib/util/discoverGames.js +181 -0
  22. package/lib/util/discoverGames.js.map +1 -0
  23. package/lib/util/validatePuzzmoFile.d.ts +1 -1
  24. package/lib/util/validatePuzzmoFile.js +2 -2
  25. package/lib/util/validatePuzzmoFile.js.map +1 -1
  26. package/package.json +1 -1
  27. package/src/commands/game/create.ts +186 -74
  28. package/src/commands/upload.ts +171 -61
  29. package/src/commands/validate.ts +23 -30
  30. package/src/index.ts +6 -8
  31. package/src/skills/runner.ts +35 -0
  32. package/src/util/api.ts +32 -9
  33. package/src/util/createGame.ts +60 -0
  34. package/src/util/discoverGames.ts +203 -0
  35. package/src/util/validatePuzzmoFile.ts +2 -2
  36. package/templates/minesweeper/README.md +13 -0
  37. package/templates/minesweeper/fixtures/puzzles/easy/9x9.json +16 -0
  38. package/templates/minesweeper/fixtures/puzzles/hard/16x16.json +46 -0
  39. package/templates/minesweeper/index.html +19 -0
  40. package/templates/minesweeper/package.json +17 -0
  41. package/templates/minesweeper/puzzmo.json +11 -0
  42. package/templates/minesweeper/src/appBundle.ts +122 -0
  43. package/templates/minesweeper/src/main.ts +236 -0
  44. package/templates/minesweeper/src/style.css +160 -0
  45. package/templates/minesweeper/tsconfig.json +14 -0
  46. package/templates/minesweeper/vite.config.ts +10 -0
@@ -0,0 +1,122 @@
1
+ import type { AppBundle, ThumbnailConfig } from "@puzzmo/sdk"
2
+ import { svgFontFaceCSSRaw } from "@puzzmo/sdk/fonts"
3
+ import { createEncoder, defineSchema } from "@puzzmo/sdk/inputs"
4
+
5
+ type Puzzle = { rows: number; cols: number; mines: [number, number][] }
6
+ type State = { revealed: boolean[]; flagged: boolean[] }
7
+
8
+ /** Mirrors the schema in src/main.ts so a saved input string round-trips here too. */
9
+ const stateSchema = defineSchema<State>({
10
+ version: 1,
11
+ fields: {
12
+ revealed: { type: "bitArray" },
13
+ flagged: { type: "bitArray" },
14
+ },
15
+ })
16
+
17
+ const escapeXml = (s: string) => s.replace(/[<>&"']/g, (c) => `&#${c.charCodeAt(0)};`)
18
+
19
+ /**
20
+ * Renders a small SVG preview of the puzzle. When an `inputStr` is supplied the
21
+ * preview reflects the player's progress; otherwise it shows a blank board.
22
+ *
23
+ * The host renders this in lists/share cards/completion screens — there's no DOM,
24
+ * no animation, no theme variables. We bake colors from `config.theme` directly.
25
+ */
26
+ export const renderThumbnail: AppBundle["renderThumbnail"] = (puzzleStr: string, inputStr?: string, config?: ThumbnailConfig): string => {
27
+ const puzzle = JSON.parse(puzzleStr) as Puzzle
28
+ const { rows, cols } = puzzle
29
+ const cells = rows * cols
30
+ const mineSet = new Set(puzzle.mines.map(([r, c]) => r * cols + c))
31
+
32
+ let revealed: boolean[] = new Array(cells).fill(false)
33
+ let flagged: boolean[] = new Array(cells).fill(false)
34
+
35
+ if (inputStr) {
36
+ try {
37
+ const codec = createEncoder(stateSchema, { revealed: { length: cells }, flagged: { length: cells } })
38
+ const decoded = codec.decode(codec.migrate(inputStr))
39
+ revealed = decoded.revealed
40
+ flagged = decoded.flagged
41
+ } catch {
42
+ // Bad/old state: fall back to a blank thumbnail.
43
+ }
44
+ }
45
+
46
+ const theme = config?.theme
47
+ const bg = theme?.a_bg ?? "#1a1a1a"
48
+ const cellHidden = theme?.g_unsolved ?? "#3a3a3a"
49
+ const cellShown = theme?.g_bg ?? "#1f1f1f"
50
+ const border = theme?.g_outline ?? "#4a4a4a"
51
+ const mineColor = theme?.error ?? "#ff5555"
52
+ const flagColor = theme?.subBrand ?? "#ffaa55"
53
+ const numberColor = theme?.g_textDark ?? "#1b1b28"
54
+
55
+ const size = 200
56
+ const pad = 4
57
+ const gap = 1
58
+ const cellW = (size - pad * 2 - gap * (cols - 1)) / cols
59
+ const cellH = (size - pad * 2 - gap * (rows - 1)) / rows
60
+
61
+ const parts: string[] = []
62
+ parts.push(`<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 ${size} ${size}">`)
63
+ // Embed the @font-face rule so the SVG renders with Puzzmo's bundled font
64
+ // even outside the game (host previews, share cards, opengraph).
65
+ parts.push(`<style>${svgFontFaceCSSRaw(["RedHatMono-Bold"])}</style>`)
66
+ parts.push(`<rect width="${size}" height="${size}" fill="${escapeXml(bg)}"/>`)
67
+
68
+ for (let i = 0; i < cells; i++) {
69
+ const r = Math.floor(i / cols)
70
+ const c = i % cols
71
+ const x = pad + c * (cellW + gap)
72
+ const y = pad + r * (cellH + gap)
73
+ const isRevealed = revealed[i]
74
+ const fill = isRevealed ? cellShown : cellHidden
75
+ parts.push(
76
+ `<rect x="${x.toFixed(2)}" y="${y.toFixed(2)}" width="${cellW.toFixed(2)}" height="${cellH.toFixed(2)}" fill="${escapeXml(fill)}" stroke="${escapeXml(border)}" stroke-width="0.5"/>`,
77
+ )
78
+
79
+ if (isRevealed && mineSet.has(i)) {
80
+ const cx = x + cellW / 2
81
+ const cy = y + cellH / 2
82
+ const rad = Math.min(cellW, cellH) * 0.25
83
+ parts.push(`<circle cx="${cx.toFixed(2)}" cy="${cy.toFixed(2)}" r="${rad.toFixed(2)}" fill="${escapeXml(mineColor)}"/>`)
84
+ } else if (isRevealed && !mineSet.has(i)) {
85
+ const n = countAdjacent(mineSet, i, rows, cols)
86
+ if (n > 0) {
87
+ const cx = x + cellW / 2
88
+ const cy = y + cellH / 2 + cellH * 0.32
89
+ const fontSize = Math.min(cellW, cellH) * 0.7
90
+ parts.push(
91
+ `<text x="${cx.toFixed(2)}" y="${cy.toFixed(2)}" text-anchor="middle" font-family="RedHatMono-Bold, ui-monospace, monospace" font-size="${fontSize.toFixed(1)}" font-weight="700" fill="${escapeXml(numberColor)}">${n}</text>`,
92
+ )
93
+ }
94
+ } else if (flagged[i]) {
95
+ const cx = x + cellW / 2
96
+ const cy = y + cellH / 2 + cellH * 0.32
97
+ const fontSize = Math.min(cellW, cellH) * 0.7
98
+ parts.push(
99
+ `<text x="${cx.toFixed(2)}" y="${cy.toFixed(2)}" text-anchor="middle" font-family="RedHatMono-Bold, ui-monospace, monospace" font-size="${fontSize.toFixed(1)}" fill="${escapeXml(flagColor)}">⚑</text>`,
100
+ )
101
+ }
102
+ }
103
+
104
+ parts.push(`</svg>`)
105
+ return parts.join("")
106
+ }
107
+
108
+ const countAdjacent = (mineSet: Set<number>, i: number, rows: number, cols: number): number => {
109
+ const r = Math.floor(i / cols)
110
+ const c = i % cols
111
+ let n = 0
112
+ for (let dr = -1; dr <= 1; dr++) {
113
+ for (let dc = -1; dc <= 1; dc++) {
114
+ if (dr === 0 && dc === 0) continue
115
+ const nr = r + dr
116
+ const nc = c + dc
117
+ if (nr < 0 || nr >= rows || nc < 0 || nc >= cols) continue
118
+ if (mineSet.has(nr * cols + nc)) n++
119
+ }
120
+ }
121
+ return n
122
+ }
@@ -0,0 +1,236 @@
1
+ import { createPuzzmoSDK, type Theme } from "@puzzmo/sdk"
2
+ import { createEncoder, defineSchema, type EncoderResult } from "@puzzmo/sdk/inputs"
3
+
4
+ /** Decoded puzzle definition shipped from the host. The host serializes this as JSON. */
5
+ type Puzzle = { rows: number; cols: number; mines: [number, number][] }
6
+
7
+ /** Persisted player progress. `won` is derived, not stored. */
8
+ type State = { revealed: boolean[]; flagged: boolean[] }
9
+
10
+ const MINE_PENALTY_MS = 60_000
11
+
12
+ /**
13
+ * Schema for serializing per-cell boolean state. `bitArray` packs 4 cells per hex char,
14
+ * so a 16x16 board ≈ 64 chars instead of a multi-kilobyte JSON blob.
15
+ */
16
+ const stateSchema = defineSchema<State>({
17
+ version: 1,
18
+ fields: {
19
+ revealed: { type: "bitArray" },
20
+ flagged: { type: "bitArray" },
21
+ },
22
+ })
23
+
24
+ /**
25
+ * Construct the host SDK. Resolves messaging with the embed, manages the timer, and
26
+ * surfaces lifecycle events ("start", "settingsUpdate", etc.).
27
+ */
28
+ const sdk = createPuzzmoSDK()
29
+
30
+ const boardEl = document.getElementById("board")!
31
+ const hudLeftEl = document.getElementById("hudLeft")!
32
+ const hudRightEl = document.getElementById("hudRight")!
33
+
34
+ let puzzle: Puzzle = { rows: 9, cols: 9, mines: [] }
35
+ let state: State = { revealed: [], flagged: [] }
36
+ let mineSet = new Set<number>()
37
+ let stateCodec: EncoderResult<State> | null = null
38
+ let won = false
39
+
40
+ /** Maps the Puzzmo Theme onto the CSS custom properties consumed by style.css. */
41
+ const applyTheme = (theme: Theme) => {
42
+ const r = document.documentElement.style
43
+ r.setProperty("--bg", theme.a_bg)
44
+ r.setProperty("--panel", theme.a_infoBG)
45
+ r.setProperty("--cell", theme.g_unsolved)
46
+ r.setProperty("--cell-revealed", theme.g_bg)
47
+ r.setProperty("--border", theme.g_outline)
48
+ r.setProperty("--text", theme.fg)
49
+ r.setProperty("--text-on-cell", theme.g_textDark)
50
+ r.setProperty("--accent", theme.key)
51
+ r.setProperty("--mine", theme.error)
52
+ r.setProperty("--flag", theme.subBrand)
53
+ r.setProperty("--n1", theme.player)
54
+ r.setProperty("--n2", theme.alt1)
55
+ r.setProperty("--n3", theme.alt2)
56
+ r.setProperty("--n4", theme.alt3)
57
+ r.setProperty("--n5", theme.error)
58
+ r.setProperty("--n6", theme.keyStrong)
59
+ r.setProperty("--n7", theme.playerLight)
60
+ r.setProperty("--n8", theme.alwaysDark)
61
+ }
62
+
63
+ const idx = (r: number, c: number) => r * puzzle.cols + c
64
+
65
+ const neighbours = (i: number): number[] => {
66
+ const r = Math.floor(i / puzzle.cols)
67
+ const c = i % puzzle.cols
68
+ const out: number[] = []
69
+ for (let dr = -1; dr <= 1; dr++) {
70
+ for (let dc = -1; dc <= 1; dc++) {
71
+ if (dr === 0 && dc === 0) continue
72
+ const nr = r + dr
73
+ const nc = c + dc
74
+ if (nr < 0 || nr >= puzzle.rows || nc < 0 || nc >= puzzle.cols) continue
75
+ out.push(idx(nr, nc))
76
+ }
77
+ }
78
+ return out
79
+ }
80
+
81
+ const countAdjacentMines = (i: number) => neighbours(i).filter((n) => mineSet.has(n)).length
82
+
83
+ const reveal = (i: number, isUserClick: boolean) => {
84
+ if (state.revealed[i] || state.flagged[i]) return
85
+ state.revealed[i] = true
86
+ if (mineSet.has(i)) {
87
+ /**
88
+ * `addPenalty(ms)` adds to the timer's tracked "added time", surfaced separately
89
+ * in `display()` and reported via `gameCompleted({ additionalTimeAddedSecs })`.
90
+ */
91
+ if (isUserClick) sdk.timer.addPenalty(MINE_PENALTY_MS)
92
+ return
93
+ }
94
+ if (countAdjacentMines(i) === 0) for (const n of neighbours(i)) reveal(n, false)
95
+ }
96
+
97
+ const checkWin = () => {
98
+ const safe = puzzle.rows * puzzle.cols - mineSet.size
99
+ let revealedSafe = 0
100
+ for (let i = 0; i < state.revealed.length; i++) if (state.revealed[i] && !mineSet.has(i)) revealedSafe++
101
+ if (revealedSafe === safe) won = true
102
+ }
103
+
104
+ const flaggedCount = () => state.flagged.reduce((acc, v) => acc + (v ? 1 : 0), 0)
105
+
106
+ const render = () => {
107
+ boardEl.innerHTML = ""
108
+ for (let i = 0; i < puzzle.rows * puzzle.cols; i++) {
109
+ const cell = document.createElement("button")
110
+ cell.className = "cell"
111
+ cell.dataset.i = String(i)
112
+ if (state.revealed[i]) {
113
+ cell.classList.add("revealed")
114
+ if (mineSet.has(i)) {
115
+ cell.classList.add("mine")
116
+ cell.textContent = "✸"
117
+ } else {
118
+ const n = countAdjacentMines(i)
119
+ if (n > 0) {
120
+ cell.dataset.n = String(n)
121
+ cell.textContent = String(n)
122
+ }
123
+ }
124
+ } else if (state.flagged[i]) {
125
+ cell.classList.add("flagged")
126
+ cell.textContent = "⚑"
127
+ }
128
+ boardEl.appendChild(cell)
129
+ }
130
+ if (won) {
131
+ hudLeftEl.textContent = "completed"
132
+ hudRightEl.textContent = ""
133
+ } else {
134
+ hudLeftEl.textContent = `${mineSet.size} mines`
135
+ hudRightEl.textContent = `${flaggedCount()} flags`
136
+ }
137
+ }
138
+
139
+ /**
140
+ * Called once on win. Reports gameplay metrics back to the host via:
141
+ * - `sdk.gameCompleted(gameplay, augmentations)` — durable record + deeds
142
+ * - `sdk.showCompletionScreen(content, gameplay, completed)` — the host's win UI
143
+ */
144
+ const onComplete = () => {
145
+ const elapsed = Math.round(sdk.timer.timeSecs())
146
+ const penalty = Math.round(sdk.timer.addedTimeSecs())
147
+ const data = { elapsedTimeSecs: elapsed, additionalTimeAddedSecs: penalty, pointsAwarded: 0, completed: true }
148
+ sdk.gameCompleted(data, { deeds: [] })
149
+ sdk.showCompletionScreen([{ type: "md", text: penalty > 0 ? `**Cleared!** With ${penalty}s in penalties.` : "**Cleared!**" }], data, true)
150
+ }
151
+
152
+ const persistState = () => {
153
+ if (!stateCodec) return
154
+ /**
155
+ * `sdk.updateGameState(string)` ships an opaque string back to the host so progress
156
+ * survives a reload. We use the SDK's input encoder for compactness over JSON.
157
+ */
158
+ sdk.updateGameState(stateCodec.encode(state))
159
+ }
160
+
161
+ const handleClick = (e: MouseEvent) => {
162
+ const target = e.target as HTMLElement
163
+ if (!target.classList.contains("cell")) return
164
+ if (won) return
165
+ const i = Number(target.dataset.i)
166
+ if (e.shiftKey || e.button === 2) {
167
+ if (!state.revealed[i]) state.flagged[i] = !state.flagged[i]
168
+ } else {
169
+ reveal(i, true)
170
+ checkWin()
171
+ }
172
+ persistState()
173
+ render()
174
+ if (won) onComplete()
175
+ }
176
+
177
+ const blankState = (cells: number): State => ({ revealed: new Array(cells).fill(false), flagged: new Array(cells).fill(false) })
178
+
179
+ const setupBoard = (data: Puzzle, savedState?: State) => {
180
+ puzzle = data
181
+ mineSet = new Set(data.mines.map(([r, c]) => idx(r, c)))
182
+ const cells = data.rows * data.cols
183
+ state = savedState ?? blankState(cells)
184
+ /**
185
+ * `bitArray` requires each field's length in the encoder context to round-trip
186
+ * correctly. We rebuild the codec whenever the board dimensions change.
187
+ */
188
+ stateCodec = createEncoder(stateSchema, {
189
+ revealed: { length: cells },
190
+ flagged: { length: cells },
191
+ })
192
+ won = false
193
+ // Publish board dimensions to CSS so grid tracks + cell font-size scale purely from the stylesheet.
194
+ boardEl.style.setProperty("--cols", String(data.cols))
195
+ boardEl.style.setProperty("--rows", String(data.rows))
196
+ checkWin()
197
+ render()
198
+ }
199
+
200
+ /**
201
+ * Lifecycle entrypoint:
202
+ * - `sdk.gameReady()` blocks until the host sends puzzle/state/theme.
203
+ * - `sdk.gameLoaded()` tells the host we're done bootstrapping; the host will
204
+ * then dispatch `start` (or restore a paused session).
205
+ */
206
+ const init = async () => {
207
+ const { puzzleString, inputString, theme } = await sdk.gameReady()
208
+ if (theme) applyTheme(theme)
209
+ const parsed = puzzleString ? (JSON.parse(puzzleString) as Puzzle) : puzzle
210
+ const cells = parsed.rows * parsed.cols
211
+ /**
212
+ * Decode prior progress with the encoder so older save formats can be migrated
213
+ * via the schema's `migrations` map (none yet, but wired up for free).
214
+ */
215
+ const tempCodec = createEncoder(stateSchema, { revealed: { length: cells }, flagged: { length: cells } })
216
+ const saved = inputString ? (tempCodec.decode(tempCodec.migrate(inputString)) as State) : undefined
217
+ setupBoard(parsed, saved)
218
+ sdk.gameLoaded()
219
+ }
220
+
221
+ sdk.on("start", () => {
222
+ render()
223
+ })
224
+
225
+ /** The host re-emits its theme (and other settings) when the player toggles light/dark. */
226
+ sdk.on("settingsUpdate", (data: { theme?: Theme }) => {
227
+ if (data?.theme) applyTheme(data.theme)
228
+ })
229
+
230
+ boardEl.addEventListener("click", handleClick)
231
+ boardEl.addEventListener("contextmenu", (e) => {
232
+ e.preventDefault()
233
+ handleClick(e)
234
+ })
235
+
236
+ init()
@@ -0,0 +1,160 @@
1
+ /* Puzzmo subset fonts. ASCII-only — always pair with a system fallback. */
2
+ @font-face {
3
+ font-family: "Poppins-SemiBold";
4
+ src: url("https://www.puzzmo.com/assets/fonts-subset/Poppins-SemiBold-subset.ttf");
5
+ font-display: swap;
6
+ }
7
+ @font-face {
8
+ font-family: "RedHatMono-Bold";
9
+ src: url("https://www.puzzmo.com/assets/fonts-subset/RedHatMono-Bold-subset.ttf");
10
+ font-display: swap;
11
+ }
12
+
13
+ /* Defaults are fallbacks for the simulator before the host sends a theme.
14
+ Real values are set on :root from the Puzzmo Theme object in applyTheme(). */
15
+ :root {
16
+ --bg: #1a1a1a;
17
+ --panel: #2a2a2a;
18
+ --cell: #3a3a3a;
19
+ --cell-revealed: #1f1f1f;
20
+ --border: #4a4a4a;
21
+ --text: #f0f0f0;
22
+ --text-on-cell: #1b1b28;
23
+ --accent: #f0c040;
24
+ --mine: #ff5555;
25
+ --flag: #ffaa55;
26
+ --n1: #6cf;
27
+ --n2: #6f9;
28
+ --n3: #fc6;
29
+ --n4: #f96;
30
+ --n5: #f66;
31
+ --n6: #c6f;
32
+ --n7: #fcf;
33
+ --n8: #fff;
34
+ }
35
+
36
+ * {
37
+ box-sizing: border-box;
38
+ }
39
+
40
+ body {
41
+ margin: 0;
42
+ min-height: 100vh;
43
+ background: var(--bg);
44
+ color: var(--text);
45
+ font-family: "Poppins-SemiBold", system-ui, sans-serif;
46
+ display: flex;
47
+ justify-content: center;
48
+ align-items: center;
49
+ padding: 16px;
50
+ }
51
+
52
+ main {
53
+ display: flex;
54
+ flex-direction: column;
55
+ gap: 12px;
56
+ align-items: center;
57
+ width: 100%;
58
+ }
59
+
60
+ h1 {
61
+ margin: 0;
62
+ font-size: 18px;
63
+ font-weight: 600;
64
+ letter-spacing: 0.5px;
65
+ }
66
+
67
+ header {
68
+ display: flex;
69
+ flex-direction: column;
70
+ gap: 8px;
71
+ align-items: center;
72
+ }
73
+
74
+ .hud {
75
+ display: flex;
76
+ gap: 16px;
77
+ font-size: 14px;
78
+ background: var(--panel);
79
+ padding: 6px 12px;
80
+ border-radius: 6px;
81
+ min-width: 200px;
82
+ justify-content: space-between;
83
+ }
84
+
85
+ .board {
86
+ /* Square board: prefer the smaller of (viewport width minus body padding) and
87
+ (viewport height minus header allowance), clamped to a sane range so the
88
+ board never disappears on tiny screens nor overwhelms huge ones.
89
+ Stored as a custom property so `.cell` can reuse the same length expression
90
+ to size its font without measuring anything in JS. */
91
+ --board-size: clamp(280px, min(100vw - 32px, 100vh - 200px), 900px);
92
+ display: grid;
93
+ grid-template-columns: repeat(var(--cols, 9), 1fr);
94
+ grid-template-rows: repeat(var(--rows, 9), 1fr);
95
+ gap: 2px;
96
+ background: var(--panel);
97
+ padding: 6px;
98
+ border-radius: 6px;
99
+ user-select: none;
100
+ touch-action: manipulation;
101
+ width: var(--board-size);
102
+ aspect-ratio: 1;
103
+ }
104
+
105
+ .cell {
106
+ background: var(--cell);
107
+ border: 1px solid var(--border);
108
+ border-radius: 3px;
109
+ display: flex;
110
+ align-items: center;
111
+ justify-content: center;
112
+ font-family: "RedHatMono-Bold", ui-monospace, monospace;
113
+ font-weight: 700;
114
+ cursor: pointer;
115
+ color: var(--text-on-cell);
116
+ /* Cell width ≈ board-size / cols. Glyph at ~55% of cell width. CSS variables
117
+ inherit from `.board`, so no JS measurement needed. */
118
+ font-size: calc(var(--board-size) * 0.55 / var(--cols, 9));
119
+ padding: 0;
120
+ line-height: 1;
121
+ overflow: hidden;
122
+ }
123
+
124
+ .cell.revealed {
125
+ background: var(--cell-revealed);
126
+ cursor: default;
127
+ }
128
+
129
+ .cell.flagged {
130
+ color: var(--flag);
131
+ }
132
+
133
+ .cell.mine {
134
+ color: var(--mine);
135
+ }
136
+
137
+ .cell[data-n="1"] {
138
+ color: var(--n1);
139
+ }
140
+ .cell[data-n="2"] {
141
+ color: var(--n2);
142
+ }
143
+ .cell[data-n="3"] {
144
+ color: var(--n3);
145
+ }
146
+ .cell[data-n="4"] {
147
+ color: var(--n4);
148
+ }
149
+ .cell[data-n="5"] {
150
+ color: var(--n5);
151
+ }
152
+ .cell[data-n="6"] {
153
+ color: var(--n6);
154
+ }
155
+ .cell[data-n="7"] {
156
+ color: var(--n7);
157
+ }
158
+ .cell[data-n="8"] {
159
+ color: var(--n8);
160
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "Bundler",
6
+ "strict": true,
7
+ "esModuleInterop": true,
8
+ "skipLibCheck": true,
9
+ "isolatedModules": true,
10
+ "noEmit": true,
11
+ "types": ["vite/client"]
12
+ },
13
+ "include": ["src", "vite.config.ts"]
14
+ }
@@ -0,0 +1,10 @@
1
+ import { defineConfig } from "vite"
2
+ import { puzzmoSimulator, appBundlePlugin } from "@puzzmo/sdk/vite"
3
+
4
+ export default defineConfig({
5
+ plugins: [puzzmoSimulator({ fixturesGlob: "/fixtures/puzzles/**/*.json" }), appBundlePlugin()],
6
+ build: {
7
+ outDir: "dist",
8
+ assetsDir: "assets",
9
+ },
10
+ })