A Word Game Development - rangram.com January 23, 2025
The story behind my recently launched word game, rangram.com
I'm a fan of word games. Like Wordl, and the NY Times Spelling Bee game. I had an idea pop into my head.
I might get a cease and desist but “pangram” is a common word, any resemblance to the NYT Spelling Bee game is intentional, and is kind of an homage to it.
The game is located at www.rangram.com. Here are the nerd details!
First off, getting a good word list is a pain! But certainly infinitely easier than building a word list. So I'm thankful for that! But yeah, a lot of word lists I find on the internet run the gamut on quality. Some have punctuated words, some with numbers in them (they're numbers, not words), and some with various curse words and horrible slurs. The one I ended up using did have all of the slurs you can imagine, but it suffixed anything questionable with a “!”, so they were easy to filter out.
I don't blame them for including them, they are words, unfortunately. I am very thankful that they marked them, so I don't accidentally offend millions of people because a racist or sexist or otherwise offensive word pops up in my game.
I found what I think is the best word list here (the anagram dictionary), and it's licensed under the MIT license (basically, I'm free to do with it what I please). Amazing.
Next! There are a ton of words there. 89000+. I had to get that down to pangrams, and words that could be made from those pangrams. And in my particular case, I also needed to whittle it down to like 5 or 6 words, and also ensure that of the words that are included, make sure every letter in the pangram is represented! So that it is solvable with all clues unveiled.
This obviously would take a very long time if done by hand. So I wrote a program, which I'll walk through now.
Filters
The first step of getting words that I want, is to filter the list of words so that they are all at least length 4 and have no punctuation, or just letters, more aptly.
The filter function just runs them all against a regular expression and adds those that match. ^[a-z]+$ . Easiest regex ever.
The getMins function returns those that are at least N length. In this case I wanted all 4+ length words
With all of the words of at least 4 length, I wanted to get the words with 7 unique letters. This is pretty easy, although inefficient, with a map. I do this in my getPangrams function.
After that, multiple words can share the same 7 unique letters, hence there being multiple pangrams in a given puzzle sometimes. I group them by key and return a list of [key, words] where words is a list of pangrams, and key is the 7 unique letters, sorted alphabetically. This is in the function getUnique.
Next, but not last, I want to know what words can be formed from the unique letters in the pangram. I form a new structure for this with the Key (the unique 7 letters in the pangram), the list of pangrams, a new list of “LengthCount” which is the count of each length of word that can be made from those letters (like, 10 4-letter words, 12 5-letter words, etc), and the list of words, not including the pangrams (this is important :D). I do this in my function I named getSolves. This returns a complex structure so I save it as json.
This final step is extremely important!! I wanted to make a fair game, but also at the same time, people aren't guessing the words in my list of words, they're only trying to figure out the pangram, so I don't need every word. Here's the link to the method getProbableSolves.
getProbableSolves
First, it takes the list of solves that were gathered in the previous step. The next parameter is the minwords, which is passed in via the flags on the command line. And maxwords as well.
minwords is the minimum amount of words that I want in my final puzzle. This is the total amount of words, not caring about the length of it. The “solve” must have this amount, or it won't be in the game. I typically set this to 15.
maxwords is the maximum amount of words that I want for each length of word. This will keep the size of the game down on the screen! So I won't have like 30 words and the top part of the screen isn't visible anymore because it's scrolled for miles.
This function can also balloon (times 7 to be exact) the list of solves, because for each solve, it will make each letter the center letter, and then get all of the words that can be made with that letter required.
However, the most important step is this final part. I wanted words in the final puzzle that will eventually have all of the letters of the pangram. And if you're taking just 5 words from each length, as I am, that's not guaranteed. So if someone reveals all of the hints, every letter is in there somewhere (maybe not for every length of clue, but within all clues as a whole).
So before I take 5 words from each length, I actually sort them descending based on how many letters it uses in the pangram. So, flour would be sorted before four. This part doesn't care about the length.
So that's it. After that, I take the output from getProbableSolves and it's literally, byte for byte, the input to my game. Pretty confident in it without verifying the 46 megabyte file that it created! No one has time for that. Better to be a good developer who doesn't need to verify :) The program resulted in 62562 unique puzzles for me to use. At one per day, I have enough for 171 years!
The Game
The game is made up of the server and the client. The client is just the website, with html and javascript. It's vanilla javascript, but the way it communicates with the server is through fetch. It builds the DOM dynamically, and uses local storage for the game state. What isn't tested yet is when a new puzzle is ready… we'll see! I'll be up to test.
The only “table” in the backend is for a puzzle. Datastore is a document database. So it has all of the data in the one record. The lengths, the words, the pangrams, etc.
The random number generator is set up with a seed, so I knew the first puzzle from today. I got a perfect score ;) However, I do think I need to make sure that I don't select the same one twice. Or not, with 171 years worth of puzzles. There will be dups due to the nature of multiple puzzles that share the same pangrams but with a different center letter. I'm not too worried about it.
But the backend has just 3 methods. 1. Get the puzzle metadata which will return the key (rgpuzzle-YYYYmmdd) and the puzzle hint lengths (how many words of each length there are in the hint words). 2. Get a guess. This will take the words that you were already given as a hint, for a specific length, and return the next word in the list. 3. Make a guess. This will tell you whether you're correct or not, as well if you had already guessed it.
The front end displays that information using javascript fetch and DOM manipulation. That code can be easily seen on the website, as I'm not really obscuring it! Here's me literally inspecting the website in debugger
And game.js
This game is easily “hackable” or “gameable”. You could find the pangram, go into dev tools and clear your local storage, and enter the pangrams to get a perfect score. You could also use dev tools to modify the html on the “gameover” screen. I'm not worried about it :) I mean, I could solve Wordl on the website on my computer, then solve it in one guess on my phone. These are things I'm not worried about. However, for the backend, I fear I would expose some hidden things about my Google account or the Google Cloud setup that I have, so I'll keep that repo private!
This was fun to make, and hopefully it's fun to play! It took me about a day total. It's 879 lines of Go, and 413 lines of javascript. I will take any feedback you have to offer!
Happy coding!