DEV Community πŸ‘©β€πŸ’»πŸ‘¨β€πŸ’»

Cover image for How I built an NYT Spelling Bee solver with Elixir
Adam Davis
Adam Davis

Posted on • Originally published at brewinstallbuzzwords.com

How I built an NYT Spelling Bee solver with Elixir

While Wordle has caught Twitter feeds by storm, there’s something I love about playing the New York Times Spelling Bee game each morning.

It works by providing a set of letters, and you have to come up with as many words as you can that:

  • Only use the provided letters
  • Use the center letter at least once
  • Are at least 4 characters long

Here’s what a game might look like:

Screenshot of a game of NYT Spelling Bee

Recently, I took it upon myself to build a solver for this game using Elixir. Most of the code is in this post, but if you'd like to see the whole project you can check it out on GitHub.

Defining success

Because the words in Spelling Bee are curated and intentionally exclude obscure words, we should expect that this program will suggest words that are not included in the solution.

So I’ll be judging the success of this program by the percentage of the solution words that are identified, ignoring suggested words that are not part of the official solution.

Getting a list of words

My approach was pretty simple. I needed to start with a list of English words, then filter based on the criteria of the game.

To get a list of English words, I used the word_list package that I created.

Starting the project

Once I created my new project, I imported my word_list package by adding it in the deps array in mix.exs

defp deps do
  [
    {:word_list, "~> 0.1.0"}
  ]
end
Enter fullscreen mode Exit fullscreen mode

Then, I installed the dependency by running mix deps.get

Test-driven development

Next, I wrote some basic tests so I can define some of the basic functionality.

defmodule SpellingBeeSolverTest do
  use ExUnit.Case
  doctest SpellingBeeSolver

  test "finds some valid words" do
    word_stream = SpellingBeeSolver.solve("t", ["m","y","r","i","f","o"])

    assert "mortify" in word_stream
    assert "fifty" in word_stream
  end

  test "all words include center letter" do
    word_stream = SpellingBeeSolver.solve("t", ["m","y","r","i","f","o"])

    assert Enum.all?(word_stream, fn word -> String.contains?(word, "t") end)
  end
end
Enter fullscreen mode Exit fullscreen mode

These tests make sure that at least some valid words are found, and that all of the words include the center letter.

Writing the code

The solve/2 function takes the following approach:

  1. Get the stream of English words
  2. Apply a filter for length greater than 3, since words must be at least four letters long
  3. Apply a filter that checks that the word contains the center letter
  4. Apply a filter that checks that all letters in the word are either on the edge or the center letter

The following is the contents of the lib/spelling_bee_solver.ex file:

defmodule SpellingBeeSolver do
  def solve(center, edges) do
    WordList.getStream!()
    |> Stream.filter(fn word -> String.length(word) > 3 end)
    |> Stream.filter(fn word -> String.contains?(word, center) end)
    |> Stream.filter(fn word ->
      String.split(word, "", trim: true)
      |> Enum.all?(fn letter -> letter in edges ++ [center] end)
    end)
  end

  def printSolution(center, edges) do
    solve(center, edges)
    |> Enum.each(fn x -> IO.puts(x) end)
  end
end
Enter fullscreen mode Exit fullscreen mode

To print the solution, the inputs can instead be passed to printSolution/2

But does it work?

I tested my program against the solutions of three different days, and my program found all the solutions every time.

Success! βœ…

There were several extra words found each time, but since the solutions are a curated set of words this is to be expected.

Room for improvement

While I’m sure there’s more I could do, these are some areas that I think have room for improvement:

  • It would probably be more convenient to pass in a string for the edges instead of an array of strings
  • I haven’t done any input validation, so there could be some unexpected behavior if the input isn’t provided in the proper format
  • Currently, the program must be run in iex or imported into another Elixir project in order to be functional. It would be more practical to put this logic into a cli tool or a web interface.
  • If this were included as part of a larger project, it would be nice to track which words weren’t part of the previous solutions in order to avoid showing them in the future.

More content

If you liked this, you might also like some of my other posts. If you want to be notified of my new posts, follow me on Dev or subscribe to my brief monthly newsletter.

Top comments (6)

Collapse
 
harshhhdev profile image
Harsh Singh

Okay, this is cool. Elixir has always been a language that I wanted to try out, but haven't gotten the time to >:(

Collapse
 
brewinstallbuzzwords profile image
Adam Davis

It's a pretty fun language to learn! I actually have a series that follows my journey of learning it. Some of my own tutorials are there, along with links to other resources that I've found helpful.

Collapse
 
harshhhdev profile image
Harsh Singh

Cool! I’ll definitely dip my feet with Elixir over the summer when I have more time πŸ˜…

Collapse
 
spoder profile image
Spoder

Super sweet!

Collapse
 
brewinstallbuzzwords profile image
Adam Davis

Thanks!

Collapse
 
brewinstallbuzzwords profile image
Adam Davis

What project would you like to see next?

Are there any Elixir topics you'd like to see me write about?

Tired of sifting through your feed?

Find the content you want to see.

Change your feed algorithm by adjusting your experience level and give weights to the tags you follow.