Last active
October 13, 2023 15:08
-
-
Save ddlsmurf/caa21518504bb84f8ccd4b4cd33a1e50 to your computer and use it in GitHub Desktop.
Playing around with wordle logic
This file contains hidden or 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
defmodule WordleCheat do | |
defmodule Utils do | |
def chars_of(str) when is_binary(str), do: str |> String.downcase() |> String.graphemes() | |
def map_inc(map, key) when is_map(map), do: Map.update(map, key, 1, &(&1 + 1)) | |
def map_dec(map, key) when is_map(map), do: Map.update(map, key, -1, &(&1 - 1)) | |
def map_dec_or_del(map, key) when is_map(map) do | |
case Map.get(map, key) do | |
1 -> Map.delete(map, key) | |
n when n > 1 -> Map.put(map, key, n - 1) | |
end | |
end | |
end | |
defmodule LetterCount do | |
@highest_score 100000 | |
def from(word, into \\ %{}) | |
def from(word, into) when is_map(into) and is_list(word) do | |
word | |
|> Enum.reduce(into, fn el, acc -> Utils.map_inc(acc, el) end) | |
end | |
def from(word, into) when is_map(into), do: word |> String.graphemes() |> from(into) | |
def from_list(words), do: words |> Enum.reduce(%{}, &from/2) | |
def word_value(word, letter_counts, highest_score \\ @highest_score) do | |
word | |
|> String.graphemes | |
|> Enum.uniq | |
|> Enum.reduce(0, fn grapheme, score -> | |
score + Map.get(letter_counts, grapheme, -highest_score) | |
end) | |
end | |
defp word_values_sorted(words, letter_counts, highest_score \\ @highest_score) do | |
words | |
|> Enum.map(fn e -> { e, word_value(e, letter_counts, highest_score)} end) | |
|> Enum.filter(fn {_, e} -> e > 0 end) | |
|> Enum.sort(fn {a_str, a}, {b_str, b} -> | |
if a == b, do: a_str < b_str, else: b < a | |
end) | |
end | |
def sort_word_list_by_their_count(words) do | |
letter_count = from_list(words) | |
# Enum.max_by(letter_count, &(elem(&1, 1))) | |
word_values_sorted(words, letter_count) | |
end | |
end | |
defmodule Match do | |
@typedoc """ | |
A match is the result of comparing an attempt to the (secret) goal word. | |
It is a list with an item for each letter. Each item is a tuple of {kind, letter}. | |
The kind is one of: | |
- `:=` for letters in the right place | |
- `:<>` for letters in the wrong place | |
- `:!` for letters not in the goal | |
It has an ascii representation of the format: | |
([kind]letter)+ | |
where kind is `~` when the following letter is in the wrong position, | |
`!` if the following letter is not in the goal, or `=` if the letter | |
is in the right place. If the kind is absent, it will be either `=` or | |
`!` depending on `@ascii_implicit_kind_is_equals`. | |
For example, if the target word is `oreas` and the attempt is `serai`: | |
WordleCheat.Match.from_attempt("serai", "oreas") | |
# oreas | |
# serai | |
# ~~~=! | |
[<>: "s", <>: "e", <>: "r", =: "a", !: "i"] | |
The ascii of which, if `@ascii_implicit_kind_is_equals == true`, is: | |
~s~e~ra!i | |
The ascii of which, if `@ascii_implicit_kind_is_equals == false`, is: | |
~s~e~r=ai | |
""" | |
@wordlen 5 | |
def word_length, do: 5 | |
defguard is_valid_letters(value) when is_list(value) and length(value) == @wordlen | |
@type kind :: := | :<> | :! | |
@type slot :: { kind, iodata() } | |
@type t :: [slot] | |
@spec kind_to_color(kind) :: atom | |
defp kind_to_color(:=), do: [:bright, :light_green_background, :black] | |
defp kind_to_color(:<>), do: [:bright, :light_yellow_background, :black] | |
defp kind_to_color(:!), do: [:bright, :light_black_background, :white] | |
@spec to_ansi(t) :: iodata() | |
def to_ansi(match) do | |
match | |
|> Enum.map(fn {kind, char} -> | |
[ kind_to_color(kind), " ", char, " ", :reset, " "] | |
end) | |
|> List.flatten() | |
|> IO.ANSI.format() | |
end | |
@ascii_implicit_kind_is_equals false | |
def ascii_implicit_kind_is_equals, do: @ascii_implicit_kind_is_equals | |
@spec kind_to_ascii(t) :: binary | |
defp kind_to_ascii(:<>), do: "~" | |
if @ascii_implicit_kind_is_equals do | |
defp kind_to_ascii(:=), do: "" | |
defp kind_to_ascii(:!), do: "!" | |
else | |
defp kind_to_ascii(:=), do: "=" | |
defp kind_to_ascii(:!), do: "" | |
end | |
@spec kind_from_ascii(binary) :: kind | |
defp kind_from_ascii("="), do: := | |
defp kind_from_ascii("~"), do: :<> | |
defp kind_from_ascii("!"), do: :! | |
if @ascii_implicit_kind_is_equals do | |
defp kind_from_ascii(""), do: := | |
else | |
defp kind_from_ascii(""), do: :! | |
end | |
@spec to_ascii(t) :: iodata() | |
def to_ascii(match) do | |
match | |
|> Enum.map(fn {kind, char} -> | |
[ kind_to_ascii(kind), char ] | |
end) | |
|> List.flatten() | |
|> Enum.join() | |
end | |
@spec attempt_from(t) :: iodata() | |
def attempt_from(match) do | |
match | |
|> Enum.map(fn {_kind, char} -> char end) | |
|> Enum.join() | |
end | |
@spec is_complete?(t) :: boolean | |
def is_complete?(match), do: Enum.all?(match, fn {op, _} -> op == := end) | |
@ascii_valid_rx ~r/^(?:[~=!]?[a-z]){#{@wordlen}}$/i | |
@ascii_items_rx ~r/([~=!])?([a-z])/i | |
def from_ascii(match) when is_binary(match) do | |
match = match |> String.trim |> String.downcase | |
unless String.match?(match, @ascii_valid_rx) do | |
:error | |
else | |
matches = Regex.scan(@ascii_items_rx, match) | |
wordlen = @wordlen | |
^wordlen = Enum.count(matches) | |
{:ok, matches |> Enum.map(fn [_, kind, char] -> {kind_from_ascii(kind), char} end)} | |
end | |
end | |
@spec from_attempt(binary, binary) :: t | |
def from_attempt(attempt, word) when is_binary(attempt) and is_binary(word) do | |
from_attempt(Utils.chars_of(attempt), Utils.chars_of(word)) | |
end | |
@spec from_attempt(list, list) :: t | |
def from_attempt(attempt, word) | |
when (is_list(attempt) and length(attempt) == @wordlen) | |
and (is_list(word) and length(word) == @wordlen) do | |
word_graphemes = WordleCheat.LetterCount.from(word) | |
match = | |
Enum.zip(attempt, word) | |
|> Enum.map(fn {at, wo} when at == wo -> {:=, at} | |
{at, _wo} -> {(if Map.get(word_graphemes, at), do: :<>, else: :!), at} | |
end) | |
word_graphemes_without_exact = | |
match | |
|> Enum.reduce(word_graphemes, fn {:=, char}, acc -> Utils.map_dec(acc, char) | |
_, a -> a end) | |
{match, _} = | |
match | |
|> Enum.reduce({[], word_graphemes_without_exact}, | |
fn c = {op, _}, {match, graphemes_left} when op == := or op == :! -> | |
{[c | match], graphemes_left} | |
c = {:<>, char}, {match, graphemes_left} -> | |
if Map.get(graphemes_left, char) > 0 do | |
{[c | match], Utils.map_dec(graphemes_left, char)} | |
else | |
{[{:!, char} | match], graphemes_left} | |
end | |
end | |
) | |
match = Enum.reverse(match) | |
match | |
end | |
end | |
defmodule Conditions do | |
@moduledoc """ | |
Conditions is a tuple containing the constraints inferred from | |
failed match attempts. Structure: | |
{ have, havent, at, not_at } | |
- `have` is a word count the goal is known to have (in total, including | |
those who's positions are known) | |
- `havent` is a MapSet of letters known to not be in the goal word. | |
`@havent_includes_extraneous_letters` sets whether it includes letters | |
at known position, that are known to also be no more of. This logic | |
may not be fully togglable, for the moment it's a marker for code | |
that depends on it. | |
- `at` is a list of as many characters as the goal word, with either | |
the character in a string if it's known to be at that index, or `:_`. | |
- `not_at` is a list of as many characters as the goal word, with either | |
`:_` if nothing is known, or a MapSet of characters known to not be at | |
the corresponding position. | |
""" | |
# Condition's havent should not include letters that are also matches | |
# even if it reveals there are too many of them ???? | |
@havent_includes_extraneous_letters false | |
@type char_count_map :: %{ binary => number } | |
@type char_bool_map :: %MapSet{} | |
@type char_pos_list :: [ binary | :_ ] | |
@type char_pos_map_list :: [ char_bool_map | :_ ] | |
@type must_have :: char_count_map | |
@type must_not_have :: char_bool_map | |
@type must_be_at :: char_pos_list | |
@type must_not_be_at :: char_pos_map_list | |
@type t :: { must_have, must_not_have, must_be_at, must_not_be_at } | |
def new, do: { %{}, MapSet.new(), List.duplicate(:_, Match.word_length), List.duplicate(:_, Match.word_length), MapSet.new() } | |
defp new_with_tuples do | |
{have, havent, at, not_at, tried} = new() | |
{have, havent, at |> List.to_tuple, not_at |> List.to_tuple, tried} | |
end | |
defp add_char_to_map(:_, char), do: add_char_to_map(MapSet.new(), char) | |
defp add_char_to_map(map, char), do: MapSet.put(map, char) | |
defp add_char_to_tuple_of_maps(tuple, index, char), do: | |
put_elem(tuple, index, add_char_to_map(elem(tuple, index), char)) | |
@use_tried_field false | |
@spec from_match(Match.t) :: t | |
def from_match(match) do | |
{ have, havent, at, not_at, tried } = | |
match | |
|> Enum.with_index() | |
|> Enum.reduce(new_with_tuples(), | |
fn {{op, char}, index}, { have, havent, at, not_at, tried } -> | |
tried = if @use_tried_field, do: MapSet.put(tried, Match.attempt_from(match)), else: tried | |
case op do | |
:= -> | |
{Utils.map_inc(have, char), havent, put_elem(at, index, char), not_at, tried} | |
:<> -> | |
{Utils.map_inc(have, char), havent, at, add_char_to_tuple_of_maps(not_at, index, char), tried} | |
:! -> | |
{have, MapSet.put(havent, char), at, add_char_to_tuple_of_maps(not_at, index, char), tried} | |
end | |
end) | |
havent = if @havent_includes_extraneous_letters do | |
havent | |
else | |
have |> Enum.reduce(havent, fn {char, _}, havent -> MapSet.delete(havent, char) end) | |
end | |
{ have, havent, at |> Tuple.to_list, not_at |> Tuple.to_list, tried } | |
end | |
def count_free_positions({_, _, at, _, _}), do: Enum.count(at, &(&1 == :_)) | |
def get_unplaced_letters({have, _, at, _, _}) do | |
at | |
|> Enum.reduce(have, fn :_, have -> have | |
char, have -> Utils.map_dec_or_del(have, char) | |
end) | |
end | |
defp to_condition(x) when is_tuple(x), do: x | |
defp to_condition(x) when is_list(x), do: from_match(x) | |
def merge({have1, havent1, at1, not_at1, tried1}, {have2, havent2, at2, not_at2, tried2}) do | |
{ | |
have2 |> Enum.reduce(have1, fn {char, count}, have -> | |
Map.update(have, char, count, &(max(&1, count))) | |
end), | |
havent2 |> Enum.reduce(havent1, &MapSet.put(&2, &1)), | |
List.zip([at1, at2]) | |
|> Enum.map(fn {c1, :_} when c1 != :_ -> c1 | |
{:_, c2} when c2 != :_ -> c2 | |
{c1, c2} when c1 == c2 -> c1 | |
{:_, :_} -> :_ | |
{_, _} -> raise "Conditions are incompatible" | |
end), | |
List.zip([not_at1, not_at2]) | |
|> Enum.map(fn {:_, :_} -> :_ | |
{:_, b} -> b | |
{a, :_} -> a | |
{a, b} -> MapSet.union(a, b) end), | |
MapSet.union(tried1, tried2) | |
} | |
end | |
def merge(x1, x2), do: merge(to_condition(x1), to_condition(x2)) | |
def describe({ _have, havent, at, not_at, _tried } = conds) do | |
to_place = get_unplaced_letters(conds) | |
free_count = count_free_positions(conds) | |
havent_count = MapSet.size(havent) | |
havent_string = havent |> Enum.sort() |> Enum.join("") |> inspect | |
at_string = at | |
|> Enum.map(&(if &1, do: &1, else: ".")) | |
|> Enum.join() | |
|> inspect() | |
to_place_count = to_place |> Enum.reduce(0, fn {_, n}, acc -> acc + n end) | |
to_place_string = | |
to_place | |
|> Enum.map(fn {char, 1} -> char | |
{char, x} -> "#{char}(#{x})" end) | |
|> Enum.join("") | |
|> inspect | |
not_at_string = if Enum.any?(not_at, &(&1 != :_)) do | |
positions = | |
not_at | |
|> Enum.map(fn :_ -> "" | |
set -> Enum.join(set, "") | |
end) | |
|> Enum.join(",") | |
" !(#{positions})" | |
else | |
"" | |
end | |
[ | |
# "#WordleCheat.Conditions< ", | |
at_string, | |
" ", | |
cond do | |
free_count == 0 -> | |
"Solved!" | |
free_count == Match.word_length and to_place_count == 0 and havent_count == 0 and not_at_string == "" -> | |
"(no conditions)" | |
to_place_count == 0 and havent_count == 0 -> | |
false = @havent_includes_extraneous_letters # I think this shouldn't be possible | |
"- no other info - must have repeated letters too many times" | |
to_place_count == 0 -> | |
"no #{havent_string}" | |
havent_count == 0 and free_count == to_place_count -> | |
"to place: #{to_place_string} (no missing letters)" | |
havent_count == 0 -> | |
"to place: #{to_place_string} (#{free_count - to_place_count} unknown slots)" | |
free_count == to_place_count -> | |
"to place: #{to_place_string} (#{havent_string} excluded but uneeded)" | |
true -> | |
"to place: #{to_place_string}, but no: #{havent_string}" | |
end, | |
not_at_string, | |
# ">" | |
] |> Enum.join() | |
end | |
import Match, only: [is_valid_letters: 1] | |
def match({_have, _havent, _at, _not_at, _tried} = conds, word) when is_binary(word), do: | |
match(conds, Utils.chars_of(word)) | |
def match({have, havent, at, not_at, _tried}, word) when is_valid_letters(word) do | |
have_impossible_chars = | |
Enum.zip(not_at, word) | |
|> Enum.any?(fn {:_, _} -> false | |
{set, char} -> MapSet.member?(set, char) end) | |
if have_impossible_chars do | |
:char_at_pos_of_not_at | |
else | |
scan_result = | |
Enum.zip(at, word) | |
|> Enum.reduce({have, %{}}, | |
fn _, error when is_atom(error) -> error | |
{:_, c_word}, {pending, extra} -> | |
if MapSet.member?(havent, c_word) do | |
false = @havent_includes_extraneous_letters | |
:letter_in_havent_list | |
else | |
{ pending, Utils.map_inc(extra, c_word) } | |
end | |
{c_at, c_word}, _ when c_at != c_word -> :wrong_letter_at_position | |
{c_at, c_word}, {pending, extra} when c_at == c_word -> | |
{ Utils.map_dec_or_del(pending, c_at), extra } | |
end) | |
case scan_result do | |
{ letters_in_conds, letters_in_word } -> | |
# letters_in_conds: Letters known to be somewhere | |
# letters_in_word: letters from the word not matched by at | |
known_letter_to_place_all_present_in_word = | |
letters_in_conds | |
|> Enum.all?(fn {char, count} -> Map.get(letters_in_word, char, 0) >= count end) | |
if known_letter_to_place_all_present_in_word, do: :ok, else: :letter_to_place_not_in_word | |
x when is_atom(x) -> x | |
end | |
end | |
end | |
end | |
end | |
defmodule CLIHistogram do | |
defp block_index_from_fractional(blocks, fractional) when fractional >= 0 and fractional <= 1, do: | |
round(fractional * (tuple_size(blocks) - 1)) | |
defp block_index_from_fractional(_blocks, fractional) when fractional < 0, do: 0 | |
defp block_index_from_fractional(blocks, fractional) when fractional > 1, do: tuple_size(blocks) - 1 | |
defp block_from_fractional(blocks, fractional), do: elem(blocks, block_index_from_fractional(blocks, fractional)) | |
defp repeat_string(str, count) when count <= 0, do: str | |
defp repeat_string(str, count), do: String.pad_leading("", count, str) | |
defp str_to_blocks(blocks), do: blocks |> String.graphemes |> List.to_tuple() | |
defp str_center(width, str), do: | |
repeat_string(" ", div((width - String.length(str)) , 2)) <> str | |
defp parag_center(width, text), do: | |
text |> String.split("\n") |> Enum.map_join("\n", &str_center(width, &1)) | |
defp average(nums), do: Enum.sum(nums) / Enum.count(nums) | |
defp stats(nums) do | |
avg = average(nums) | |
{avg, nums |> Enum.map(&((&1 - avg) ** 2)) |> average() |> :math.sqrt()} | |
end | |
defp print_stats(numbers, total_width) do | |
{avg, stdev} = stats(numbers) | |
IO.puts(parag_center(total_width, "avg: #{Float.round(avg, 2)}, stddev: #{Float.round(stdev, 2)}")) | |
{min, max} = Enum.min_max(numbers) | |
IO.puts(parag_center(total_width, "#{Enum.count(numbers)} numbers between #{min} and #{max}")) | |
end | |
defp build_rows(height, cols_normalised, col_sep, col_width, blocks, count_min, count_max) do | |
1..height | |
|> Enum.map_join("\n", fn row_index -> | |
row = 1 + height - row_index | |
row_string = | |
cols_normalised | |
|> Enum.map_join(col_sep, fn col_value -> | |
block = block_from_fractional(blocks, col_value - row + 1) | |
repeat_string(block, col_width) | |
end) | |
row_string <> | |
if row_index == 1, do: " ▔▔ #{count_max}", else: ( | |
if row_index == height, do: " ▁▁ #{count_min}", else: "" | |
) | |
end) | |
end | |
@print_default_options [ | |
block_chars: " ▁▂▃▄▅▆▇█", | |
col_sep: " ", | |
col_width: 2, | |
height: 10, | |
from_zero: false, | |
print_stats: true, | |
] | |
def print(numbers, options \\ []) do | |
options = Keyword.merge(@print_default_options, options, fn _k, _v1, v2 -> v2 end) | |
height = Keyword.get(options, :height) | |
col_sep = Keyword.get(options, :col_sep) | |
col_width = Keyword.get(options, :col_width) | |
from_zero = Keyword.get(options, :from_zero) | |
block_chars = Keyword.get(options, :block_chars) | |
title = Keyword.get(options, :title) | |
print_stats = Keyword.get(options, :print_stats) | |
blocks = str_to_blocks(block_chars) | |
frequencies = Enum.frequencies(numbers) | |
freq_keys = Map.keys(frequencies) | |
{value_min, value_max} = Enum.min_max(freq_keys) | |
{count_min, count_max} = Enum.min_max(Map.values(frequencies)) | |
{count_min, count_window} = if from_zero, do: {0, count_max}, else: {count_min, count_max - count_min} | |
count_window = if count_window != 0, do: count_window, else: 1 | |
cols = value_min..value_max | |
cols_normalised = cols | |
|> Enum.map(fn col_index -> | |
max(0, ((Map.get(frequencies, col_index, 0) - count_min) / count_window) * height) | |
end) | |
col_sep_width = String.length(col_sep) | |
total_width = Enum.count(cols) * (col_width + col_sep_width) - col_sep_width | |
if title, do: IO.puts(parag_center(total_width, title)) | |
if print_stats, do: print_stats(numbers, total_width) | |
# IO.inspect([frequencies: frequencies, cols_normalised: cols_normalised]) | |
build_rows(height, cols_normalised, col_sep, col_width, blocks, count_min, count_max) | |
|> IO.puts() | |
cols | |
|> Enum.map_join(col_sep, &String.pad_leading(to_string(&1), col_width)) | |
|> IO.puts | |
numbers | |
end | |
end | |
defmodule Run do | |
@wordfile "/usr/share/dict/words" | |
defp load_words do | |
@wordfile | |
|> File.stream! | |
|> Stream.map(&String.trim/1) | |
|> Stream.filter(&(String.length(&1) == WordleCheat.Match.word_length)) | |
|> Stream.map(&String.downcase/1) | |
|> Enum.sort() | |
|> Enum.uniq() | |
|> Enum.into([]) | |
end | |
defmodule MatchTest do | |
defstruct attempt: nil, goal: nil, match_if_implicit_equals: nil, match_if_implicit_not: nil | |
def new(attempt, goal, match_if_implicit_equals, match_if_implicit_not) do | |
%MatchTest{ | |
attempt: attempt, | |
goal: goal, | |
match_if_implicit_equals: match_if_implicit_equals, | |
match_if_implicit_not: match_if_implicit_not, | |
} | |
end | |
def match_ascii(%MatchTest{} = set) do | |
if WordleCheat.Match.ascii_implicit_kind_is_equals() do | |
set.match_if_implicit_equals | |
else | |
set.match_if_implicit_not | |
end | |
end | |
end | |
def match_demo(attempt, word, expect \\ nil) do | |
IO.puts("Matching #{inspect attempt} against #{inspect word}:") | |
match = WordleCheat.Match.from_attempt(attempt, word) | |
IO.puts([" word: ", word]) | |
IO.puts([" try: ", attempt]) | |
IO.puts([" match: ", WordleCheat.Match.to_ansi(match), " ", (if WordleCheat.Match.is_complete?(match), do: " V", else: " X")]) | |
match_ascii = WordleCheat.Match.to_ascii(match) | |
IO.puts([" match: ", match_ascii]) | |
if expect, do: ^expect = match_ascii | |
conds = WordleCheat.Conditions.from_match(match) | |
# IO.inspect(conds) | |
IO.puts([" cond: ", WordleCheat.Conditions.describe(conds)]) | |
{match, conds} | |
end | |
def match_demo(%MatchTest{attempt: attempt, goal: goal} = match_test) do | |
match_demo(attempt, goal, MatchTest.match_ascii(match_test)) | |
end | |
defp io_gets_attempt(wordlist) do | |
result = IO.gets("Guess: ") |> String.trim() |> String.downcase() | |
word_format_valid = String.match?(result, ~r/^[a-z]{#{WordleCheat.Match.word_length}}$/i) | |
word_in_list = word_format_valid and Enum.find(wordlist, &(&1 == result)) | |
if result == "q" do | |
:quit | |
else | |
if word_in_list do | |
result | |
else | |
cond do | |
!word_format_valid -> IO.puts(" Invalid format, must be #{WordleCheat.Match.word_length} letters") | |
!word_in_list -> IO.puts(" That word is not in the list") | |
end | |
io_gets_attempt(wordlist) | |
end | |
end | |
end | |
defp play(wordlist, word, steps \\ 1) do | |
attempt = io_gets_attempt(wordlist) | |
if attempt == :quit do | |
IO.puts("The word was: #{word}") | |
else | |
match = WordleCheat.Match.from_attempt(attempt, word) | |
indent = " " | |
IO.puts([indent, WordleCheat.Match.to_ansi(match)]) | |
IO.puts([indent, match |> Enum.map(fn {:=, _} -> "=" | |
{:<>, _} -> "~" | |
{:!, _} -> " " end)]) | |
if WordleCheat.Match.is_complete?(match) do | |
IO.puts("Found it in: #{steps} steps") | |
else | |
if steps == 6, do: IO.puts("(to stop press 'q')") | |
play(wordlist, word, steps + 1) | |
end | |
end | |
end | |
defp play(wordlist) when is_list(wordlist), do: play(wordlist, Enum.random(wordlist)) | |
def play(), do: play(load_words()) | |
def play_for(word), do: play(load_words(), word) | |
defp io_gets_match() do | |
case WordleCheat.Match.from_ascii(IO.gets("Match result: ") |> String.trim) do | |
:error -> | |
IO.puts(" -> Invalid match string. Prefix yellow letters with ~, red letters with ! and green with nothing") | |
io_gets_match() | |
{:ok, match} -> | |
match | |
end | |
end | |
defp cheat_run(wordlist, conditions \\ WordleCheat.Conditions.new()) do | |
IO.puts([" Overall conditions: ", WordleCheat.Conditions.describe(conditions)]) | |
options = | |
wordlist | |
|> Enum.filter(&(WordleCheat.Conditions.match(conditions, &1) == :ok)) | |
|> WordleCheat.LetterCount.sort_word_list_by_their_count() | |
IO.puts(" #{Enum.count(options)} option(s)") | |
entries = options | |
|> Enum.map(&(elem(&1, 0))) | |
|> Enum.take(5) | |
|> Enum.join(" or ") | |
IO.puts(" Try: #{entries}") | |
match = io_gets_match() | |
IO.puts([" Got: ", WordleCheat.Match.to_ansi(match)]) | |
conds = WordleCheat.Conditions.from_match(match) | |
conditions = WordleCheat.Conditions.merge(conditions, conds) | |
IO.puts([" New conditions from the match: ", WordleCheat.Conditions.describe(conds)]) | |
IO.puts("") | |
if WordleCheat.Match.is_complete?(match) do | |
exit(:all_is_good) | |
else | |
cheat_run(wordlist, conditions) | |
end | |
end | |
def cheat_run(), do: cheat_run(load_words()) | |
defp sim_run_pick_strategy(wordlist, conditions, verbose) do | |
filtered_list = | |
wordlist | |
|> Enum.filter(&(WordleCheat.Conditions.match(conditions, &1) == :ok)) | |
options = | |
filtered_list | |
|> WordleCheat.LetterCount.sort_word_list_by_their_count() | |
if verbose, do: IO.inspect(options_left: Enum.count(options)) | |
[ {entry, _score} | _ ] = options | |
{entry, filtered_list} | |
end | |
defp sim_run(wordlist, word, verbose, conditions \\ WordleCheat.Conditions.new(), attempts \\ 1) do | |
if verbose, do: IO.puts("Overall condition: #{WordleCheat.Conditions.describe(conditions)}") | |
{entry, wordlist} = sim_run_pick_strategy(wordlist, conditions, verbose) | |
{_have, _havent, _at, _not_at, tried} = conditions | |
if MapSet.member?(tried, entry), do: | |
IO.inspect(LOOP_DETECTED: [entry: entry, word: word, conditions: conditions]) | |
{match, conds} = if verbose do | |
match_demo(entry, word) | |
else | |
match = WordleCheat.Match.from_attempt(entry, word) | |
conds = WordleCheat.Conditions.from_match(match) | |
{match, conds} | |
end | |
if MapSet.member?(tried, entry), do: | |
IO.inspect(LOOP_DETECTED2: [match: match, conds: conds]) | |
cond do | |
attempts >= 20 -> :error | |
WordleCheat.Match.is_complete?(match) -> | |
if verbose, do: IO.puts("Solved: #{entry} in #{attempts} attempt(s) !") | |
attempts | |
true -> | |
# wordlist = List. delete(wordlist, entry) | |
sim_run(wordlist, word, verbose, WordleCheat.Conditions.merge(conditions, conds), attempts + 1) | |
end | |
end | |
def sim_run(word, verbose \\ true), do: sim_run(load_words(), word, verbose) | |
defp mini_tests_check_cond({have, havent, at, not_at, _tried}, expected_have, expected_havent, expected_at, expected_not_at) | |
when byte_size(expected_at) == 5 do | |
^have = expected_have |> WordleCheat.LetterCount.from | |
^havent = expected_havent | |
|> WordleCheat.Utils.chars_of() | |
|> Enum.reduce(MapSet.new(), &MapSet.put(&2, &1)) | |
^at = expected_at | |
|> WordleCheat.Utils.chars_of() | |
|> Enum.map(fn "_" -> :_ | |
x -> x end) | |
|> Enum.into([]) | |
^not_at = expected_not_at | |
|> String.split(",") | |
|> Enum.map(&String.trim/1) | |
|> Enum.map(fn "" -> :_ | |
letters -> | |
letters | |
|> WordleCheat.Utils.chars_of() | |
|> Enum.reduce(MapSet.new(), fn char, set -> MapSet.put(set, char) end) | |
end) | |
# todo check tried | |
end | |
def mini_tests() do | |
{m1, _} = match_demo(MatchTest.new("aabbc", "bbcaa", "~a~a~b~b~c", "~a~a~b~b~c")) | |
{m2, _} = match_demo(MatchTest.new("axxxx", "aaaaa", "a!x!x!x!x", "=axxxx")) | |
IO.puts("\n=> merge:") | |
conds = WordleCheat.Conditions.merge(m1, m2) | |
IO.puts(WordleCheat.Conditions.describe(conds)) | |
IO.inspect(conds: conds) | |
mini_tests_check_cond(conds, "aabbc", "x", "a____", "a,a,b,b,c") | |
match_demo(MatchTest.new("aaaaa", "axxxx", "a!a!a!a!a", "=aaaaa")) | |
match_demo(MatchTest.new("aacbb", "bbcaa", "~a~ac~b~b", "~a~a=c~b~b")) | |
match_demo(MatchTest.new("accba", "bbaaa", "~a!c!c~ba", "~acc~b=a")) | |
match_demo(MatchTest.new("accba", "accba", "accba", "=a=c=c=b=a")) | |
match_demo(MatchTest.new("accba", "wvxyz", "!a!c!c!b!a", "accba")) | |
end | |
defp take_random_sample_of_approx(words, approx_count) do | |
include_probability = approx_count / Enum.count(words) | |
words |> Enum.filter(fn _ -> :rand.uniform() <= include_probability end) | |
end | |
def sim_run_eval(approx_sample_size \\ 1000) do | |
words = load_words() | |
sample_words = take_random_sample_of_approx(words, approx_sample_size) | |
sample_count = Enum.count(sample_words) | |
IO.puts("Sample size: #{sample_count}") | |
{microseconds, solve_counts} = :timer.tc(fn -> | |
sample_words | |
|> Enum.map(&sim_run(words, &1, false)) | |
end) | |
secs = microseconds / 1000000 | |
error_count = solve_counts |> Enum.count(&(&1 == :error)) | |
solve_counts = solve_counts |> Enum.filter(&is_number/1) | |
solves_in_too_many_counts = solve_counts |> Enum.filter(&(&1 > 6)) |> Enum.count() | |
average_steps = Enum.sum(solve_counts) / Enum.count(solve_counts) | |
IO.puts("Average solve time: #{Float.round(secs / sample_count, 4)}s (#{Float.round(secs, 1)}s total)") | |
IO.puts("Average solve steps: #{Float.round(average_steps, 2)} (max: #{Enum.max(solve_counts)}, min: #{Enum.min(solve_counts)})") | |
IO.puts("Failed to solve: #{error_count} (#{Float.round(error_count * 100 / sample_count, 2)} %)") | |
IO.puts("Solved in >6: #{solves_in_too_many_counts} (#{Float.round(solves_in_too_many_counts * 100 / sample_count, 2)} %)") | |
IO.puts("") | |
CLIHistogram.print(solve_counts, title: "Number of solves by step count", from_zero: true) | |
end | |
end | |
# Run.mini_tests() # just random debug prints | |
# Run.cheat_run() # enter the results as you play to get suggestions | |
# Run.play() # play a game | |
# Run.play_for("didna") # play a game with a predetermined goal | |
# Run.sim_run("didna") # try running the guesser against a given goal word | |
Run.sim_run_eval() # evaluate guessing strategy speed/success (Run.sim_run_pick_strategy) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment