Spaces:
Runtime error
Runtime error
Ron Au
commited on
Commit
·
70cf894
1
Parent(s):
064c252
Initial D
Browse files- .dockerignore +11 -0
- .eslintignore +13 -0
- .eslintrc.cjs +15 -0
- .gitignore +12 -0
- .npmrc +1 -0
- .prettierignore +13 -0
- .prettierrc +9 -0
- Dockerfile +20 -0
- README.md +2 -1
- jsconfig.json +17 -0
- package-lock.json +0 -0
- package.json +31 -0
- src/app.d.ts +12 -0
- src/app.html +12 -0
- src/lib/images/github.svg +16 -0
- src/lib/images/svelte-logo.svg +1 -0
- src/lib/images/svelte-welcome.png +0 -0
- src/lib/images/svelte-welcome.webp +0 -0
- src/routes/+layout.svelte +53 -0
- src/routes/+page.js +3 -0
- src/routes/+page.svelte +59 -0
- src/routes/Counter.svelte +106 -0
- src/routes/Header.svelte +129 -0
- src/routes/about/+page.js +9 -0
- src/routes/about/+page.svelte +26 -0
- src/routes/styles.css +107 -0
- src/routes/sverdle/+page.server.js +70 -0
- src/routes/sverdle/+page.svelte +410 -0
- src/routes/sverdle/game.js +72 -0
- src/routes/sverdle/how-to-play/+page.js +9 -0
- src/routes/sverdle/how-to-play/+page.svelte +95 -0
- src/routes/sverdle/reduced-motion.js +26 -0
- src/routes/sverdle/words.server.js +0 -0
- static/favicon.png +0 -0
- static/robots.txt +3 -0
- svelte.config.js +10 -0
- vite.config.js +6 -0
.dockerignore
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.git
|
| 2 |
+
.git*
|
| 3 |
+
.eslint*
|
| 4 |
+
.prettier*
|
| 5 |
+
.svelte-kit
|
| 6 |
+
Dockerfile*
|
| 7 |
+
docker-compose.yml
|
| 8 |
+
node_modules
|
| 9 |
+
build
|
| 10 |
+
public
|
| 11 |
+
README.md
|
.eslintignore
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.DS_Store
|
| 2 |
+
node_modules
|
| 3 |
+
/build
|
| 4 |
+
/.svelte-kit
|
| 5 |
+
/package
|
| 6 |
+
.env
|
| 7 |
+
.env.*
|
| 8 |
+
!.env.example
|
| 9 |
+
|
| 10 |
+
# Ignore files for PNPM, NPM and YARN
|
| 11 |
+
pnpm-lock.yaml
|
| 12 |
+
package-lock.json
|
| 13 |
+
yarn.lock
|
.eslintrc.cjs
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
module.exports = {
|
| 2 |
+
root: true,
|
| 3 |
+
extends: ['eslint:recommended', 'prettier'],
|
| 4 |
+
plugins: ['svelte3'],
|
| 5 |
+
overrides: [{ files: ['*.svelte'], processor: 'svelte3/svelte3' }],
|
| 6 |
+
parserOptions: {
|
| 7 |
+
sourceType: 'module',
|
| 8 |
+
ecmaVersion: 2020
|
| 9 |
+
},
|
| 10 |
+
env: {
|
| 11 |
+
browser: true,
|
| 12 |
+
es2017: true,
|
| 13 |
+
node: true
|
| 14 |
+
}
|
| 15 |
+
};
|
.gitignore
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.DS_Store
|
| 2 |
+
node_modules
|
| 3 |
+
/build
|
| 4 |
+
/.svelte-kit
|
| 5 |
+
/package
|
| 6 |
+
.env
|
| 7 |
+
.env.*
|
| 8 |
+
!.env.example
|
| 9 |
+
.vercel
|
| 10 |
+
.output
|
| 11 |
+
vite.config.js.timestamp-*
|
| 12 |
+
vite.config.ts.timestamp-*
|
.npmrc
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
engine-strict=true
|
.prettierignore
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.DS_Store
|
| 2 |
+
node_modules
|
| 3 |
+
/build
|
| 4 |
+
/.svelte-kit
|
| 5 |
+
/package
|
| 6 |
+
.env
|
| 7 |
+
.env.*
|
| 8 |
+
!.env.example
|
| 9 |
+
|
| 10 |
+
# Ignore files for PNPM, NPM and YARN
|
| 11 |
+
pnpm-lock.yaml
|
| 12 |
+
package-lock.json
|
| 13 |
+
yarn.lock
|
.prettierrc
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"useTabs": true,
|
| 3 |
+
"singleQuote": true,
|
| 4 |
+
"trailingComma": "none",
|
| 5 |
+
"printWidth": 100,
|
| 6 |
+
"plugins": ["prettier-plugin-svelte"],
|
| 7 |
+
"pluginSearchDirs": ["."],
|
| 8 |
+
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
|
| 9 |
+
}
|
Dockerfile
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from node:18-alpine
|
| 2 |
+
|
| 3 |
+
WORKDIR /app
|
| 4 |
+
COPY . .
|
| 5 |
+
COPY package.json package-lock.json
|
| 6 |
+
RUN npm install
|
| 7 |
+
COPY . .
|
| 8 |
+
RUN npm run build
|
| 9 |
+
|
| 10 |
+
EXPOSE 3000
|
| 11 |
+
CMD ["node", "build"]
|
| 12 |
+
|
| 13 |
+
# RUN npm install
|
| 14 |
+
# RUN npm run build
|
| 15 |
+
|
| 16 |
+
# WORKDIR /app
|
| 17 |
+
# RUN rm -rf ./*
|
| 18 |
+
# COPY --from=build /app/package.json .
|
| 19 |
+
# COPY --from=build /app/build .
|
| 20 |
+
# RUN npm run
|
README.md
CHANGED
|
@@ -1,11 +1,12 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
emoji: 🐨
|
| 4 |
colorFrom: purple
|
| 5 |
colorTo: pink
|
| 6 |
sdk: docker
|
| 7 |
pinned: false
|
| 8 |
license: openrail
|
|
|
|
| 9 |
---
|
| 10 |
|
| 11 |
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
|
|
|
| 1 |
---
|
| 2 |
+
title: SvelteKit Docker
|
| 3 |
emoji: 🐨
|
| 4 |
colorFrom: purple
|
| 5 |
colorTo: pink
|
| 6 |
sdk: docker
|
| 7 |
pinned: false
|
| 8 |
license: openrail
|
| 9 |
+
app_port: 3000
|
| 10 |
---
|
| 11 |
|
| 12 |
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
jsconfig.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"extends": "./.svelte-kit/tsconfig.json",
|
| 3 |
+
"compilerOptions": {
|
| 4 |
+
"allowJs": true,
|
| 5 |
+
"checkJs": true,
|
| 6 |
+
"esModuleInterop": true,
|
| 7 |
+
"forceConsistentCasingInFileNames": true,
|
| 8 |
+
"resolveJsonModule": true,
|
| 9 |
+
"skipLibCheck": true,
|
| 10 |
+
"sourceMap": true,
|
| 11 |
+
"strict": true
|
| 12 |
+
}
|
| 13 |
+
// Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias and https://kit.svelte.dev/docs/configuration#files
|
| 14 |
+
//
|
| 15 |
+
// If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
|
| 16 |
+
// from the referenced tsconfig.json - TypeScript does not merge them in
|
| 17 |
+
}
|
package-lock.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "sk-docker",
|
| 3 |
+
"version": "0.0.1",
|
| 4 |
+
"scripts": {
|
| 5 |
+
"dev": "vite dev",
|
| 6 |
+
"build": "vite build",
|
| 7 |
+
"preview": "vite preview",
|
| 8 |
+
"check": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json",
|
| 9 |
+
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json --watch",
|
| 10 |
+
"lint": "prettier --plugin-search-dir . --check . && eslint .",
|
| 11 |
+
"format": "prettier --plugin-search-dir . --write ."
|
| 12 |
+
},
|
| 13 |
+
"devDependencies": {
|
| 14 |
+
"@fontsource/fira-mono": "^4.5.10",
|
| 15 |
+
"@neoconfetti/svelte": "^1.0.0",
|
| 16 |
+
"@sveltejs/adapter-auto": "^2.0.0",
|
| 17 |
+
"@sveltejs/adapter-node": "^1.2.0",
|
| 18 |
+
"@sveltejs/kit": "^1.5.0",
|
| 19 |
+
"@types/cookie": "^0.5.1",
|
| 20 |
+
"eslint": "^8.28.0",
|
| 21 |
+
"eslint-config-prettier": "^8.5.0",
|
| 22 |
+
"eslint-plugin-svelte3": "^4.0.0",
|
| 23 |
+
"prettier": "^2.8.0",
|
| 24 |
+
"prettier-plugin-svelte": "^2.8.1",
|
| 25 |
+
"svelte": "^3.54.0",
|
| 26 |
+
"svelte-check": "^3.0.1",
|
| 27 |
+
"typescript": "^4.9.3",
|
| 28 |
+
"vite": "^4.0.0"
|
| 29 |
+
},
|
| 30 |
+
"type": "module"
|
| 31 |
+
}
|
src/app.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// See https://kit.svelte.dev/docs/types#app
|
| 2 |
+
// for information about these interfaces
|
| 3 |
+
declare global {
|
| 4 |
+
namespace App {
|
| 5 |
+
// interface Error {}
|
| 6 |
+
// interface Locals {}
|
| 7 |
+
// interface PageData {}
|
| 8 |
+
// interface Platform {}
|
| 9 |
+
}
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
export {};
|
src/app.html
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="utf-8" />
|
| 5 |
+
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
| 6 |
+
<meta name="viewport" content="width=device-width" />
|
| 7 |
+
%sveltekit.head%
|
| 8 |
+
</head>
|
| 9 |
+
<body data-sveltekit-preload-data="hover">
|
| 10 |
+
<div style="display: contents">%sveltekit.body%</div>
|
| 11 |
+
</body>
|
| 12 |
+
</html>
|
src/lib/images/github.svg
ADDED
|
|
src/lib/images/svelte-logo.svg
ADDED
|
|
src/lib/images/svelte-welcome.png
ADDED
|
src/lib/images/svelte-welcome.webp
ADDED
|
src/routes/+layout.svelte
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<script>
|
| 2 |
+
import Header from './Header.svelte';
|
| 3 |
+
import './styles.css';
|
| 4 |
+
</script>
|
| 5 |
+
|
| 6 |
+
<div class="app">
|
| 7 |
+
<Header />
|
| 8 |
+
|
| 9 |
+
<main>
|
| 10 |
+
<slot />
|
| 11 |
+
</main>
|
| 12 |
+
|
| 13 |
+
<footer>
|
| 14 |
+
<p>visit <a href="https://kit.svelte.dev">kit.svelte.dev</a> to learn SvelteKit</p>
|
| 15 |
+
</footer>
|
| 16 |
+
</div>
|
| 17 |
+
|
| 18 |
+
<style>
|
| 19 |
+
.app {
|
| 20 |
+
display: flex;
|
| 21 |
+
flex-direction: column;
|
| 22 |
+
min-height: 100vh;
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
main {
|
| 26 |
+
flex: 1;
|
| 27 |
+
display: flex;
|
| 28 |
+
flex-direction: column;
|
| 29 |
+
padding: 1rem;
|
| 30 |
+
width: 100%;
|
| 31 |
+
max-width: 64rem;
|
| 32 |
+
margin: 0 auto;
|
| 33 |
+
box-sizing: border-box;
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
footer {
|
| 37 |
+
display: flex;
|
| 38 |
+
flex-direction: column;
|
| 39 |
+
justify-content: center;
|
| 40 |
+
align-items: center;
|
| 41 |
+
padding: 12px;
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
footer a {
|
| 45 |
+
font-weight: bold;
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
@media (min-width: 480px) {
|
| 49 |
+
footer {
|
| 50 |
+
padding: 12px 0;
|
| 51 |
+
}
|
| 52 |
+
}
|
| 53 |
+
</style>
|
src/routes/+page.js
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// since there's no dynamic data here, we can prerender
|
| 2 |
+
// it so that it gets served as a static asset in production
|
| 3 |
+
export const prerender = true;
|
src/routes/+page.svelte
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<script>
|
| 2 |
+
import Counter from './Counter.svelte';
|
| 3 |
+
import welcome from '$lib/images/svelte-welcome.webp';
|
| 4 |
+
import welcome_fallback from '$lib/images/svelte-welcome.png';
|
| 5 |
+
</script>
|
| 6 |
+
|
| 7 |
+
<svelte:head>
|
| 8 |
+
<title>Home</title>
|
| 9 |
+
<meta name="description" content="Svelte demo app" />
|
| 10 |
+
</svelte:head>
|
| 11 |
+
|
| 12 |
+
<section>
|
| 13 |
+
<h1>
|
| 14 |
+
<span class="welcome">
|
| 15 |
+
<picture>
|
| 16 |
+
<source srcset={welcome} type="image/webp" />
|
| 17 |
+
<img src={welcome_fallback} alt="Welcome" />
|
| 18 |
+
</picture>
|
| 19 |
+
</span>
|
| 20 |
+
|
| 21 |
+
to your new<br />SvelteKit app
|
| 22 |
+
</h1>
|
| 23 |
+
|
| 24 |
+
<h2>
|
| 25 |
+
try editing <strong>src/routes/+page.svelte</strong>
|
| 26 |
+
</h2>
|
| 27 |
+
|
| 28 |
+
<Counter />
|
| 29 |
+
</section>
|
| 30 |
+
|
| 31 |
+
<style>
|
| 32 |
+
section {
|
| 33 |
+
display: flex;
|
| 34 |
+
flex-direction: column;
|
| 35 |
+
justify-content: center;
|
| 36 |
+
align-items: center;
|
| 37 |
+
flex: 0.6;
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
h1 {
|
| 41 |
+
width: 100%;
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
.welcome {
|
| 45 |
+
display: block;
|
| 46 |
+
position: relative;
|
| 47 |
+
width: 100%;
|
| 48 |
+
height: 0;
|
| 49 |
+
padding: 0 0 calc(100% * 495 / 2048) 0;
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
.welcome img {
|
| 53 |
+
position: absolute;
|
| 54 |
+
width: 100%;
|
| 55 |
+
height: 100%;
|
| 56 |
+
top: 0;
|
| 57 |
+
display: block;
|
| 58 |
+
}
|
| 59 |
+
</style>
|
src/routes/Counter.svelte
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<script>
|
| 2 |
+
import { spring } from 'svelte/motion';
|
| 3 |
+
|
| 4 |
+
let count = 0;
|
| 5 |
+
|
| 6 |
+
const displayed_count = spring();
|
| 7 |
+
$: displayed_count.set(count);
|
| 8 |
+
$: offset = modulo($displayed_count, 1);
|
| 9 |
+
|
| 10 |
+
/**
|
| 11 |
+
* @param {number} n
|
| 12 |
+
* @param {number} m
|
| 13 |
+
*/
|
| 14 |
+
function modulo(n, m) {
|
| 15 |
+
// handle negative numbers
|
| 16 |
+
return ((n % m) + m) % m;
|
| 17 |
+
}
|
| 18 |
+
</script>
|
| 19 |
+
|
| 20 |
+
<div class="counter">
|
| 21 |
+
<button on:click={() => (count -= 1)} aria-label="Decrease the counter by one">
|
| 22 |
+
<svg aria-hidden="true" viewBox="0 0 1 1">
|
| 23 |
+
<path d="M0,0.5 L1,0.5" />
|
| 24 |
+
</svg>
|
| 25 |
+
</button>
|
| 26 |
+
|
| 27 |
+
<div class="counter-viewport">
|
| 28 |
+
<div class="counter-digits" style="transform: translate(0, {100 * offset}%)">
|
| 29 |
+
<strong class="hidden" aria-hidden="true">{Math.floor($displayed_count + 1)}</strong>
|
| 30 |
+
<strong>{Math.floor($displayed_count)}</strong>
|
| 31 |
+
</div>
|
| 32 |
+
</div>
|
| 33 |
+
|
| 34 |
+
<button on:click={() => (count += 1)} aria-label="Increase the counter by one">
|
| 35 |
+
<svg aria-hidden="true" viewBox="0 0 1 1">
|
| 36 |
+
<path d="M0,0.5 L1,0.5 M0.5,0 L0.5,1" />
|
| 37 |
+
</svg>
|
| 38 |
+
</button>
|
| 39 |
+
</div>
|
| 40 |
+
|
| 41 |
+
<style>
|
| 42 |
+
.counter {
|
| 43 |
+
display: flex;
|
| 44 |
+
border-top: 1px solid rgba(0, 0, 0, 0.1);
|
| 45 |
+
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
|
| 46 |
+
margin: 1rem 0;
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
.counter button {
|
| 50 |
+
width: 2em;
|
| 51 |
+
padding: 0;
|
| 52 |
+
display: flex;
|
| 53 |
+
align-items: center;
|
| 54 |
+
justify-content: center;
|
| 55 |
+
border: 0;
|
| 56 |
+
background-color: transparent;
|
| 57 |
+
touch-action: manipulation;
|
| 58 |
+
font-size: 2rem;
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
.counter button:hover {
|
| 62 |
+
background-color: var(--color-bg-1);
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
svg {
|
| 66 |
+
width: 25%;
|
| 67 |
+
height: 25%;
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
path {
|
| 71 |
+
vector-effect: non-scaling-stroke;
|
| 72 |
+
stroke-width: 2px;
|
| 73 |
+
stroke: #444;
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
.counter-viewport {
|
| 77 |
+
width: 8em;
|
| 78 |
+
height: 4em;
|
| 79 |
+
overflow: hidden;
|
| 80 |
+
text-align: center;
|
| 81 |
+
position: relative;
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
.counter-viewport strong {
|
| 85 |
+
position: absolute;
|
| 86 |
+
display: flex;
|
| 87 |
+
width: 100%;
|
| 88 |
+
height: 100%;
|
| 89 |
+
font-weight: 400;
|
| 90 |
+
color: var(--color-theme-1);
|
| 91 |
+
font-size: 4rem;
|
| 92 |
+
align-items: center;
|
| 93 |
+
justify-content: center;
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
+
.counter-digits {
|
| 97 |
+
position: absolute;
|
| 98 |
+
width: 100%;
|
| 99 |
+
height: 100%;
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
.hidden {
|
| 103 |
+
top: -100%;
|
| 104 |
+
user-select: none;
|
| 105 |
+
}
|
| 106 |
+
</style>
|
src/routes/Header.svelte
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<script>
|
| 2 |
+
import { page } from '$app/stores';
|
| 3 |
+
import logo from '$lib/images/svelte-logo.svg';
|
| 4 |
+
import github from '$lib/images/github.svg';
|
| 5 |
+
</script>
|
| 6 |
+
|
| 7 |
+
<header>
|
| 8 |
+
<div class="corner">
|
| 9 |
+
<a href="https://kit.svelte.dev">
|
| 10 |
+
<img src={logo} alt="SvelteKit" />
|
| 11 |
+
</a>
|
| 12 |
+
</div>
|
| 13 |
+
|
| 14 |
+
<nav>
|
| 15 |
+
<svg viewBox="0 0 2 3" aria-hidden="true">
|
| 16 |
+
<path d="M0,0 L1,2 C1.5,3 1.5,3 2,3 L2,0 Z" />
|
| 17 |
+
</svg>
|
| 18 |
+
<ul>
|
| 19 |
+
<li aria-current={$page.url.pathname === '/' ? 'page' : undefined}>
|
| 20 |
+
<a href="/">Home</a>
|
| 21 |
+
</li>
|
| 22 |
+
<li aria-current={$page.url.pathname === '/about' ? 'page' : undefined}>
|
| 23 |
+
<a href="/about">About</a>
|
| 24 |
+
</li>
|
| 25 |
+
<li aria-current={$page.url.pathname.startsWith('/sverdle') ? 'page' : undefined}>
|
| 26 |
+
<a href="/sverdle">Sverdle</a>
|
| 27 |
+
</li>
|
| 28 |
+
</ul>
|
| 29 |
+
<svg viewBox="0 0 2 3" aria-hidden="true">
|
| 30 |
+
<path d="M0,0 L0,3 C0.5,3 0.5,3 1,2 L2,0 Z" />
|
| 31 |
+
</svg>
|
| 32 |
+
</nav>
|
| 33 |
+
|
| 34 |
+
<div class="corner">
|
| 35 |
+
<a href="https://github.com/sveltejs/kit">
|
| 36 |
+
<img src={github} alt="GitHub" />
|
| 37 |
+
</a>
|
| 38 |
+
</div>
|
| 39 |
+
</header>
|
| 40 |
+
|
| 41 |
+
<style>
|
| 42 |
+
header {
|
| 43 |
+
display: flex;
|
| 44 |
+
justify-content: space-between;
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
.corner {
|
| 48 |
+
width: 3em;
|
| 49 |
+
height: 3em;
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
.corner a {
|
| 53 |
+
display: flex;
|
| 54 |
+
align-items: center;
|
| 55 |
+
justify-content: center;
|
| 56 |
+
width: 100%;
|
| 57 |
+
height: 100%;
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
.corner img {
|
| 61 |
+
width: 2em;
|
| 62 |
+
height: 2em;
|
| 63 |
+
object-fit: contain;
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
nav {
|
| 67 |
+
display: flex;
|
| 68 |
+
justify-content: center;
|
| 69 |
+
--background: rgba(255, 255, 255, 0.7);
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
svg {
|
| 73 |
+
width: 2em;
|
| 74 |
+
height: 3em;
|
| 75 |
+
display: block;
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
path {
|
| 79 |
+
fill: var(--background);
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
ul {
|
| 83 |
+
position: relative;
|
| 84 |
+
padding: 0;
|
| 85 |
+
margin: 0;
|
| 86 |
+
height: 3em;
|
| 87 |
+
display: flex;
|
| 88 |
+
justify-content: center;
|
| 89 |
+
align-items: center;
|
| 90 |
+
list-style: none;
|
| 91 |
+
background: var(--background);
|
| 92 |
+
background-size: contain;
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
li {
|
| 96 |
+
position: relative;
|
| 97 |
+
height: 100%;
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
li[aria-current='page']::before {
|
| 101 |
+
--size: 6px;
|
| 102 |
+
content: '';
|
| 103 |
+
width: 0;
|
| 104 |
+
height: 0;
|
| 105 |
+
position: absolute;
|
| 106 |
+
top: 0;
|
| 107 |
+
left: calc(50% - var(--size));
|
| 108 |
+
border: var(--size) solid transparent;
|
| 109 |
+
border-top: var(--size) solid var(--color-theme-1);
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
nav a {
|
| 113 |
+
display: flex;
|
| 114 |
+
height: 100%;
|
| 115 |
+
align-items: center;
|
| 116 |
+
padding: 0 0.5rem;
|
| 117 |
+
color: var(--color-text);
|
| 118 |
+
font-weight: 700;
|
| 119 |
+
font-size: 0.8rem;
|
| 120 |
+
text-transform: uppercase;
|
| 121 |
+
letter-spacing: 0.1em;
|
| 122 |
+
text-decoration: none;
|
| 123 |
+
transition: color 0.2s linear;
|
| 124 |
+
}
|
| 125 |
+
|
| 126 |
+
a:hover {
|
| 127 |
+
color: var(--color-theme-1);
|
| 128 |
+
}
|
| 129 |
+
</style>
|
src/routes/about/+page.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { dev } from '$app/environment';
|
| 2 |
+
|
| 3 |
+
// we don't need any JS on this page, though we'll load
|
| 4 |
+
// it in dev so that we get hot module replacement
|
| 5 |
+
export const csr = dev;
|
| 6 |
+
|
| 7 |
+
// since there's no dynamic data here, we can prerender
|
| 8 |
+
// it so that it gets served as a static asset in production
|
| 9 |
+
export const prerender = true;
|
src/routes/about/+page.svelte
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<svelte:head>
|
| 2 |
+
<title>About</title>
|
| 3 |
+
<meta name="description" content="About this app" />
|
| 4 |
+
</svelte:head>
|
| 5 |
+
|
| 6 |
+
<div class="text-column">
|
| 7 |
+
<h1>About this app</h1>
|
| 8 |
+
|
| 9 |
+
<p>
|
| 10 |
+
This is a <a href="https://kit.svelte.dev">SvelteKit</a> app. You can make your own by typing the
|
| 11 |
+
following into your command line and following the prompts:
|
| 12 |
+
</p>
|
| 13 |
+
|
| 14 |
+
<pre>npm create svelte@latest</pre>
|
| 15 |
+
|
| 16 |
+
<p>
|
| 17 |
+
The page you're looking at is purely static HTML, with no client-side interactivity needed.
|
| 18 |
+
Because of that, we don't need to load any JavaScript. Try viewing the page's source, or opening
|
| 19 |
+
the devtools network panel and reloading.
|
| 20 |
+
</p>
|
| 21 |
+
|
| 22 |
+
<p>
|
| 23 |
+
The <a href="/sverdle">Sverdle</a> page illustrates SvelteKit's data loading and form handling. Try
|
| 24 |
+
using it with JavaScript disabled!
|
| 25 |
+
</p>
|
| 26 |
+
</div>
|
src/routes/styles.css
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
@import '@fontsource/fira-mono';
|
| 2 |
+
|
| 3 |
+
:root {
|
| 4 |
+
--font-body: Arial, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu,
|
| 5 |
+
Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
| 6 |
+
--font-mono: 'Fira Mono', monospace;
|
| 7 |
+
--color-bg-0: rgb(202, 216, 228);
|
| 8 |
+
--color-bg-1: hsl(209, 36%, 86%);
|
| 9 |
+
--color-bg-2: hsl(224, 44%, 95%);
|
| 10 |
+
--color-theme-1: #ff3e00;
|
| 11 |
+
--color-theme-2: #4075a6;
|
| 12 |
+
--color-text: rgba(0, 0, 0, 0.7);
|
| 13 |
+
--column-width: 42rem;
|
| 14 |
+
--column-margin-top: 4rem;
|
| 15 |
+
font-family: var(--font-body);
|
| 16 |
+
color: var(--color-text);
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
body {
|
| 20 |
+
min-height: 100vh;
|
| 21 |
+
margin: 0;
|
| 22 |
+
background-attachment: fixed;
|
| 23 |
+
background-color: var(--color-bg-1);
|
| 24 |
+
background-size: 100vw 100vh;
|
| 25 |
+
background-image: radial-gradient(
|
| 26 |
+
50% 50% at 50% 50%,
|
| 27 |
+
rgba(255, 255, 255, 0.75) 0%,
|
| 28 |
+
rgba(255, 255, 255, 0) 100%
|
| 29 |
+
),
|
| 30 |
+
linear-gradient(180deg, var(--color-bg-0) 0%, var(--color-bg-1) 15%, var(--color-bg-2) 50%);
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
h1,
|
| 34 |
+
h2,
|
| 35 |
+
p {
|
| 36 |
+
font-weight: 400;
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
p {
|
| 40 |
+
line-height: 1.5;
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
a {
|
| 44 |
+
color: var(--color-theme-1);
|
| 45 |
+
text-decoration: none;
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
a:hover {
|
| 49 |
+
text-decoration: underline;
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
h1 {
|
| 53 |
+
font-size: 2rem;
|
| 54 |
+
text-align: center;
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
h2 {
|
| 58 |
+
font-size: 1rem;
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
pre {
|
| 62 |
+
font-size: 16px;
|
| 63 |
+
font-family: var(--font-mono);
|
| 64 |
+
background-color: rgba(255, 255, 255, 0.45);
|
| 65 |
+
border-radius: 3px;
|
| 66 |
+
box-shadow: 2px 2px 6px rgb(255 255 255 / 25%);
|
| 67 |
+
padding: 0.5em;
|
| 68 |
+
overflow-x: auto;
|
| 69 |
+
color: var(--color-text);
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
.text-column {
|
| 73 |
+
display: flex;
|
| 74 |
+
max-width: 48rem;
|
| 75 |
+
flex: 0.6;
|
| 76 |
+
flex-direction: column;
|
| 77 |
+
justify-content: center;
|
| 78 |
+
margin: 0 auto;
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
input,
|
| 82 |
+
button {
|
| 83 |
+
font-size: inherit;
|
| 84 |
+
font-family: inherit;
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
button:focus:not(:focus-visible) {
|
| 88 |
+
outline: none;
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
@media (min-width: 720px) {
|
| 92 |
+
h1 {
|
| 93 |
+
font-size: 2.4rem;
|
| 94 |
+
}
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
.visually-hidden {
|
| 98 |
+
border: 0;
|
| 99 |
+
clip: rect(0 0 0 0);
|
| 100 |
+
height: auto;
|
| 101 |
+
margin: 0;
|
| 102 |
+
overflow: hidden;
|
| 103 |
+
padding: 0;
|
| 104 |
+
position: absolute;
|
| 105 |
+
width: 1px;
|
| 106 |
+
white-space: nowrap;
|
| 107 |
+
}
|
src/routes/sverdle/+page.server.js
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { fail } from '@sveltejs/kit';
|
| 2 |
+
import { Game } from './game';
|
| 3 |
+
|
| 4 |
+
/** @type {import('./$types').PageServerLoad} */
|
| 5 |
+
export const load = ({ cookies }) => {
|
| 6 |
+
const game = new Game(cookies.get('sverdle'));
|
| 7 |
+
|
| 8 |
+
return {
|
| 9 |
+
/**
|
| 10 |
+
* The player's guessed words so far
|
| 11 |
+
*/
|
| 12 |
+
guesses: game.guesses,
|
| 13 |
+
|
| 14 |
+
/**
|
| 15 |
+
* An array of strings like '__x_c' corresponding to the guesses, where 'x' means
|
| 16 |
+
* an exact match, and 'c' means a close match (right letter, wrong place)
|
| 17 |
+
*/
|
| 18 |
+
answers: game.answers,
|
| 19 |
+
|
| 20 |
+
/**
|
| 21 |
+
* The correct answer, revealed if the game is over
|
| 22 |
+
*/
|
| 23 |
+
answer: game.answers.length >= 6 ? game.answer : null
|
| 24 |
+
};
|
| 25 |
+
};
|
| 26 |
+
|
| 27 |
+
/** @type {import('./$types').Actions} */
|
| 28 |
+
export const actions = {
|
| 29 |
+
/**
|
| 30 |
+
* Modify game state in reaction to a keypress. If client-side JavaScript
|
| 31 |
+
* is available, this will happen in the browser instead of here
|
| 32 |
+
*/
|
| 33 |
+
update: async ({ request, cookies }) => {
|
| 34 |
+
const game = new Game(cookies.get('sverdle'));
|
| 35 |
+
|
| 36 |
+
const data = await request.formData();
|
| 37 |
+
const key = data.get('key');
|
| 38 |
+
|
| 39 |
+
const i = game.answers.length;
|
| 40 |
+
|
| 41 |
+
if (key === 'backspace') {
|
| 42 |
+
game.guesses[i] = game.guesses[i].slice(0, -1);
|
| 43 |
+
} else {
|
| 44 |
+
game.guesses[i] += key;
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
cookies.set('sverdle', game.toString());
|
| 48 |
+
},
|
| 49 |
+
|
| 50 |
+
/**
|
| 51 |
+
* Modify game state in reaction to a guessed word. This logic always runs on
|
| 52 |
+
* the server, so that people can't cheat by peeking at the JavaScript
|
| 53 |
+
*/
|
| 54 |
+
enter: async ({ request, cookies }) => {
|
| 55 |
+
const game = new Game(cookies.get('sverdle'));
|
| 56 |
+
|
| 57 |
+
const data = await request.formData();
|
| 58 |
+
const guess = /** @type {string[]} */ (data.getAll('guess'));
|
| 59 |
+
|
| 60 |
+
if (!game.enter(guess)) {
|
| 61 |
+
return fail(400, { badGuess: true });
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
cookies.set('sverdle', game.toString());
|
| 65 |
+
},
|
| 66 |
+
|
| 67 |
+
restart: async ({ cookies }) => {
|
| 68 |
+
cookies.delete('sverdle');
|
| 69 |
+
}
|
| 70 |
+
};
|
src/routes/sverdle/+page.svelte
ADDED
|
@@ -0,0 +1,410 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<script>
|
| 2 |
+
import { confetti } from '@neoconfetti/svelte';
|
| 3 |
+
import { enhance } from '$app/forms';
|
| 4 |
+
|
| 5 |
+
import { reduced_motion } from './reduced-motion';
|
| 6 |
+
|
| 7 |
+
/** @type {import('./$types').PageData} */
|
| 8 |
+
export let data;
|
| 9 |
+
|
| 10 |
+
/** @type {import('./$types').ActionData} */
|
| 11 |
+
export let form;
|
| 12 |
+
|
| 13 |
+
/** Whether or not the user has won */
|
| 14 |
+
$: won = data.answers.at(-1) === 'xxxxx';
|
| 15 |
+
|
| 16 |
+
/** The index of the current guess */
|
| 17 |
+
$: i = won ? -1 : data.answers.length;
|
| 18 |
+
|
| 19 |
+
/** Whether the current guess can be submitted */
|
| 20 |
+
$: submittable = data.guesses[i]?.length === 5;
|
| 21 |
+
|
| 22 |
+
/**
|
| 23 |
+
* A map of classnames for all letters that have been guessed,
|
| 24 |
+
* used for styling the keyboard
|
| 25 |
+
* @type {Record<string, 'exact' | 'close' | 'missing'>}
|
| 26 |
+
*/
|
| 27 |
+
let classnames;
|
| 28 |
+
|
| 29 |
+
/**
|
| 30 |
+
* A map of descriptions for all letters that have been guessed,
|
| 31 |
+
* used for adding text for assistive technology (e.g. screen readers)
|
| 32 |
+
* @type {Record<string, string>}
|
| 33 |
+
*/
|
| 34 |
+
let description;
|
| 35 |
+
|
| 36 |
+
$: {
|
| 37 |
+
classnames = {};
|
| 38 |
+
description = {};
|
| 39 |
+
|
| 40 |
+
data.answers.forEach((answer, i) => {
|
| 41 |
+
const guess = data.guesses[i];
|
| 42 |
+
|
| 43 |
+
for (let i = 0; i < 5; i += 1) {
|
| 44 |
+
const letter = guess[i];
|
| 45 |
+
|
| 46 |
+
if (answer[i] === 'x') {
|
| 47 |
+
classnames[letter] = 'exact';
|
| 48 |
+
description[letter] = 'correct';
|
| 49 |
+
} else if (!classnames[letter]) {
|
| 50 |
+
classnames[letter] = answer[i] === 'c' ? 'close' : 'missing';
|
| 51 |
+
description[letter] = answer[i] === 'c' ? 'present' : 'absent';
|
| 52 |
+
}
|
| 53 |
+
}
|
| 54 |
+
});
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
/**
|
| 58 |
+
* Modify the game state without making a trip to the server,
|
| 59 |
+
* if client-side JavaScript is enabled
|
| 60 |
+
* @param {MouseEvent} event
|
| 61 |
+
*/
|
| 62 |
+
function update(event) {
|
| 63 |
+
const guess = data.guesses[i];
|
| 64 |
+
const key = /** @type {HTMLButtonElement} */ (event.target).getAttribute('data-key');
|
| 65 |
+
|
| 66 |
+
if (key === 'backspace') {
|
| 67 |
+
data.guesses[i] = guess.slice(0, -1);
|
| 68 |
+
if (form?.badGuess) form.badGuess = false;
|
| 69 |
+
} else if (guess.length < 5) {
|
| 70 |
+
data.guesses[i] += key;
|
| 71 |
+
}
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
/**
|
| 75 |
+
* Trigger form logic in response to a keydown event, so that
|
| 76 |
+
* desktop users can use the keyboard to play the game
|
| 77 |
+
* @param {KeyboardEvent} event
|
| 78 |
+
*/
|
| 79 |
+
function keydown(event) {
|
| 80 |
+
if (event.metaKey) return;
|
| 81 |
+
|
| 82 |
+
document
|
| 83 |
+
.querySelector(`[data-key="${event.key}" i]`)
|
| 84 |
+
?.dispatchEvent(new MouseEvent('click', { cancelable: true }));
|
| 85 |
+
}
|
| 86 |
+
</script>
|
| 87 |
+
|
| 88 |
+
<svelte:window on:keydown={keydown} />
|
| 89 |
+
|
| 90 |
+
<svelte:head>
|
| 91 |
+
<title>Sverdle</title>
|
| 92 |
+
<meta name="description" content="A Wordle clone written in SvelteKit" />
|
| 93 |
+
</svelte:head>
|
| 94 |
+
|
| 95 |
+
<h1 class="visually-hidden">Sverdle</h1>
|
| 96 |
+
|
| 97 |
+
<form
|
| 98 |
+
method="POST"
|
| 99 |
+
action="?/enter"
|
| 100 |
+
use:enhance={() => {
|
| 101 |
+
// prevent default callback from resetting the form
|
| 102 |
+
return ({ update }) => {
|
| 103 |
+
update({ reset: false });
|
| 104 |
+
};
|
| 105 |
+
}}
|
| 106 |
+
>
|
| 107 |
+
<a class="how-to-play" href="/sverdle/how-to-play">How to play</a>
|
| 108 |
+
|
| 109 |
+
<div class="grid" class:playing={!won} class:bad-guess={form?.badGuess}>
|
| 110 |
+
{#each Array(6) as _, row}
|
| 111 |
+
{@const current = row === i}
|
| 112 |
+
<h2 class="visually-hidden">Row {row + 1}</h2>
|
| 113 |
+
<div class="row" class:current>
|
| 114 |
+
{#each Array(5) as _, column}
|
| 115 |
+
{@const answer = data.answers[row]?.[column]}
|
| 116 |
+
{@const value = data.guesses[row]?.[column] ?? ''}
|
| 117 |
+
{@const selected = current && column === data.guesses[row].length}
|
| 118 |
+
{@const exact = answer === 'x'}
|
| 119 |
+
{@const close = answer === 'c'}
|
| 120 |
+
{@const missing = answer === '_'}
|
| 121 |
+
<div class="letter" class:exact class:close class:missing class:selected>
|
| 122 |
+
{value}
|
| 123 |
+
<span class="visually-hidden">
|
| 124 |
+
{#if exact}
|
| 125 |
+
(correct)
|
| 126 |
+
{:else if close}
|
| 127 |
+
(present)
|
| 128 |
+
{:else if missing}
|
| 129 |
+
(absent)
|
| 130 |
+
{:else}
|
| 131 |
+
empty
|
| 132 |
+
{/if}
|
| 133 |
+
</span>
|
| 134 |
+
<input name="guess" disabled={!current} type="hidden" {value} />
|
| 135 |
+
</div>
|
| 136 |
+
{/each}
|
| 137 |
+
</div>
|
| 138 |
+
{/each}
|
| 139 |
+
</div>
|
| 140 |
+
|
| 141 |
+
<div class="controls">
|
| 142 |
+
{#if won || data.answers.length >= 6}
|
| 143 |
+
{#if !won && data.answer}
|
| 144 |
+
<p>the answer was "{data.answer}"</p>
|
| 145 |
+
{/if}
|
| 146 |
+
<button data-key="enter" class="restart selected" formaction="?/restart">
|
| 147 |
+
{won ? 'you won :)' : `game over :(`} play again?
|
| 148 |
+
</button>
|
| 149 |
+
{:else}
|
| 150 |
+
<div class="keyboard">
|
| 151 |
+
<button data-key="enter" class:selected={submittable} disabled={!submittable}>enter</button>
|
| 152 |
+
|
| 153 |
+
<button
|
| 154 |
+
on:click|preventDefault={update}
|
| 155 |
+
data-key="backspace"
|
| 156 |
+
formaction="?/update"
|
| 157 |
+
name="key"
|
| 158 |
+
value="backspace"
|
| 159 |
+
>
|
| 160 |
+
back
|
| 161 |
+
</button>
|
| 162 |
+
|
| 163 |
+
{#each ['qwertyuiop', 'asdfghjkl', 'zxcvbnm'] as row}
|
| 164 |
+
<div class="row">
|
| 165 |
+
{#each row as letter}
|
| 166 |
+
<button
|
| 167 |
+
on:click|preventDefault={update}
|
| 168 |
+
data-key={letter}
|
| 169 |
+
class={classnames[letter]}
|
| 170 |
+
disabled={data.guesses[i].length === 5}
|
| 171 |
+
formaction="?/update"
|
| 172 |
+
name="key"
|
| 173 |
+
value={letter}
|
| 174 |
+
aria-label="{letter} {description[letter] || ''}"
|
| 175 |
+
>
|
| 176 |
+
{letter}
|
| 177 |
+
</button>
|
| 178 |
+
{/each}
|
| 179 |
+
</div>
|
| 180 |
+
{/each}
|
| 181 |
+
</div>
|
| 182 |
+
{/if}
|
| 183 |
+
</div>
|
| 184 |
+
</form>
|
| 185 |
+
|
| 186 |
+
{#if won}
|
| 187 |
+
<div
|
| 188 |
+
style="position: absolute; left: 50%; top: 30%"
|
| 189 |
+
use:confetti={{
|
| 190 |
+
particleCount: $reduced_motion ? 0 : undefined,
|
| 191 |
+
force: 0.7,
|
| 192 |
+
stageWidth: window.innerWidth,
|
| 193 |
+
stageHeight: window.innerHeight,
|
| 194 |
+
colors: ['#ff3e00', '#40b3ff', '#676778']
|
| 195 |
+
}}
|
| 196 |
+
/>
|
| 197 |
+
{/if}
|
| 198 |
+
|
| 199 |
+
<style>
|
| 200 |
+
form {
|
| 201 |
+
width: 100%;
|
| 202 |
+
height: 100%;
|
| 203 |
+
display: flex;
|
| 204 |
+
flex-direction: column;
|
| 205 |
+
align-items: center;
|
| 206 |
+
justify-content: center;
|
| 207 |
+
gap: 1rem;
|
| 208 |
+
flex: 1;
|
| 209 |
+
}
|
| 210 |
+
|
| 211 |
+
.how-to-play {
|
| 212 |
+
color: var(--color-text);
|
| 213 |
+
}
|
| 214 |
+
|
| 215 |
+
.how-to-play::before {
|
| 216 |
+
content: 'i';
|
| 217 |
+
display: inline-block;
|
| 218 |
+
font-size: 0.8em;
|
| 219 |
+
font-weight: 900;
|
| 220 |
+
width: 1em;
|
| 221 |
+
height: 1em;
|
| 222 |
+
padding: 0.2em;
|
| 223 |
+
line-height: 1;
|
| 224 |
+
border: 1.5px solid var(--color-text);
|
| 225 |
+
border-radius: 50%;
|
| 226 |
+
text-align: center;
|
| 227 |
+
margin: 0 0.5em 0 0;
|
| 228 |
+
position: relative;
|
| 229 |
+
top: -0.05em;
|
| 230 |
+
}
|
| 231 |
+
|
| 232 |
+
.grid {
|
| 233 |
+
--width: min(100vw, 40vh, 380px);
|
| 234 |
+
max-width: var(--width);
|
| 235 |
+
align-self: center;
|
| 236 |
+
justify-self: center;
|
| 237 |
+
width: 100%;
|
| 238 |
+
height: 100%;
|
| 239 |
+
display: flex;
|
| 240 |
+
flex-direction: column;
|
| 241 |
+
justify-content: flex-start;
|
| 242 |
+
}
|
| 243 |
+
|
| 244 |
+
.grid .row {
|
| 245 |
+
display: grid;
|
| 246 |
+
grid-template-columns: repeat(5, 1fr);
|
| 247 |
+
grid-gap: 0.2rem;
|
| 248 |
+
margin: 0 0 0.2rem 0;
|
| 249 |
+
}
|
| 250 |
+
|
| 251 |
+
@media (prefers-reduced-motion: no-preference) {
|
| 252 |
+
.grid.bad-guess .row.current {
|
| 253 |
+
animation: wiggle 0.5s;
|
| 254 |
+
}
|
| 255 |
+
}
|
| 256 |
+
|
| 257 |
+
.grid.playing .row.current {
|
| 258 |
+
filter: drop-shadow(3px 3px 10px var(--color-bg-0));
|
| 259 |
+
}
|
| 260 |
+
|
| 261 |
+
.letter {
|
| 262 |
+
aspect-ratio: 1;
|
| 263 |
+
width: 100%;
|
| 264 |
+
display: flex;
|
| 265 |
+
align-items: center;
|
| 266 |
+
justify-content: center;
|
| 267 |
+
text-align: center;
|
| 268 |
+
box-sizing: border-box;
|
| 269 |
+
text-transform: lowercase;
|
| 270 |
+
border: none;
|
| 271 |
+
font-size: calc(0.08 * var(--width));
|
| 272 |
+
border-radius: 2px;
|
| 273 |
+
background: white;
|
| 274 |
+
margin: 0;
|
| 275 |
+
color: rgba(0, 0, 0, 0.7);
|
| 276 |
+
}
|
| 277 |
+
|
| 278 |
+
.letter.missing {
|
| 279 |
+
background: rgba(255, 255, 255, 0.5);
|
| 280 |
+
color: rgba(0, 0, 0, 0.5);
|
| 281 |
+
}
|
| 282 |
+
|
| 283 |
+
.letter.exact {
|
| 284 |
+
background: var(--color-theme-2);
|
| 285 |
+
color: white;
|
| 286 |
+
}
|
| 287 |
+
|
| 288 |
+
.letter.close {
|
| 289 |
+
border: 2px solid var(--color-theme-2);
|
| 290 |
+
}
|
| 291 |
+
|
| 292 |
+
.selected {
|
| 293 |
+
outline: 2px solid var(--color-theme-1);
|
| 294 |
+
}
|
| 295 |
+
|
| 296 |
+
.controls {
|
| 297 |
+
text-align: center;
|
| 298 |
+
justify-content: center;
|
| 299 |
+
height: min(18vh, 10rem);
|
| 300 |
+
}
|
| 301 |
+
|
| 302 |
+
.keyboard {
|
| 303 |
+
--gap: 0.2rem;
|
| 304 |
+
position: relative;
|
| 305 |
+
display: flex;
|
| 306 |
+
flex-direction: column;
|
| 307 |
+
gap: var(--gap);
|
| 308 |
+
height: 100%;
|
| 309 |
+
}
|
| 310 |
+
|
| 311 |
+
.keyboard .row {
|
| 312 |
+
display: flex;
|
| 313 |
+
justify-content: center;
|
| 314 |
+
gap: 0.2rem;
|
| 315 |
+
flex: 1;
|
| 316 |
+
}
|
| 317 |
+
|
| 318 |
+
.keyboard button,
|
| 319 |
+
.keyboard button:disabled {
|
| 320 |
+
--size: min(8vw, 4vh, 40px);
|
| 321 |
+
background-color: white;
|
| 322 |
+
color: black;
|
| 323 |
+
width: var(--size);
|
| 324 |
+
border: none;
|
| 325 |
+
border-radius: 2px;
|
| 326 |
+
font-size: calc(var(--size) * 0.5);
|
| 327 |
+
margin: 0;
|
| 328 |
+
}
|
| 329 |
+
|
| 330 |
+
.keyboard button.exact {
|
| 331 |
+
background: var(--color-theme-2);
|
| 332 |
+
color: white;
|
| 333 |
+
}
|
| 334 |
+
|
| 335 |
+
.keyboard button.missing {
|
| 336 |
+
opacity: 0.5;
|
| 337 |
+
}
|
| 338 |
+
|
| 339 |
+
.keyboard button.close {
|
| 340 |
+
border: 2px solid var(--color-theme-2);
|
| 341 |
+
}
|
| 342 |
+
|
| 343 |
+
.keyboard button:focus {
|
| 344 |
+
background: var(--color-theme-1);
|
| 345 |
+
color: white;
|
| 346 |
+
outline: none;
|
| 347 |
+
}
|
| 348 |
+
|
| 349 |
+
.keyboard button[data-key='enter'],
|
| 350 |
+
.keyboard button[data-key='backspace'] {
|
| 351 |
+
position: absolute;
|
| 352 |
+
bottom: 0;
|
| 353 |
+
width: calc(1.5 * var(--size));
|
| 354 |
+
height: calc(1 / 3 * (100% - 2 * var(--gap)));
|
| 355 |
+
text-transform: uppercase;
|
| 356 |
+
font-size: calc(0.3 * var(--size));
|
| 357 |
+
padding-top: calc(0.15 * var(--size));
|
| 358 |
+
}
|
| 359 |
+
|
| 360 |
+
.keyboard button[data-key='enter'] {
|
| 361 |
+
right: calc(50% + 3.5 * var(--size) + 0.8rem);
|
| 362 |
+
}
|
| 363 |
+
|
| 364 |
+
.keyboard button[data-key='backspace'] {
|
| 365 |
+
left: calc(50% + 3.5 * var(--size) + 0.8rem);
|
| 366 |
+
}
|
| 367 |
+
|
| 368 |
+
.keyboard button[data-key='enter']:disabled {
|
| 369 |
+
opacity: 0.5;
|
| 370 |
+
}
|
| 371 |
+
|
| 372 |
+
.restart {
|
| 373 |
+
width: 100%;
|
| 374 |
+
padding: 1rem;
|
| 375 |
+
background: rgba(255, 255, 255, 0.5);
|
| 376 |
+
border-radius: 2px;
|
| 377 |
+
border: none;
|
| 378 |
+
}
|
| 379 |
+
|
| 380 |
+
.restart:focus,
|
| 381 |
+
.restart:hover {
|
| 382 |
+
background: var(--color-theme-1);
|
| 383 |
+
color: white;
|
| 384 |
+
outline: none;
|
| 385 |
+
}
|
| 386 |
+
|
| 387 |
+
@keyframes wiggle {
|
| 388 |
+
0% {
|
| 389 |
+
transform: translateX(0);
|
| 390 |
+
}
|
| 391 |
+
10% {
|
| 392 |
+
transform: translateX(-2px);
|
| 393 |
+
}
|
| 394 |
+
30% {
|
| 395 |
+
transform: translateX(4px);
|
| 396 |
+
}
|
| 397 |
+
50% {
|
| 398 |
+
transform: translateX(-6px);
|
| 399 |
+
}
|
| 400 |
+
70% {
|
| 401 |
+
transform: translateX(+4px);
|
| 402 |
+
}
|
| 403 |
+
90% {
|
| 404 |
+
transform: translateX(-2px);
|
| 405 |
+
}
|
| 406 |
+
100% {
|
| 407 |
+
transform: translateX(0);
|
| 408 |
+
}
|
| 409 |
+
}
|
| 410 |
+
</style>
|
src/routes/sverdle/game.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { words, allowed } from './words.server';
|
| 2 |
+
|
| 3 |
+
export class Game {
|
| 4 |
+
/**
|
| 5 |
+
* Create a game object from the player's cookie, or initialise a new game
|
| 6 |
+
* @param {string | undefined} serialized
|
| 7 |
+
*/
|
| 8 |
+
constructor(serialized = undefined) {
|
| 9 |
+
if (serialized) {
|
| 10 |
+
const [index, guesses, answers] = serialized.split('-');
|
| 11 |
+
|
| 12 |
+
this.index = +index;
|
| 13 |
+
this.guesses = guesses ? guesses.split(' ') : [];
|
| 14 |
+
this.answers = answers ? answers.split(' ') : [];
|
| 15 |
+
} else {
|
| 16 |
+
this.index = Math.floor(Math.random() * words.length);
|
| 17 |
+
this.guesses = ['', '', '', '', '', ''];
|
| 18 |
+
this.answers = /** @type {string[]} */ ([]);
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
this.answer = words[this.index];
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
/**
|
| 25 |
+
* Update game state based on a guess of a five-letter word. Returns
|
| 26 |
+
* true if the guess was valid, false otherwise
|
| 27 |
+
* @param {string[]} letters
|
| 28 |
+
*/
|
| 29 |
+
enter(letters) {
|
| 30 |
+
const word = letters.join('');
|
| 31 |
+
const valid = allowed.has(word);
|
| 32 |
+
|
| 33 |
+
if (!valid) return false;
|
| 34 |
+
|
| 35 |
+
this.guesses[this.answers.length] = word;
|
| 36 |
+
|
| 37 |
+
const available = Array.from(this.answer);
|
| 38 |
+
const answer = Array(5).fill('_');
|
| 39 |
+
|
| 40 |
+
// first, find exact matches
|
| 41 |
+
for (let i = 0; i < 5; i += 1) {
|
| 42 |
+
if (letters[i] === available[i]) {
|
| 43 |
+
answer[i] = 'x';
|
| 44 |
+
available[i] = ' ';
|
| 45 |
+
}
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
// then find close matches (this has to happen
|
| 49 |
+
// in a second step, otherwise an early close
|
| 50 |
+
// match can prevent a later exact match)
|
| 51 |
+
for (let i = 0; i < 5; i += 1) {
|
| 52 |
+
if (answer[i] === '_') {
|
| 53 |
+
const index = available.indexOf(letters[i]);
|
| 54 |
+
if (index !== -1) {
|
| 55 |
+
answer[i] = 'c';
|
| 56 |
+
available[index] = ' ';
|
| 57 |
+
}
|
| 58 |
+
}
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
this.answers.push(answer.join(''));
|
| 62 |
+
|
| 63 |
+
return true;
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
/**
|
| 67 |
+
* Serialize game state so it can be set as a cookie
|
| 68 |
+
*/
|
| 69 |
+
toString() {
|
| 70 |
+
return `${this.index}-${this.guesses.join(' ')}-${this.answers.join(' ')}`;
|
| 71 |
+
}
|
| 72 |
+
}
|
src/routes/sverdle/how-to-play/+page.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { dev } from '$app/environment';
|
| 2 |
+
|
| 3 |
+
// we don't need any JS on this page, though we'll load
|
| 4 |
+
// it in dev so that we get hot module replacement
|
| 5 |
+
export const csr = dev;
|
| 6 |
+
|
| 7 |
+
// since there's no dynamic data here, we can prerender
|
| 8 |
+
// it so that it gets served as a static asset in production
|
| 9 |
+
export const prerender = true;
|
src/routes/sverdle/how-to-play/+page.svelte
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<svelte:head>
|
| 2 |
+
<title>How to play Sverdle</title>
|
| 3 |
+
<meta name="description" content="How to play Sverdle" />
|
| 4 |
+
</svelte:head>
|
| 5 |
+
|
| 6 |
+
<div class="text-column">
|
| 7 |
+
<h1>How to play Sverdle</h1>
|
| 8 |
+
|
| 9 |
+
<p>
|
| 10 |
+
Sverdle is a clone of <a href="https://www.nytimes.com/games/wordle/index.html">Wordle</a>, the
|
| 11 |
+
word guessing game. To play, enter a five-letter English word. For example:
|
| 12 |
+
</p>
|
| 13 |
+
|
| 14 |
+
<div class="example">
|
| 15 |
+
<span class="close">r</span>
|
| 16 |
+
<span class="missing">i</span>
|
| 17 |
+
<span class="close">t</span>
|
| 18 |
+
<span class="missing">z</span>
|
| 19 |
+
<span class="exact">y</span>
|
| 20 |
+
</div>
|
| 21 |
+
|
| 22 |
+
<p>
|
| 23 |
+
The <span class="exact">y</span> is in the right place. <span class="close">r</span> and
|
| 24 |
+
<span class="close">t</span>
|
| 25 |
+
are the right letters, but in the wrong place. The other letters are wrong, and can be discarded.
|
| 26 |
+
Let's make another guess:
|
| 27 |
+
</p>
|
| 28 |
+
|
| 29 |
+
<div class="example">
|
| 30 |
+
<span class="exact">p</span>
|
| 31 |
+
<span class="exact">a</span>
|
| 32 |
+
<span class="exact">r</span>
|
| 33 |
+
<span class="exact">t</span>
|
| 34 |
+
<span class="exact">y</span>
|
| 35 |
+
</div>
|
| 36 |
+
|
| 37 |
+
<p>This time we guessed right! You have <strong>six</strong> guesses to get the word.</p>
|
| 38 |
+
|
| 39 |
+
<p>
|
| 40 |
+
Unlike the original Wordle, Sverdle runs on the server instead of in the browser, making it
|
| 41 |
+
impossible to cheat. It uses <code><form></code> and cookies to submit data, meaning you can
|
| 42 |
+
even play with JavaScript disabled!
|
| 43 |
+
</p>
|
| 44 |
+
</div>
|
| 45 |
+
|
| 46 |
+
<style>
|
| 47 |
+
span {
|
| 48 |
+
display: inline-flex;
|
| 49 |
+
justify-content: center;
|
| 50 |
+
align-items: center;
|
| 51 |
+
font-size: 0.8em;
|
| 52 |
+
width: 2.4em;
|
| 53 |
+
height: 2.4em;
|
| 54 |
+
background-color: white;
|
| 55 |
+
box-sizing: border-box;
|
| 56 |
+
border-radius: 2px;
|
| 57 |
+
border-width: 2px;
|
| 58 |
+
color: rgba(0, 0, 0, 0.7);
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
.missing {
|
| 62 |
+
background: rgba(255, 255, 255, 0.5);
|
| 63 |
+
color: rgba(0, 0, 0, 0.5);
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
.close {
|
| 67 |
+
border-style: solid;
|
| 68 |
+
border-color: var(--color-theme-2);
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
.exact {
|
| 72 |
+
background: var(--color-theme-2);
|
| 73 |
+
color: white;
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
.example {
|
| 77 |
+
display: flex;
|
| 78 |
+
justify-content: flex-start;
|
| 79 |
+
margin: 1rem 0;
|
| 80 |
+
gap: 0.2rem;
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
.example span {
|
| 84 |
+
font-size: 1.4rem;
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
p span {
|
| 88 |
+
position: relative;
|
| 89 |
+
border-width: 1px;
|
| 90 |
+
border-radius: 1px;
|
| 91 |
+
font-size: 0.4em;
|
| 92 |
+
transform: scale(2) translate(0, -10%);
|
| 93 |
+
margin: 0 1em;
|
| 94 |
+
}
|
| 95 |
+
</style>
|
src/routes/sverdle/reduced-motion.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { readable } from 'svelte/store';
|
| 2 |
+
import { browser } from '$app/environment';
|
| 3 |
+
|
| 4 |
+
const reduced_motion_query = '(prefers-reduced-motion: reduce)';
|
| 5 |
+
|
| 6 |
+
const get_initial_motion_preference = () => {
|
| 7 |
+
if (!browser) return false;
|
| 8 |
+
return window.matchMedia(reduced_motion_query).matches;
|
| 9 |
+
};
|
| 10 |
+
|
| 11 |
+
export const reduced_motion = readable(get_initial_motion_preference(), (set) => {
|
| 12 |
+
if (browser) {
|
| 13 |
+
/**
|
| 14 |
+
* @param {MediaQueryListEvent} event
|
| 15 |
+
*/
|
| 16 |
+
const set_reduced_motion = (event) => {
|
| 17 |
+
set(event.matches);
|
| 18 |
+
};
|
| 19 |
+
const media_query_list = window.matchMedia(reduced_motion_query);
|
| 20 |
+
media_query_list.addEventListener('change', set_reduced_motion);
|
| 21 |
+
|
| 22 |
+
return () => {
|
| 23 |
+
media_query_list.removeEventListener('change', set_reduced_motion);
|
| 24 |
+
};
|
| 25 |
+
}
|
| 26 |
+
});
|
src/routes/sverdle/words.server.js
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
static/favicon.png
ADDED
|
|
static/robots.txt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# https://www.robotstxt.org/robotstxt.html
|
| 2 |
+
User-agent: *
|
| 3 |
+
Disallow:
|
svelte.config.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import adapter from '@sveltejs/adapter-node';
|
| 2 |
+
|
| 3 |
+
/** @type {import('@sveltejs/kit').Config} */
|
| 4 |
+
const config = {
|
| 5 |
+
kit: {
|
| 6 |
+
adapter: adapter()
|
| 7 |
+
}
|
| 8 |
+
};
|
| 9 |
+
|
| 10 |
+
export default config;
|
vite.config.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { sveltekit } from '@sveltejs/kit/vite';
|
| 2 |
+
import { defineConfig } from 'vite';
|
| 3 |
+
|
| 4 |
+
export default defineConfig({
|
| 5 |
+
plugins: [sveltekit()]
|
| 6 |
+
});
|