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

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