Skip to content

Commit

Permalink
Add complements to try (unsuccessfully) to solve the game faster
Browse files Browse the repository at this point in the history
  • Loading branch information
idontwantcookies committed Jan 15, 2022
1 parent 67367a0 commit 86fa402
Show file tree
Hide file tree
Showing 10 changed files with 280 additions and 205 deletions.
75 changes: 63 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,21 +1,72 @@
# WordleSolver

**TODO: Add description**
This is a very simple, CLI-based program to solve password/word games of the likes of
[Wordle](https://www.powerlanguage.co.uk/wordle/). You will need to download a dictionary of words
for the language you want to play it with. For portuguese/br, you can find a great one [right here]
(https://www.ime.usp.br/~pf/dicios/index.html). I didn't include it in the project because even
though it is generated under a GPL licence, it has probably taken IME a lot of effort in order to
create it and they should be given credit where credit is due.

## Installation
This software works in a very simple manner. Given a list of words, you can choose what you want
to do with it: you can convert all words to lowercase, filter valid/invalid words using regex, trim
whitespaces, convert diacritics into base ascii letters, etc. Currently, only Portuguese and English
languages processing are supported, but feel free to add more parsers under
`language/your_language.ex`.

If [available in Hex](https://hex.pm/docs/publish), the package can be installed
by adding `wordle_solver` to your list of dependencies in `mix.exs`:
You will need Elixir/Erlang installed. To run the program, simply type into command line:

```bash
iex -S mix
```

In order to start using, let's say you want to play a game in PT-BR. If you donwloaded your
dictionary into `dicts/pt_br.txt`, you can start a simple Wordle game by running:

```elixir
iex> words = "dicts/pt_br.txt"
iex> |> Parser.import_dictionary() # reads a dict file and converts it into a list of strings
iex> |> Parser.trim() # trims all trailing whitespace
iex> |> Language.normalize(:pt_br) # removes accents and diacritics
iex> |> Parser.filter_number_of_letters(5) # drops words with 6+ or 4- letters
iex> |> Parser.filter_valid() # removes words that aren't exclusively lowcase a-z
iex> |> WordStats.order_by_scores() # calculates each word's score based on how many good letters it has and then sorts in desc score order
["rosea", "serao", "roias", "roais", "raios", "orais", "raies", "areis",
"aires", "seria", "sarei", "reais", "eiras", "meiao", "moera", "aremo",
"remoa", "aureo", "ecoai", "ecoar", "ateio", "terao", "rotea", "reato",
"lerao", "ecoas", "coesa", "secao", "acoes", "escoa", "aceso", "estao",
"tesao", "onera", "aloes", "leoas", "lesao", "odeia", "eroda", "rodea",
"adore", "doera", "maior", "irmao", "roiam", "mario", "morai", "riamo",
"raiou", "apeio", ...]
```

The first element from the list is always the best guess this program has for the current word.
Let's say you input that suggestion `ROSEA` and you get: green, yellow, black, black,
black. You should translate green to 2, yellow to 1 and black (or red) to 0. Our feedback is,
therefore, "21000", meaning that R is in the right place, O is correct but in the wrong place, and
S, E, A are not found in our word. Let's input this feedback:

```elixir
iex> Wordle.feedback(words, "21000")
["ruimo", "rimou", "ritmo", "ruido", "ruivo", "rifou", "rindo",
"rirmo", "rublo", "rigor", "rumor", "rumou", "rugou", "rufou", "rubro"]
```

Again, `ruimo` is the best guess. Let's say, however, you don't like that suggestion very much, and
you think `ruido` is a better guess. If you try that word in the game and you get a feedback like
"22202", you pass the word of choice as third input to `Wordle.feedback/3`:

```elixir
iex> |> Wordle.feedback("ruido", "22202")
["ruimo", "ruivo"]
```
Now, there are only 2 possible words. Let's say `ruivo` is the right one:

```elixir
def deps do
[
{:wordle_solver, "~> 0.1.0"}
]
end
iex> |> Wordle.feedback("22222", "ruivo")
["ruivo"]
```

Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc)
and published on [HexDocs](https://hexdocs.pm). Once published, the docs can
be found at <https://hexdocs.pm/wordle_solver>.
And that's it!

You can also invoke `Wordle.solve/2` to check how many attempts it takes for the software to find
your chosen word.
18 changes: 8 additions & 10 deletions lib/word_stats.ex
Original file line number Diff line number Diff line change
Expand Up @@ -6,30 +6,28 @@ defmodule WordStats do
iex> words = ["hi", "hello"]
iex> frequencies = WordStats.letter_frequencies(words)
%{
"e" => 0.14285714285714285,
"h" => 0.2857142857142857,
"i" => 0.14285714285714285,
"l" => 0.2857142857142857,
"o" => 0.14285714285714285
}
%{"e" => 1, "h" => 2, "i" => 1, "l" => 2, "o" => 1}
iex> WordStats.order_by_scores(words, frequencies)
["hello", "hi"]
"""

@spec letter_frequencies([binary]) :: map
def letter_frequencies(words) do
total = Enum.reduce(words, 0, fn word, acc -> acc + String.length(word) end)

words
|> Enum.map(&get_letter_count/1)
|> Enum.reduce(%{}, fn counts, acc ->
Map.merge(acc, counts, fn _key, count1, count2 -> count1 + count2 end)
end)
|> Enum.map(fn {letter, count} -> {letter, count / total} end)
|> Enum.map(fn {letter, count} -> {letter, count} end)
|> Map.new()
end

@spec order_by_scores([binary]) :: [binary]
def order_by_scores(words) do
scores = letter_frequencies(words)
order_by_scores(words, scores)
end

@spec order_by_scores([binary], map()) :: [binary]
def order_by_scores(words, letter_frequencies) do
words
Expand Down
163 changes: 25 additions & 138 deletions lib/wordle.ex
Original file line number Diff line number Diff line change
@@ -1,159 +1,46 @@
defmodule Wordle do
@moduledoc ~S"""
Basic state of an wordle game. The struct in this file retains information about the game,
Basic state of an solver game. The struct in this file retains information about the game,
including the sorted word list, according to how likely it is to be the right word; all words that
have been suggested so far; the scores each letter has; and the name of the file that was used as
a dictionary.
Usage:
iex> wordle = "dicts/test.txt"
iex> |> Parser.import_dictionary()
iex> |> Parser.trim()
iex> |> Language.normalize(:en)
iex> |> Parser.filter_number_of_letters(5)
iex> |> Parser.filter_valid()
iex> |> Wordle.new()
%Wordle{
scores: %{
"c" => 0.13333333333333333,
"d" => 0.06666666666666667,
"e" => 0.13333333333333333,
"f" => 0.06666666666666667,
"l" => 0.06666666666666667,
"r" => 0.06666666666666667,
"s" => 0.13333333333333333,
"t" => 0.06666666666666667,
"u" => 0.06666666666666667,
"y" => 0.06666666666666667,
"a" => 0.13333333333333333
},
suggestions: ["faces"],
words: ["faces", "clear", "study"]
}
iex> wordle
iex> |> Wordle.solve("study")
{
:ok,
%Wordle{
scores: %{
"a" => 0.13333333333333333,
"c" => 0.13333333333333333,
"d" => 0.06666666666666667,
"e" => 0.13333333333333333,
"f" => 0.06666666666666667,
"l" => 0.06666666666666667,
"r" => 0.06666666666666667,
"s" => 0.13333333333333333,
"t" => 0.06666666666666667,
"u" => 0.06666666666666667,
"y" => 0.06666666666666667
},
suggestions: ["study", "faces"],
words: ["study"]
}
}
iex> words = ~w(done cant wont play stay opus)
iex> Wordle.solve(words, "play")
{:ok, ["play", "done"]}
"""

@type t :: %Wordle{
words: list(binary),
scores: map,
suggestions: [binary]
}
alias Wordle.{Game, Solver}

defstruct [:words, suggestions: [], scores: %{}]

defmodule UnsolvableError do
defexception [:message]
end

@spec new([binary]) :: t
def new(word_list) when is_list(word_list),
do: %Wordle{words: word_list} |> calculate_scores() |> suggest()

@spec feedback(Wordle.t(), binary) :: Wordle.t()
def feedback(wordle = %Wordle{suggestions: [best_guess | _]}, feedback) do
feedback
|> String.codepoints()
|> Enum.with_index()
|> Enum.reduce(wordle, fn {letter_feedback, pos}, acc ->
letter = String.at(best_guess, pos)

case letter_feedback do
"0" -> wrong_letter(acc, letter)
"1" -> wrong_position(acc, letter, pos)
"2" -> right_position(acc, letter, pos)
end
end)
|> suggest()
end

@spec wrong_letter(Wordle.t(), binary) :: Wordle.t()
def wrong_letter(wordle, letter) do
words = Enum.reject(wordle.words, &String.contains?(&1, letter))

%Wordle{wordle | words: words}
end

@spec wrong_position(Wordle.t(), binary, integer) :: Wordle.t()
def wrong_position(wordle, letter, position) do
words =
wordle.words
|> Enum.filter(&String.contains?(&1, letter))
|> Enum.reject(&(String.at(&1, position) == letter))

%Wordle{wordle | words: words}
@spec solve([binary], binary) :: {:ok | :error, [binary]}
def solve(wordlist, right_word) do
solve(wordlist, right_word, [], wordlist)
end

@spec right_position(Wordle.t(), binary, integer) :: Wordle.t()
def right_position(wordle, letter, position) do
words = Enum.filter(wordle.words, &(String.at(&1, position) == letter))

%Wordle{wordle | words: words}
end

@spec solve(Wordle.t(), binary) :: {:ok | :error, Wordle.t()}
def solve(wordle = %Wordle{suggestions: [suggestion | _tl]}, suggestion), do: {:ok, wordle}
def solve(wordle = %Wordle{words: []}, _right_word), do: {:error, wordle}

def solve(wordle, right_word) do
feedback = build_feedback(wordle, right_word)
@spec solve([binary], binary, [binary], [binary]) :: {:ok | :error, [binary]}
def solve(wordlist, right_word, guesses, complement)

wordle
|> feedback(feedback)
|> solve(right_word)
end
def solve([], _right_word, guesses, _complement), do: {:error, guesses}
def solve([best_guess | _], best_guess, guesses, _complement), do: {:ok, [best_guess | guesses]}

@spec calculate_scores(Wordle.t()) :: Wordle.t()
defp calculate_scores(wordle) do
scores = WordStats.letter_frequencies(wordle.words)
words = WordStats.order_by_scores(wordle.words, scores)
def solve(wordlist, right_word, guesses, complement) do
guess = best_guess(wordlist, complement)
{guesses, feedback} = Game.guess(right_word, guess, guesses)
complement = Solver.complement(complement, guess)
wordlist = Solver.feedback(wordlist, guess, feedback)

%Wordle{wordle | scores: scores, words: words}
solve(wordlist, right_word, guesses, complement)
end

@spec suggest(Wordle.t()) :: Wordle.t()
def suggest(wordle = %Wordle{words: []}), do: wordle

def suggest(wordle) do
[best_guess | _tl] = wordle.words
@spec feedback([binary], binary, binary) :: [binary]
defdelegate feedback(wordlist, guess, feedback), to: Solver

%Wordle{wordle | suggestions: [best_guess | wordle.suggestions]}
end
@spec feedback([binary], binary) :: [binary]
defdelegate feedback(wordlist, feedback), to: Solver

@spec build_feedback(Wordle.t(), binary) :: binary
defp build_feedback(wordle, right_word) do
[suggestion | _] = wordle.words

suggestion
|> String.codepoints()
|> Enum.with_index()
|> Enum.map_join(fn {letter, pos} ->
cond do
letter == String.at(right_word, pos) -> "2"
String.contains?(right_word, letter) -> "1"
true -> "0"
end
end)
end
@spec best_guess([binary], [binary]) :: binary
def best_guess(_wordlist, [top_score_complement | _]), do: top_score_complement
def best_guess([top_score_word | _], []), do: top_score_word
end
40 changes: 40 additions & 0 deletions lib/wordle/game.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
defmodule Wordle.Game do
@moduledoc ~S"""
Basic state of an wordle game. The struct in this file retains information about the game,
including the expected word and which guesses have already been taken.
Usage:
iex> {guesses, _feedback} = Game.guess("word", "test")
{["test"], "0000"}
iex> {_guesses, _feedback} = Game.guess("word", "dont", guesses)
{["dont", "test"], "1200"}
"""

defmodule UnsolvableError do
defexception [:message]
end

@spec guess(binary, binary, [binary]) :: {[binary], binary}
def guess(right_word, guess, guesses \\ []) do
if String.length(right_word) != String.length(guess) do
raise ArgumentError,
"guessed word #{guess}, but it should have been #{String.length(right_word)} characters long."
end

guesses = [guess | guesses]

feedback =
guess
|> String.codepoints()
|> Enum.with_index()
|> Enum.map_join(fn {letter, pos} ->
cond do
letter == String.at(right_word, pos) -> "2"
String.contains?(right_word, letter) -> "1"
true -> "0"
end
end)

{guesses, feedback}
end
end
Loading

0 comments on commit 86fa402

Please sign in to comment.