Wordle
Prøli hør her makker
Markup
<BasePopover message="Prøli hør her makker" triggerId="error-message" />
<dialog id="game-over-dialog">
<h2 id="dialog-title"></h2>
<p id="dialog-message"></p>
<button id="play-again-button">Play Again</button>
</dialog>
<div id="board">
<div class="row" data-row="1">
<div class="cell" data-col="1"></div>
<div class="cell" data-col="2"></div>
<div class="cell" data-col="3"></div>
<div class="cell" data-col="4"></div>
<div class="cell" data-col="5"></div>
</div>
<div class="row" data-row="2">
<div class="cell" data-col="1"></div>
<div class="cell" data-col="2"></div>
<div class="cell" data-col="3"></div>
<div class="cell" data-col="4"></div>
<div class="cell" data-col="5"></div>
</div>
<div class="row" data-row="3">
<div class="cell" data-col="1"></div>
<div class="cell" data-col="2"></div>
<div class="cell" data-col="3"></div>
<div class="cell" data-col="4"></div>
<div class="cell" data-col="5"></div>
</div>
<div class="row" data-row="4">
<div class="cell" data-col="1"></div>
<div class="cell" data-col="2"></div>
<div class="cell" data-col="3"></div>
<div class="cell" data-col="4"></div>
<div class="cell" data-col="5"></div>
</div>
<div class="row" data-row="5">
<div class="cell" data-col="1"></div>
<div class="cell" data-col="2"></div>
<div class="cell" data-col="3"></div>
<div class="cell" data-col="4"></div>
<div class="cell" data-col="5"></div>
</div>
<div class="row" data-row="6">
<div class="cell" data-col="1"></div>
<div class="cell" data-col="2"></div>
<div class="cell" data-col="3"></div>
<div class="cell" data-col="4"></div>
<div class="cell" data-col="5"></div>
</div>
</div>
<div class="keyboard">
<div class="row">
<button class="key" data-key="q" aria-label="Press Q key">Q</button>
<button class="key" data-key="w" aria-label="Press W key">W</button>
<button class="key" data-key="e" aria-label="Press E key">E</button>
<button class="key" data-key="r" aria-label="Press R key">R</button>
<button class="key" data-key="t" aria-label="Press T key">T</button>
<button class="key" data-key="y" aria-label="Press Y key">Y</button>
<button class="key" data-key="u" aria-label="Press U key">U</button>
<button class="key" data-key="i" aria-label="Press I key">I</button>
<button class="key" data-key="o" aria-label="Press O key">O</button>
<button class="key" data-key="p" aria-label="Press P key">P</button>
</div>
<div class="row">
<button class="key" data-key="a" aria-label="Press A key">A</button>
<button class="key" data-key="s" aria-label="Press S key">S</button>
<button class="key" data-key="d" aria-label="Press D key">D</button>
<button class="key" data-key="f" aria-label="Press F key">F</button>
<button class="key" data-key="g" aria-label="Press G key">G</button>
<button class="key" data-key="h" aria-label="Press H key">H</button>
<button class="key" data-key="j" aria-label="Press J key">J</button>
<button class="key" data-key="k" aria-label="Press K key">K</button>
<button class="key" data-key="l" aria-label="Press L key">L</button>
</div>
<div class="row">
<button class="key" data-key="Enter" aria-label="Press Enter key">ENTER</button>
<button class="key" data-key="z" aria-label="Press Z key">Z</button>
<button class="key" data-key="x" aria-label="Press X key">X</button>
<button class="key" data-key="c" aria-label="Press C key">C</button>
<button class="key" data-key="v" aria-label="Press V key">V</button>
<button class="key" data-key="b" aria-label="Press B key">B</button>
<button class="key" data-key="n" aria-label="Press N key">N</button>
<button class="key" data-key="m" aria-label="Press M key">M</button>
<button class="key" data-key="Backspace" aria-label="Press Backspace key">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" class="backspace-icon">
<path
fill="currentColor"
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>
</div>
Styles
#board {
--gap: 0.5rem;
max-inline-size: 400px;
margin-inline: auto;
display: flex;
flex-direction: column;
gap: var(--gap);
}
@keyframes pulse {
0% {
transform: scale(1);
}
50% {
transform: scale(1.1);
}
100% {
transform: scale(1);
}
}
.cell:not(:empty) {
animation: pulse 0.2s ease-out;
}
.row {
display: flex;
gap: var(--gap);
flex: 1;
}
.cell {
background-color: transparent;
border: 1px solid var(--color-border);
flex: 1;
aspect-ratio: 1;
display: flex;
align-items: center;
justify-content: center;
font-size: 2rem;
transform-style: preserve-3d;
transform: rotateX(0deg);
position: relative;
}
@keyframes flip {
0% {
transform: rotateX(0deg);
}
50% {
transform: rotateX(90deg);
background-color: transparent;
}
100% {
transform: rotateX(0deg);
background-color: var(--color-back);
}
}
.keyboard {
margin-block: 2rem;
--gap: 0.3rem;
gap: 0.3rem;
display: flex;
flex-direction: column;
align-items: center;
}
#error-message {
color: red;
text-align: center;
font-size: 1.5rem;
font-weight: bold;
margin-block: 1rem;
display: none;
}
.cell.correct {
--color-back: darkgreen;
}
.cell.present {
--color-back: rgb(185, 185, 69);
}
.cell.absent {
--color-back: gray;
}
.key {
font-size: clamp(1.125rem, 0.7597rem + 1.5584vw, 1.5rem);
padding-inline: 0.5em;
min-inline-size: 1ch;
padding-block: 0.9em;
touch-action: manipulation;
}
.key[data-key='Enter'] {
font-size: 0.8em;
}
.backspace-icon {
width: 20px;
height: 20px;
}
.key.correct {
background-color: darkgreen;
}
.key.present {
background-color: rgb(185, 185, 69);
}
.key.absent {
background-color: gray;
}
dialog {
margin: auto;
padding: var(--space-lg);
border-radius: var(--space-xs);
border: 1px solid var(--color-border);
box-shadow:
0 4px 6px -1px rgb(0 0 0 / 0.1),
0 2px 6px -2px rgb(0 0 0 / 0.1);
text-align: center;
}
dialog::backdrop {
background-color: hsl(0 0 0 / 0.3);
}
dialog button {
margin-top: var(--space-md);
padding: 0.5rem 1rem;
background-color: var(--color-accent);
color: var(--bg-brand);
border: none;
border-radius: 4px;
cursor: pointer;
transition: opacity 0.2s ease;
}
dialog button:hover {
opacity: 0.9;
}
Script
// Game initialization
const board = document.getElementById('board')
const rows = board.querySelectorAll('[data-row]')
const keyboard = document.querySelector('.keyboard')
const playAgainButton = document.getElementById('play-again-button')
const words = validWords.split('\n')
const guessableWords = validGuesses.split('\n')
const targetWord = words[Math.floor(Math.random() * words.length)]
const validLetters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
const gameState = {
currentRow: 1,
currentCol: 0,
currentLetter: '',
word: '',
targetWord: targetWord,
targetWordLetters: targetWord.split(''),
guesses: [],
animating: false,
gameOver: false,
}
// Game state management
function resetGame() {
gameState.currentRow = 1
gameState.currentCol = 0
gameState.currentLetter = ''
gameState.word = ''
gameState.guesses = []
gameState.animating = false
gameState.gameOver = false
gameState.targetWord = words[Math.floor(Math.random() * words.length)]
gameState.targetWordLetters = gameState.targetWord.split('')
// Reset board
rows.forEach((row) => {
const cells = row.querySelectorAll('.cell')
cells.forEach((cell) => {
cell.innerText = ''
cell.className = 'cell'
cell.style.animation = ''
})
})
// Reset keyboard
const keys = keyboard.querySelectorAll('.key')
keys.forEach((key) => {
key.className = 'key'
})
}
// Board navigation
function incrementCol() {
if (gameState.currentCol < 5) {
gameState.currentCol++
}
}
function decrementCol() {
if (gameState.currentCol > 0) {
gameState.currentCol--
}
}
function getCurrentCell() {
const currentRow = getCurrentRow()
return currentRow?.querySelector(`[data-col="${gameState.currentCol}"]`)
}
function getCurrentRow() {
return Array.from(rows).find((row) => row.dataset.row === gameState.currentRow.toString())
}
// Word input handling
function writeLetter(letter) {
if (gameState.word.length === 5) return
gameState.currentLetter = letter
incrementCol()
const currentCell = getCurrentCell()
if (!currentCell) return
currentCell.innerText = letter
gameState.word += letter
}
function deleteLetter() {
const currentCell = getCurrentCell()
if (!currentCell) return
currentCell.innerText = ''
decrementCol()
gameState.word = gameState.word.slice(0, -1)
gameState.currentLetter = ''
}
// Animation and visual feedback
async function flipCells(cells) {
const promises = Array.from(cells).map((cell, index) => {
return new Promise((resolve) => {
cell.style.animation = `flip 0.5s ease forwards ${index * 0.5}s`
cell.addEventListener('animationend', resolve, { once: true })
})
})
await Promise.all(promises)
}
async function addGuess() {
const guessLetters = gameState.word.split('')
const result = guessLetters.map((letter, index) => {
if (letter === gameState.targetWordLetters[index]) {
return { letter, status: 'correct' }
} else if (gameState.targetWordLetters.includes(letter)) {
return { letter, status: 'present' }
} else {
return { letter, status: 'absent' }
}
})
const currentRow = getCurrentRow()
if (!currentRow) return
const cells = currentRow.querySelectorAll('.cell')
cells.forEach((cell, index) => {
cell.classList.add(result[index].status)
})
gameState.animating = true
await flipCells(cells)
// Update keyboard colors
const keyboardKeys = keyboard.querySelectorAll('.key')
keyboardKeys.forEach((key) => {
const status = result.find(({ letter }) => letter === key.dataset.key)?.status
if (status) {
key.classList.remove('correct', 'present', 'absent')
key.classList.add(status)
}
})
gameState.animating = false
}
// Game flow control
function moveToNextRow() {
gameState.guesses.push(gameState.word)
gameState.word = ''
gameState.currentCol = 0
gameState.currentLetter = ''
gameState.currentRow++
}
function showError() {
togglePopover('error-message')
}
function showGameOverDialog(isWin) {
const dialog = document.getElementById('game-over-dialog')
const title = document.getElementById('dialog-title')
const message = document.getElementById('dialog-message')
console.log('runninng show modal')
if (isWin) {
title.textContent = 'Yassss queen! 🎉'
message.textContent = `You found the word "${gameState.targetWord}" in ${gameState.currentRow} tries!`
} else {
title.textContent = 'Game Over! 😢'
message.textContent = `The word was "${gameState.targetWord}". Better luck next time!`
}
gameState.gameOver = true
dialog.showModal()
}
// Input handling
async function handleKeyPress(letter) {
if (gameState.animating || gameState.gameOver) return
switch (letter) {
case 'Backspace':
deleteLetter()
break
case 'Enter':
if (gameState.word.length === 5) {
const isValid = guessableWords.includes(gameState.word.toLowerCase())
if (isValid) {
await addGuess()
console.log(gameState)
if (gameState.word === gameState.targetWord) {
showGameOverDialog(true)
} else if (gameState.currentRow >= 6) {
showGameOverDialog(false)
}
moveToNextRow()
} else {
showError()
}
}
break
default:
if (validLetters.includes(letter.toUpperCase())) {
writeLetter(letter)
}
break
}
}
// UI utilities
function togglePopover(id) {
const popover = document.getElementById(id)
if (popover) {
popover.showPopover()
}
setTimeout(() => {
popover.hidePopover()
}, 3000)
}
// Event listeners
playAgainButton.addEventListener('click', () => {
resetGame()
document.getElementById('game-over-dialog').close()
})
keyboard.addEventListener('click', (e) => {
const key = e.target.closest('.key')
if (!key) return
const letter = key.dataset.key
handleKeyPress(letter)
})
window.addEventListener('keydown', async (e) => {
if (e.repeat) return
if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return
handleKeyPress(e.key)
})