Projects > Anagram Survival

Anagram Survival


Table Of Contents

You can play the game here.

The idea for this project was to make a simple word game that anybody could play and hopefully have fun doing so. I used this project as a way to brush up on my reactjs skills as I had mostly dealt with react previously by adding custom react components to an existing site through Raisely. So I wanted to build a project from start to finish instead. I typically like to use vanilla JavaScript for small projects or frameworks like Svelte for more interactive ones, so this was new for me.

The Game

The basic idea is to try and come up with anagrams of the current word/letters. The catch is that once you submit a valid word, you can now only use the letters from that word. You get a point for each word you submit and the game ends when you have ran out of valid words.

For example, a game could go like this:

Turn 1
current letters: REACT
submitted word -> CARE

Turn 2
current letters: CARE
submitted word -> CAR

Turn 3
current letters: CAR

Building the UI

Colour Scheme

The colour scheme was created on coolors.co using some pastel colours to fit what I thought a modern word game might look like.

Fonts

I used Google Fonts to look for some aesthetically pleasing fonts that would suit the game.

I chose a serif font for displaying the remaining letters as this would give the best legibility (especially for capital letters). The font chosen was Rosarivo.

For the other text such as headings or instructions I chose the sans-serif font Reddit Mono for a modern look.

The fonts and font imports were optimised with Transfonter.

Layout and Styling

The rest of the styling such as layout, buttons, etc was done using plain CSS as this is the easiest option for me.

Valid words and starting words

Precomputing scores

Initially I thought it would be important to choose starting words that allowed for higher possible scores, and also for the user to know what the highest score is for that word. In order to have this information available I had to precompute the scores for all of the words in the word list (~25,000 words).

I used a simple recursive approach with memoization to find the highest possible score for each word. I started by finding all the anagrams for each word and stored this data. Then for each word, find which anagram to choose based on the highest score of each anagram, and then base the current word’s score of this. It involves a recursive function to find the score of each anagram and the solution comes together nicely.

In the end I chose the top 2,000 words with the highest possible scores and this set became the starting words. The text file containing the words and their corresponding highest scores is shown below.

CONVERSATIONALIST 26
CONVERSATIONAL 25
CONVERSATIONS 25
ENVIRONMENTALIST 25
EXTRATERRESTRIALS 25
OVERCOMPENSATING 25
RATTLESNAKES 25
REVOLUTIONARIES 25
UNCHARACTERISTICALLY 25
ALTERNATIVES 24
... 1990 more lines
View full filestartingWords.txt

And the text file containing the starting words and an optimal solution for each is shown below.

CONVERSATIONALIST CONVERSATIONALIST,CONVERSATIONAL,CONSERVATION,CONVERSATION,CONTAINERS,CREATIONS,REACTIONS,ANCESTOR,COASTER,CATERS,CRATES,REACTS,TRACES,RATES,STARE,TEARS,EAST,EATS,SEAT,TEAS,ATE,EAT,ETA,TAE,TEA,AT,TA
CONVERSATIONAL CONVERSATIONAL,CONSERVATION,CONVERSATION,CONTAINERS,CREATIONS,REACTIONS,ANCESTOR,COASTER,CATERS,CRATES,REACTS,TRACES,RATES,STARE,TEARS,EAST,EATS,SEAT,TEAS,ATE,EAT,ETA,TAE,TEA,AT,TA
CONVERSATIONS CONVERSATIONS,CONSERVATION,CONVERSATION,CONTAINERS,CREATIONS,REACTIONS,ANCESTOR,COASTER,CATERS,CRATES,REACTS,TRACES,RATES,STARE,TEARS,EAST,EATS,SEAT,TEAS,ATE,EAT,ETA,TAE,TEA,AT,TA
ENVIRONMENTALIST ENVIRONMENTALIST,REVELATIONS,RELATIVES,VERSATILE,EARLIEST,RELATES,STEALER,ALERTS,ALTERS,SLATER,LEAST,SLATE,STALE,STEAL,TALES,EAST,EATS,SEAT,TEAS,ATE,EAT,ETA,TAE,TEA,AT,TA
EXTRATERRESTRIALS EXTRATERRESTRIALS,EXTRATERRESTRIAL,TERRESTRIAL,RETAILERS,EARLIEST,RELATES,STEALER,ALERTS,ALTERS,SLATER,LEAST,SLATE,STALE,STEAL,TALES,EAST,EATS,SEAT,TEAS,ATE,EAT,ETA,TAE,TEA,AT,TA
OVERCOMPENSATING OVERCOMPENSATING,CONSERVATION,CONVERSATION,CONTAINERS,CREATIONS,REACTIONS,ANCESTOR,COASTER,CATERS,CRATES,REACTS,TRACES,RATES,STARE,TEARS,EAST,EATS,SEAT,TEAS,ATE,EAT,ETA,TAE,TEA,AT,TA
RATTLESNAKES RATTLESNAKES,RATTLESNAKE,ALTERNATES,TRANSLATE,RATTLES,STARLET,STARTLE,ALERTS,ALTERS,SLATER,LEAST,SLATE,STALE,STEAL,TALES,EAST,EATS,SEAT,TEAS,ATE,EAT,ETA,TAE,TEA,AT,TA
REVOLUTIONARIES REVOLUTIONARIES,REVELATIONS,RELATIVES,VERSATILE,EARLIEST,RELATES,STEALER,ALERTS,ALTERS,SLATER,LEAST,SLATE,STALE,STEAL,TALES,EAST,EATS,SEAT,TEAS,ATE,EAT,ETA,TAE,TEA,AT,TA
UNCHARACTERISTICALLY UNCHARACTERISTICALLY,UNREALISTIC,REALISTIC,ARTICLES,RECITALS,CARTELS,SCARLET,ALERTS,ALTERS,SLATER,LEAST,SLATE,STALE,STEAL,TALES,EAST,EATS,SEAT,TEAS,ATE,EAT,ETA,TAE,TEA,AT,TA
ALTERNATIVES ALTERNATIVES,ALTERNATES,TRANSLATE,RATTLES,STARLET,STARTLE,ALERTS,ALTERS,SLATER,LEAST,SLATE,STALE,STEAL,TALES,EAST,EATS,SEAT,TEAS,ATE,EAT,ETA,TAE,TEA,AT,TA
... 1990 more lines
View full filestartingWordsOptimalPlay.txt

choosing starting words deterministically based on date

I wanted the game to be similar to Wordle in that a word would be chosen each day (or custom number of hours), and that anybody accessing the game in any location in the world would have the same starting word. This can be done on the client-side by using a deterministic function to take the date (the number of milliseconds elapsed since January 1, 1970, 00:00:00 UTC, the Unix epoch) and convert it into an index into the array of starting words. Example code is below. The hash function is used to randomise the order of starting words (deterministically), rather than cycling through the list since it was in alphabetical order.

const NUM_HOURS_FOR_NEW_WORD = 24;

function universalHash(key, size) {
    const A = 13;
    const B = 7;
    const PRIME = 31;
    let hash = 0;
    for (let i = 0; i < key.length; i++) {
        hash = (hash * A + key.charCodeAt(i)) % PRIME;
    }
    hash = (hash * B) % PRIME;
    return hash % size;
} 

const currentTime = Math.floor(
    Date.now() / (NUM_HOURS_FOR_NEW_WORD * 60 * 60 * 1000)
);
const index = universalHash(currentTime.toString(), words.length);

Future Plans

Site last deployed on: