sv 0.11.2 → 0.11.3

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 (36) hide show
  1. package/dist/add-BWQarWDB.mjs +7000 -0
  2. package/dist/add-nRRWTjzp.d.mts +35 -0
  3. package/dist/bin.mjs +25 -1845
  4. package/dist/{index-7xp7FWpU.d.mts → core-CnPhgWST.d.mts} +294 -46
  5. package/dist/lib/core.d.mts +2 -0
  6. package/dist/lib/core.mjs +3 -0
  7. package/dist/lib/index.d.mts +16 -0
  8. package/dist/lib/index.mjs +4 -4
  9. package/dist/lib/testing.d.mts +108 -0
  10. package/dist/lib/testing.mjs +970 -927
  11. package/dist/{package-manager-CySZrSUa.mjs → package-manager-DkCPtZM1.mjs} +219 -1328
  12. package/dist/shared.json +30 -4
  13. package/dist/templates/addon/assets/DOT-gitignore +27 -0
  14. package/dist/templates/addon/assets/src/index.js +52 -0
  15. package/dist/templates/addon/assets/tests/addon.test.js +50 -0
  16. package/dist/templates/addon/assets/tests/setup/global.js +14 -0
  17. package/dist/templates/addon/assets/tests/setup/suite.js +130 -0
  18. package/dist/templates/addon/assets/vitest.config.js +16 -0
  19. package/dist/templates/addon/files.types=checkjs.json +1 -0
  20. package/dist/templates/addon/files.types=none.json +1 -0
  21. package/dist/templates/addon/files.types=typescript.json +1 -0
  22. package/dist/templates/addon/meta.json +4 -0
  23. package/dist/templates/addon/package.json +32 -0
  24. package/dist/templates/demo/files.types=checkjs.json +5 -5
  25. package/dist/templates/demo/files.types=none.json +5 -5
  26. package/dist/templates/demo/files.types=typescript.json +5 -5
  27. package/dist/templates/demo/package.json +1 -1
  28. package/dist/templates/library/package.json +1 -1
  29. package/dist/templates/minimal/package.json +1 -1
  30. package/dist/{core-D715tamU.mjs → utils-DjBRIDJG.mjs} +26494 -25089
  31. package/package.json +7 -7
  32. package/dist/index.d.mts +0 -2
  33. package/dist/index2.d.mts +0 -65
  34. package/dist/lib/core/index.mjs +0 -4
  35. package/dist/official-P5OKi7QM.mjs +0 -2586
  36. package/dist/testing.d.mts +0 -50
@@ -5,7 +5,7 @@
5
5
  },
6
6
  {
7
7
  "name": "src/routes/+page.svelte",
8
- "contents": "<script>\n\timport Counter from './Counter.svelte';\n\timport welcome from '$lib/images/svelte-welcome.webp';\n\timport welcomeFallback from '$lib/images/svelte-welcome.png';\n</script>\n\n<svelte:head>\n\t<title>Home</title>\n\t<meta name=\"description\" content=\"Svelte demo app\" />\n</svelte:head>\n\n<section>\n\t<h1>\n\t\t<span class=\"welcome\">\n\t\t\t<picture>\n\t\t\t\t<source srcset={welcome} type=\"image/webp\" />\n\t\t\t\t<img src={welcomeFallback} alt=\"Welcome\" />\n\t\t\t</picture>\n\t\t</span>\n\n\t\tto your new<br />SvelteKit app\n\t</h1>\n\n\t<h2>\n\t\ttry editing <strong>src/routes/+page.svelte</strong>\n\t</h2>\n\n\t<Counter />\n</section>\n\n<style>\n\tsection {\n\t\tdisplay: flex;\n\t\tflex-direction: column;\n\t\tjustify-content: center;\n\t\talign-items: center;\n\t\tflex: 0.6;\n\t}\n\n\th1 {\n\t\twidth: 100%;\n\t}\n\n\t.welcome {\n\t\tdisplay: block;\n\t\tposition: relative;\n\t\twidth: 100%;\n\t\theight: 0;\n\t\tpadding: 0 0 calc(100% * 495 / 2048) 0;\n\t}\n\n\t.welcome img {\n\t\tposition: absolute;\n\t\twidth: 100%;\n\t\theight: 100%;\n\t\ttop: 0;\n\t\tdisplay: block;\n\t}\n</style>\n"
8
+ "contents": "<script>\n\timport welcomeFallback from '$lib/images/svelte-welcome.png';\n\timport welcome from '$lib/images/svelte-welcome.webp';\n\n\timport Counter from './Counter.svelte';\n</script>\n\n<svelte:head>\n\t<title>Home</title>\n\t<meta name=\"description\" content=\"Svelte demo app\" />\n</svelte:head>\n\n<section>\n\t<h1>\n\t\t<span class=\"welcome\">\n\t\t\t<picture>\n\t\t\t\t<source srcset={welcome} type=\"image/webp\" />\n\t\t\t\t<img src={welcomeFallback} alt=\"Welcome\" />\n\t\t\t</picture>\n\t\t</span>\n\n\t\tto your new<br />SvelteKit app\n\t</h1>\n\n\t<h2>\n\t\ttry editing <strong>src/routes/+page.svelte</strong>\n\t</h2>\n\n\t<Counter />\n</section>\n\n<style>\n\tsection {\n\t\tdisplay: flex;\n\t\tflex-direction: column;\n\t\tjustify-content: center;\n\t\talign-items: center;\n\t\tflex: 0.6;\n\t}\n\n\th1 {\n\t\twidth: 100%;\n\t}\n\n\t.welcome {\n\t\tdisplay: block;\n\t\tposition: relative;\n\t\twidth: 100%;\n\t\theight: 0;\n\t\tpadding: 0 0 calc(100% * 495 / 2048) 0;\n\t}\n\n\t.welcome img {\n\t\tposition: absolute;\n\t\twidth: 100%;\n\t\theight: 100%;\n\t\ttop: 0;\n\t\tdisplay: block;\n\t}\n</style>\n"
9
9
  },
10
10
  {
11
11
  "name": "src/routes/+page.js",
@@ -17,7 +17,7 @@
17
17
  },
18
18
  {
19
19
  "name": "src/routes/Header.svelte",
20
- "contents": "<script>\n\timport { resolve } from '$app/paths';\n\timport { page } from '$app/state';\n\timport logo from '$lib/images/svelte-logo.svg';\n\timport github from '$lib/images/github.svg';\n</script>\n\n<header>\n\t<div class=\"corner\">\n\t\t<a href=\"https://svelte.dev/docs/kit\">\n\t\t\t<img src={logo} alt=\"SvelteKit\" />\n\t\t</a>\n\t</div>\n\n\t<nav>\n\t\t<svg viewBox=\"0 0 2 3\" aria-hidden=\"true\">\n\t\t\t<path d=\"M0,0 L1,2 C1.5,3 1.5,3 2,3 L2,0 Z\" />\n\t\t</svg>\n\t\t<ul>\n\t\t\t<li aria-current={page.url.pathname === '/' ? 'page' : undefined}>\n\t\t\t\t<a href={resolve('/')}>Home</a>\n\t\t\t</li>\n\t\t\t<li aria-current={page.url.pathname === '/about' ? 'page' : undefined}>\n\t\t\t\t<a href={resolve('/about')}>About</a>\n\t\t\t</li>\n\t\t\t<li aria-current={page.url.pathname.startsWith('/sverdle') ? 'page' : undefined}>\n\t\t\t\t<a href={resolve('/sverdle')}>Sverdle</a>\n\t\t\t</li>\n\t\t</ul>\n\t\t<svg viewBox=\"0 0 2 3\" aria-hidden=\"true\">\n\t\t\t<path d=\"M0,0 L0,3 C0.5,3 0.5,3 1,2 L2,0 Z\" />\n\t\t</svg>\n\t</nav>\n\n\t<div class=\"corner\">\n\t\t<a href=\"https://github.com/sveltejs/kit\">\n\t\t\t<img src={github} alt=\"GitHub\" />\n\t\t</a>\n\t</div>\n</header>\n\n<style>\n\theader {\n\t\tdisplay: flex;\n\t\tjustify-content: space-between;\n\t}\n\n\t.corner {\n\t\twidth: 3em;\n\t\theight: 3em;\n\t}\n\n\t.corner a {\n\t\tdisplay: flex;\n\t\talign-items: center;\n\t\tjustify-content: center;\n\t\twidth: 100%;\n\t\theight: 100%;\n\t}\n\n\t.corner img {\n\t\twidth: 2em;\n\t\theight: 2em;\n\t\tobject-fit: contain;\n\t}\n\n\tnav {\n\t\tdisplay: flex;\n\t\tjustify-content: center;\n\t\t--background: rgba(255, 255, 255, 0.7);\n\t}\n\n\tsvg {\n\t\twidth: 2em;\n\t\theight: 3em;\n\t\tdisplay: block;\n\t}\n\n\tpath {\n\t\tfill: var(--background);\n\t}\n\n\tul {\n\t\tposition: relative;\n\t\tpadding: 0;\n\t\tmargin: 0;\n\t\theight: 3em;\n\t\tdisplay: flex;\n\t\tjustify-content: center;\n\t\talign-items: center;\n\t\tlist-style: none;\n\t\tbackground: var(--background);\n\t\tbackground-size: contain;\n\t}\n\n\tli {\n\t\tposition: relative;\n\t\theight: 100%;\n\t}\n\n\tli[aria-current='page']::before {\n\t\t--size: 6px;\n\t\tcontent: '';\n\t\twidth: 0;\n\t\theight: 0;\n\t\tposition: absolute;\n\t\ttop: 0;\n\t\tleft: calc(50% - var(--size));\n\t\tborder: var(--size) solid transparent;\n\t\tborder-top: var(--size) solid var(--color-theme-1);\n\t}\n\n\tnav a {\n\t\tdisplay: flex;\n\t\theight: 100%;\n\t\talign-items: center;\n\t\tpadding: 0 0.5rem;\n\t\tcolor: var(--color-text);\n\t\tfont-weight: 700;\n\t\tfont-size: 0.8rem;\n\t\ttext-transform: uppercase;\n\t\tletter-spacing: 0.1em;\n\t\ttext-decoration: none;\n\t\ttransition: color 0.2s linear;\n\t}\n\n\ta:hover {\n\t\tcolor: var(--color-theme-1);\n\t}\n</style>\n"
20
+ "contents": "<script>\n\timport { resolve } from '$app/paths';\n\timport { page } from '$app/state';\n\timport github from '$lib/images/github.svg';\n\timport logo from '$lib/images/svelte-logo.svg';\n</script>\n\n<header>\n\t<div class=\"corner\">\n\t\t<a href=\"https://svelte.dev/docs/kit\">\n\t\t\t<img src={logo} alt=\"SvelteKit\" />\n\t\t</a>\n\t</div>\n\n\t<nav>\n\t\t<svg viewBox=\"0 0 2 3\" aria-hidden=\"true\">\n\t\t\t<path d=\"M0,0 L1,2 C1.5,3 1.5,3 2,3 L2,0 Z\" />\n\t\t</svg>\n\t\t<ul>\n\t\t\t<li aria-current={page.url.pathname === '/' ? 'page' : undefined}>\n\t\t\t\t<a href={resolve('/')}>Home</a>\n\t\t\t</li>\n\t\t\t<li aria-current={page.url.pathname === '/about' ? 'page' : undefined}>\n\t\t\t\t<a href={resolve('/about')}>About</a>\n\t\t\t</li>\n\t\t\t<li aria-current={page.url.pathname.startsWith('/sverdle') ? 'page' : undefined}>\n\t\t\t\t<a href={resolve('/sverdle')}>Sverdle</a>\n\t\t\t</li>\n\t\t</ul>\n\t\t<svg viewBox=\"0 0 2 3\" aria-hidden=\"true\">\n\t\t\t<path d=\"M0,0 L0,3 C0.5,3 0.5,3 1,2 L2,0 Z\" />\n\t\t</svg>\n\t</nav>\n\n\t<div class=\"corner\">\n\t\t<a href=\"https://github.com/sveltejs/kit\">\n\t\t\t<img src={github} alt=\"GitHub\" />\n\t\t</a>\n\t</div>\n</header>\n\n<style>\n\theader {\n\t\tdisplay: flex;\n\t\tjustify-content: space-between;\n\t}\n\n\t.corner {\n\t\twidth: 3em;\n\t\theight: 3em;\n\t}\n\n\t.corner a {\n\t\tdisplay: flex;\n\t\talign-items: center;\n\t\tjustify-content: center;\n\t\twidth: 100%;\n\t\theight: 100%;\n\t}\n\n\t.corner img {\n\t\twidth: 2em;\n\t\theight: 2em;\n\t\tobject-fit: contain;\n\t}\n\n\tnav {\n\t\tdisplay: flex;\n\t\tjustify-content: center;\n\t\t--background: rgba(255, 255, 255, 0.7);\n\t}\n\n\tsvg {\n\t\twidth: 2em;\n\t\theight: 3em;\n\t\tdisplay: block;\n\t}\n\n\tpath {\n\t\tfill: var(--background);\n\t}\n\n\tul {\n\t\tposition: relative;\n\t\tpadding: 0;\n\t\tmargin: 0;\n\t\theight: 3em;\n\t\tdisplay: flex;\n\t\tjustify-content: center;\n\t\talign-items: center;\n\t\tlist-style: none;\n\t\tbackground: var(--background);\n\t\tbackground-size: contain;\n\t}\n\n\tli {\n\t\tposition: relative;\n\t\theight: 100%;\n\t}\n\n\tli[aria-current='page']::before {\n\t\t--size: 6px;\n\t\tcontent: '';\n\t\twidth: 0;\n\t\theight: 0;\n\t\tposition: absolute;\n\t\ttop: 0;\n\t\tleft: calc(50% - var(--size));\n\t\tborder: var(--size) solid transparent;\n\t\tborder-top: var(--size) solid var(--color-theme-1);\n\t}\n\n\tnav a {\n\t\tdisplay: flex;\n\t\theight: 100%;\n\t\talign-items: center;\n\t\tpadding: 0 0.5rem;\n\t\tcolor: var(--color-text);\n\t\tfont-weight: 700;\n\t\tfont-size: 0.8rem;\n\t\ttext-transform: uppercase;\n\t\tletter-spacing: 0.1em;\n\t\ttext-decoration: none;\n\t\ttransition: color 0.2s linear;\n\t}\n\n\ta:hover {\n\t\tcolor: var(--color-theme-1);\n\t}\n</style>\n"
21
21
  },
22
22
  {
23
23
  "name": "src/routes/about/+page.svelte",
@@ -29,15 +29,15 @@
29
29
  },
30
30
  {
31
31
  "name": "src/routes/sverdle/+page.server.js",
32
- "contents": "import { fail } from '@sveltejs/kit';\nimport { Game } from './game.js';\n\nexport const load = ({ cookies }) => {\n\tconst game = new Game(cookies.get('sverdle'));\n\n\treturn {\n\t\t/**\n\t\t * The player's guessed words so far\n\t\t */\n\t\tguesses: game.guesses,\n\n\t\t/**\n\t\t * An array of strings like '__x_c' corresponding to the guesses, where 'x' means\n\t\t * an exact match, and 'c' means a close match (right letter, wrong place)\n\t\t */\n\t\tanswers: game.answers,\n\n\t\t/**\n\t\t * The correct answer, revealed if the game is over\n\t\t */\n\t\tanswer: game.answers.length >= 6 ? game.answer : null\n\t};\n};\n\nexport const actions = {\n\t/**\n\t * Modify game state in reaction to a keypress. If client-side JavaScript\n\t * is available, this will happen in the browser instead of here\n\t */\n\tupdate: async ({ request, cookies }) => {\n\t\tconst game = new Game(cookies.get('sverdle'));\n\n\t\tconst data = await request.formData();\n\t\tconst key = data.get('key');\n\n\t\tconst i = game.answers.length;\n\n\t\tif (key === 'backspace') {\n\t\t\tgame.guesses[i] = game.guesses[i].slice(0, -1);\n\t\t} else {\n\t\t\tgame.guesses[i] += key;\n\t\t}\n\n\t\tcookies.set('sverdle', game.toString(), { path: '/' });\n\t},\n\n\t/**\n\t * Modify game state in reaction to a guessed word. This logic always runs on\n\t * the server, so that people can't cheat by peeking at the JavaScript\n\t */\n\tenter: async ({ request, cookies }) => {\n\t\tconst game = new Game(cookies.get('sverdle'));\n\n\t\tconst data = await request.formData();\n\t\tconst guess = (data.getAll('guess'));\n\n\t\tif (!game.enter(guess)) {\n\t\t\treturn fail(400, { badGuess: true });\n\t\t}\n\n\t\tcookies.set('sverdle', game.toString(), { path: '/' });\n\t},\n\n\trestart: async ({ cookies }) => {\n\t\tcookies.delete('sverdle', { path: '/' });\n\t}\n};\n"
32
+ "contents": "import { fail } from '@sveltejs/kit';\n\nimport { Game } from './game.js';\n\nexport const load = ({ cookies }) => {\n\tconst game = new Game(cookies.get('sverdle'));\n\n\treturn {\n\t\t/**\n\t\t * The player's guessed words so far\n\t\t */\n\t\tguesses: game.guesses,\n\n\t\t/**\n\t\t * An array of strings like '__x_c' corresponding to the guesses, where 'x' means\n\t\t * an exact match, and 'c' means a close match (right letter, wrong place)\n\t\t */\n\t\tanswers: game.answers,\n\n\t\t/**\n\t\t * The correct answer, revealed if the game is over\n\t\t */\n\t\tanswer: game.answers.length >= 6 ? game.answer : null\n\t};\n};\n\nexport const actions = {\n\t/**\n\t * Modify game state in reaction to a keypress. If client-side JavaScript\n\t * is available, this will happen in the browser instead of here\n\t */\n\tupdate: async ({ request, cookies }) => {\n\t\tconst game = new Game(cookies.get('sverdle'));\n\n\t\tconst data = await request.formData();\n\t\tconst key = data.get('key');\n\n\t\tconst i = game.answers.length;\n\n\t\tif (key === 'backspace') {\n\t\t\tgame.guesses[i] = game.guesses[i].slice(0, -1);\n\t\t} else {\n\t\t\tgame.guesses[i] += key;\n\t\t}\n\n\t\tcookies.set('sverdle', game.toString(), { path: '/' });\n\t},\n\n\t/**\n\t * Modify game state in reaction to a guessed word. This logic always runs on\n\t * the server, so that people can't cheat by peeking at the JavaScript\n\t */\n\tenter: async ({ request, cookies }) => {\n\t\tconst game = new Game(cookies.get('sverdle'));\n\n\t\tconst data = await request.formData();\n\t\tconst guess = (data.getAll('guess'));\n\n\t\tif (!game.enter(guess)) {\n\t\t\treturn fail(400, { badGuess: true });\n\t\t}\n\n\t\tcookies.set('sverdle', game.toString(), { path: '/' });\n\t},\n\n\trestart: async ({ cookies }) => {\n\t\tcookies.delete('sverdle', { path: '/' });\n\t}\n};\n"
33
33
  },
34
34
  {
35
35
  "name": "src/routes/sverdle/+page.svelte",
36
- "contents": "<script>\n\timport { enhance } from '$app/forms';\n\timport { resolve } from '$app/paths';\n\timport { confetti } from '@neoconfetti/svelte';\n\n\timport { MediaQuery } from 'svelte/reactivity';\n\n\tlet { data, form = $bindable() } = $props();\n\n\t/** Whether the user prefers reduced motion */\n\tconst reducedMotion = new MediaQuery('(prefers-reduced-motion: reduce)');\n\n\t/** Whether or not the user has won */\n\tlet won = $derived(data.answers.at(-1) === 'xxxxx');\n\n\t/** The index of the current guess */\n\tlet i = $derived(won ? -1 : data.answers.length);\n\n\t/** The current guess */\n\tlet currentGuess = $derived(data.guesses[i] || '');\n\n\t/** Whether the current guess can be submitted */\n\tlet submittable = $derived(currentGuess.length === 5);\n\n\tconst { classnames, description } = $derived.by(() => {\n\t\t/**\n\t\t * A map of classnames for all letters that have been guessed,\n\t\t * used for styling the keyboard\n\t\t */\n\t\tlet classnames = {};\n\t\t/**\n\t\t * A map of descriptions for all letters that have been guessed,\n\t\t * used for adding text for assistive technology (e.g. screen readers)\n\t\t */\n\t\tlet description = {};\n\t\tdata.answers.forEach((answer, i) => {\n\t\t\tconst guess = data.guesses[i];\n\t\t\tfor (let i = 0; i < 5; i += 1) {\n\t\t\t\tconst letter = guess[i];\n\t\t\t\tif (answer[i] === 'x') {\n\t\t\t\t\tclassnames[letter] = 'exact';\n\t\t\t\t\tdescription[letter] = 'correct';\n\t\t\t\t} else if (!classnames[letter]) {\n\t\t\t\t\tclassnames[letter] = answer[i] === 'c' ? 'close' : 'missing';\n\t\t\t\t\tdescription[letter] = answer[i] === 'c' ? 'present' : 'absent';\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\t\treturn { classnames, description };\n\t});\n\n\t/**\n\t * Modify the game state without making a trip to the server,\n\t * if client-side JavaScript is enabled\n\t */\n\tfunction update(event) {\n\t\tevent.preventDefault();\n\t\tconst key = (event.target).getAttribute('data-key');\n\n\t\tif (key === 'backspace') {\n\t\t\tcurrentGuess = currentGuess.slice(0, -1);\n\t\t\tif (form?.badGuess) form.badGuess = false;\n\t\t} else if (currentGuess.length < 5) {\n\t\t\tcurrentGuess += key;\n\t\t}\n\t}\n\n\t/**\n\t * Trigger form logic in response to a keydown event, so that\n\t * desktop users can use the keyboard to play the game\n\t */\n\tfunction keydown(event) {\n\t\tif (event.metaKey) return;\n\n\t\tif (event.key === 'Enter' && !submittable) return;\n\n\t\tdocument\n\t\t\t.querySelector(`[data-key=\"${event.key}\" i]`)\n\t\t\t?.dispatchEvent(new MouseEvent('click', { cancelable: true, bubbles: true }));\n\t}\n</script>\n\n<svelte:window onkeydown={keydown} />\n\n<svelte:head>\n\t<title>Sverdle</title>\n\t<meta name=\"description\" content=\"A Wordle clone written in SvelteKit\" />\n</svelte:head>\n\n<h1 class=\"visually-hidden\">Sverdle</h1>\n\n<form\n\tmethod=\"post\"\n\taction=\"?/enter\"\n\tuse:enhance={() => {\n\t\t// prevent default callback from resetting the form\n\t\treturn ({ update }) => {\n\t\t\tupdate({ reset: false });\n\t\t};\n\t}}\n>\n\t<a class=\"how-to-play\" href={resolve('/sverdle/how-to-play')}>How to play</a>\n\n\t<div class=\"grid\" class:playing={!won} class:bad-guess={form?.badGuess}>\n\t\t{#each Array.from(Array(6).keys()) as row (row)}\n\t\t\t{@const current = row === i}\n\t\t\t<h2 class=\"visually-hidden\">Row {row + 1}</h2>\n\t\t\t<div class=\"row\" class:current>\n\t\t\t\t{#each Array.from(Array(5).keys()) as column (column)}\n\t\t\t\t\t{@const guess = current ? currentGuess : data.guesses[row]}\n\t\t\t\t\t{@const answer = data.answers[row]?.[column]}\n\t\t\t\t\t{@const value = guess?.[column] ?? ''}\n\t\t\t\t\t{@const selected = current && column === guess.length}\n\t\t\t\t\t{@const exact = answer === 'x'}\n\t\t\t\t\t{@const close = answer === 'c'}\n\t\t\t\t\t{@const missing = answer === '_'}\n\t\t\t\t\t<div class=\"letter\" class:exact class:close class:missing class:selected>\n\t\t\t\t\t\t{value}\n\t\t\t\t\t\t<span class=\"visually-hidden\">\n\t\t\t\t\t\t\t{#if exact}\n\t\t\t\t\t\t\t\t(correct)\n\t\t\t\t\t\t\t{:else if close}\n\t\t\t\t\t\t\t\t(present)\n\t\t\t\t\t\t\t{:else if missing}\n\t\t\t\t\t\t\t\t(absent)\n\t\t\t\t\t\t\t{:else}\n\t\t\t\t\t\t\t\tempty\n\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t</span>\n\t\t\t\t\t\t<input name=\"guess\" disabled={!current} type=\"hidden\" {value} />\n\t\t\t\t\t</div>\n\t\t\t\t{/each}\n\t\t\t</div>\n\t\t{/each}\n\t</div>\n\n\t<div class=\"controls\">\n\t\t{#if won || data.answers.length >= 6}\n\t\t\t{#if !won && data.answer}\n\t\t\t\t<p>the answer was \"{data.answer}\"</p>\n\t\t\t{/if}\n\t\t\t<button data-key=\"enter\" class=\"restart selected\" formaction=\"?/restart\">\n\t\t\t\t{won ? 'you won :)' : `game over :(`} play again?\n\t\t\t</button>\n\t\t{:else}\n\t\t\t<div class=\"keyboard\">\n\t\t\t\t<button data-key=\"enter\" class:selected={submittable} disabled={!submittable}>enter</button>\n\n\t\t\t\t<button\n\t\t\t\t\tonclick={update}\n\t\t\t\t\tdata-key=\"backspace\"\n\t\t\t\t\tformaction=\"?/update\"\n\t\t\t\t\tname=\"key\"\n\t\t\t\t\tvalue=\"backspace\"\n\t\t\t\t>\n\t\t\t\t\tback\n\t\t\t\t</button>\n\n\t\t\t\t{#each ['qwertyuiop', 'asdfghjkl', 'zxcvbnm'] as row (row)}\n\t\t\t\t\t<div class=\"row\">\n\t\t\t\t\t\t{#each row as letter, index (index)}\n\t\t\t\t\t\t\t<button\n\t\t\t\t\t\t\t\tonclick={update}\n\t\t\t\t\t\t\t\tdata-key={letter}\n\t\t\t\t\t\t\t\tclass={classnames[letter]}\n\t\t\t\t\t\t\t\tdisabled={submittable}\n\t\t\t\t\t\t\t\tformaction=\"?/update\"\n\t\t\t\t\t\t\t\tname=\"key\"\n\t\t\t\t\t\t\t\tvalue={letter}\n\t\t\t\t\t\t\t\taria-label=\"{letter} {description[letter] || ''}\"\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t{letter}\n\t\t\t\t\t\t\t</button>\n\t\t\t\t\t\t{/each}\n\t\t\t\t\t</div>\n\t\t\t\t{/each}\n\t\t\t</div>\n\t\t{/if}\n\t</div>\n</form>\n\n{#if won}\n\t<div\n\t\tstyle=\"position: absolute; left: 50%; top: 30%\"\n\t\tuse:confetti={{\n\t\t\tparticleCount: reducedMotion.current ? 0 : undefined,\n\t\t\tforce: 0.7,\n\t\t\tstageWidth: window.innerWidth,\n\t\t\tstageHeight: window.innerHeight,\n\t\t\tcolors: ['#ff3e00', '#40b3ff', '#676778']\n\t\t}}\n\t></div>\n{/if}\n\n<style>\n\tform {\n\t\twidth: 100%;\n\t\theight: 100%;\n\t\tdisplay: flex;\n\t\tflex-direction: column;\n\t\talign-items: center;\n\t\tjustify-content: center;\n\t\tgap: 1rem;\n\t\tflex: 1;\n\t}\n\n\t.how-to-play {\n\t\tcolor: var(--color-text);\n\t}\n\n\t.how-to-play::before {\n\t\tcontent: 'i';\n\t\tdisplay: inline-block;\n\t\tfont-size: 0.8em;\n\t\tfont-weight: 900;\n\t\twidth: 1em;\n\t\theight: 1em;\n\t\tpadding: 0.2em;\n\t\tline-height: 1;\n\t\tborder: 1.5px solid var(--color-text);\n\t\tborder-radius: 50%;\n\t\ttext-align: center;\n\t\tmargin: 0 0.5em 0 0;\n\t\tposition: relative;\n\t\ttop: -0.05em;\n\t}\n\n\t.grid {\n\t\t--width: min(100vw, 40vh, 380px);\n\t\tmax-width: var(--width);\n\t\talign-self: center;\n\t\tjustify-self: center;\n\t\twidth: 100%;\n\t\theight: 100%;\n\t\tdisplay: flex;\n\t\tflex-direction: column;\n\t\tjustify-content: flex-start;\n\t}\n\n\t.grid .row {\n\t\tdisplay: grid;\n\t\tgrid-template-columns: repeat(5, 1fr);\n\t\tgrid-gap: 0.2rem;\n\t\tmargin: 0 0 0.2rem 0;\n\t}\n\n\t@media (prefers-reduced-motion: no-preference) {\n\t\t.grid.bad-guess .row.current {\n\t\t\tanimation: wiggle 0.5s;\n\t\t}\n\t}\n\n\t.grid.playing .row.current {\n\t\tfilter: drop-shadow(3px 3px 10px var(--color-bg-0));\n\t}\n\n\t.letter {\n\t\taspect-ratio: 1;\n\t\twidth: 100%;\n\t\tdisplay: flex;\n\t\talign-items: center;\n\t\tjustify-content: center;\n\t\ttext-align: center;\n\t\tbox-sizing: border-box;\n\t\ttext-transform: lowercase;\n\t\tborder: none;\n\t\tfont-size: calc(0.08 * var(--width));\n\t\tborder-radius: 2px;\n\t\tbackground: white;\n\t\tmargin: 0;\n\t\tcolor: rgba(0, 0, 0, 0.7);\n\t}\n\n\t.letter.missing {\n\t\tbackground: rgba(255, 255, 255, 0.5);\n\t\tcolor: rgba(0, 0, 0, 0.5);\n\t}\n\n\t.letter.exact {\n\t\tbackground: var(--color-theme-2);\n\t\tcolor: white;\n\t}\n\n\t.letter.close {\n\t\tborder: 2px solid var(--color-theme-2);\n\t}\n\n\t.selected {\n\t\toutline: 2px solid var(--color-theme-1);\n\t}\n\n\t.controls {\n\t\ttext-align: center;\n\t\tjustify-content: center;\n\t\theight: min(18vh, 10rem);\n\t}\n\n\t.keyboard {\n\t\t--gap: 0.2rem;\n\t\tposition: relative;\n\t\tdisplay: flex;\n\t\tflex-direction: column;\n\t\tgap: var(--gap);\n\t\theight: 100%;\n\t}\n\n\t.keyboard .row {\n\t\tdisplay: flex;\n\t\tjustify-content: center;\n\t\tgap: 0.2rem;\n\t\tflex: 1;\n\t}\n\n\t.keyboard button,\n\t.keyboard button:disabled {\n\t\t--size: min(8vw, 4vh, 40px);\n\t\tbackground-color: white;\n\t\tcolor: black;\n\t\twidth: var(--size);\n\t\tborder: none;\n\t\tborder-radius: 2px;\n\t\tfont-size: calc(var(--size) * 0.5);\n\t\tmargin: 0;\n\t}\n\n\t.keyboard button.exact {\n\t\tbackground: var(--color-theme-2);\n\t\tcolor: white;\n\t}\n\n\t.keyboard button.missing {\n\t\topacity: 0.5;\n\t}\n\n\t.keyboard button.close {\n\t\tborder: 2px solid var(--color-theme-2);\n\t}\n\n\t.keyboard button:focus {\n\t\tbackground: var(--color-theme-1);\n\t\tcolor: white;\n\t\toutline: none;\n\t}\n\n\t.keyboard button[data-key='enter'],\n\t.keyboard button[data-key='backspace'] {\n\t\tposition: absolute;\n\t\tbottom: 0;\n\t\twidth: calc(1.5 * var(--size));\n\t\theight: calc(1 / 3 * (100% - 2 * var(--gap)));\n\t\ttext-transform: uppercase;\n\t\tfont-size: calc(0.3 * var(--size));\n\t\tpadding-top: calc(0.15 * var(--size));\n\t}\n\n\t.keyboard button[data-key='enter'] {\n\t\tright: calc(50% + 3.5 * var(--size) + 0.8rem);\n\t}\n\n\t.keyboard button[data-key='backspace'] {\n\t\tleft: calc(50% + 3.5 * var(--size) + 0.8rem);\n\t}\n\n\t.keyboard button[data-key='enter']:disabled {\n\t\topacity: 0.5;\n\t}\n\n\t.restart {\n\t\twidth: 100%;\n\t\tpadding: 1rem;\n\t\tbackground: rgba(255, 255, 255, 0.5);\n\t\tborder-radius: 2px;\n\t\tborder: none;\n\t}\n\n\t.restart:focus,\n\t.restart:hover {\n\t\tbackground: var(--color-theme-1);\n\t\tcolor: white;\n\t\toutline: none;\n\t}\n\n\t@keyframes wiggle {\n\t\t0% {\n\t\t\ttransform: translateX(0);\n\t\t}\n\t\t10% {\n\t\t\ttransform: translateX(-2px);\n\t\t}\n\t\t30% {\n\t\t\ttransform: translateX(4px);\n\t\t}\n\t\t50% {\n\t\t\ttransform: translateX(-6px);\n\t\t}\n\t\t70% {\n\t\t\ttransform: translateX(+4px);\n\t\t}\n\t\t90% {\n\t\t\ttransform: translateX(-2px);\n\t\t}\n\t\t100% {\n\t\t\ttransform: translateX(0);\n\t\t}\n\t}\n</style>\n"
36
+ "contents": "<script>\n\timport { enhance } from '$app/forms';\n\timport { resolve } from '$app/paths';\n\timport { confetti } from '@neoconfetti/svelte';\n\timport { MediaQuery } from 'svelte/reactivity';\n\n\tlet { data, form = $bindable() } = $props();\n\n\t/** Whether the user prefers reduced motion */\n\tconst reducedMotion = new MediaQuery('(prefers-reduced-motion: reduce)');\n\n\t/** Whether or not the user has won */\n\tlet won = $derived(data.answers.at(-1) === 'xxxxx');\n\n\t/** The index of the current guess */\n\tlet i = $derived(won ? -1 : data.answers.length);\n\n\t/** The current guess */\n\tlet currentGuess = $derived(data.guesses[i] || '');\n\n\t/** Whether the current guess can be submitted */\n\tlet submittable = $derived(currentGuess.length === 5);\n\n\tconst { classnames, description } = $derived.by(() => {\n\t\t/**\n\t\t * A map of classnames for all letters that have been guessed,\n\t\t * used for styling the keyboard\n\t\t */\n\t\tlet classnames = {};\n\t\t/**\n\t\t * A map of descriptions for all letters that have been guessed,\n\t\t * used for adding text for assistive technology (e.g. screen readers)\n\t\t */\n\t\tlet description = {};\n\t\tdata.answers.forEach((answer, i) => {\n\t\t\tconst guess = data.guesses[i];\n\t\t\tfor (let i = 0; i < 5; i += 1) {\n\t\t\t\tconst letter = guess[i];\n\t\t\t\tif (answer[i] === 'x') {\n\t\t\t\t\tclassnames[letter] = 'exact';\n\t\t\t\t\tdescription[letter] = 'correct';\n\t\t\t\t} else if (!classnames[letter]) {\n\t\t\t\t\tclassnames[letter] = answer[i] === 'c' ? 'close' : 'missing';\n\t\t\t\t\tdescription[letter] = answer[i] === 'c' ? 'present' : 'absent';\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\t\treturn { classnames, description };\n\t});\n\n\t/**\n\t * Modify the game state without making a trip to the server,\n\t * if client-side JavaScript is enabled\n\t */\n\tfunction update(event) {\n\t\tevent.preventDefault();\n\t\tconst key = (event.target).getAttribute('data-key');\n\n\t\tif (key === 'backspace') {\n\t\t\tcurrentGuess = currentGuess.slice(0, -1);\n\t\t\tif (form?.badGuess) form.badGuess = false;\n\t\t} else if (currentGuess.length < 5) {\n\t\t\tcurrentGuess += key;\n\t\t}\n\t}\n\n\t/**\n\t * Trigger form logic in response to a keydown event, so that\n\t * desktop users can use the keyboard to play the game\n\t */\n\tfunction keydown(event) {\n\t\tif (event.metaKey) return;\n\n\t\tif (event.key === 'Enter' && !submittable) return;\n\n\t\tdocument\n\t\t\t.querySelector(`[data-key=\"${event.key}\" i]`)\n\t\t\t?.dispatchEvent(new MouseEvent('click', { cancelable: true, bubbles: true }));\n\t}\n</script>\n\n<svelte:window onkeydown={keydown} />\n\n<svelte:head>\n\t<title>Sverdle</title>\n\t<meta name=\"description\" content=\"A Wordle clone written in SvelteKit\" />\n</svelte:head>\n\n<h1 class=\"visually-hidden\">Sverdle</h1>\n\n<form\n\tmethod=\"post\"\n\taction=\"?/enter\"\n\tuse:enhance={() => {\n\t\t// prevent default callback from resetting the form\n\t\treturn ({ update }) => {\n\t\t\tupdate({ reset: false });\n\t\t};\n\t}}\n>\n\t<a class=\"how-to-play\" href={resolve('/sverdle/how-to-play')}>How to play</a>\n\n\t<div class=\"grid\" class:playing={!won} class:bad-guess={form?.badGuess}>\n\t\t{#each Array.from(Array(6).keys()) as row (row)}\n\t\t\t{@const current = row === i}\n\t\t\t<h2 class=\"visually-hidden\">Row {row + 1}</h2>\n\t\t\t<div class=\"row\" class:current>\n\t\t\t\t{#each Array.from(Array(5).keys()) as column (column)}\n\t\t\t\t\t{@const guess = current ? currentGuess : data.guesses[row]}\n\t\t\t\t\t{@const answer = data.answers[row]?.[column]}\n\t\t\t\t\t{@const value = guess?.[column] ?? ''}\n\t\t\t\t\t{@const selected = current && column === guess.length}\n\t\t\t\t\t{@const exact = answer === 'x'}\n\t\t\t\t\t{@const close = answer === 'c'}\n\t\t\t\t\t{@const missing = answer === '_'}\n\t\t\t\t\t<div class=\"letter\" class:exact class:close class:missing class:selected>\n\t\t\t\t\t\t{value}\n\t\t\t\t\t\t<span class=\"visually-hidden\">\n\t\t\t\t\t\t\t{#if exact}\n\t\t\t\t\t\t\t\t(correct)\n\t\t\t\t\t\t\t{:else if close}\n\t\t\t\t\t\t\t\t(present)\n\t\t\t\t\t\t\t{:else if missing}\n\t\t\t\t\t\t\t\t(absent)\n\t\t\t\t\t\t\t{:else}\n\t\t\t\t\t\t\t\tempty\n\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t</span>\n\t\t\t\t\t\t<input name=\"guess\" disabled={!current} type=\"hidden\" {value} />\n\t\t\t\t\t</div>\n\t\t\t\t{/each}\n\t\t\t</div>\n\t\t{/each}\n\t</div>\n\n\t<div class=\"controls\">\n\t\t{#if won || data.answers.length >= 6}\n\t\t\t{#if !won && data.answer}\n\t\t\t\t<p>the answer was \"{data.answer}\"</p>\n\t\t\t{/if}\n\t\t\t<button data-key=\"enter\" class=\"restart selected\" formaction=\"?/restart\">\n\t\t\t\t{won ? 'you won :)' : `game over :(`} play again?\n\t\t\t</button>\n\t\t{:else}\n\t\t\t<div class=\"keyboard\">\n\t\t\t\t<button data-key=\"enter\" class:selected={submittable} disabled={!submittable}>enter</button>\n\n\t\t\t\t<button\n\t\t\t\t\tonclick={update}\n\t\t\t\t\tdata-key=\"backspace\"\n\t\t\t\t\tformaction=\"?/update\"\n\t\t\t\t\tname=\"key\"\n\t\t\t\t\tvalue=\"backspace\"\n\t\t\t\t>\n\t\t\t\t\tback\n\t\t\t\t</button>\n\n\t\t\t\t{#each ['qwertyuiop', 'asdfghjkl', 'zxcvbnm'] as row (row)}\n\t\t\t\t\t<div class=\"row\">\n\t\t\t\t\t\t{#each row as letter, index (index)}\n\t\t\t\t\t\t\t<button\n\t\t\t\t\t\t\t\tonclick={update}\n\t\t\t\t\t\t\t\tdata-key={letter}\n\t\t\t\t\t\t\t\tclass={classnames[letter]}\n\t\t\t\t\t\t\t\tdisabled={submittable}\n\t\t\t\t\t\t\t\tformaction=\"?/update\"\n\t\t\t\t\t\t\t\tname=\"key\"\n\t\t\t\t\t\t\t\tvalue={letter}\n\t\t\t\t\t\t\t\taria-label=\"{letter} {description[letter] || ''}\"\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t{letter}\n\t\t\t\t\t\t\t</button>\n\t\t\t\t\t\t{/each}\n\t\t\t\t\t</div>\n\t\t\t\t{/each}\n\t\t\t</div>\n\t\t{/if}\n\t</div>\n</form>\n\n{#if won}\n\t<div\n\t\tstyle=\"position: absolute; left: 50%; top: 30%\"\n\t\tuse:confetti={{\n\t\t\tparticleCount: reducedMotion.current ? 0 : undefined,\n\t\t\tforce: 0.7,\n\t\t\tstageWidth: window.innerWidth,\n\t\t\tstageHeight: window.innerHeight,\n\t\t\tcolors: ['#ff3e00', '#40b3ff', '#676778']\n\t\t}}\n\t></div>\n{/if}\n\n<style>\n\tform {\n\t\twidth: 100%;\n\t\theight: 100%;\n\t\tdisplay: flex;\n\t\tflex-direction: column;\n\t\talign-items: center;\n\t\tjustify-content: center;\n\t\tgap: 1rem;\n\t\tflex: 1;\n\t}\n\n\t.how-to-play {\n\t\tcolor: var(--color-text);\n\t}\n\n\t.how-to-play::before {\n\t\tcontent: 'i';\n\t\tdisplay: inline-block;\n\t\tfont-size: 0.8em;\n\t\tfont-weight: 900;\n\t\twidth: 1em;\n\t\theight: 1em;\n\t\tpadding: 0.2em;\n\t\tline-height: 1;\n\t\tborder: 1.5px solid var(--color-text);\n\t\tborder-radius: 50%;\n\t\ttext-align: center;\n\t\tmargin: 0 0.5em 0 0;\n\t\tposition: relative;\n\t\ttop: -0.05em;\n\t}\n\n\t.grid {\n\t\t--width: min(100vw, 40vh, 380px);\n\t\tmax-width: var(--width);\n\t\talign-self: center;\n\t\tjustify-self: center;\n\t\twidth: 100%;\n\t\theight: 100%;\n\t\tdisplay: flex;\n\t\tflex-direction: column;\n\t\tjustify-content: flex-start;\n\t}\n\n\t.grid .row {\n\t\tdisplay: grid;\n\t\tgrid-template-columns: repeat(5, 1fr);\n\t\tgrid-gap: 0.2rem;\n\t\tmargin: 0 0 0.2rem 0;\n\t}\n\n\t@media (prefers-reduced-motion: no-preference) {\n\t\t.grid.bad-guess .row.current {\n\t\t\tanimation: wiggle 0.5s;\n\t\t}\n\t}\n\n\t.grid.playing .row.current {\n\t\tfilter: drop-shadow(3px 3px 10px var(--color-bg-0));\n\t}\n\n\t.letter {\n\t\taspect-ratio: 1;\n\t\twidth: 100%;\n\t\tdisplay: flex;\n\t\talign-items: center;\n\t\tjustify-content: center;\n\t\ttext-align: center;\n\t\tbox-sizing: border-box;\n\t\ttext-transform: lowercase;\n\t\tborder: none;\n\t\tfont-size: calc(0.08 * var(--width));\n\t\tborder-radius: 2px;\n\t\tbackground: white;\n\t\tmargin: 0;\n\t\tcolor: rgba(0, 0, 0, 0.7);\n\t}\n\n\t.letter.missing {\n\t\tbackground: rgba(255, 255, 255, 0.5);\n\t\tcolor: rgba(0, 0, 0, 0.5);\n\t}\n\n\t.letter.exact {\n\t\tbackground: var(--color-theme-2);\n\t\tcolor: white;\n\t}\n\n\t.letter.close {\n\t\tborder: 2px solid var(--color-theme-2);\n\t}\n\n\t.selected {\n\t\toutline: 2px solid var(--color-theme-1);\n\t}\n\n\t.controls {\n\t\ttext-align: center;\n\t\tjustify-content: center;\n\t\theight: min(18vh, 10rem);\n\t}\n\n\t.keyboard {\n\t\t--gap: 0.2rem;\n\t\tposition: relative;\n\t\tdisplay: flex;\n\t\tflex-direction: column;\n\t\tgap: var(--gap);\n\t\theight: 100%;\n\t}\n\n\t.keyboard .row {\n\t\tdisplay: flex;\n\t\tjustify-content: center;\n\t\tgap: 0.2rem;\n\t\tflex: 1;\n\t}\n\n\t.keyboard button,\n\t.keyboard button:disabled {\n\t\t--size: min(8vw, 4vh, 40px);\n\t\tbackground-color: white;\n\t\tcolor: black;\n\t\twidth: var(--size);\n\t\tborder: none;\n\t\tborder-radius: 2px;\n\t\tfont-size: calc(var(--size) * 0.5);\n\t\tmargin: 0;\n\t}\n\n\t.keyboard button.exact {\n\t\tbackground: var(--color-theme-2);\n\t\tcolor: white;\n\t}\n\n\t.keyboard button.missing {\n\t\topacity: 0.5;\n\t}\n\n\t.keyboard button.close {\n\t\tborder: 2px solid var(--color-theme-2);\n\t}\n\n\t.keyboard button:focus {\n\t\tbackground: var(--color-theme-1);\n\t\tcolor: white;\n\t\toutline: none;\n\t}\n\n\t.keyboard button[data-key='enter'],\n\t.keyboard button[data-key='backspace'] {\n\t\tposition: absolute;\n\t\tbottom: 0;\n\t\twidth: calc(1.5 * var(--size));\n\t\theight: calc(1 / 3 * (100% - 2 * var(--gap)));\n\t\ttext-transform: uppercase;\n\t\tfont-size: calc(0.3 * var(--size));\n\t\tpadding-top: calc(0.15 * var(--size));\n\t}\n\n\t.keyboard button[data-key='enter'] {\n\t\tright: calc(50% + 3.5 * var(--size) + 0.8rem);\n\t}\n\n\t.keyboard button[data-key='backspace'] {\n\t\tleft: calc(50% + 3.5 * var(--size) + 0.8rem);\n\t}\n\n\t.keyboard button[data-key='enter']:disabled {\n\t\topacity: 0.5;\n\t}\n\n\t.restart {\n\t\twidth: 100%;\n\t\tpadding: 1rem;\n\t\tbackground: rgba(255, 255, 255, 0.5);\n\t\tborder-radius: 2px;\n\t\tborder: none;\n\t}\n\n\t.restart:focus,\n\t.restart:hover {\n\t\tbackground: var(--color-theme-1);\n\t\tcolor: white;\n\t\toutline: none;\n\t}\n\n\t@keyframes wiggle {\n\t\t0% {\n\t\t\ttransform: translateX(0);\n\t\t}\n\t\t10% {\n\t\t\ttransform: translateX(-2px);\n\t\t}\n\t\t30% {\n\t\t\ttransform: translateX(4px);\n\t\t}\n\t\t50% {\n\t\t\ttransform: translateX(-6px);\n\t\t}\n\t\t70% {\n\t\t\ttransform: translateX(+4px);\n\t\t}\n\t\t90% {\n\t\t\ttransform: translateX(-2px);\n\t\t}\n\t\t100% {\n\t\t\ttransform: translateX(0);\n\t\t}\n\t}\n</style>\n"
37
37
  },
