Skip to content

Instantly share code, notes, and snippets.

@ddlsmurf
Last active October 13, 2023 15:08
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ddlsmurf/caa21518504bb84f8ccd4b4cd33a1e50 to your computer and use it in GitHub Desktop.
Save ddlsmurf/caa21518504bb84f8ccd4b4cd33a1e50 to your computer and use it in GitHub Desktop.
Playing around with wordle logic
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