Blazordle - a Wordle solver (and some JetBrains AI Assistant)

Tue, Dec 12, 2023 17-minute read

I love words. And puzzles. And logic. And so Wordle is right up there for me. Not in a ‘posting it on Twitter’ sort of a way, though. Something about the simplicity of the gameplay and the English language (and the success story of a simple viral app, of course) is just cool. Right?

But it only recently occurred to me - what it would be like to write a solver for it1? How hard would it be? Not because I need or wanted a solver to help play the game, but simply because I was intrigued by the complexity. It was also a decent excuse to write some code, something I don’t get to do a lot nowadays, and maybe a chance to play with some shiny things in .NET8 2. And also the recently announced AI Assistant in JetBrains Rider… could I write my usual janky code and then have an AI smarten it up right nice?

Let’s find out!

Caveat-y-note-y-thing: I produced this solver purely for the programming challenge and a bit of fun. There are already plenty of other solvers out there (although not sure of any in Blazor!3) and I have no particular feelings about how you use it. If you want to ‘cheat’ at Wordle, well, that’s up to you. I also haven’t spent any real time on the interface (surprising, I know) and so it probably doesn’t work very well… particularly on mobile.

If you are just looking for the solver, then head over here. Or hell, Google it and you’ll find a million others.

Basic requirements

The premise of Wordle is pretty similar - the game chooses a 5 letter word and you have 6 guesses to figure out what it is. After each guess, the game gives you some feedback. Each letter of your guess can either be:

  1. Correct - it’s the right letter in the right place (green background)
  2. RightButWrong - the letter exists in the word, but you have it in the wrong location (yellow background)
  3. Wrong - the letter is not in the word at all (grey background)

There is a little bit of nuance here since words with repeating letters are allowed so for instance you could have a word with two letter E’s in it where one is in the right place and one is in the wrong place. These rules determined the logic for implementing the solver.

The main Wordle game interface is lovely and tactile - you enter the letters in squares and there is nice animated feedback for each guess. The thinking for my basic solution therefore was to mirror the input interface where you input your guess, as well as the feedback from Wordle and use that to update a list of possible solutions.

Solving algorithm

The problem fits within a branch mathematics known as discrete optimisation and is specifically a constraint satisfaction problem. The size of solution space is fixed (discrete) and so this actually makes it a less complex to solve than continuous optimisation.

Given a set of possible solutions, the goal is to minimise (reduce) the number of possible solutions down to as few as possible. Given the constraints (above) we know that the most efficient order of optimisation is green (known good positions), grey (known bad letters) and finally yellow (good letters but in the wrong place - all we know is where they don’t exist.)

Project setup and interface

I span up the default Blazor server project template (without the gubbins like counter click). That gives you a nice blank page and not a lot else. The only UI bits I really wanted to spend time on were:

  1. Guess entry (and mirroring the letter highlight from Wordle)
  2. The ’live update’ of the possible solutions as you provide the feedback

I asked ChatGPT to create the basic Wordle game HTML/CSS:


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Wordle-like Input Form</title>
<style>
  .wordle-board {
    display: flex;
    flex-direction: column;
    width: fit-content;
    margin: auto;
    padding: 20px;
  }
  .wordle-row {
    display: flex;
    margin-bottom: 5px;
  }
  .wordle-row:last-child {
    margin-bottom: 0;
  }
  .wordle-box {
    width: 58px; /* size of the box */
    height: 58px; /* size of the box */
    margin-right: 6px; /* spacing between boxes */
    font-size: 32px; /* size of the text */
    border: 2px solid #d3d6da;
    text-transform: uppercase; /* make all letters uppercase */
    text-align: center;
    line-height: 58px;
    box-sizing: border-box;
  }
  .wordle-box:last-child {
    margin-right: 0;
  }
</style>
</head>
<body>

<div class="wordle-board">
  <!-- Each .wordle-row represents a row for guesses -->
  <div class="wordle-row">
    <input type="text" maxlength="1" class="wordle-box">
    <input type="text" maxlength="1" class="wordle-box">
    <input type="text" maxlength="1" class="wordle-box">
    <input type="text" maxlength="1" class="wordle-box">
    <input type="text" maxlength="1" class="wordle-box">
  </div>
  <!-- Repeat the above div for as many guesses as you need -->