38
38
  {
39
39
  "name": "src/routes/sverdle/game.js",
40
- "contents": "import { words, allowed } from './words.server.js';\n\nexport class Game {\n\t/**\n\t * Create a game object from the player's cookie, or initialise a new game\n\t */\n\tconstructor(serialized = undefined) {\n\t\tif (serialized) {\n\t\t\tconst [index, guesses, answers] = serialized.split('-');\n\n\t\t\tthis.index = +index;\n\t\t\tthis.guesses = guesses ? guesses.split(' ') : [];\n\t\t\tthis.answers = answers ? answers.split(' ') : [];\n\t\t} else {\n\t\t\tthis.index = Math.floor(Math.random() * words.length);\n\t\t\tthis.guesses = ['', '', '', '', '', ''];\n\t\t\tthis.answers = ([]);\n\t\t}\n\n\t\tthis.answer = words[this.index];\n\t}\n\n\t/**\n\t * Update game state based on a guess of a five-letter word. Returns\n\t * true if the guess was valid, false otherwise\n\t */\n\tenter(letters) {\n\t\tconst word = letters.join('');\n\t\tconst valid = allowed.has(word);\n\n\t\tif (!valid) return false;\n\n\t\tthis.guesses[this.answers.length] = word;\n\n\t\tconst available = Array.from(this.answer);\n\t\tconst answer = Array(5).fill('_');\n\n\t\t// first, find exact matches\n\t\tfor (let i = 0; i < 5; i += 1) {\n\t\t\tif (letters[i] === available[i]) {\n\t\t\t\tanswer[i] = 'x';\n\t\t\t\tavailable[i] = ' ';\n\t\t\t}\n\t\t}\n\n\t\t// then find close matches (this has to happen\n\t\t// in a second step, otherwise an early close\n\t\t// match can prevent a later exact match)\n\t\tfor (let i = 0; i < 5; i += 1) {\n\t\t\tif (answer[i] === '_') {\n\t\t\t\tconst index = available.indexOf(letters[i]);\n\t\t\t\tif (index !== -1) {\n\t\t\t\t\tanswer[i] = 'c';\n\t\t\t\t\tavailable[index] = ' ';\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tthis.answers.push(answer.join(''));\n\n\t\treturn true;\n\t}\n\n\t/**\n\t * Serialize game state so it can be set as a cookie\n\t */\n\ttoString() {\n\t\treturn `${this.index}-${this.guesses.join(' ')}-${this.answers.join(' ')}`;\n\t}\n}\n"
40
+ "contents": "import { allowed, words } from './words.server.js';\n\nexport class Game {\n\t/**\n\t * Create a game object from the player's cookie, or initialise a new game\n\t */\n\tconstructor(serialized = undefined) {\n\t\tif (serialized) {\n\t\t\tconst [index, guesses, answers] = serialized.split('-');\n\n\t\t\tthis.index = +index;\n\t\t\tthis.guesses = guesses ? guesses.split(' ') : [];\n\t\t\tthis.answers = answers ? answers.split(' ') : [];\n\t\t} else {\n\t\t\tthis.index = Math.floor(Math.random() * words.length);\n\t\t\tthis.guesses = ['', '', '', '', '', ''];\n\t\t\tthis.answers = ([]);\n\t\t}\n\n\t\tthis.answer = words[this.index];\n\t}\n\n\t/**\n\t * Update game state based on a guess of a five-letter word. Returns\n\t * true if the guess was valid, false otherwise\n\t */\n\tenter(letters) {\n\t\tconst word = letters.join('');\n\t\tconst valid = allowed.has(word);\n\n\t\tif (!valid) return false;\n\n\t\tthis.guesses[this.answers.length] = word;\n\n\t\tconst available = Array.from(this.answer);\n\t\tconst answer = Array(5).fill('_');\n\n\t\t// first, find exact matches\n\t\tfor (let i = 0; i < 5; i += 1) {\n\t\t\tif (letters[i] === available[i]) {\n\t\t\t\tanswer[i] = 'x';\n\t\t\t\tavailable[i] = ' ';\n\t\t\t}\n\t\t}\n\n\t\t// then find close matches (this has to happen\n\t\t// in a second step, otherwise an early close\n\t\t// match can prevent a later exact match)\n\t\tfor (let i = 0; i < 5; i += 1) {\n\t\t\tif (answer[i] === '_') {\n\t\t\t\tconst index = available.indexOf(letters[i]);\n\t\t\t\tif (index !== -1) {\n\t\t\t\t\tanswer[i] = 'c';\n\t\t\t\t\tavailable[index] = ' ';\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tthis.answers.push(answer.join(''));\n\n\t\treturn true;\n\t}\n\n\t/**\n\t * Serialize game state so it can be set as a cookie\n\t */\n\ttoString() {\n\t\treturn `${this.index}-${this.guesses.join(' ')}-${this.answers.join(' ')}`;\n\t}\n}\n"
41
41
  },
