No big redesign necessary, simply replacing does most of the trick.
Here is my current best using ThreadX
and StaticArrays
…
using StaticArrays
using ThreadsX
function _readline(path)::SVector{5, Char}
word = readline(path)
SVector{5, Char}(word...)
end
function _readlines(path)::Vector{SVector{5, Char}}
words = Vector{SVector{5, Char}}()
for word in readlines(path)
push!(words, SVector{5, Char}(word...))
end
println(length(words))
words
end
const words = _readlines("solutions.txt")
const non_solution_words = _readlines("non-solution-guesses.txt")
const allowed_guesses = vcat(words, non_solution_words)
@enum ConstraintType has_no has_but_not_at has_at
struct Constraint
type::ConstraintType
letter::Char
index::Int
end
function isequal(c0::ConstraintType, c1::ConstraintType)::Bool
c0.type == c1.type && c0.letter == c1.letter && (c0.type == has_no || c0.index == c1.index)
end
function hash(c::ConstraintType, h::UInt = 0)::UInt
hash(c.type, h) ⊻ hash(c.letter, h ⊻ 1) ⊻ hash(c.index, h ⊻ 2)
end
function constraints(guess::SVector{5, Char}, actual::SVector{5, Char})::Vector{Constraint}
c = Vector{Constraint}()
for (i, g, a) in zip(1:5, guess, actual)
if g == a
push!(c, Constraint(has_at, g, i))
elseif g ∈ actual
push!(c, Constraint(has_but_not_at, g, i))
else
push!(c, Constraint(has_no, g, i))
end
end
c
end
# Code: . = has_no, x = has_but_not_at, o = has_at
function parse_constraints(template::SVector{5, Char}, guess::SVector{5, Char})::Vector{Constraint}
constraints = Vector{Constraint}()
for (i, c) in zip(1:5, template)
if c == 'x'
push!(constraints, Constraint(has_but_not_at, guess[i], i))
elseif c == 'o'
push!(constraints, Constraint(has_at, guess[i], i))
else
push!(constraints, Constraint(has_no, guess[i], i))
end
end
constraints
end
function match_constraint(word::SVector{5, Char}, constraint::Constraint)::Bool
if constraint.type == has_at
word[constraint.index] == constraint.letter
elseif constraint.type == has_no
constraint.letter ∉ word
elseif constraint.type == has_but_not_at
(word[constraint.index] != constraint.letter) && (constraint.letter ∈ word)
else
@assert false
end
end
# Average number of possible words left after performing a given guess,
# and receiving the corresponding constraint information.
function avg_remaining(guess::SVector{5, Char}, words::Vector{SVector{5, Char}})::Float64
rem = ThreadsX.sum(words) do wo
cs = constraints(guess, wo)
sum(words) do wi
all(cs) do c
match_constraint(wi, c)
end ? 1 : 0
end
end
rem / length(words)
end
struct Choice
word::SVector{5, Char}
avg_remaining::Float64
end
function rank_guesses(guesses::Vector{SVector{5, Char}}, words::Vector{SVector{5, Char}})::Vector{Choice}
wt = length(guesses)
avg_time = 0
choices = Vector{Choice}(undef, wt)
for (wi, g) in zip(0:wt-1, guesses)
print("\x1b[1K\x1b[GRanking guesses... ", wi, "/", wt, " words (", Int(ceil(avg_time*(wt-wi)/60)), " min left)")
avg_time = (wi*avg_time + @elapsed choices[wi+1] = Choice(g, avg_remaining(g, words))) / (wi+1)
end
print("\x1b[1K\x1b[G")
sort!(choices, by = c -> c.avg_remaining)
end
function main()
global words; global allowed_guesses
remaining_words = copy(words) # List of words that currently fit all known constraints.
while length(remaining_words) > 1
best_guesses = rank_guesses(allowed_guesses, remaining_words)[1:10]
for (i, g) in zip(1:10, best_guesses)
println(i, ". ", g.word, " (keeps ", g.avg_remaining, " words on average)")
end
println("Insert your guess: ")
guess = _readline(stdin)
println("Insert the results (o = letter at right spot, x = wrong spot, . = not in the word): ")
constraint_template = _readline(stdin)
remaining_words = filter(w -> match_constraints(w, parse_constraints(constraint_template, guess)), remaining_words)
end
println("Solution: ", remaining_words[1], ".")
end