</div>

</body>
</html>

which was plenty good enough for my needs.

I got the basics of the game working by plumbing in the HTML and then converting the guess board in to a razor component which meant I could reuse it. I created a quick class to capture a ‘Letter’ (spot the primary constructor ftw!):


public class Letter(int position)
{
    public enum LetterState
    {
        None,
        Right,
        RightWrong,
        Wrong
    }
    
    public string Name => $"letter{Position}";
    public InputText? Input { get; set; }
    public int Position { get; } = position;
    public string? Value { get; set; }
    public LetterState State { get; private set; } = LetterState.None;

    public void SetState()
    {
        State = State switch
        {
            LetterState.None => LetterState.Right,
            LetterState.Right => LetterState.RightWrong,
            LetterState.RightWrong => LetterState.Wrong,
            _ => LetterState.None
        };
    }
    
    public override string ToString()
    {
        return $"{Value}:{Position}:{State}";
    }
}

and this can be used in my component:

private List<Letter> Letters { get; } =
    [
        new Letter(1),
        new Letter(2),
        new Letter(3),
        new Letter(4),
        new Letter(5)
    ];

and rendered:

<div class="wordle-row">
    @foreach (var letter in Letters)
    {
        <InputText maxlength="1" class="wordle-box" @key="letter" @ref="letter.Input" name="@letter.Name" @bind-Value="letter.Value" @oninput="_ => LetterEntered(letter)" @onclick="() => LetterClicked(letter)" style="@GetStyleForState(letter.State)"/>
    }
</div>

This is slightly janky since my class needs a reference to Microsoft.AspNetCore.Components.Forms to store the ElementReference to be able to focus the control in the page. There’s probably a better way, but I was OK with this.

I had another quick chat with ChatGPT about generating a wordlist for me, which it resolutely refused to do! So I had to go old school and use Google, like some sort of neanderthal. That was relatively quick to do and found a nice list of around 5000 common 5-letter words. Finally, I span up a quick .NET8 App Service in Azure and since I’m using Github for the repo, just for giggles, configured a CD pipeline in the Deployment Center. This was frighteningly simple to do!

So I had an app that looked a bit like a Wordle solver… but didn’t actually do anything.

Communicating back and forth with the component(s)

Although a Wordle game allows up to 6 guesses, in my implementation I just need to start with one and then be able to dynamically add more as I may need them. In order to pass information between the components and the parent, I’m using a Service pattern, which is dependency injected in to the component. The service is very simple - it’s moreorless just a (single) place for each of the components to talk to with the user input.

Possibly the only interesting bit is to register an event on it:

public event Action? OnChange;

private void NotifyStateChanged() => OnChange?.Invoke();

The parent component can thus subscribe to that event:

protected override void OnInitialized()
    {
        _possibleWords.Shuffle();
        _filteredWords = _possibleWords.ToList();
        GuessService.OnChange += FilterWordList;
    }

which allows the live update of the possible solutions when something in one of the component changes. Nifty.

The approach is simple - as the ‘State’ of a letter changes (by clicking on it), the component tells the service, which raises an event that the parent is listening to. It then calls its ‘filter’ method - to filter the wordlist down, based on the constraints.

Filter logic

I’ve been trialling Github CoPilot for a while, but it occurred to me that this might be a fun way to give the JetBrains AI Assistant something to do.

So I fired out some code to scaffold a basic implemention of the filtering rules:


public void FilterWordList()
    {
        _filteredWords = new List<string>();
        
        // check those that have known positions and then later filter those
        if (_guessService.HasPerfectMatch)
        {
            foreach (var word in _possibleWords)
            {
                var isMatch = true;
                foreach (var guess in _guessService.guesses.Where(x=>x.state == GuessService.LetterState.Right))
                {
                    isMatch &= HasCharAtMatchingPosition(word, guess.value, guess.position);
                }
                
                if(isMatch)
                    _filteredWords.Add(word);
            }
        }
        else
        {
            _filteredWords = _possibleWords.ToList();
        }
        
        // now filter out the noise
        if (_guessService.HasWronguns)
        {
            var baddies = new List<string>();
            foreach (var wrong in _guessService.guesses.Where(x => x.state == GuessService.LetterState.Wrong).Select(x=>x.value))
            {
                foreach (var word in _filteredWords.Where(word => word.Contains(wrong)))
                {
                    baddies.Add(word);
                }
            }

            foreach (var baddy in baddies)
            {
                _filteredWords.Remove(baddy);
            }
        }
        
        if (_guessService.HasImperfections)
        {
            var others = new List<string>();
            foreach (var other in _guessService.guesses.Where(x => x.state == GuessService.LetterState.RightWrong))
            {
                foreach (var word in _filteredWords.Where(word => !word.Contains(other.value) || HasCharAtMatchingPosition(word, other.value, other.position)))
                {
                    others.Add(word);
                }
            }

            foreach (var other in others)
            {
                _filteredWords.Remove(other);
            }
        }
        
        StateHasChanged();
    }

