This commit is contained in:
Evan You 2022-01-23 18:35:21 +08:00
commit e15e2f5b3d
15 changed files with 13766 additions and 0 deletions

5
.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
node_modules
.DS_Store
dist
dist-ssr
*.local

3
README.md Normal file
View File

@ -0,0 +1,3 @@
# Vue Wordle
A Vue implementation of the [Wordle game](https://www.powerlanguage.co.uk/wordle/). This repository is open sourced for learning purposes only - the original creator(s) of Wordle own all applicable rights to the game itself.

1
env.d.ts vendored Normal file
View File

@ -0,0 +1 @@
/// <reference types="vue/macros-global" />

13
index.html Normal file
View File

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite App</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

16
package.json Normal file
View File

@ -0,0 +1,16 @@
{
"name": "vue-wordle",
"version": "0.0.0",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"vue": "^3.2.25"
},
"devDependencies": {
"@vitejs/plugin-vue": "^2.0.0",
"vite": "^2.7.2"
}
}

403
pnpm-lock.yaml generated Normal file
View File

@ -0,0 +1,403 @@
lockfileVersion: 5.3
specifiers:
'@vitejs/plugin-vue': ^2.0.0
vite: ^2.7.2
vue: ^3.2.25
dependencies:
vue: 3.2.28
devDependencies:
'@vitejs/plugin-vue': 2.1.0_vite@2.7.13+vue@3.2.28
vite: 2.7.13
packages:
/@babel/parser/7.16.12:
resolution: {integrity: sha512-VfaV15po8RiZssrkPweyvbGVSe4x2y+aciFCgn0n0/SJMR22cwofRV1mtnJQYcSB1wUTaA/X1LnA3es66MCO5A==}
engines: {node: '>=6.0.0'}
hasBin: true
dev: false
/@vitejs/plugin-vue/2.1.0_vite@2.7.13+vue@3.2.28:
resolution: {integrity: sha512-AZ78WxvFMYd8JmM/GBV6a6SGGTU0GgN/0/4T+FnMMsLzFEzTeAUwuraapy50ifHZsC+G5SvWs86bvaCPTneFlA==}
engines: {node: '>=12.0.0'}
peerDependencies:
vite: ^2.5.10
vue: ^3.2.25
dependencies:
vite: 2.7.13
vue: 3.2.28
dev: true
/@vue/compiler-core/3.2.28:
resolution: {integrity: sha512-mQpfEjmHVxmWKaup0HL6tLMv2HqjjJu7XT4/q0IoUXYXC4xKG8lIVn5YChJqxBTLPuQjzas7u7i9L4PAWJZRtA==}
dependencies:
'@babel/parser': 7.16.12
'@vue/shared': 3.2.28
estree-walker: 2.0.2
source-map: 0.6.1
dev: false
/@vue/compiler-dom/3.2.28:
resolution: {integrity: sha512-KA4yXceLteKC7VykvPnViUixemQw3A+oii+deSbZJOQKQKVh1HLosI10qxa8ImPCyun41+wG3uGR+tW7eu1W6Q==}
dependencies:
'@vue/compiler-core': 3.2.28
'@vue/shared': 3.2.28
dev: false
/@vue/compiler-sfc/3.2.28:
resolution: {integrity: sha512-zB0WznfEBb4CbGBHzhboHDKVO5nxbkbxxFo9iVlxObP7a9/qvA5kkZEuT7nXP52f3b3qEfmVTjIT23Lo1ndZdQ==}
dependencies:
'@babel/parser': 7.16.12
'@vue/compiler-core': 3.2.28
'@vue/compiler-dom': 3.2.28
'@vue/compiler-ssr': 3.2.28
'@vue/reactivity-transform': 3.2.28
'@vue/shared': 3.2.28
estree-walker: 2.0.2
magic-string: 0.25.7
postcss: 8.4.5
source-map: 0.6.1
dev: false
/@vue/compiler-ssr/3.2.28:
resolution: {integrity: sha512-z8rck1PDTu20iLyip9lAvIhaO40DUJrw3Zv0mS4Apfh3PlfWpF5dhsO5g0dgt213wgYsQIYVIlU9cfrYapqRgg==}
dependencies:
'@vue/compiler-dom': 3.2.28
'@vue/shared': 3.2.28
dev: false
/@vue/reactivity-transform/3.2.28:
resolution: {integrity: sha512-zE8idNkOPnBDd2tKSIk84hOQZ+jXKvSy5FoIIVlcNEJHnCFnQ3maqeSJ9KoB2Rf6EXUhFTiTDNRlYlXmT2uHbQ==}
dependencies:
'@babel/parser': 7.16.12
'@vue/compiler-core': 3.2.28
'@vue/shared': 3.2.28
estree-walker: 2.0.2
magic-string: 0.25.7
dev: false
/@vue/reactivity/3.2.28:
resolution: {integrity: sha512-WamM5LGv7JIarW+EYAzYFqYonZXjTnOjNW0sBO93jRE9I1ReAwfH8NvQXkPA3JZ3fuF6SGDdG8Y9/+dKjd/1Gw==}
dependencies:
'@vue/shared': 3.2.28
dev: false
/@vue/runtime-core/3.2.28:
resolution: {integrity: sha512-sVbBMFUt42JatTlXbdH6tVcLPw1eEOrrVQWI+j6/nJVzR852RURaT6DhdR0azdYscxq4xmmBctE0VQmlibBOFw==}
dependencies:
'@vue/reactivity': 3.2.28
'@vue/shared': 3.2.28
dev: false
/@vue/runtime-dom/3.2.28:
resolution: {integrity: sha512-Jg7cxZanEXXGu1QnZILFLnDrM+MIFN8VAullmMZiJEZziHvhygRMpi0ahNy/8OqGwtTze1JNhLdHRBO+q2hbmg==}
dependencies:
'@vue/runtime-core': 3.2.28
'@vue/shared': 3.2.28
csstype: 2.6.19
dev: false
/@vue/server-renderer/3.2.28_vue@3.2.28:
resolution: {integrity: sha512-S+MhurgkPabRvhdDl8R6efKBmniJqBbbWIYTXADaJIKFLFLQCW4gcYUTbxuebzk6j3z485vpekhrHHymTF52Pg==}
peerDependencies:
vue: 3.2.28
dependencies:
'@vue/compiler-ssr': 3.2.28
'@vue/shared': 3.2.28
vue: 3.2.28
dev: false
/@vue/shared/3.2.28:
resolution: {integrity: sha512-eMQ8s9j8FpbGHlgUAaj/coaG3Q8YtMsoWL/RIHTsE3Ex7PUTyr7V91vB5HqWB5Sn8m4RXTHGO22/skoTUYvp0A==}
dev: false
/csstype/2.6.19:
resolution: {integrity: sha512-ZVxXaNy28/k3kJg0Fou5MiYpp88j7H9hLZp8PDC3jV0WFjfH5E9xHb56L0W59cPbKbcHXeP4qyT8PrHp8t6LcQ==}
dev: false
/esbuild-android-arm64/0.13.15:
resolution: {integrity: sha512-m602nft/XXeO8YQPUDVoHfjyRVPdPgjyyXOxZ44MK/agewFFkPa8tUo6lAzSWh5Ui5PB4KR9UIFTSBKh/RrCmg==}
cpu: [arm64]
os: [android]
requiresBuild: true
dev: true
optional: true
/esbuild-darwin-64/0.13.15:
resolution: {integrity: sha512-ihOQRGs2yyp7t5bArCwnvn2Atr6X4axqPpEdCFPVp7iUj4cVSdisgvEKdNR7yH3JDjW6aQDw40iQFoTqejqxvQ==}
cpu: [x64]
os: [darwin]
requiresBuild: true
dev: true
optional: true
/esbuild-darwin-arm64/0.13.15:
resolution: {integrity: sha512-i1FZssTVxUqNlJ6cBTj5YQj4imWy3m49RZRnHhLpefFIh0To05ow9DTrXROTE1urGTQCloFUXTX8QfGJy1P8dQ==}
cpu: [arm64]
os: [darwin]
requiresBuild: true
dev: true
optional: true
/esbuild-freebsd-64/0.13.15:
resolution: {integrity: sha512-G3dLBXUI6lC6Z09/x+WtXBXbOYQZ0E8TDBqvn7aMaOCzryJs8LyVXKY4CPnHFXZAbSwkCbqiPuSQ1+HhrNk7EA==}
cpu: [x64]
os: [freebsd]
requiresBuild: true
dev: true
optional: true
/esbuild-freebsd-arm64/0.13.15:
resolution: {integrity: sha512-KJx0fzEDf1uhNOZQStV4ujg30WlnwqUASaGSFPhznLM/bbheu9HhqZ6mJJZM32lkyfGJikw0jg7v3S0oAvtvQQ==}
cpu: [arm64]
os: [freebsd]
requiresBuild: true
dev: true
optional: true
/esbuild-linux-32/0.13.15:
resolution: {integrity: sha512-ZvTBPk0YWCLMCXiFmD5EUtB30zIPvC5Itxz0mdTu/xZBbbHJftQgLWY49wEPSn2T/TxahYCRDWun5smRa0Tu+g==}
cpu: [ia32]
os: [linux]
requiresBuild: true
dev: true
optional: true
/esbuild-linux-64/0.13.15:
resolution: {integrity: sha512-eCKzkNSLywNeQTRBxJRQ0jxRCl2YWdMB3+PkWFo2BBQYC5mISLIVIjThNtn6HUNqua1pnvgP5xX0nHbZbPj5oA==}
cpu: [x64]
os: [linux]
requiresBuild: true
dev: true
optional: true
/esbuild-linux-arm/0.13.15:
resolution: {integrity: sha512-wUHttDi/ol0tD8ZgUMDH8Ef7IbDX+/UsWJOXaAyTdkT7Yy9ZBqPg8bgB/Dn3CZ9SBpNieozrPRHm0BGww7W/jA==}
cpu: [arm]
os: [linux]
requiresBuild: true
dev: true
optional: true
/esbuild-linux-arm64/0.13.15:
resolution: {integrity: sha512-bYpuUlN6qYU9slzr/ltyLTR9YTBS7qUDymO8SV7kjeNext61OdmqFAzuVZom+OLW1HPHseBfJ/JfdSlx8oTUoA==}
cpu: [arm64]
os: [linux]
requiresBuild: true
dev: true
optional: true
/esbuild-linux-mips64le/0.13.15:
resolution: {integrity: sha512-KlVjIG828uFPyJkO/8gKwy9RbXhCEUeFsCGOJBepUlpa7G8/SeZgncUEz/tOOUJTcWMTmFMtdd3GElGyAtbSWg==}
cpu: [mips64el]
os: [linux]
requiresBuild: true
dev: true
optional: true
/esbuild-linux-ppc64le/0.13.15:
resolution: {integrity: sha512-h6gYF+OsaqEuBjeesTBtUPw0bmiDu7eAeuc2OEH9S6mV9/jPhPdhOWzdeshb0BskRZxPhxPOjqZ+/OqLcxQwEQ==}
cpu: [ppc64]
os: [linux]
requiresBuild: true
dev: true
optional: true
/esbuild-netbsd-64/0.13.15:
resolution: {integrity: sha512-3+yE9emwoevLMyvu+iR3rsa+Xwhie7ZEHMGDQ6dkqP/ndFzRHkobHUKTe+NCApSqG5ce2z4rFu+NX/UHnxlh3w==}
cpu: [x64]
os: [netbsd]
requiresBuild: true
dev: true
optional: true
/esbuild-openbsd-64/0.13.15:
resolution: {integrity: sha512-wTfvtwYJYAFL1fSs8yHIdf5GEE4NkbtbXtjLWjM3Cw8mmQKqsg8kTiqJ9NJQe5NX/5Qlo7Xd9r1yKMMkHllp5g==}
cpu: [x64]
os: [openbsd]
requiresBuild: true
dev: true
optional: true
/esbuild-sunos-64/0.13.15:
resolution: {integrity: sha512-lbivT9Bx3t1iWWrSnGyBP9ODriEvWDRiweAs69vI+miJoeKwHWOComSRukttbuzjZ8r1q0mQJ8Z7yUsDJ3hKdw==}
cpu: [x64]
os: [sunos]
requiresBuild: true
dev: true
optional: true
/esbuild-windows-32/0.13.15:
resolution: {integrity: sha512-fDMEf2g3SsJ599MBr50cY5ve5lP1wyVwTe6aLJsM01KtxyKkB4UT+fc5MXQFn3RLrAIAZOG+tHC+yXObpSn7Nw==}
cpu: [ia32]
os: [win32]
requiresBuild: true
dev: true
optional: true
/esbuild-windows-64/0.13.15:
resolution: {integrity: sha512-9aMsPRGDWCd3bGjUIKG/ZOJPKsiztlxl/Q3C1XDswO6eNX/Jtwu4M+jb6YDH9hRSUflQWX0XKAfWzgy5Wk54JQ==}
cpu: [x64]
os: [win32]
requiresBuild: true
dev: true
optional: true
/esbuild-windows-arm64/0.13.15:
resolution: {integrity: sha512-zzvyCVVpbwQQATaf3IG8mu1IwGEiDxKkYUdA4FpoCHi1KtPa13jeScYDjlW0Qh+ebWzpKfR2ZwvqAQkSWNcKjA==}
cpu: [arm64]
os: [win32]
requiresBuild: true
dev: true
optional: true
/esbuild/0.13.15:
resolution: {integrity: sha512-raCxt02HBKv8RJxE8vkTSCXGIyKHdEdGfUmiYb8wnabnaEmHzyW7DCHb5tEN0xU8ryqg5xw54mcwnYkC4x3AIw==}
hasBin: true
requiresBuild: true
optionalDependencies:
esbuild-android-arm64: 0.13.15
esbuild-darwin-64: 0.13.15
esbuild-darwin-arm64: 0.13.15
esbuild-freebsd-64: 0.13.15
esbuild-freebsd-arm64: 0.13.15
esbuild-linux-32: 0.13.15
esbuild-linux-64: 0.13.15
esbuild-linux-arm: 0.13.15
esbuild-linux-arm64: 0.13.15
esbuild-linux-mips64le: 0.13.15
esbuild-linux-ppc64le: 0.13.15
esbuild-netbsd-64: 0.13.15
esbuild-openbsd-64: 0.13.15
esbuild-sunos-64: 0.13.15
esbuild-windows-32: 0.13.15
esbuild-windows-64: 0.13.15
esbuild-windows-arm64: 0.13.15
dev: true
/estree-walker/2.0.2:
resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==}
dev: false
/fsevents/2.3.2:
resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
os: [darwin]
requiresBuild: true
dev: true
optional: true
/function-bind/1.1.1:
resolution: {integrity: sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==}
dev: true
/has/1.0.3:
resolution: {integrity: sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==}
engines: {node: '>= 0.4.0'}
dependencies:
function-bind: 1.1.1
dev: true
/is-core-module/2.8.1:
resolution: {integrity: sha512-SdNCUs284hr40hFTFP6l0IfZ/RSrMXF3qgoRHd3/79unUTvrFO/JoXwkGm+5J/Oe3E/b5GsnG330uUNgRpu1PA==}
dependencies:
has: 1.0.3
dev: true
/magic-string/0.25.7:
resolution: {integrity: sha512-4CrMT5DOHTDk4HYDlzmwu4FVCcIYI8gauveasrdCu2IKIFOJ3f0v/8MDGJCDL9oD2ppz/Av1b0Nj345H9M+XIA==}
dependencies:
sourcemap-codec: 1.4.8
dev: false
/nanoid/3.2.0:
resolution: {integrity: sha512-fmsZYa9lpn69Ad5eDn7FMcnnSR+8R34W9qJEijxYhTbfOWzr22n1QxCMzXLK+ODyW2973V3Fux959iQoUxzUIA==}
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true
/path-parse/1.0.7:
resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==}
dev: true
/picocolors/1.0.0:
resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==}
/postcss/8.4.5:
resolution: {integrity: sha512-jBDboWM8qpaqwkMwItqTQTiFikhs/67OYVvblFFTM7MrZjt6yMKd6r2kgXizEbTTljacm4NldIlZnhbjr84QYg==}
engines: {node: ^10 || ^12 || >=14}
dependencies:
nanoid: 3.2.0
picocolors: 1.0.0
source-map-js: 1.0.2
/resolve/1.22.0:
resolution: {integrity: sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw==}
hasBin: true
dependencies:
is-core-module: 2.8.1
path-parse: 1.0.7
supports-preserve-symlinks-flag: 1.0.0
dev: true
/rollup/2.66.0:
resolution: {integrity: sha512-L6mKOkdyP8HK5kKJXaiWG7KZDumPJjuo1P+cfyHOJPNNTK3Moe7zCH5+fy7v8pVmHXtlxorzaBjvkBMB23s98g==}
engines: {node: '>=10.0.0'}
hasBin: true
optionalDependencies:
fsevents: 2.3.2
dev: true
/source-map-js/1.0.2:
resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==}
engines: {node: '>=0.10.0'}
/source-map/0.6.1:
resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==}
engines: {node: '>=0.10.0'}
dev: false
/sourcemap-codec/1.4.8:
resolution: {integrity: sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==}
dev: false
/supports-preserve-symlinks-flag/1.0.0:
resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
engines: {node: '>= 0.4'}
dev: true
/vite/2.7.13:
resolution: {integrity: sha512-Mq8et7f3aK0SgSxjDNfOAimZGW9XryfHRa/uV0jseQSilg+KhYDSoNb9h1rknOy6SuMkvNDLKCYAYYUMCE+IgQ==}
engines: {node: '>=12.2.0'}
hasBin: true
peerDependencies:
less: '*'
sass: '*'
stylus: '*'
peerDependenciesMeta:
less:
optional: true
sass:
optional: true
stylus:
optional: true
dependencies:
esbuild: 0.13.15
postcss: 8.4.5
resolve: 1.22.0
rollup: 2.66.0
optionalDependencies:
fsevents: 2.3.2
dev: true
/vue/3.2.28:
resolution: {integrity: sha512-U+jBwVh3RQ9AgceLFdT7i2FFujoC+kYuGrKo5y8aLluWKZWPS40WgA2pyYHaiSX9ydCbEGr3rc/JzdqskzD95g==}
dependencies:
'@vue/compiler-dom': 3.2.28
'@vue/compiler-sfc': 3.2.28
'@vue/runtime-dom': 3.2.28
'@vue/server-renderer': 3.2.28_vue@3.2.28
'@vue/shared': 3.2.28
dev: false

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