42
42
  {
43
43
  "name": "src/routes/sverdle/how-to-play/+page.svelte",
@@ -9,7 +9,7 @@
9
9
  },
10
10
  {
11
11
  "name": "src/routes/+page.svelte",
12
- "contents": "<script lang=\"ts\">\n\timport Counter from './Counter.svelte';\n\timport welcome from '$lib/images/svelte-welcome.webp';\n\timport welcomeFallback from '$lib/images/svelte-welcome.png';\n</script>\n\n<svelte:head>\n\t<title>Home</title>\n\t<meta name=\"description\" content=\"Svelte demo app\" />\n</svelte:head>\n\n<section>\n\t<h1>\n\t\t<span class=\"welcome\">\n\t\t\t<picture>\n\t\t\t\t<source srcset={welcome} type=\"image/webp\" />\n\t\t\t\t<img src={welcomeFallback} alt=\"Welcome\" />\n\t\t\t</picture>\n\t\t</span>\n\n\t\tto your new<br />SvelteKit app\n\t</h1>\n\n\t<h2>\n\t\ttry editing <strong>src/routes/+page.svelte</strong>\n\t</h2>\n\n\t<Counter />\n</section>\n\n<style>\n\tsection {\n\t\tdisplay: flex;\n\t\tflex-direction: column;\n\t\tjustify-content: center;\n\t\talign-items: center;\n\t\tflex: 0.6;\n\t}\n\n\th1 {\n\t\twidth: 100%;\n\t}\n\n\t.welcome {\n\t\tdisplay: block;\n\t\tposition: relative;\n\t\twidth: 100%;\n\t\theight: 0;\n\t\tpadding: 0 0 calc(100% * 495 / 2048) 0;\n\t}\n\n\t.welcome img {\n\t\tposition: absolute;\n\t\twidth: 100%;\n\t\theight: 100%;\n\t\ttop: 0;\n\t\tdisplay: block;\n\t}\n</style>\n"
12
+ "contents": "<script lang=\"ts\">\n\timport welcomeFallback from '$lib/images/svelte-welcome.png';\n\timport welcome from '$lib/images/svelte-welcome.webp';\n\n\timport Counter from './Counter.svelte';\n</script>\n\n<svelte:head>\n\t<title>Home</title>\n\t<meta name=\"description\" content=\"Svelte demo app\" />\n</svelte:head>\n\n<section>\n\t<h1>\n\t\t<span class=\"welcome\">\n\t\t\t<picture>\n\t\t\t\t<source srcset={welcome} type=\"image/webp\" />\n\t\t\t\t<img src={welcomeFallback} alt=\"Welcome\" />\n\t\t\t</picture>\n\t\t</span>\n\n\t\tto your new<br />SvelteKit app\n\t</h1>\n\n\t<h2>\n\t\ttry editing <strong>src/routes/+page.svelte</strong>\n\t</h2>\n\n\t<Counter />\n</section>\n\n<style>\n\tsection {\n\t\tdisplay: flex;\n\t\tflex-direction: column;\n\t\tjustify-content: center;\n\t\talign-items: center;\n\t\tflex: 0.6;\n\t}\n\n\th1 {\n\t\twidth: 100%;\n\t}\n\n\t.welcome {\n\t\tdisplay: block;\n\t\tposition: relative;\n\t\twidth: 100%;\n\t\theight: 0;\n\t\tpadding: 0 0 calc(100% * 495 / 2048) 0;\n\t}\n\n\t.welcome img {\n\t\tposition: absolute;\n\t\twidth: 100%;\n\t\theight: 100%;\n\t\ttop: 0;\n\t\tdisplay: block;\n\t}\n</style>\n"
13
13
  },
14
14
  {
15
15
  "name": "src/routes/+page.ts",
@@ -21,7 +21,7 @@
21
21
  },
22
22
  {
23
23
  "name": "src/routes/Header.svelte",
24
- "contents": "<script lang=\"ts\">\n\timport { resolve } from '$app/paths';\n\timport { page } from '$app/state';\n\timport logo from '$lib/images/svelte-logo.svg';\n\timport github from '$lib/images/github.svg';\n</script>\n\n<header>\n\t<div class=\"corner\">\n\t\t<a href=\"https://svelte.dev/docs/kit\">\n\t\t\t<img src={logo} alt=\"SvelteKit\" />\n\t\t</a>\n\t</div>\n\n\t<nav>\n\t\t<svg viewBox=\"0 0 2 3\" aria-hidden=\"true\">\n\t\t\t<path d=\"M0,0 L1,2 C1.5,3 1.5,3 2,3 L2,0 Z\" />\n\t\t</svg>\n\t\t<ul>\n\t\t\t<li aria-current={page.url.pathname === '/' ? 'page' : undefined}>\n\t\t\t\t<a href={resolve('/')}>Home</a>\n\t\t\t</li>\n\t\t\t<li aria-current={page.url.pathname === '/about' ? 'page' : undefined}>\n\t\t\t\t<a href={resolve('/about')}>About</a>\n\t\t\t</li>\n\t\t\t<li aria-current={page.url.pathname.startsWith('/sverdle') ? 'page' : undefined}>\n\t\t\t\t<a href={resolve('/sverdle')}>Sverdle</a>\n\t\t\t</li>\n\t\t</ul>\n\t\t<svg viewBox=\"0 0 2 3\" aria-hidden=\"true\">\n\t\t\t<path d=\"M0,0 L0,3 C0.5,3 0.5,3 1,2 L2,0 Z\" />\n\t\t</svg>\n\t</nav>\n\n\t<div class=\"corner\">\n\t\t<a href=\"https://github.com/sveltejs/kit\">\n\t\t\t<img src={github} alt=\"GitHub\" />\n\t\t</a>\n\t</div>\n</header>\n\n<style>\n\theader {\n\t\tdisplay: flex;\n\t\tjustify-content: space-between;\n\t}\n\n\t.corner {\n\t\twidth: 3em;\n\t\theight: 3em;\n\t}\n\n\t.corner a {\n\t\tdisplay: flex;\n\t\talign-items: center;\n\t\tjustify-content: center;\n\t\twidth: 100%;\n\t\theight: 100%;\n\t}\n\n\t.corner img {\n\t\twidth: 2em;\n\t\theight: 2em;\n\t\tobject-fit: contain;\n\t}\n\n\tnav {\n\t\tdisplay: flex;\n\t\tjustify-content: center;\n\t\t--background: rgba(255, 255, 255, 0.7);\n\t}\n\n\tsvg {\n\t\twidth: 2em;\n\t\theight: 3em;\n\t\tdisplay: block;\n\t}\n\n\tpath {\n\t\tfill: var(--background);\n\t}\n\n\tul {\n\t\tposition: relative;\n\t\tpadding: 0;\n\t\tmargin: 0;\n\t\theight: 3em;\n\t\tdisplay: flex;\n\t\tjustify-content: center;\n\t\talign-items: center;\n\t\tlist-style: none;\n\t\tbackground: var(--background);\n\t\tbackground-size: contain;\n\t}\n\n\tli {\n\t\tposition: relative;\n\t\theight: 100%;\n\t}\n\n\tli[aria-current='page']::before {\n\t\t--size: 6px;\n\t\tcontent: '';\n\t\twidth: 0;\n\t\theight: 0;\n\t\tposition: absolute;\n\t\ttop: 0;\n\t\tleft: calc(50% - var(--size));\n\t\tborder: var(--size) solid transparent;\n\t\tborder-top: var(--size) solid var(--color-theme-1);\n\t}\n\n\tnav a {\n\t\tdisplay: flex;\n\t\theight: 100%;\n\t\talign-items: center;\n\t\tpadding: 0 0.5rem;\n\t\tcolor: var(--color-text);\n\t\tfont-weight: 700;\n\t\tfont-size: 0.8rem;\n\t\ttext-transform: uppercase;\n\t\tletter-spacing: 0.1em;\n\t\ttext-decoration: none;\n\t\ttransition: color 0.2s linear;\n\t}\n\n\ta:hover {\n\t\tcolor: var(--color-theme-1);\n\t}\n</style>\n"
24
+ "contents": "<script lang=\"ts\">\n\timport { resolve } from '$app/paths';\n\timport { page } from '$app/state';\n\timport github from '$lib/images/github.svg';\n\timport logo from '$lib/images/svelte-logo.svg';\n</script>\n\n<header>\n\t<div class=\"corner\">\n\t\t<a href=\"https://svelte.dev/docs/kit\">\n\t\t\t<img src={logo} alt=\"SvelteKit\" />\n\t\t</a>\n\t</div>\n\n\t<nav>\n\t\t<svg viewBox=\"0 0 2 3\" aria-hidden=\"true\">\n\t\t\t<path d=\"M0,0 L1,2 C1.5,3 1.5,3 2,3 L2,0 Z\" />\n\t\t</svg>\n\t\t<ul>\n\t\t\t<li aria-current={page.url.pathname === '/' ? 'page' : undefined}>\n\t\t\t\t<a href={resolve('/')}>Home</a>\n\t\t\t</li>\n\t\t\t<li aria-current={page.url.pathname === '/about' ? 'page' : undefined}>\n\t\t\t\t<a href={resolve('/about')}>About</a>\n\t\t\t</li>\n\t\t\t<li aria-current={page.url.pathname.startsWith('/sverdle') ? 'page' : undefined}>\n\t\t\t\t<a href={resolve('/sverdle')}>Sverdle</a>\n\t\t\t</li>\n\t\t</ul>\n\t\t<svg viewBox=\"0 0 2 3\" aria-hidden=\"true\">\n\t\t\t<path d=\"M0,0 L0,3 C0.5,3 0.5,3 1,2 L2,0 Z\" />\n\t\t</svg>\n\t</nav>\n\n\t<div class=\"corner\">\n\t\t<a href=\"https://github.com/sveltejs/kit\">\n\t\t\t<img src={github} alt=\"GitHub\" />\n\t\t</a>\n\t</div>\n</header>\n\n<style>\n\theader {\n\t\tdisplay: flex;\n\t\tjustify-content: space-between;\n\t}\n\n\t.corner {\n\t\twidth: 3em;\n\t\theight: 3em;\n\t}\n\n\t.corner a {\n\t\tdisplay: flex;\n\t\talign-items: center;\n\t\tjustify-content: center;\n\t\twidth: 100%;\n\t\theight: 100%;\n\t}\n\n\t.corner img {\n\t\twidth: 2em;\n\t\theight: 2em;\n\t\tobject-fit: contain;\n\t}\n\n\tnav {\n\t\tdisplay: flex;\n\t\tjustify-content: center;\n\t\t--background: rgba(255, 255, 255, 0.7);\n\t}\n\n\tsvg {\n\t\twidth: 2em;\n\t\theight: 3em;\n\t\tdisplay: block;\n\t}\n\n\tpath {\n\t\tfill: var(--background);\n\t}\n\n\tul {\n\t\tposition: relative;\n\t\tpadding: 0;\n\t\tmargin: 0;\n\t\theight: 3em;\n\t\tdisplay: flex;\n\t\tjustify-content: center;\n\t\talign-items: center;\n\t\tlist-style: none;\n\t\tbackground: var(--background);\n\t\tbackground-size: contain;\n\t}\n\n\tli {\n\t\tposition: relative;\n\t\theight: 100%;\n\t}\n\n\tli[aria-current='page']::before {\n\t\t--size: 6px;\n\t\tcontent: '';\n\t\twidth: 0;\n\t\theight: 0;\n\t\tposition: absolute;\n\t\ttop: 0;\n\t\tleft: calc(50% - var(--size));\n\t\tborder: var(--size) solid transparent;\n\t\tborder-top: var(--size) solid var(--color-theme-1);\n\t}\n\n\tnav a {\n\t\tdisplay: flex;\n\t\theight: 100%;\n\t\talign-items: center;\n\t\tpadding: 0 0.5rem;\n\t\tcolor: var(--color-text);\n\t\tfont-weight: 700;\n\t\tfont-size: 0.8rem;\n\t\ttext-transform: uppercase;\n\t\tletter-spacing: 0.1em;\n\t\ttext-decoration: none;\n\t\ttransition: color 0.2s linear;\n\t}\n\n\ta:hover {\n\t\tcolor: var(--color-theme-1);\n\t}\n</style>\n"
25
25
  },
26
26
  {
27
27
  "name": "src/routes/about/+page.svelte",
@@ -33,15 +33,15 @@
33
33
  },
