initial commit
This commit is contained in:
@@ -0,0 +1,147 @@
|
||||
import pytest
|
||||
|
||||
from wordle_solver.game import Clue, score_guess
|
||||
from wordle_solver.solver import Solver, rank_guesses
|
||||
|
||||
G = Clue.GREEN
|
||||
Y = Clue.YELLOW
|
||||
B = Clue.GREY
|
||||
|
||||
ALL_GREEN = (G, G, G, G, G)
|
||||
|
||||
|
||||
def make_solver(*extra):
|
||||
"""Solver with a small, controlled answer list."""
|
||||
answers = ["crane", "crave", "trace", "grace", "brace"]
|
||||
return Solver(answers, list(extra))
|
||||
|
||||
|
||||
class TestSolverApply:
|
||||
def test_all_candidates_initially(self):
|
||||
s = make_solver()
|
||||
assert set(s.candidates) == {"crane", "crave", "trace", "grace", "brace"}
|
||||
|
||||
def test_apply_narrows_candidates(self):
|
||||
s = make_solver()
|
||||
# scoring "crane" against "crane" → all green → only "crane" remains
|
||||
s.apply("crane", ALL_GREEN)
|
||||
assert s.candidates == ["crane"]
|
||||
|
||||
def test_apply_chain(self):
|
||||
s = make_solver()
|
||||
# first clue: 'c' correct, 'r' present, rest absent/wrong
|
||||
pattern1 = score_guess("crane", "crave")
|
||||
s.apply("crane", pattern1)
|
||||
# 'crave' must still be a candidate; 'grace' and 'brace' may or may not be
|
||||
assert "crave" in s.candidates
|
||||
assert "crane" not in s.candidates
|
||||
|
||||
def test_apply_empty_candidates_on_no_match(self):
|
||||
s = make_solver()
|
||||
# impossible pattern eliminates everything
|
||||
s.apply("zzzzz", ALL_GREEN)
|
||||
assert s.candidates == []
|
||||
|
||||
def test_reset_restores_all_candidates(self):
|
||||
s = make_solver()
|
||||
s.apply("crane", ALL_GREEN)
|
||||
s.reset()
|
||||
assert set(s.candidates) == {"crane", "crave", "trace", "grace", "brace"}
|
||||
|
||||
def test_reset_clears_history(self):
|
||||
s = make_solver()
|
||||
s.apply("crane", ALL_GREEN)
|
||||
s.reset()
|
||||
assert s.history == []
|
||||
|
||||
def test_is_solved_after_all_green(self):
|
||||
s = make_solver()
|
||||
s.apply("crane", ALL_GREEN)
|
||||
assert s.is_solved()
|
||||
|
||||
def test_is_solved_false_initially(self):
|
||||
assert not make_solver().is_solved()
|
||||
|
||||
|
||||
class TestHardModeConstraints:
|
||||
def test_green_positions_extracted(self):
|
||||
s = make_solver()
|
||||
s.apply("crane", (G, B, B, B, B)) # 'c' locked at position 0
|
||||
green, _ = s.hard_mode_constraints()
|
||||
assert green == {0: "c"}
|
||||
|
||||
def test_yellow_minimum_count(self):
|
||||
s = make_solver()
|
||||
s.apply("crane", (B, Y, B, B, B)) # 'r' must appear at least once
|
||||
_, min_count = s.hard_mode_constraints()
|
||||
assert min_count["r"] >= 1
|
||||
|
||||
def test_multiple_greens_accumulated(self):
|
||||
s = make_solver()
|
||||
s.apply("crane", (G, G, B, B, B))
|
||||
s.apply("crave", (G, G, B, G, B))
|
||||
green, _ = s.hard_mode_constraints()
|
||||
assert green[0] == "c"
|
||||
assert green[1] == "r"
|
||||
assert green[3] == "v"
|
||||
|
||||
def test_duplicate_yellow_counts_correctly(self):
|
||||
s = make_solver()
|
||||
# Two 'a's in a guess, both yellow → need at least 2 'a's
|
||||
s.apply("llama", (B, B, Y, B, Y))
|
||||
_, min_count = s.hard_mode_constraints()
|
||||
assert min_count["a"] >= 2
|
||||
|
||||
|
||||
class TestHardModePool:
|
||||
def test_no_history_returns_all_words(self):
|
||||
s = make_solver("extra1", "extra2")
|
||||
pool = s.hard_mode_pool()
|
||||
assert set(pool) == set(s.all_words)
|
||||
|
||||
def test_green_constraint_filters_pool(self):
|
||||
s = make_solver()
|
||||
s.apply("crane", (G, B, B, B, B)) # first letter must be 'c'
|
||||
pool = s.hard_mode_pool()
|
||||
assert all(w[0] == "c" for w in pool)
|
||||
|
||||
def test_yellow_constraint_filters_pool(self):
|
||||
s = make_solver()
|
||||
s.apply("crane", (B, Y, B, B, B)) # 'r' must be present
|
||||
pool = s.hard_mode_pool()
|
||||
assert all("r" in w for w in pool)
|
||||
|
||||
def test_word_violating_green_excluded(self):
|
||||
s = make_solver()
|
||||
s.apply("crane", (G, B, B, B, B)) # 'c' at position 0
|
||||
pool = s.hard_mode_pool()
|
||||
assert "trace" not in pool # starts with 't'
|
||||
|
||||
|
||||
class TestRankGuesses:
|
||||
def test_returns_at_most_top_n(self):
|
||||
answers = ["crane", "crave", "trace", "grace", "brace"]
|
||||
pool = answers[:]
|
||||
results = rank_guesses(pool, answers, top_n=3)
|
||||
assert len(results) <= 3
|
||||
|
||||
def test_possible_answers_tagged(self):
|
||||
answers = ["crane", "crave"]
|
||||
pool = answers + ["zzzzz"]
|
||||
results = rank_guesses(pool, answers, top_n=10)
|
||||
by_word = {w: is_ans for w, _, is_ans in results}
|
||||
assert by_word["crane"] is True
|
||||
assert by_word["crave"] is True
|
||||
assert by_word["zzzzz"] is False
|
||||
|
||||
def test_scores_descending(self):
|
||||
answers = ["crane", "crave", "trace", "grace", "brace"]
|
||||
results = rank_guesses(answers, answers, top_n=5)
|
||||
scores = [s for _, s, _ in results]
|
||||
assert scores == sorted(scores, reverse=True)
|
||||
|
||||
def test_single_candidate_returned(self):
|
||||
answers = ["crane"]
|
||||
results = rank_guesses(answers, answers, top_n=5)
|
||||
assert len(results) == 1
|
||||
assert results[0][0] == "crane"
|
||||
Reference in New Issue
Block a user