200
src/Game.vue Normal file
View File

@ -0,0 +1,200 @@
<script setup lang="ts">
import { onUnmounted } from 'vue'
import { allWords } from './words'
import Keyboard from './Keyboard.vue'
import { LetterState } from './types'
// try to guess this by actually playing the game :)
const answer = atob('aGVsbG8=')
class Tile {
letter = ''
state = LetterState.INITIAL
}
const board = $ref(
Array.from({ length: 6 }, () => {
return Array.from({ length: 5 }, () => new Tile())
})
)
let allowInput = true
let currentRowIndex = $ref(0)
const currentRow = $computed(() => board[currentRowIndex])
// keep track of revealed letter state for the keyboard
const letterStates: Record<string, LetterState> = $ref({})
window.addEventListener('keyup', onKeyup)
onUnmounted(() => {
window.removeEventListener('keyup', onKeyup)
})
function onKeyup(e: KeyboardEvent) {
onKey(e.key)
}
function onKey(key: string) {
if (!allowInput) return
if (/^[a-z]$/.test(key)) {
fillTile(key)
} else if (key === 'Backspace') {
clearTile()
} else if (key === 'Enter') {
completeRow()
}
}
function fillTile(letter: string) {
for (const tile of currentRow) {
if (!tile.letter) {
tile.letter = letter
break
}
}
}
function clearTile() {
for (const tile of [...currentRow].reverse()) {
if (tile.letter) {
tile.letter = ''
break
}
}
}
function completeRow() {
if (currentRow.every((tile) => tile.letter)) {
const word = currentRow.map((tile) => tile.letter).join('')
if (!allWords.includes(word) && word !== answer) {
alert(`"${word}" is not a valid word.`)
return
}
let correct = true
currentRow.forEach((tile, i) => {
if (answer.includes(tile.letter)) {
if (answer[i] === tile.letter) {
tile.state = letterStates[tile.letter] = LetterState.CORRECT
} else {
tile.state = LetterState.PRESENT
if (!letterStates[tile.letter]) {
letterStates[tile.letter] = LetterState.PRESENT
}
correct = false
}
} else {
tile.state = letterStates[tile.letter] = LetterState.ABSENT
correct = false
}
})
if (correct) {
// yay!
allowInput = false
requestIdleCallback(() => alert('yay!'))
} else if (currentRowIndex < board.length - 1) {
// go the next row
currentRowIndex++
} else {
// game over :(
allowInput = false
requestIdleCallback(() => alert('oops'))
}
}
}
</script>
<template>
<header>
<h1>VVORDLE</h1>
<a
id="source-link"
href="https://github.com/yyx990803/vue-wordle"
target="_blank"
>Source</a
>
</header>
<div id="board">
<div class="row" v-for="row in board">
<div
v-for="(tile, index) in row"
:class="[
'tile',
tile.letter ? 'filled' : 'empty',
tile.state && 'revealed'
]"
>
<div class="front" :style="{ transitionDelay: `${index * 200}ms` }">
{{ tile.letter }}
</div>
<div
:class="['back', tile.state]"
:style="{ transitionDelay: `${index * 200}ms` }"
>
{{ tile.letter }}
</div>
</div>
</div>
</div>
<Keyboard @key="onKey" :letter-states="letterStates" />
</template>
<style scoped>
#board {
display: grid;
grid-template-rows: repeat(6, 1fr);
grid-gap: 5px;
padding: 10px;
box-sizing: border-box;
width: 350px;
height: 420px;
margin: 0px auto;
}
.row {
display: grid;
grid-template-columns: repeat(5, 1fr);
grid-gap: 5px;
}
.tile {
width: 100%;
font-size: 2rem;
line-height: 2rem;
font-weight: bold;
vertical-align: middle;
text-transform: uppercase;
user-select: none;
position: relative;
}
.tile .front,
.tile .back {
box-sizing: border-box;
display: inline-flex;
justify-content: center;
align-items: center;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
transition: transform 0.6s;
backface-visibility: hidden;
}
.tile .front {
border: 2px solid #ccc;
}
.tile .back {
transform: rotateX(180deg);
}
.tile.filled .front {
border-color: #999;
}
.tile:not(.empty) {
border: none;
}
.tile.revealed .front {
transform: rotateX(180deg);
}
.tile.revealed .back {
transform: rotateX(0deg);
}
</style>

82
src/Keyboard.vue Normal file
View File

@ -0,0 +1,82 @@
<script setup lang="ts">
import { LetterState } from './types'
defineProps<{
letterStates: Record<string, LetterState>
}>()
const rows = [
'qwertyuiop'.split(''),
'asdfghjkl'.split(''),
['Enter', ...'zxcvbnm'.split(''), 'Backspace']
]
</script>
<template>
<div id="keyboard">
<div class="row" v-for="(row, i) in rows">
<div class="spacer" v-if="i === 1"></div>
<button
v-for="key in row"
:class="[key.length > 1 && 'big', letterStates[key]]"
@click="$emit('key', key)"
>
<span v-if="key !== 'Backspace'">{{ key }}</span>
<svg
v-else
xmlns="http://www.w3.org/2000/svg"
height="24"
viewBox="0 0 24 24"
width="24"
>
<path
fill="var(--color-tone-1)"
d="M22 3H7c-.69 0-1.23.35-1.59.88L0 12l5.41 8.11c.36.53.9.89 1.59.89h15c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H7.07L2.4 12l4.66-7H22v14zm-11.59-2L14 13.41 17.59 17 19 15.59 15.41 12 19 8.41 17.59 7 14 10.59 10.41 7 9 8.41 12.59 12 9 15.59z"
></path>
</svg>
</button>
<div class="spacer" v-if="i === 1"></div>
</div>
</div>
</template>
<style scoped>
#keyboard {
margin: 30px 8px 0;
user-select: none;
}
.row {
display: flex;
width: 100%;
margin: 0 auto 8px;
touch-action: manipulation;
}
.spacer {
flex: 0.5;
}
button {
font-family: inherit;
font-weight: bold;
border: 0;
padding: 0;
margin: 0 6px 0 0;
height: 58px;
border-radius: 4px;
cursor: pointer;
user-select: none;
background-color: #d3d6da;
color: #1a1a1b;
flex: 1;
display: flex;
justify-content: center;
align-items: center;
text-transform: uppercase;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0.3);
}
button:last-of-type {
margin: 0;
}
button.big {
flex: 1.5;
}
</style>

40
src/game.css Normal file
View File

@ -0,0 +1,40 @@
body {
font-family: 'Clear Sans', 'Helvetica Neue', Arial, sans-serif;
text-align: center;
max-width: 500px;
margin: 0px auto;
}
h1 {
margin: 10px 0;
}
header {
border-bottom: 1px solid #ccc;
margin-bottom: 30px;
position: relative;
}
#source-link {
position: absolute;
right: 0;
top: 0.5em;
}
.correct,
.present,
.absent {
color: #fff;
}
.correct {
background-color: #6aaa64;
}
.present {
background-color: #c9b458;
}
.absent {
background-color: #787c7e;
}

5
src/main.ts Normal file
View File

@ -0,0 +1,5 @@
import { createApp } from 'vue'
import Game from './Game.vue'
import './game.css'
createApp(Game).mount('#app')

6
src/types.ts Normal file
View File

@ -0,0 +1,6 @@
export const enum LetterState {
INITIAL = 0,
CORRECT = 'correct',
PRESENT = 'present',
ABSENT = 'absent'
}

12980
src/words.ts Normal file

File diff suppressed because it is too large Load Diff

3
tsconfig.json Normal file
View File

@ -0,0 +1,3 @@
{
"include": ["env.d.ts", "src/**/*"]
}

9
vite.config.js Normal file
View File

@ -0,0 +1,9 @@
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue({
reactivityTransform: true
})]
})