34
34
  {
35
35
  "name": "src/routes/sverdle/+page.server.ts",
36
- "contents": "import { fail } from '@sveltejs/kit';\nimport { Game } from './game.ts';\nimport type { PageServerLoad, Actions } from './$types';\n\nexport const load = (({ cookies }) => {\n\tconst game = new Game(cookies.get('sverdle'));\n\n\treturn {\n\t\t/**\n\t\t * The player's guessed words so far\n\t\t */\n\t\tguesses: game.guesses,\n\n\t\t/**\n\t\t * An array of strings like '__x_c' corresponding to the guesses, where 'x' means\n\t\t * an exact match, and 'c' means a close match (right letter, wrong place)\n\t\t */\n\t\tanswers: game.answers,\n\n\t\t/**\n\t\t * The correct answer, revealed if the game is over\n\t\t */\n\t\tanswer: game.answers.length >= 6 ? game.answer : null\n\t};\n}) satisfies PageServerLoad;\n\nexport const actions = {\n\t/**\n\t * Modify game state in reaction to a keypress. If client-side JavaScript\n\t * is available, this will happen in the browser instead of here\n\t */\n\tupdate: async ({ request, cookies }) => {\n\t\tconst game = new Game(cookies.get('sverdle'));\n\n\t\tconst data = await request.formData();\n\t\tconst key = data.get('key');\n\n\t\tconst i = game.answers.length;\n\n\t\tif (key === 'backspace') {\n\t\t\tgame.guesses[i] = game.guesses[i].slice(0, -1);\n\t\t} else {\n\t\t\tgame.guesses[i] += key;\n\t\t}\n\n\t\tcookies.set('sverdle', game.toString(), { path: '/' });\n\t},\n\n\t/**\n\t * Modify game state in reaction to a guessed word. This logic always runs on\n\t * the server, so that people can't cheat by peeking at the JavaScript\n\t */\n\tenter: async ({ request, cookies }) => {\n\t\tconst game = new Game(cookies.get('sverdle'));\n\n\t\tconst data = await request.formData();\n\t\tconst guess = data.getAll('guess') as string[];\n\n\t\tif (!game.enter(guess)) {\n\t\t\treturn fail(400, { badGuess: true });\n\t\t}\n\n\t\tcookies.set('sverdle', game.toString(), { path: '/' });\n\t},\n\n\trestart: async ({ cookies }) => {\n\t\tcookies.delete('sverdle', { path: '/' });\n\t}\n} satisfies Actions;\n"
36
+ "contents": "import { fail } from '@sveltejs/kit';\n\nimport type { Actions, PageServerLoad } from './$types';\nimport { Game } from './game.ts';\n\nexport const load = (({ cookies }) => {\n\tconst game = new Game(cookies.get('sverdle'));\n\n\treturn {\n\t\t/**\n\t\t * The player's guessed words so far\n\t\t */\n\t\tguesses: game.guesses,\n\n\t\t/**\n\t\t * An array of strings like '__x_c' corresponding to the guesses, where 'x' means\n\t\t * an exact match, and 'c' means a close match (right letter, wrong place)\n\t\t */\n\t\tanswers: game.answers,\n\n\t\t/**\n\t\t * The correct answer, revealed if the game is over\n\t\t */\n\t\tanswer: game.answers.length >= 6 ? game.answer : null\n\t};\n}) satisfies PageServerLoad;\n\nexport const actions = {\n\t/**\n\t * Modify game state in reaction to a keypress. If client-side JavaScript\n\t * is available, this will happen in the browser instead of here\n\t */\n\tupdate: async ({ request, cookies }) => {\n\t\tconst game = new Game(cookies.get('sverdle'));\n\n\t\tconst data = await request.formData();\n\t\tconst key = data.get('key');\n\n\t\tconst i = game.answers.length;\n\n\t\tif (key === 'backspace') {\n\t\t\tgame.guesses[i] = game.guesses[i].slice(0, -1);\n\t\t} else {\n\t\t\tgame.guesses[i] += key;\n\t\t}\n\n\t\tcookies.set('sverdle', game.toString(), { path: '/' });\n\t},\n\n\t/**\n\t * Modify game state in reaction to a guessed word. This logic always runs on\n\t * the server, so that people can't cheat by peeking at the JavaScript\n\t */\n\tenter: async ({ request, cookies }) => {\n\t\tconst game = new Game(cookies.get('sverdle'));\n\n\t\tconst data = await request.formData();\n\t\tconst guess = data.getAll('guess') as string[];\n\n\t\tif (!game.enter(guess)) {\n\t\t\treturn fail(400, { badGuess: true });\n\t\t}\n\n\t\tcookies.set('sverdle', game.toString(), { path: '/' });\n\t},\n\n\trestart: async ({ cookies }) => {\n\t\tcookies.delete('sverdle', { path: '/' });\n\t}\n} satisfies Actions;\n"
37
37
  },
