PalmGround

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)
})