diff --git a/bun.lockb b/bun.lockb index d2053e0..4906c0a 100644 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index 844e067..4fdc307 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "check": "svelte-check --tsconfig ./tsconfig.json && tsc -p tsconfig.node.json" }, "devDependencies": { + "@iconify/svelte": "^4.1.0", "@sveltejs/vite-plugin-svelte": "^5.0.0", "@tsconfig/svelte": "^5.0.4", "sass-embedded": "^1.83.0", diff --git a/src/App.svelte b/src/App.svelte index 442bf3f..ef3144b 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -2,15 +2,17 @@ import svelteLogo from './assets/svelte.svg' import viteLogo from '/vite.svg' import Number from './lib/Number.svelte' - import { cfg, eStates, Fmt, JsonTy, Misc, nStates, randInt, range, zero8, type Checkpoint, type i8s } from "./utils"; + import { Backend, cfg, eStates, Fmt, JsonTy, Misc, nStates, randInt, range, zero8, type Checkpoint, type i8s, type MetaCheckpoint } from "./utils"; import Line from "./lib/Line.svelte"; import { solve } from "./solver"; + import Upload from './lib/Upload.svelte'; + import PuzzleInfo from './lib/PuzzleInfo.svelte'; const params = new URLSearchParams(location.search) const hasTouch = Misc.hasTouch() // Can pass in puzzle data from props - interface Props { puzzleId?: string, puzzleData?: Checkpoint } + interface Props { puzzleId?: string, puzzleData?: MetaCheckpoint } export let { puzzleId, puzzleData }: Props = {} const pid = puzzleId ?? 'slitherlink' @@ -53,6 +55,7 @@ colors = pt.colors updateColors() } + let upload: Checkpoint | null // Run something on the edges of a cell function updateEdges(x: number, y: number, condition: (st: number) => boolean, op: (st: number) => number) { @@ -198,7 +201,7 @@ updateArea.forEach(([dx, dy, _]) => checkPos(x + dx, y + dy)) } - async function editModeReduce(zeros: boolean = false, n = 20) { + async function editModeReduce(zeros: boolean = false, n = 10) { [solutionHStates, solutionVStates] = [hStates.slice(), vStates.slice()] if (n === 0) return // On each iteration, randomly mask n numbers and try to solve the puzzle @@ -221,8 +224,9 @@ vertStates.some((st, idx) => (st === eStates.selected) !== (solutionVStates[idx] === eStates.selected))) { masked.forEach(idx => nMask[idx] = 0) console.log('Solution does not match') - setTimeout(() => editModeReduce(zeros, n - 1), 100) - } else setTimeout(() => editModeReduce(zeros, n), 100) + } + // setTimeout(() => editModeReduce(zeros, n - 1), 100) + // } else setTimeout(() => editModeReduce(zeros, n), 100) console.log(nMask) } @@ -284,14 +288,18 @@ Logo
- Welcome to SlitherLink! 🧩 The rules are simple: Draw lines between the dots to create one big loop (no crossings, no branches). The numbers are your hints – they tell you how many lines should surround them. Left-click to draw, right-click to mark with an X. Can you crack the perfect path? + Welcome to SlitherLink! 🧩 The rules are simple: Draw lines between the dots to create one big loop (no crossings, no branches). The numbers are your hints – they tell you how many lines should surround them. Left-click to draw, right-click to mark with an X. Can you crack the perfect path? YouTube Tutorial
{#if statusMsg}
{statusMsg}
{/if} - {#if !editMode}
{Fmt.duration(elapsed)}
{/if} +
+ {#if puzzleData && !editMode}
+ +
{Fmt.duration(elapsed)}
+
{/if}
dragging && clickDiv(e)} on:mouseup={() => dragging = false} @@ -313,6 +321,7 @@ {/each} {/each}
+
{#if editMode}
@@ -338,7 +347,6 @@ }}>Clear Numbers - {/if}
@@ -354,6 +362,10 @@ {/each} {/if} +
+ + +
{/if} @@ -386,4 +398,6 @@ {/if} + + {#if upload} upload = null}/> {/if} diff --git a/src/Launcher.svelte b/src/Launcher.svelte index a295256..d0198a2 100644 --- a/src/Launcher.svelte +++ b/src/Launcher.svelte @@ -5,15 +5,20 @@ // Get puzzle id from params const urlParams = new URLSearchParams(window.location.search); const puzzleId = urlParams.get("puzzle"); + const edit = urlParams.get("edit"); let puzzleData: Checkpoint | null = null; let error: string | null = null; - if (!puzzleId) window.location.href = "/?puzzle=meow"; - else Backend.get(puzzleId).then((data) => (puzzleData = data)).catch(e => (error = e)); + if (!edit) { + if (!puzzleId) window.location.href = "/?puzzle=meow"; + else Backend.get(puzzleId).then((data) => (puzzleData = data)).catch(e => (error = e)); + } {#if puzzleData !== null && puzzleId !== null} +{:else if edit} + {:else}
diff --git a/src/app.sass b/src/app.sass index c34cf68..f894451 100644 --- a/src/app.sass +++ b/src/app.sass @@ -89,8 +89,8 @@ main height: 1em .rules - max-width: 900px - opacity: 0.9 + font-size: 0.9em + max-width: 800px .btn-div display: flex @@ -118,9 +118,6 @@ main font-family: 'Teko', sans-serif font-size: 2em color: colors.$accent - margin-bottom: -24px - - // Larger gap between letters letter-spacing: 0.3em .status @@ -129,30 +126,9 @@ main &.error color: colors.$cross -.overlay - position: fixed - inset: 0 - - background-color: rgba(0, 0, 0, 0.5) - +.timer-line + // Two items, left and right display: flex - justify-content: center - align-items: center - - z-index: 1000 - backdrop-filter: blur(5px) - - h2, p - user-select: none - margin: 0 - - > div - background-color: colors.$background - padding: 2rem - border-radius: 8px - - display: flex - flex-direction: column - gap: 1rem - - max-width: 400px + justify-content: space-between + gap: 10px + margin-bottom: -12px diff --git a/src/aqua.sass b/src/aqua.sass new file mode 100644 index 0000000..6c55197 --- /dev/null +++ b/src/aqua.sass @@ -0,0 +1,148 @@ +@use "colors" + +$font: Quicksand, Inter, LXGW Wenkai, Microsoft YaHei, -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, Avenir, Helvetica, Arial, sans-serif +$c-main: #b3c6ff +$c-sub: rgba(0, 0, 0, 0.77) +$c-good: #b3ffb9 +$c-darker: #646cff +$c-bg: #242424 +$c-error: #ff6b6b +$c-shadow: rgba(0, 0, 0, 0.1) + +$ov-light: rgba(white, 0.04) +$ov-lighter: rgba(white, 0.08) +$ov-dark: rgba(black, 0.1) +$ov-darker: rgba(black, 0.18) + +$nav-height: 4rem +$w-mobile: 560px +$w-max: 900px + +$grad-special: linear-gradient(90deg, #ffee94, #ffb798, #ffa3e5, #ebff94) + +$border-radius: 12px +$gap: 20px + +$transition: all 0.25s + +.card + display: flex + flex-direction: column + + border-radius: $border-radius + padding: 12px 16px + background: $ov-light + +blockquote + $c1: rgba(255, 149, 149, 0.05) + $c2: rgba(255, 152, 152, 0.12) + background: repeating-linear-gradient(45deg, $c1, $c1 10px, $c2 10px, $c2 20px) + padding: 10px 20px 10px 20px + margin: 16px 0 + border-left: solid #ff7c7c 3px + border-radius: $border-radius + +input, textarea + border-radius: $border-radius + border: 1px solid transparent + padding: 0.6em 1.2em + font-size: 1em + font-weight: 500 + font-family: inherit + background-color: $ov-lighter + transition: $transition + box-sizing: border-box + resize: vertical + &:focus, &:focus-visible + border: 1px solid $c-main + outline: none + &.error + border: 1px solid $c-error + +select + border-radius: $border-radius + border: 1px solid transparent + padding: 0.6em 1.2em + font-size: 1em + font-weight: 500 + font-family: inherit + background-color: $ov-lighter + transition: $transition + box-sizing: border-box + +input[type="checkbox"] + width: 1.2em + height: 1.2em + margin: 0 + padding: 0 + border: 1px solid $c-main + background-color: $ov-lighter + appearance: none + cursor: pointer + flex-shrink: 0 + + &:checked + background-color: $c-main + border-color: $c-main + +label + cursor: pointer + +.overlay + position: fixed + inset: 0 + + background-color: rgba(0, 0, 0, 0.5) + + display: flex + justify-content: center + align-items: center + + z-index: 1000 + backdrop-filter: blur(5px) + + h2, p + user-select: none + margin: 0 + + > div + background-color: colors.$background + padding: 2rem + border-radius: 8px + + display: flex + flex-direction: column + gap: 1rem + + min-width: 300px + max-width: 400px + +.actions + display: flex + flex-direction: column + gap: 16px + + button + width: 100% + +.loading.overlay + font-size: 28rem + + :global(.icon) + opacity: 0.5 + + > span + position: absolute + inset: 0 + display: flex + justify-content: center + align-items: center + background: transparent + + letter-spacing: 20px + margin-left: 20px + + font-size: 1.5rem + +.error + color: $c-error \ No newline at end of file diff --git a/src/lib/PuzzleInfo.svelte b/src/lib/PuzzleInfo.svelte new file mode 100644 index 0000000..aea9725 --- /dev/null +++ b/src/lib/PuzzleInfo.svelte @@ -0,0 +1,28 @@ + + +
+
+ {puzzleData?.name} + by {puzzleData?.author} +
+
+ {puzzleData?.description} +
+
+ + \ No newline at end of file diff --git a/src/lib/Upload.svelte b/src/lib/Upload.svelte new file mode 100644 index 0000000..5cce9a6 --- /dev/null +++ b/src/lib/Upload.svelte @@ -0,0 +1,58 @@ + + +{#if data} +
+
+ {#if !id} +

Upload Puzzle

+ + {#if error} +

{error}

+ {:else} +

Upload your puzzle to the server

+ {/if} + + + + + + {:else} +

Uploaded 🥳

+ Your puzzle has been uploaded to the server + +

You can share it by the following link:

+

{url}

+

Thank you for your creation!

+ + {/if} +
+
+{/if} diff --git a/src/main.ts b/src/main.ts index c1e2e93..87ab958 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,5 +1,6 @@ import { mount } from 'svelte' import './app.sass' +import './aqua.sass' import './puzzle.sass' import './fonts/caveat.sass' import Launcher from './Launcher.svelte' diff --git a/src/utils.ts b/src/utils.ts index e2b721f..3b550c9 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -3,6 +3,8 @@ export type i8s = Int8Array export interface Checkpoint { rows: number, cols: number, hStates: i8s, vStates: i8s, hColors: i8s, vColors: i8s, numbers: i8s, nMask: i8s, colors: string[] } +export interface MetaCheckpoint extends Checkpoint { name: string, author: string, description: string, date?: string } + export const cfg = { cellW: 20, lineW: 4, @@ -75,7 +77,10 @@ export const Backend = { if (!r.ok) throw new Error(`Failed to get puzzle: ${r.status}`) return r.text() }).then(JsonTy.parse), - post: (data: Checkpoint) => fetch(`${cfg.backend}/`, { method: "post", body: JsonTy.stringify(data) }), + post: (data: MetaCheckpoint) => fetch(`${cfg.backend}/`, { method: "post", body: JsonTy.stringify(data) }).then(r => { + if (!r.ok) throw new Error(`Failed to post puzzle: ${r.status}`) + return r.text() + }).then(JsonTy.parse), } export const Misc = {