Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions bot/bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,12 @@ async def guess(self, ctx: commands.Context, word: str = "") -> None:

if result.is_found:
await ctx.send(f"🎉 {ctx.author.name} found the word '{word}'!")
elif result.already_cited:
if result.entry.score is not None:
pct = int(result.entry.score * 100)
await ctx.send(f"'{word}' has already been suggested ({pct}% similarity).")
else:
await ctx.send(f"'{word}' has already been suggested.")
elif result.entry.score is not None:
pct = int(result.entry.score * 100)
await ctx.send(f"'{word}': {pct}% similarity")
Expand Down
9 changes: 7 additions & 2 deletions game/state.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,13 @@ class GuessResult:
Attributes:
entry: The :class:`GuessEntry` that was recorded.
is_found: ``True`` if this guess matched the target word.
already_cited: ``True`` if the same word had already been submitted
earlier in this round (by any player).
"""

entry: GuessEntry
is_found: bool
already_cited: bool


class GameState:
Expand Down Expand Up @@ -118,6 +121,7 @@ def submit_guess(self, user: str, word: str) -> GuessResult:
normalized = clean_word(word)
target_normalized = clean_word(self._target_word)

already_cited = any(e.normalized_word == normalized for e in self._history)
found = normalized == target_normalized

score: float | None
Expand All @@ -134,12 +138,13 @@ def submit_guess(self, user: str, word: str) -> GuessResult:
normalized_word=normalized,
score=score,
)
self._history.append(entry)
if not already_cited:
self._history.append(entry)

if found:
self._is_found = True

return GuessResult(entry=entry, is_found=found)
return GuessResult(entry=entry, is_found=found, already_cited=already_cited)

# ------------------------------------------------------------------
# State inspection
Expand Down
47 changes: 47 additions & 0 deletions tests/test_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -374,6 +374,53 @@ async def test_guess_unknown_word_reports_vocabulary_miss(self):
message = ctx.send.call_args[0][0]
assert "vocabulary" in message.lower()

async def test_guess_already_cited_word_with_score_sends_distinct_message(self):
bot = _make_bot(cooldown=0)
bot._game_state = GameState(scorer=_FakeScorer())
bot._game_state.start_new_game("chat", Difficulty.EASY)
ctx = _make_ctx()
ctx.author.name = "alice"
# First submission of "chien"
await _guess_fn(bot, ctx, "chien")
# Second submission of the same word
ctx2 = _make_ctx()
ctx2.author.name = "bob"
await _guess_fn(bot, ctx2, "chien")
message = ctx2.send.call_args[0][0]
assert "already" in message.lower()
assert "%" in message

async def test_guess_already_cited_word_without_score_sends_distinct_message(self):
bot = _make_bot(cooldown=0)
bot._game_state.start_new_game("chat", Difficulty.EASY)
ctx = _make_ctx()
ctx.author.name = "alice"
# No scorer, so non-exact guess produces score=None
await _guess_fn(bot, ctx, "unknownword")
ctx2 = _make_ctx()
ctx2.author.name = "bob"
await _guess_fn(bot, ctx2, "unknownword")
message = ctx2.send.call_args[0][0]
assert "already" in message.lower()

async def test_guess_exact_match_not_reported_as_already_cited(self):
"""A winning guess should show the win message even if the word was cited before."""
bot = _make_bot(cooldown=0)
bot._game_state = GameState(scorer=_FakeScorer())
bot._game_state.start_new_game("chat", Difficulty.EASY)
ctx = _make_ctx()
ctx.author.name = "alice"
# Someone submits the winning word first (game won)
await _guess_fn(bot, ctx, "chat")
# Another user submits the same winning word again
ctx2 = _make_ctx()
ctx2.author.name = "bob"
await _guess_fn(bot, ctx2, "chat")
message = ctx2.send.call_args[0][0]
# Should show winner message, not "already cited"
assert "bob" in message.lower()
assert "chat" in message.lower()


# ---------------------------------------------------------------------------
# event_error
Expand Down
77 changes: 75 additions & 2 deletions tests/test_game_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,69 @@ def test_guess_result_contains_entry(self):
assert result.entry.user == "alice"


# ---------------------------------------------------------------------------
# TestAlreadyCited
# ---------------------------------------------------------------------------


class TestAlreadyCited:
def test_first_guess_is_not_already_cited(self):
"""The first submission of a word should not be marked as already cited."""
gs = _make_state()
gs.start_new_game("chat", Difficulty.EASY)
result = gs.submit_guess("alice", "chien")
assert not result.already_cited

def test_repeated_word_is_already_cited(self):
"""A word submitted a second time by any player should be already cited."""
gs = _make_state()
gs.start_new_game("chat", Difficulty.EASY)
gs.submit_guess("alice", "chien")
result = gs.submit_guess("bob", "chien")
assert result.already_cited

def test_same_user_repeated_word_is_already_cited(self):
"""A word submitted twice by the same user should be already cited."""
gs = _make_state()
gs.start_new_game("chat", Difficulty.EASY)
gs.submit_guess("alice", "chien")
result = gs.submit_guess("alice", "chien")
assert result.already_cited

def test_different_word_is_not_already_cited(self):
"""A word not yet in history should not be marked as already cited."""
gs = _make_state()
gs.start_new_game("chat", Difficulty.EASY)
gs.submit_guess("alice", "chien")
result = gs.submit_guess("bob", "maison")
assert not result.already_cited

def test_already_cited_normalised_match(self):
"""Words that normalise to the same form should be detected as already cited."""
gs = _make_state()
gs.start_new_game("chat", Difficulty.EASY)
gs.submit_guess("alice", "Chien")
result = gs.submit_guess("bob", "CHIEN")
assert result.already_cited

def test_already_cited_word_not_appended_to_history(self):
"""Already-cited guesses should not be recorded again in history."""
gs = _make_state()
gs.start_new_game("chat", Difficulty.EASY)
gs.submit_guess("alice", "chien")
gs.submit_guess("bob", "chien")
assert gs.attempt_count == 1

def test_already_cited_resets_on_new_game(self):
"""After starting a new game, previously cited words are no longer cited."""
gs = _make_state()
gs.start_new_game("chat", Difficulty.EASY)
gs.submit_guess("alice", "chien")
gs.start_new_game("maison", Difficulty.EASY)
result = gs.submit_guess("bob", "chien")
assert not result.already_cited


# ---------------------------------------------------------------------------
# TestGameStateLeaderboard
# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -184,10 +247,20 @@ def test_top_guesses_respects_n(self):
"""top_guesses(n) should return at most n entries."""
gs = _make_state(with_scorer=True)
gs.start_new_game("chat", Difficulty.EASY)
for i in range(5):
gs.submit_guess(f"user{i}", "ch")
for word in ["a", "ab", "abc", "abcd", "abcde"]:
gs.submit_guess("alice", word)
assert len(gs.top_guesses(n=3)) == 3

def test_already_cited_word_excluded_from_top_guesses(self):
"""A word submitted a second time should not appear twice in the leaderboard."""
gs = _make_state(with_scorer=True)
gs.start_new_game("chat", Difficulty.EASY)
gs.submit_guess("alice", "chien")
gs.submit_guess("bob", "chien")
top = gs.top_guesses()
normalized_words = [e.normalized_word for e in top]
assert normalized_words.count("chien") == 1

def test_score_stored_in_entry_with_scorer(self):
"""When a scorer is provided, GuessEntry.score should be set."""
gs = _make_state(with_scorer=True)
Expand Down