It works, but was not going to win any prizes. Ordinarily, ReSharper would kick in at this point and turn all those nested ForEach statements in to 2 lines of LINQ. But instead, I started by asking AI Assistant to describe what the code was doing, and its response:

The FilterWordList method filters a list of words _possibleWords based on certain criteria and modifies _filteredWords which is a subset of _possibleWords. This method seems to be part of a guessing game like code breaker or hangman, where we have some knowledge about the positions and correctness of certain letters in the words. Here’s a breakdown of what the code does: Perfect Match Filtering: The code first checks if there are any perfect matches, i.e., instances where a guessed letter is in the correct position within the word (as indicated by GuessService.LetterState.Right). If so, it iterates through each word in _possibleWords and checks if all letters in the correct positions match for the word. If it matches, the word is added to _filteredWords. Wrong Characters Filtering: It then checks if there were any wrong guesses (i.e., GuessService.LetterState.Wrong). If so, it goes through each wrongly guessed characters and removes any word from _filteredWords that contains this incorrect character. Imperfect Match Filtering: Lastly, if there are any imperfect matches (i.e., guessed letter is correct but in the wrong position as signified by GuessService.LetterState.RightWrong), the words containing guessed letter in the correct position or not containing it at all are removed from _filteredWords. It finally calls StateHasChanged() method, which is a command in Blazor to tell the system that the state has changed and the UI needs to be updated. Thus, the main goal of this method is to reduce the list of possible words based on the information about guessed letters and their positions.

Which is of course, pretty much spot on. This is definitely fairly impressive, particularly its determination around the purpose (as part of a game) and figuring out the nuance of the ‘Imperfect Match Filtering’. I also like that it recognised StateHasChanged as being a Blazor specific command.

So let’s see if it really knew what it was talking about, by asking it to suggest improvements to that code. Its response:

Here’s how I’d refactor your FilterWordList method. I noticed that your code could benefit from using the List.RemoveAll method which should improve performance as it eliminates the need to create temporary lists and reduces the number of iterations over the collection. I’ve switched from using respective foreach statements to directly using Linq where possible, which reduces the amount of code and improves readability. I’ve replaced the creation and use of ‘baddies’ and ‘others’ lists with the List.RemoveAll method, which simplifies the code. Also using All method in Linq to check if all characters are at the matching position is more efficient and makes the code easier to read and understand. Note: You should adjust the _filteredWords list modifications according to your project’s specific requirements.

And the code it generated:


public void FilterWordList()
{
    _filteredWords = new List<string>();

    // check those that have known positions and then later filter those
    if (_guessService.HasPerfectMatch)
    {
	    // Use predicate inside Where to avoid repetitive checking
        _filteredWords = _possibleWords.Where(word =>
        {
            return _guessService.guesses
                .Where(x => x.state == GuessService.LetterState.Right)
                .All(guess => HasCharAtMatchingPosition(word, guess.value, guess.position));
        }).ToList();
    }
    else 
    {
	    _filteredWords = _possibleWords.ToList();
    }

    // now filter out the noise
    if (_guessService.HasWronguns)
    {
        _filteredWords.RemoveAll(word =>
        {
            return _guessService.guesses
                .Where(x => x.state == GuessService.LetterState.Wrong)
                .Select(x => x.value)
                .Any(wrong => word.Contains(wrong));
        });
    }

    if (_guessService.HasImperfections)
    {
        _filteredWords.RemoveAll(word => 
        {
            return _guessService.guesses
                .Where(x => x.state == GuessService.LetterState.RightWrong)
                .Any(other => !word.Contains(other.value) || HasCharAtMatchingPosition(word, other.value, other.position));
        });
    }

    StateHasChanged();
}

