import { invalid } from '@sveltejs/kit'; import { words, allowed } from './words.server'; import type { PageServerLoad, Actions } from './$types'; export const load: PageServerLoad = ({ cookies }) => { const game = new Game(cookies.get('sverdle')); return { /** * The player's guessed words so far */ guesses: game.guesses, /** * An array of strings like '__x_c' corresponding to the guesses, where 'x' means * an exact match, and 'c' means a close match (right letter, wrong place) */ answers: game.answers, /** * The correct answer, revealed if the game is over */ answer: game.answers.length >= 6 ? game.answer : null }; }; export const actions: Actions = { /** * Modify game state in reaction to a keypress. If client-side JavaScript * is available, this will happen in the browser instead of here */ update: async ({ request, cookies }) => { const game = new Game(cookies.get('sverdle')); const data = await request.formData(); const key = data.get('key'); const i = game.answers.length; if (key === 'backspace') { game.guesses[i] = game.guesses[i].slice(0, -1); } else { game.guesses[i] += key; } cookies.set('sverdle', game.toString()); }, /** * Modify game state in reaction to a guessed word. This logic always runs on * the server, so that people can't cheat by peeking at the JavaScript */ enter: async ({ request, cookies }) => { const game = new Game(cookies.get('sverdle')); const data = await request.formData(); const guess = data.getAll('guess') as string[]; if (!game.enter(guess)) { return invalid(400, { badGuess: true }); } cookies.set('sverdle', game.toString()); }, restart: async ({ cookies }) => { cookies.delete('sverdle'); } }; class Game { index: number; guesses: string[]; answers: string[]; answer: string; /** * Create a game object from the player's cookie, or initialise a new game */ constructor(serialized: string | undefined) { if (serialized) { const [index, guesses, answers] = serialized.split('-'); this.index = +index; this.guesses = guesses ? guesses.split(' ') : []; this.answers = answers ? answers.split(' ') : []; } else { this.index = Math.floor(Math.random() * words.length); this.guesses = ['', '', '', '', '', '']; this.answers = [] ; } this.answer = words[this.index]; } /** * Update game state based on a guess of a five-letter word. Returns * true if the guess was valid, false otherwise */ enter(letters: string[]) { const word = letters.join(''); const valid = allowed.has(word); if (!valid) return false; this.guesses[this.answers.length] = word; const available = Array.from(this.answer); const answer = Array(5).fill('_'); // first, find exact matches for (let i = 0; i < 5; i += 1) { if (letters[i] === available[i]) { answer[i] = 'x'; available[i] = ' '; } } // then find close matches (this has to happen // in a second step, otherwise an early close // match can prevent a later exact match) for (let i = 0; i < 5; i += 1) { if (answer[i] === '_') { const index = available.indexOf(letters[i]); if (index !== -1) { answer[i] = 'c'; available[index] = ' '; } } } this.answers.push(answer.join('')); return true; } /** * Serialize game state so it can be set as a cookie */ toString() { return `${this.index}-${this.guesses.join(' ')}-${this.answers.join(' ')}`; } }