init
This commit is contained in:
200
src/Game.vue
Normal file
200
src/Game.vue
Normal 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
82
src/Keyboard.vue
Normal 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
40
src/game.css
Normal 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
5
src/main.ts
Normal 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
6
src/types.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export const enum LetterState {
|
||||
INITIAL = 0,
|
||||
CORRECT = 'correct',
|
||||
PRESENT = 'present',
|
||||
ABSENT = 'absent'
|
||||
}
|
||||
12980
src/words.ts
Normal file
12980
src/words.ts
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user