Which is excellent - very clear, readable fluent LINQ, and helpful use of comments.

Surprisingly, the built-in suggestions and context actions (i.e. ReSharper) didn’t try and refactor the code to that degree. (Which often has the habit of making the resultant code completely unmaintainable!)

So at this point I really started to fear for my job, and just asked it flat out to write the whole app for me.

This wasn’t quite so impressive, though, as it moreorless just said nope:

using System;
using System.Collections.Generic;
using System.Linq;

class Program
{
    static void Main()
    {
        // This would be the mystery word in real game, it's for simulation here.
        var targetWord = "apple"; 

        // This is your word list, could be reading from a dictionary file
        var wordList = new List<string> {"apple", "grape", "lemon", "mango", "peach"};

        // Wordle game loop
        int attempts = 1;
        while (true)
        {
            // Making guess from wordList. 
            // In real scenario you'd need a smarter way, maybe using frequency analysis of English words
            var guess = wordList[0];

            Console.WriteLine($"Attempt {attempts}: {guess}");

            if (guess == targetWord)
            {
                Console.WriteLine("Congratulations! You have guessed the word!");
                break;
            }
            else
            {
                // Remove guess since it's wrong
                wordList.RemoveAt(0); 

                if (wordList.Count == 0)
                {
                    Console.WriteLine("Sorry, word not found within given attempts !");
                    break;
                }
            }

            attempts++;
        }

        Console.ReadLine();
    }
}

ChatGPT did very marginally better, but still didn’t provide anything like a solution:

This solver simply guesses the word from a word list sequentially. It removes the faulty guess and takes up next word from the list in the next attempt. But, to write an efficient solver, you’d want to implement some sort of logic to use the feedback from the game. For example, if one letter is reported to be correct but in the wrong place, you might try permutating letters in the word to produce the next guess. It’s seriously complex task you could embark on to make it as efficient as possible!

using System;
using System.Collections.Generic;
using System.Linq;

public class WordleSolver
{
    private List<string> possibleWords;

    public WordleSolver()
    {
        // Initialize the list of words
        possibleWords = LoadWords();
    }

    private List<string> LoadWords()
    {
        // Load your list of words here
        return new List<string>(); // Placeholder
    }

    public string MakeGuess()
    {
        // Implement your guessing strategy
        // For now, just pick a random word
        return possibleWords[new Random().Next(possibleWords.Count)];
    }

    public void ProcessFeedback(string guess, string feedback)
    {
        // Update the possibleWords list based on the feedback
        // 'G' for Green, 'Y' for Yellow, 'B' for Gray/Black
        // Example: "GYBGB"
    }

    // Additional methods as needed
}

class Program
{
    static void Main(string[] args)
    {
        var solver = new WordleSolver();
        bool wordFound = false;

        while (!wordFound)
        {
            string guess = solver.MakeGuess();
            Console.WriteLine("Guess: " + guess);

            // Simulate or get feedback from the game
            Console.WriteLine("Enter feedback (G, Y, B): ");
            string feedback = Console.ReadLine();

            if (feedback.All(f => f == 'G'))
            {
                Console.WriteLine("Word found!");
                wordFound = true;
            }
            else
            {
                solver.ProcessFeedback(guess, feedback);
            }
        }
    }
}

Phew - I won’t be out of a job just yet then. (Although these result were hardly a surprise since LLMs are becoming lazy!)

Moar

v1 of the app works and should be available to play with here. (It’s only on a freebie Azure app service so it may not always be alive!) The source code is available at Github.

It works well enough right now, and usually solves a Wordle within 4 to 5 moves. (There’s obviously a chance it will fail to solve one due only to the word not being on the wordlist. We should also really remove words that have already appeared in previous games since they are unlikely to repeat them.)

WordleSolver1

Today’s wordle of ‘WOULD’ though caused a few interesting issues. the solver arrived at the solution in 4 or 5 moves for starting words of ‘ROGUE’ (my go-to starting word!) but also ‘ADIEU’ which is allegedly amongst the best. This is with no optimisation around the ordering of the result set (in fact it’s just displayed in a random order.)

So an interesting challenge would be to see if it could solve them more quickly or efficiently and really the only opportunity here is providing the user with more guidance on which guess to use in moves 2 onwards. Since the NYTimes editors presumably pick the target words quasi-randomly (i.e., they are going to pick ‘fairly common’ words) there is no clear options here, other than:

  1. Prioritising words with high-frequency letters (i.e.., vowels, S, R, T etc.) if only to maximise the number of solutions that are eliminated after each wrong move
  2. De-prioritising words with low-frequency letters (i.e.., avoiding obscure words that contain e.g., X, Z, J etc.)
  3. Prioritising based on how many vowels we’ve seen (or not)
  4. Performing some word frequency / density / commonality analysis, and prioritising those that are ‘more common’ (tenuous)
  5. A much more rigorous linguistic analysis of the words

Starting with options 1 and 2, I updated the load of the wordlist to incorporate a weighting for each word, based on the frequency of its letters in the English language (the source of which AI Assistant helped with):

public class LetterFrequencyAnalyzer
{
    // Letter frequencies in English language text
    private readonly Dictionary<char, double> letterFrequencies = new Dictionary<char, double>
    {
        {'a', 8.167}, {'b', 1.492}, {'c', 2.782}, {'d', 4.253}, {'e', 12.702},
        {'f', 2.228}, {'g', 2.015}, {'h', 6.094}, {'i', 6.966}, {'j', 0.153},
        {'k', 0.772}, {'l', 4.025}, {'m', 2.406}, {'n', 6.749}, {'o', 7.507},
        {'p', 1.929}, {'q', 0.095}, {'r', 5.987}, {'s', 6.327}, {'t', 9.056},
        {'u', 2.758}, {'v', 0.978}, {'w', 2.360}, {'x', 0.150}, {'y', 1.974}, {'z', 0.074}
    };

    public double CalculateWordWeight(string word)
    {
        return word.ToLower()
                   .Where(char.IsLetter)
                   .Sum(c => letterFrequencies.ContainsKey(c) ? letterFrequencies[c] : 0);
    }
}

and then ordered the result set based on the weight of each word descending. I tried the same wordle (‘WOULD’) again, starting with ‘ADIEU’ starting word… and this time it didn’t solve it within 6 guesses. So this actually made it worse! As a further test, I updated the frequency analysis to recompute the frequency based only on the letters in the source wordlist, and this time it resolved to the same result as before (i.e. solving within 6 guesses.)

This tends to imply that optimisation of the next guess ranking needs to be a lot more sophisticated with some linguistic analysis.

Last hurrah

As I final attempt, I changed strategy, and this time started with two guesses from ’the most popular words’ startlist where the letter were (mostly) distinct - ‘WORSE’ and ‘PAINT’. The theory here being that by far the most valuable feedback is the correct position of known letters, so by maximising your chances of finding them (by effectively have two starting guesses) you rapidly narrow down the set of possible solutions. In this case, it found the solution in just four moves.

WordleSolver2

If you switch to ‘WORSE’ and ‘ADIEU’ then it gets there in just three moves!

WordleSolver3

(So this really highlights the importance of the luck of your starting word - in this case matching ‘WO’ at the start.)

Close it down

This was definitely a good challenge but definitely not as complex as I was expecting. Complexity definitely remains in making a recommendation about which guess should come next. And probably some calculation of the sheer importance of luck / chance in this, which I suspect is by far the biggest factor, would yield most intel.

Hope this was fun / useful / interesting - do leave comments below if so. I’m going to move on to something else…!


  1. The initial idea was actually to write a solver for Murdle. As I started scratching out ideas for how to do that, I realised that that was actually going to be pretty hard, so before trying that, I really needed to dust off my programming brain, and figured that a Wordle solver would be a simpler challenge and a good place to start. I’m moving on to the Murdle solver next! ↩︎

  2. So the first thing I did was to update Rider to the latest version and then download the .NET8 SDK. That was all mostly fine, but at time of writing a lot of the cool new stuff isn’t supported in Rider yet. D’oh. ↩︎

  3. Whilst trying to find an available hostname when creating the Azure App Service, I stumbled across this one, which is Blazor a pretty sucky implementation, IMHO. ↩︎