-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add complements to try (unsuccessfully) to solve the game faster
- Loading branch information
1 parent
67367a0
commit 86fa402
Showing
10 changed files
with
280 additions
and
205 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
Oops, something went wrong.