DANIEL
STOKES

Building Wordalike

January 2, 2023 at 9:45 PM

There's a lot of fun challenges to solve in building a wordle clone. Here's how we made ours!

If you haven't tried Wordalike I encourage you to take a second to check it out and then come back here. It's a wordle clone that doesn't stop after one word!

This started as just an afternoon of curiosity; thinking about how I might architect a wordle game and snowballed into spending a lot more time and solving fun challenges to create a polished wordle clone with a unique twist. Turns out creating a game in vue (or any other modern reactive front end framework for that matter) requires you to think a bit differently. I was used to developing games by thinking about state and events but was curious how that would be translated to the world modern web ui framework.

Game Design

This game was designed and tested by myself and Scott Welsh. At a glance it might not be clear what game designing needed to happen since this is a clone of a finished game but there was quite a lot. Designing how the game would progress from one word to the next was a big challenge that took multiple iterations, giving the player the unused rows as a bonus was a feature that came out of game testing, improving guess entry with a cursor, even typing and backspace behavior was iterated on.

Scott has so much talent in this area. He intuitively can tell when something doesn't feel right for the player, whether animation timing is off, game balance, user interaction and user interface design issues. This game could not have come to this point without his tremendous talent as a game designer and his help with project management.

Game Code. Components and the rest

Componentize! I came up with a structure of components as follows:

-Board
----Tile
-Keyboard
----Key

Here's an simplified snippet of the keyboard component. Isn't that fun!

keyboard.vue
<script setup>
    let row1 = ["Q","W","E","R","T","Y","U","I","O","P"];
    let row2 = ["A","S","D","F","G","H","J","K","L"];
    let row3 = ["Z","X","C","V","B","N","M"];
</script>
<template>
    <div class="keyboard">
        <div class="row">
            <Key v-for="item in row1" @press="type" :width="1" :letter="item" />
        </div>

        <div class="row">
            <Key v-for="item in row2" @press="type" :width="0.9" :letter="item" />
        </div>
        
        <div class="row">
            <Key @press="enter" :width="1.5" letter="enter" />
        
            <Key v-for="item in row3" @press="type" :width="1" :letter="item" />
        
            <Key @press="del" :width="1.5"><MenuIcon /></Key>
        </div>
    </div>
</tempalte>

Game state and actions I placed in a game.ts file which exported the variables and functions needed in the components.

game.ts
import { ref, computed, reactive} from "vue";
import { allowedWords, answers } from "./words"

export const gameBoards = reactive<any>({boards: []});

export const wordAnswers = ref<string[]>([]);

export const currentWordIndex = ref(0);

export async function Submit() {
    //implement logic and advance state
}

export function CalcTileColor() {
    //return tile color based on current answer
}

These are simplified examples but they show how all of these pieces can connect and work together. There was no need for a state store plugin or event plugin. I found it better to manipulate a single data source for the game tiles within the actions as opposed to having the components infer their individual state based on multiple inputs. Initially tile color was a vue computed but that slowed the game's performance down heavily as it had to recalculate on multiple mutations when in reality only one mutation of the data effected the tile's color.

Component Testing

During my development I had to rewrite the tile coloring algorithm multiple times. Eventually I settled on what I thought was a working solution until my brother was trying it out and it didn't work as expected. There was some edge case that needed fixing. I took a screenshot of this scenario and got to work. I created a cypress component test that simulated this scenario and sure enough it was broken. Creating the component test allowed me to tinker with the color function and verify when I had it fixed.

Deploying with a daily word

This was not always a goal of this project but during the slow progress of this project I began learning of another tool that peaked my curiosity. That tool was Astro JS. Astro is a powerful tool and I recommend reading up on it. One of its capabilities is for static site generation. I figured I could use this feature to cleverly build a new version of the game everyday and no api would be used. That was a fun challenge I thought! It was easy! Go Astro!

index.astro
---
// Welcome to Astro! Everything between these triple-dash code fences
// is your "component front matter". It never runs in the browser.
import Game from '../Game.vue';
console.log('This runs in your terminal, not the browser!');

const answer = //some word selection here
---
<!-- Below is your "component template." It's just HTML, but with
     some magic sprinkled in to help you build great templates. -->
<html>
  <head>
    <title>Wordalike</title>
  </head>
  <body>
    <Game client:only="vue" firstWord={answer}/>
  </body>
</html>

It really was that easy! I deployed the project using vercel and setup a github action on a schedule to trigger a vercel deployment hook at the start of the day.

Scraping the real wordle for the daily word

I modified the index astro file to use playwright to scrape the real wordle game for the current days answer.

This is accomplished by navigating to the game url. Simulating 5 subsequent incorrect guesses, losing, and then scraping the answer from the page. This adds a few seconds to the daily build and could stop working when wordle's markup changes but its a pretty easy and clever solution and works really well with astro's static site generation.

Concluding Thoughts

Putting all these pieces together and we are left with a clever and simple solution. All players get the same starting word without needing an api or backend and that starting word is the real wordle's daily answer.

For wordalike's final build we ended up removing the worlde word scrapping and using our own word lists on the server. Still no api maintenance just daily builds which is neet.

© Daniel Stokes 2024