38
38
  {
39
39
  "name": "src/routes/sverdle/+page.svelte",
40
- "contents": "<script lang=\"ts\">\n\timport { enhance } from '$app/forms';\n\timport { resolve } from '$app/paths';\n\timport { confetti } from '@neoconfetti/svelte';\n\timport type { ActionData, PageData } from './$types';\n\timport { MediaQuery } from 'svelte/reactivity';\n\n\tinterface Props {\n\t\tdata: PageData;\n\t\tform: ActionData;\n\t}\n\tlet { data, form = $bindable() }: Props = $props();\n\n\t/** Whether the user prefers reduced motion */\n\tconst reducedMotion = new MediaQuery('(prefers-reduced-motion: reduce)');\n\n\t/** Whether or not the user has won */\n\tlet won = $derived(data.answers.at(-1) === 'xxxxx');\n\n\t/** The index of the current guess */\n\tlet i = $derived(won ? -1 : data.answers.length);\n\n\t/** The current guess */\n\tlet currentGuess = $derived(data.guesses[i] || '');\n\n\t/** Whether the current guess can be submitted */\n\tlet submittable = $derived(currentGuess.length === 5);\n\n\tconst { classnames, description } = $derived.by(() => {\n\t\t/**\n\t\t * A map of classnames for all letters that have been guessed,\n\t\t * used for styling the keyboard\n\t\t */\n\t\tlet classnames: Record<string, 'exact' | 'close' | 'missing'> = {};\n\t\t/**\n\t\t * A map of descriptions for all letters that have been guessed,\n\t\t * used for adding text for assistive technology (e.g. screen readers)\n\t\t */\n\t\tlet description: Record<string, string> = {};\n\t\tdata.answers.forEach((answer, i) => {\n\t\t\tconst guess = data.guesses[i];\n\t\t\tfor (let i = 0; i < 5; i += 1) {\n\t\t\t\tconst letter = guess[i];\n\t\t\t\tif (answer[i] === 'x') {\n\t\t\t\t\tclassnames[letter] = 'exact';\n\t\t\t\t\tdescription[letter] = 'correct';\n\t\t\t\t} else if (!classnames[letter]) {\n\t\t\t\t\tclassnames[letter] = answer[i] === 'c' ? 'close' : 'missing';\n\t\t\t\t\tdescription[letter] = answer[i] === 'c' ? 'present' : 'absent';\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\t\treturn { classnames, description };\n\t});\n\n\t/**\n\t * Modify the game state without making a trip to the server,\n\t * if client-side JavaScript is enabled\n\t */\n\tfunction update(event: MouseEvent) {\n\t\tevent.preventDefault();\n\t\tconst key = (event.target as HTMLButtonElement).getAttribute(\n\t\t\t'data-key'\n\t\t);\n\n\t\tif (key === 'backspace') {\n\t\t\tcurrentGuess = currentGuess.slice(0, -1);\n\t\t\tif (form?.badGuess) form.badGuess = false;\n\t\t} else if (currentGuess.length < 5) {\n\t\t\tcurrentGuess += key;\n\t\t}\n\t}\n\n\t/**\n\t * Trigger form logic in response to a keydown event, so that\n\t * desktop users can use the keyboard to play the game\n\t */\n\tfunction keydown(event: KeyboardEvent) {\n\t\tif (event.metaKey) return;\n\n\t\tif (event.key === 'Enter' && !submittable) return;\n\n\t\tdocument\n\t\t\t.querySelector(`[data-key=\"${event.key}\" i]`)\n\t\t\t?.dispatchEvent(new MouseEvent('click', { cancelable: true, bubbles: true }));\n\t}\n</script>\n\n<svelte:window onkeydown={keydown} />\n\n<svelte:head>\n\t<title>Sverdle</title>\n\t<meta name=\"description\" content=\"A Wordle clone written in SvelteKit\" />\n</svelte:head>\n\n<h1 class=\"visually-hidden\">Sverdle</h1>\n\n<form\n\tmethod=\"post\"\n\taction=\"?/enter\"\n\tuse:enhance={() => {\n\t\t// prevent default callback from resetting the form\n\t\treturn ({ update }) => {\n\t\t\tupdate({ reset: false });\n\t\t};\n\t}}\n>\n\t<a class=\"how-to-play\" href={resolve('/sverdle/how-to-play')}>How to play</a>\n\n\t<div class=\"grid\" class:playing={!won} class:bad-guess={form?.badGuess}>\n\t\t{#each Array.from(Array(6).keys()) as row (row)}\n\t\t\t{@const current = row === i}\n\t\t\t<h2 class=\"visually-hidden\">Row {row + 1}</h2>\n\t\t\t<div class=\"row\" class:current>\n\t\t\t\t{#each Array.from(Array(5).keys()) as column (column)}\n\t\t\t\t\t{@const guess = current ? currentGuess : data.guesses[row]}\n\t\t\t\t\t{@const answer = data.answers[row]?.[column]}\n\t\t\t\t\t{@const value = guess?.[column] ?? ''}\n\t\t\t\t\t{@const selected = current && column === guess.length}\n\t\t\t\t\t{@const exact = answer === 'x'}\n\t\t\t\t\t{@const close = answer === 'c'}\n\t\t\t\t\t{@const missing = answer === '_'}\n\t\t\t\t\t<div class=\"letter\" class:exact class:close class:missing class:selected>\n\t\t\t\t\t\t{value}\n\t\t\t\t\t\t<span class=\"visually-hidden\">\n\t\t\t\t\t\t\t{#if exact}\n\t\t\t\t\t\t\t\t(correct)\n\t\t\t\t\t\t\t{:else if close}\n\t\t\t\t\t\t\t\t(present)\n\t\t\t\t\t\t\t{:else if missing}\n\t\t\t\t\t\t\t\t(absent)\n\t\t\t\t\t\t\t{:else}\n\t\t\t\t\t\t\t\tempty\n\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t</span>\n\t\t\t\t\t\t<input name=\"guess\" disabled={!current} type=\"hidden\" {value} />\n\t\t\t\t\t</div>\n\t\t\t\t{/each}\n\t\t\t</div>\n\t\t{/each}\n\t</div>\n\n\t<div class=\"controls\">\n\t\t{#if won || data.answers.length >= 6}\n\t\t\t{#if !won && data.answer}\n\t\t\t\t<p>the answer was \"{data.answer}\"</p>\n\t\t\t{/if}\n\t\t\t<button data-key=\"enter\" class=\"restart selected\" formaction=\"?/restart\">\n\t\t\t\t{won ? 'you won :)' : `game over :(`} play again?\n\t\t\t</button>\n\t\t{:else}\n\t\t\t<div class=\"keyboard\">\n\t\t\t\t<button data-key=\"enter\" class:selected={submittable} disabled={!submittable}>enter</button>\n\n\t\t\t\t<button\n\t\t\t\t\tonclick={update}\n\t\t\t\t\tdata-key=\"backspace\"\n\t\t\t\t\tformaction=\"?/update\"\n\t\t\t\t\tname=\"key\"\n\t\t\t\t\tvalue=\"backspace\"\n\t\t\t\t>\n\t\t\t\t\tback\n\t\t\t\t</button>\n\n\t\t\t\t{#each ['qwertyuiop', 'asdfghjkl', 'zxcvbnm'] as row (row)}\n\t\t\t\t\t<div class=\"row\">\n\t\t\t\t\t\t{#each row as letter, index (index)}\n\t\t\t\t\t\t\t<button\n\t\t\t\t\t\t\t\tonclick={update}\n\t\t\t\t\t\t\t\tdata-key={letter}\n\t\t\t\t\t\t\t\tclass={classnames[letter]}\n\t\t\t\t\t\t\t\tdisabled={submittable}\n\t\t\t\t\t\t\t\tformaction=\"?/update\"\n\t\t\t\t\t\t\t\tname=\"key\"\n\t\t\t\t\t\t\t\tvalue={letter}\n\t\t\t\t\t\t\t\taria-label=\"{letter} {description[letter] || ''}\"\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t{letter}\n\t\t\t\t\t\t\t</button>\n\t\t\t\t\t\t{/each}\n\t\t\t\t\t</div>\n\t\t\t\t{/each}\n\t\t\t</div>\n\t\t{/if}\n\t</div>\n</form>\n\n{#if won}\n\t<div\n\t\tstyle=\"position: absolute; left: 50%; top: 30%\"\n\t\tuse:confetti={{\n\t\t\tparticleCount: reducedMotion.current ? 0 : undefined,\n\t\t\tforce: 0.7,\n\t\t\tstageWidth: window.innerWidth,\n\t\t\tstageHeight: window.innerHeight,\n\t\t\tcolors: ['#ff3e00', '#40b3ff', '#676778']\n\t\t}}\n\t></div>\n{/if}\n\n<style>\n\tform {\n\t\twidth: 100%;\n\t\theight: 100%;\n\t\tdisplay: flex;\n\t\tflex-direction: column;\n\t\talign-items: center;\n\t\tjustify-content: center;\n\t\tgap: 1rem;\n\t\tflex: 1;\n\t}\n\n\t.how-to-play {\n\t\tcolor: var(--color-text);\n\t}\n\n\t.how-to-play::before {\n\t\tcontent: 'i';\n\t\tdisplay: inline-block;\n\t\tfont-size: 0.8em;\n\t\tfont-weight: 900;\n\t\twidth: 1em;\n\t\theight: 1em;\n\t\tpadding: 0.2em;\n\t\tline-height: 1;\n\t\tborder: 1.5px solid var(--color-text);\n\t\tborder-radius: 50%;\n\t\ttext-align: center;\n\t\tmargin: 0 0.5em 0 0;\n\t\tposition: relative;\n\t\ttop: -0.05em;\n\t}\n\n\t.grid {\n\t\t--width: min(100vw, 40vh, 380px);\n\t\tmax-width: var(--width);\n\t\talign-self: center;\n\t\tjustify-self: center;\n\t\twidth: 100%;\n\t\theight: 100%;\n\t\tdisplay: flex;\n\t\tflex-direction: column;\n\t\tjustify-content: flex-start;\n\t}\n\n\t.grid .row {\n\t\tdisplay: grid;\n\t\tgrid-template-columns: repeat(5, 1fr);\n\t\tgrid-gap: 0.2rem;\n\t\tmargin: 0 0 0.2rem 0;\n\t}\n\n\t@media (prefers-reduced-motion: no-preference) {\n\t\t.grid.bad-guess .row.current {\n\t\t\tanimation: wiggle 0.5s;\n\t\t}\n\t}\n\n\t.grid.playing .row.current {\n\t\tfilter: drop-shadow(3px 3px 10px var(--color-bg-0));\n\t}\n\n\t.letter {\n\t\taspect-ratio: 1;\n\t\twidth: 100%;\n\t\tdisplay: flex;\n\t\talign-items: center;\n\t\tjustify-content: center;\n\t\ttext-align: center;\n\t\tbox-sizing: border-box;\n\t\ttext-transform: lowercase;\n\t\tborder: none;\n\t\tfont-size: calc(0.08 * var(--width));\n\t\tborder-radius: 2px;\n\t\tbackground: white;\n\t\tmargin: 0;\n\t\tcolor: rgba(0, 0, 0, 0.7);\n\t}\n\n\t.letter.missing {\n\t\tbackground: rgba(255, 255, 255, 0.5);\n\t\tcolor: rgba(0, 0, 0, 0.5);\n\t}\n\n\t.letter.exact {\n\t\tbackground: var(--color-theme-2);\n\t\tcolor: white;\n\t}\n\n\t.letter.close {\n\t\tborder: 2px solid var(--color-theme-2);\n\t}\n\n\t.selected {\n\t\toutline: 2px solid var(--color-theme-1);\n\t}\n\n\t.controls {\n\t\ttext-align: center;\n\t\tjustify-content: center;\n\t\theight: min(18vh, 10rem);\n\t}\n\n\t.keyboard {\n\t\t--gap: 0.2rem;\n\t\tposition: relative;\n\t\tdisplay: flex;\n\t\tflex-direction: column;\n\t\tgap: var(--gap);\n\t\theight: 100%;\n\t}\n\n\t.keyboard .row {\n\t\tdisplay: flex;\n\t\tjustify-content: center;\n\t\tgap: 0.2rem;\n\t\tflex: 1;\n\t}\n\n\t.keyboard button,\n\t.keyboard button:disabled {\n\t\t--size: min(8vw, 4vh, 40px);\n\t\tbackground-color: white;\n\t\tcolor: black;\n\t\twidth: var(--size);\n\t\tborder: none;\n\t\tborder-radius: 2px;\n\t\tfont-size: calc(var(--size) * 0.5);\n\t\tmargin: 0;\n\t}\n\n\t.keyboard button.exact {\n\t\tbackground: var(--color-theme-2);\n\t\tcolor: white;\n\t}\n\n\t.keyboard button.missing {\n\t\topacity: 0.5;\n\t}\n\n\t.keyboard button.close {\n\t\tborder: 2px solid var(--color-theme-2);\n\t}\n\n\t.keyboard button:focus {\n\t\tbackground: var(--color-theme-1);\n\t\tcolor: white;\n\t\toutline: none;\n\t}\n\n\t.keyboard button[data-key='enter'],\n\t.keyboard button[data-key='backspace'] {\n\t\tposition: absolute;\n\t\tbottom: 0;\n\t\twidth: calc(1.5 * var(--size));\n\t\theight: calc(1 / 3 * (100% - 2 * var(--gap)));\n\t\ttext-transform: uppercase;\n\t\tfont-size: calc(0.3 * var(--size));\n\t\tpadding-top: calc(0.15 * var(--size));\n\t}\n\n\t.keyboard button[data-key='enter'] {\n\t\tright: calc(50% + 3.5 * var(--size) + 0.8rem);\n\t}\n\n\t.keyboard button[data-key='backspace'] {\n\t\tleft: calc(50% + 3.5 * var(--size) + 0.8rem);\n\t}\n\n\t.keyboard button[data-key='enter']:disabled {\n\t\topacity: 0.5;\n\t}\n\n\t.restart {\n\t\twidth: 100%;\n\t\tpadding: 1rem;\n\t\tbackground: rgba(255, 255, 255, 0.5);\n\t\tborder-radius: 2px;\n\t\tborder: none;\n\t}\n\n\t.restart:focus,\n\t.restart:hover {\n\t\tbackground: var(--color-theme-1);\n\t\tcolor: white;\n\t\toutline: none;\n\t}\n\n\t@keyframes wiggle {\n\t\t0% {\n\t\t\ttransform: translateX(0);\n\t\t}\n\t\t10% {\n\t\t\ttransform: translateX(-2px);\n\t\t}\n\t\t30% {\n\t\t\ttransform: translateX(4px);\n\t\t}\n\t\t50% {\n\t\t\ttransform: translateX(-6px);\n\t\t}\n\t\t70% {\n\t\t\ttransform: translateX(+4px);\n\t\t}\n\t\t90% {\n\t\t\ttransform: translateX(-2px);\n\t\t}\n\t\t100% {\n\t\t\ttransform: translateX(0);\n\t\t}\n\t}\n</style>\n"
40
+ "contents": "<script lang=\"ts\">\n\timport { enhance } from '$app/forms';\n\timport { resolve } from '$app/paths';\n\timport { confetti } from '@neoconfetti/svelte';\n\timport { MediaQuery } from 'svelte/reactivity';\n\n\timport type { ActionData, PageData } from './$types';\n\n\tinterface Props {\n\t\tdata: PageData;\n\t\tform: ActionData;\n\t}\n\tlet { data, form = $bindable() }: Props = $props();\n\n\t/** Whether the user prefers reduced motion */\n\tconst reducedMotion = new MediaQuery('(prefers-reduced-motion: reduce)');\n\n\t/** Whether or not the user has won */\n\tlet won = $derived(data.answers.at(-1) === 'xxxxx');\n\n\t/** The index of the current guess */\n\tlet i = $derived(won ? -1 : data.answers.length);\n\n\t/** The current guess */\n\tlet currentGuess = $derived(data.guesses[i] || '');\n\n\t/** Whether the current guess can be submitted */\n\tlet submittable = $derived(currentGuess.length === 5);\n\n\tconst { classnames, description } = $derived.by(() => {\n\t\t/**\n\t\t * A map of classnames for all letters that have been guessed,\n\t\t * used for styling the keyboard\n\t\t */\n\t\tlet classnames: Record<string, 'exact' | 'close' | 'missing'> = {};\n\t\t/**\n\t\t * A map of descriptions for all letters that have been guessed,\n\t\t * used for adding text for assistive technology (e.g. screen readers)\n\t\t */\n\t\tlet description: Record<string, string> = {};\n\t\tdata.answers.forEach((answer, i) => {\n\t\t\tconst guess = data.guesses[i];\n\t\t\tfor (let i = 0; i < 5; i += 1) {\n\t\t\t\tconst letter = guess[i];\n\t\t\t\tif (answer[i] === 'x') {\n\t\t\t\t\tclassnames[letter] = 'exact';\n\t\t\t\t\tdescription[letter] = 'correct';\n\t\t\t\t} else if (!classnames[letter]) {\n\t\t\t\t\tclassnames[letter] = answer[i] === 'c' ? 'close' : 'missing';\n\t\t\t\t\tdescription[letter] = answer[i] === 'c' ? 'present' : 'absent';\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\t\treturn { classnames, description };\n\t});\n\n\t/**\n\t * Modify the game state without making a trip to the server,\n\t * if client-side JavaScript is enabled\n\t */\n\tfunction update(event: MouseEvent) {\n\t\tevent.preventDefault();\n\t\tconst key = (event.target as HTMLButtonElement).getAttribute(\n\t\t\t'data-key'\n\t\t);\n\n\t\tif (key === 'backspace') {\n\t\t\tcurrentGuess = currentGuess.slice(0, -1);\n\t\t\tif (form?.badGuess) form.badGuess = false;\n\t\t} else if (currentGuess.length < 5) {\n\t\t\tcurrentGuess += key;\n\t\t}\n\t}\n\n\t/**\n\t * Trigger form logic in response to a keydown event, so that\n\t * desktop users can use the keyboard to play the game\n\t */\n\tfunction keydown(event: KeyboardEvent) {\n\t\tif (event.metaKey) return;\n\n\t\tif (event.key === 'Enter' && !submittable) return;\n\n\t\tdocument\n\t\t\t.querySelector(`[data-key=\"${event.key}\" i]`)\n\t\t\t?.dispatchEvent(new MouseEvent('click', { cancelable: true, bubbles: true }));\n\t}\n</script>\n\n<svelte:window onkeydown={keydown} />\n\n<svelte:head>\n\t<title>Sverdle</title>\n\t<meta name=\"description\" content=\"A Wordle clone written in SvelteKit\" />\n</svelte:head>\n\n<h1 class=\"visually-hidden\">Sverdle</h1>\n\n<form\n\tmethod=\"post\"\n\taction=\"?/enter\"\n\tuse:enhance={() => {\n\t\t// prevent default callback from resetting the form\n\t\treturn ({ update }) => {\n\t\t\tupdate({ reset: false });\n\t\t};\n\t}}\n>\n\t<a class=\"how-to-play\" href={resolve('/sverdle/how-to-play')}>How to play</a>\n\n\t<div class=\"grid\" class:playing={!won} class:bad-guess={form?.badGuess}>\n\t\t{#each Array.from(Array(6).keys()) as row (row)}\n\t\t\t{@const current = row === i}\n\t\t\t<h2 class=\"visually-hidden\">Row {row + 1}</h2>\n\t\t\t<div class=\"row\" class:current>\n\t\t\t\t{#each Array.from(Array(5).keys()) as column (column)}\n\t\t\t\t\t{@const guess = current ? currentGuess : data.guesses[row]}\n\t\t\t\t\t{@const answer = data.answers[row]?.[column]}\n\t\t\t\t\t{@const value = guess?.[column] ?? ''}\n\t\t\t\t\t{@const selected = current && column === guess.length}\n\t\t\t\t\t{@const exact = answer === 'x'}\n\t\t\t\t\t{@const close = answer === 'c'}\n\t\t\t\t\t{@const missing = answer === '_'}\n\t\t\t\t\t<div class=\"letter\" class:exact class:close class:missing class:selected>\n\t\t\t\t\t\t{value}\n\t\t\t\t\t\t<span class=\"visually-hidden\">\n\t\t\t\t\t\t\t{#if exact}\n\t\t\t\t\t\t\t\t(correct)\n\t\t\t\t\t\t\t{:else if close}\n\t\t\t\t\t\t\t\t(present)\n\t\t\t\t\t\t\t{:else if missing}\n\t\t\t\t\t\t\t\t(absent)\n\t\t\t\t\t\t\t{:else}\n\t\t\t\t\t\t\t\tempty\n\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t</span>\n\t\t\t\t\t\t<input name=\"guess\" disabled={!current} type=\"hidden\" {value} />\n\t\t\t\t\t</div>\n\t\t\t\t{/each}\n\t\t\t</div>\n\t\t{/each}\n\t</div>\n\n\t<div class=\"controls\">\n\t\t{#if won || data.answers.length >= 6}\n\t\t\t{#if !won && data.answer}\n\t\t\t\t<p>the answer was \"{data.answer}\"</p>\n\t\t\t{/if}\n\t\t\t<button data-key=\"enter\" class=\"restart selected\" formaction=\"?/restart\">\n\t\t\t\t{won ? 'you won :)' : `game over :(`} play again?\n\t\t\t</button>\n\t\t{:else}\n\t\t\t<div class=\"keyboard\">\n\t\t\t\t<button data-key=\"enter\" class:selected={submittable} disabled={!submittable}>enter</button>\n\n\t\t\t\t<button\n\t\t\t\t\tonclick={update}\n\t\t\t\t\tdata-key=\"backspace\"\n\t\t\t\t\tformaction=\"?/update\"\n\t\t\t\t\tname=\"key\"\n\t\t\t\t\tvalue=\"backspace\"\n\t\t\t\t>\n\t\t\t\t\tback\n\t\t\t\t</button>\n\n\t\t\t\t{#each ['qwertyuiop', 'asdfghjkl', 'zxcvbnm'] as row (row)}\n\t\t\t\t\t<div class=\"row\">\n\t\t\t\t\t\t{#each row as letter, index (index)}\n\t\t\t\t\t\t\t<button\n\t\t\t\t\t\t\t\tonclick={update}\n\t\t\t\t\t\t\t\tdata-key={letter}\n\t\t\t\t\t\t\t\tclass={classnames[letter]}\n\t\t\t\t\t\t\t\tdisabled={submittable}\n\t\t\t\t\t\t\t\tformaction=\"?/update\"\n\t\t\t\t\t\t\t\tname=\"key\"\n\t\t\t\t\t\t\t\tvalue={letter}\n\t\t\t\t\t\t\t\taria-label=\"{letter} {description[letter] || ''}\"\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t{letter}\n\t\t\t\t\t\t\t</button>\n\t\t\t\t\t\t{/each}\n\t\t\t\t\t</div>\n\t\t\t\t{/each}\n\t\t\t</div>\n\t\t{/if}\n\t</div>\n</form>\n\n{#if won}\n\t<div\n\t\tstyle=\"position: absolute; left: 50%; top: 30%\"\n\t\tuse:confetti={{\n\t\t\tparticleCount: reducedMotion.current ? 0 : undefined,\n\t\t\tforce: 0.7,\n\t\t\tstageWidth: window.innerWidth,\n\t\t\tstageHeight: window.innerHeight,\n\t\t\tcolors: ['#ff3e00', '#40b3ff', '#676778']\n\t\t}}\n\t></div>\n{/if}\n\n<style>\n\tform {\n\t\twidth: 100%;\n\t\theight: 100%;\n\t\tdisplay: flex;\n\t\tflex-direction: column;\n\t\talign-items: center;\n\t\tjustify-content: center;\n\t\tgap: 1rem;\n\t\tflex: 1;\n\t}\n\n\t.how-to-play {\n\t\tcolor: var(--color-text);\n\t}\n\n\t.how-to-play::before {\n\t\tcontent: 'i';\n\t\tdisplay: inline-block;\n\t\tfont-size: 0.8em;\n\t\tfont-weight: 900;\n\t\twidth: 1em;\n\t\theight: 1em;\n\t\tpadding: 0.2em;\n\t\tline-height: 1;\n\t\tborder: 1.5px solid var(--color-text);\n\t\tborder-radius: 50%;\n\t\ttext-align: center;\n\t\tmargin: 0 0.5em 0 0;\n\t\tposition: relative;\n\t\ttop: -0.05em;\n\t}\n\n\t.grid {\n\t\t--width: min(100vw, 40vh, 380px);\n\t\tmax-width: var(--width);\n\t\talign-self: center;\n\t\tjustify-self: center;\n\t\twidth: 100%;\n\t\theight: 100%;\n\t\tdisplay: flex;\n\t\tflex-direction: column;\n\t\tjustify-content: flex-start;\n\t}\n\n\t.grid .row {\n\t\tdisplay: grid;\n\t\tgrid-template-columns: repeat(5, 1fr);\n\t\tgrid-gap: 0.2rem;\n\t\tmargin: 0 0 0.2rem 0;\n\t}\n\n\t@media (prefers-reduced-motion: no-preference) {\n\t\t.grid.bad-guess .row.current {\n\t\t\tanimation: wiggle 0.5s;\n\t\t}\n\t}\n\n\t.grid.playing .row.current {\n\t\tfilter: drop-shadow(3px 3px 10px var(--color-bg-0));\n\t}\n\n\t.letter {\n\t\taspect-ratio: 1;\n\t\twidth: 100%;\n\t\tdisplay: flex;\n\t\talign-items: center;\n\t\tjustify-content: center;\n\t\ttext-align: center;\n\t\tbox-sizing: border-box;\n\t\ttext-transform: lowercase;\n\t\tborder: none;\n\t\tfont-size: calc(0.08 * var(--width));\n\t\tborder-radius: 2px;\n\t\tbackground: white;\n\t\tmargin: 0;\n\t\tcolor: rgba(0, 0, 0, 0.7);\n\t}\n\n\t.letter.missing {\n\t\tbackground: rgba(255, 255, 255, 0.5);\n\t\tcolor: rgba(0, 0, 0, 0.5);\n\t}\n\n\t.letter.exact {\n\t\tbackground: var(--color-theme-2);\n\t\tcolor: white;\n\t}\n\n\t.letter.close {\n\t\tborder: 2px solid var(--color-theme-2);\n\t}\n\n\t.selected {\n\t\toutline: 2px solid var(--color-theme-1);\n\t}\n\n\t.controls {\n\t\ttext-align: center;\n\t\tjustify-content: center;\n\t\theight: min(18vh, 10rem);\n\t}\n\n\t.keyboard {\n\t\t--gap: 0.2rem;\n\t\tposition: relative;\n\t\tdisplay: flex;\n\t\tflex-direction: column;\n\t\tgap: var(--gap);\n\t\theight: 100%;\n\t}\n\n\t.keyboard .row {\n\t\tdisplay: flex;\n\t\tjustify-content: center;\n\t\tgap: 0.2rem;\n\t\tflex: 1;\n\t}\n\n\t.keyboard button,\n\t.keyboard button:disabled {\n\t\t--size: min(8vw, 4vh, 40px);\n\t\tbackground-color: white;\n\t\tcolor: black;\n\t\twidth: var(--size);\n\t\tborder: none;\n\t\tborder-radius: 2px;\n\t\tfont-size: calc(var(--size) * 0.5);\n\t\tmargin: 0;\n\t}\n\n\t.keyboard button.exact {\n\t\tbackground: var(--color-theme-2);\n\t\tcolor: white;\n\t}\n\n\t.keyboard button.missing {\n\t\topacity: 0.5;\n\t}\n\n\t.keyboard button.close {\n\t\tborder: 2px solid var(--color-theme-2);\n\t}\n\n\t.keyboard button:focus {\n\t\tbackground: var(--color-theme-1);\n\t\tcolor: white;\n\t\toutline: none;\n\t}\n\n\t.keyboard button[data-key='enter'],\n\t.keyboard button[data-key='backspace'] {\n\t\tposition: absolute;\n\t\tbottom: 0;\n\t\twidth: calc(1.5 * var(--size));\n\t\theight: calc(1 / 3 * (100% - 2 * var(--gap)));\n\t\ttext-transform: uppercase;\n\t\tfont-size: calc(0.3 * var(--size));\n\t\tpadding-top: calc(0.15 * var(--size));\n\t}\n\n\t.keyboard button[data-key='enter'] {\n\t\tright: calc(50% + 3.5 * var(--size) + 0.8rem);\n\t}\n\n\t.keyboard button[data-key='backspace'] {\n\t\tleft: calc(50% + 3.5 * var(--size) + 0.8rem);\n\t}\n\n\t.keyboard button[data-key='enter']:disabled {\n\t\topacity: 0.5;\n\t}\n\n\t.restart {\n\t\twidth: 100%;\n\t\tpadding: 1rem;\n\t\tbackground: rgba(255, 255, 255, 0.5);\n\t\tborder-radius: 2px;\n\t\tborder: none;\n\t}\n\n\t.restart:focus,\n\t.restart:hover {\n\t\tbackground: var(--color-theme-1);\n\t\tcolor: white;\n\t\toutline: none;\n\t}\n\n\t@keyframes wiggle {\n\t\t0% {\n\t\t\ttransform: translateX(0);\n\t\t}\n\t\t10% {\n\t\t\ttransform: translateX(-2px);\n\t\t}\n\t\t30% {\n\t\t\ttransform: translateX(4px);\n\t\t}\n\t\t50% {\n\t\t\ttransform: translateX(-6px);\n\t\t}\n\t\t70% {\n\t\t\ttransform: translateX(+4px);\n\t\t}\n\t\t90% {\n\t\t\ttransform: translateX(-2px);\n\t\t}\n\t\t100% {\n\t\t\ttransform: translateX(0);\n\t\t}\n\t}\n</style>\n"
41
41
  },
42
42
  {
43
43
  "name": "src/routes/sverdle/game.ts",
44
- "contents": "import { words, allowed } from './words.server.ts';\n\nexport class Game {\n\tindex: number;\n\tguesses: string[];\n\tanswers: string[];\n\tanswer: string;\n\n\t/**\n\t * Create a game object from the player's cookie, or initialise a new game\n\t */\n\tconstructor(serialized: string | undefined = undefined) {\n\t\tif (serialized) {\n\t\t\tconst [index, guesses, answers] = serialized.split('-');\n\n\t\t\tthis.index = +index;\n\t\t\tthis.guesses = guesses ? guesses.split(' ') : [];\n\t\t\tthis.answers = answers ? answers.split(' ') : [];\n\t\t} else {\n\t\t\tthis.index = Math.floor(Math.random() * words.length);\n\t\t\tthis.guesses = ['', '', '', '', '', ''];\n\t\t\tthis.answers = [];\n\t\t}\n\n\t\tthis.answer = words[this.index];\n\t}\n\n\t/**\n\t * Update game state based on a guess of a five-letter word. Returns\n\t * true if the guess was valid, false otherwise\n\t */\n\tenter(letters: string[]) {\n\t\tconst word = letters.join('');\n\t\tconst valid = allowed.has(word);\n\n\t\tif (!valid) return false;\n\n\t\tthis.guesses[this.answers.length] = word;\n\n\t\tconst available = Array.from(this.answer);\n\t\tconst answer = Array(5).fill('_');\n\n\t\t// first, find exact matches\n\t\tfor (let i = 0; i < 5; i += 1) {\n\t\t\tif (letters[i] === available[i]) {\n\t\t\t\tanswer[i] = 'x';\n\t\t\t\tavailable[i] = ' ';\n\t\t\t}\n\t\t}\n\n\t\t// then find close matches (this has to happen\n\t\t// in a second step, otherwise an early close\n\t\t// match can prevent a later exact match)\n\t\tfor (let i = 0; i < 5; i += 1) {\n\t\t\tif (answer[i] === '_') {\n\t\t\t\tconst index = available.indexOf(letters[i]);\n\t\t\t\tif (index !== -1) {\n\t\t\t\t\tanswer[i] = 'c';\n\t\t\t\t\tavailable[index] = ' ';\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tthis.answers.push(answer.join(''));\n\n\t\treturn true;\n\t}\n\n\t/**\n\t * Serialize game state so it can be set as a cookie\n\t */\n\ttoString() {\n\t\treturn `${this.index}-${this.guesses.join(' ')}-${this.answers.join(' ')}`;\n\t}\n}\n"
44
+ "contents": "import { allowed, words } from './words.server.ts';\n\nexport class Game {\n\tindex: number;\n\tguesses: string[];\n\tanswers: string[];\n\tanswer: string;\n\n\t/**\n\t * Create a game object from the player's cookie, or initialise a new game\n\t */\n\tconstructor(serialized: string | undefined = undefined) {\n\t\tif (serialized) {\n\t\t\tconst [index, guesses, answers] = serialized.split('-');\n\n\t\t\tthis.index = +index;\n\t\t\tthis.guesses = guesses ? guesses.split(' ') : [];\n\t\t\tthis.answers = answers ? answers.split(' ') : [];\n\t\t} else {\n\t\t\tthis.index = Math.floor(Math.random() * words.length);\n\t\t\tthis.guesses = ['', '', '', '', '', ''];\n\t\t\tthis.answers = [];\n\t\t}\n\n\t\tthis.answer = words[this.index];\n\t}\n\n\t/**\n\t * Update game state based on a guess of a five-letter word. Returns\n\t * true if the guess was valid, false otherwise\n\t */\n\tenter(letters: string[]) {\n\t\tconst word = letters.join('');\n\t\tconst valid = allowed.has(word);\n\n\t\tif (!valid) return false;\n\n\t\tthis.guesses[this.answers.length] = word;\n\n\t\tconst available = Array.from(this.answer);\n\t\tconst answer = Array(5).fill('_');\n\n\t\t// first, find exact matches\n\t\tfor (let i = 0; i < 5; i += 1) {\n\t\t\tif (letters[i] === available[i]) {\n\t\t\t\tanswer[i] = 'x';\n\t\t\t\tavailable[i] = ' ';\n\t\t\t}\n\t\t}\n\n\t\t// then find close matches (this has to happen\n\t\t// in a second step, otherwise an early close\n\t\t// match can prevent a later exact match)\n\t\tfor (let i = 0; i < 5; i += 1) {\n\t\t\tif (answer[i] === '_') {\n\t\t\t\tconst index = available.indexOf(letters[i]);\n\t\t\t\tif (index !== -1) {\n\t\t\t\t\tanswer[i] = 'c';\n\t\t\t\t\tavailable[index] = ' ';\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tthis.answers.push(answer.join(''));\n\n\t\treturn true;\n\t}\n\n\t/**\n\t * Serialize game state so it can be set as a cookie\n\t */\n\ttoString() {\n\t\treturn `${this.index}-${this.guesses.join(' ')}-${this.answers.join(' ')}`;\n\t}\n}\n"
45
45
  },
46
46
  {
47
47
  "name": "src/routes/sverdle/how-to-play/+page.svelte",
@@ -1,5 +1,5 @@
1
1
  {
2
- "name": "~TODO~",
2
+ "name": "~SV-NAME-TODO~",
3
3
  "private": true,
4
4
  "version": "0.0.1",
5
5
  "type": "module",
@@ -1,5 +1,5 @@
1
1
  {
2
- "name": "~TODO~",
2
+ "name": "~SV-NAME-TODO~",
3
3
  "version": "0.0.1",
4
4
  "scripts": {
5
5
  "dev": "vite dev",
@@ -1,5 +1,5 @@
1
1
  {
2
- "name": "~TODO~",
2
+ "name": "~SV-NAME-TODO~",
3
3
  "private": true,
4
4
  "version": "0.0.1",
5
5
  "type": "module",