discordbot/cogs/games/wordle_game.py

304 lines
10 KiB
Python

import discord
from discord import ui
from typing import List, Dict, Optional, Set
class WordleGame:
"""Class to handle Wordle game logic"""
def __init__(self, word: str, max_attempts: int = 6):
"""
Initialize a new Wordle game
Args:
word: The target word to guess
max_attempts: Maximum number of attempts allowed (default: 6)
"""
self.word = word.lower()
self.max_attempts = max_attempts
self.attempts = 0
self.guesses = []
self.game_over = False
self.won = False
def make_guess(self, guess: str) -> List[Dict[str, str]]:
"""
Process a guess and return the result
Args:
guess: The word guessed by the player
Returns:
List of dictionaries with letter and status (correct, present, absent)
"""
if self.game_over:
return []
guess = guess.lower()
if len(guess) != len(self.word):
return []
self.attempts += 1
self.guesses.append(guess)
# Check if the guess is correct
if guess == self.word:
self.game_over = True
self.won = True
# Check if max attempts reached
if self.attempts >= self.max_attempts:
self.game_over = True
# Process the guess
return self.evaluate_guess(guess)
def evaluate_guess(self, guess: str) -> List[Dict[str, str]]:
"""
Evaluate a guess without modifying game state
Args:
guess: The word to evaluate
Returns:
List of dictionaries with letter and status (correct, present, absent)
"""
guess = guess.lower()
if len(guess) != len(self.word):
return []
result = []
word_chars = list(self.word)
# First pass: Find exact matches
for i, char in enumerate(guess):
if i < len(self.word) and char == self.word[i]:
result.append({"letter": char, "status": "correct"})
word_chars[i] = None # Mark as used
else:
result.append({"letter": char, "status": "unknown"})
# Second pass: Find misplaced letters
for i, item in enumerate(result):
if item["status"] == "unknown":
char = item["letter"]
if char in word_chars:
# Letter exists but in wrong position
result[i]["status"] = "present"
word_chars[word_chars.index(char)] = None # Mark as used
else:
# Letter doesn't exist in the word
result[i]["status"] = "absent"
return result
class WordleView(ui.View):
"""Discord UI View for the Wordle game"""
def __init__(self, player: discord.Member, word: str, timeout: float = 600.0):
"""
Initialize the Wordle game view
Args:
player: The Discord member playing the game
word: The target word to guess
timeout: Time in seconds before the view times out
"""
super().__init__(timeout=timeout)
self.player = player
self.game = WordleGame(word)
self.message: Optional[discord.Message] = None
self.used_letters: Set[str] = set()
async def interaction_check(self, interaction: discord.Interaction) -> bool:
"""Ensure only the player can interact with the game"""
if interaction.user.id != self.player.id:
await interaction.response.send_message("This is not your game!", ephemeral=True)
return False
return True
async def on_timeout(self) -> None:
"""Handle timeout - disable buttons and show the answer"""
if self.message:
self.game.game_over = True
await self.update_message(None, timeout=True)
def format_board(self) -> str:
"""Format the game board into a string representation"""
if not self.game.guesses:
# No guesses yet
return "No guesses yet. Use the button below to make a guess!"
board_lines = []
# Add each guess with colored squares
for guess in self.game.guesses:
# Use evaluate_guess instead of make_guess to avoid modifying game state
result = self.game.evaluate_guess(guess)
guess_line = ""
for item in result:
if item["status"] == "correct":
guess_line += f"🟩" # Green square for correct position
elif item["status"] == "present":
guess_line += f"🟨" # Yellow square for correct letter, wrong position
else:
guess_line += f"" # Black square for incorrect letter
# Track used letters
self.used_letters.add(item["letter"])
# Add the actual letters after the squares
guess_line += f" {guess.upper()}"
board_lines.append(guess_line)
return "\n".join(board_lines)
def format_keyboard(self) -> str:
"""Format a keyboard showing used letters"""
if not self.used_letters:
return ""
keyboard = [
"QWERTYUIOP",
"ASDFGHJKL",
"ZXCVBNM"
]
keyboard_lines = []
for row in keyboard:
line = ""
for letter in row:
letter_lower = letter.lower()
if letter_lower in self.used_letters:
# Determine the best status for this letter across all guesses
status = "absent" # Default to absent
# Check if this letter appears as correct in any position
correct_position = False
present_position = False
for guess in self.game.guesses:
result = self.game.evaluate_guess(guess)
for i, char in enumerate(guess):
if char == letter_lower:
if result[i]["status"] == "correct":
correct_position = True
break
elif result[i]["status"] == "present":
present_position = True
if correct_position:
status = "correct"
elif present_position:
status = "present"
if status == "correct":
line += f"🟩" # Green for correct
elif status == "present":
line += f"🟨" # Yellow for present
else:
line += f"" # Black for absent
else:
line += f"" # White for unused
keyboard_lines.append(line)
return "\n".join(keyboard_lines)
async def update_message(self, interaction: Optional[discord.Interaction] = None, timeout: bool = False) -> None:
"""Update the game message with the current state"""
if not self.message:
return
# Format the message content
content = f"# Wordle Game\n\n"
content += self.format_board()
content += f"\n\n"
# Add keyboard
keyboard = self.format_keyboard()
if keyboard:
content += f"{keyboard}\n\n"
# Add game status
attempts_left = self.game.max_attempts - self.game.attempts
content += f"Attempts: {self.game.attempts}/{self.game.max_attempts} ({attempts_left} left)\n"
# Add game result if game is over
if self.game.game_over:
self.clear_items() # Remove all buttons
if self.game.won:
content += f"\n🎉 You won! The word was **{self.game.word.upper()}**."
elif timeout:
content += f"\n⏰ Time's up! The word was **{self.game.word.upper()}**."
else:
content += f"\n❌ Game over! The word was **{self.game.word.upper()}**."
# Update the message
if interaction:
await interaction.response.edit_message(content=content, view=self)
else:
await self.message.edit(content=content, view=self)
@ui.button(label="Make a Guess", style=discord.ButtonStyle.primary)
async def guess_button(self, interaction: discord.Interaction, _: ui.Button):
"""Button to make a guess"""
# Create and send the modal
modal = WordleGuessModal(self)
await interaction.response.send_modal(modal)
class WordleGuessModal(ui.Modal, title="Enter your guess"):
"""Modal for entering a Wordle guess"""
guess = ui.TextInput(
label="Your 5-letter guess",
placeholder="Enter a 5-letter word",
min_length=5,
max_length=5,
required=True
)
def __init__(self, view: WordleView):
super().__init__()
self.wordle_view = view
async def on_submit(self, interaction: discord.Interaction):
"""Process the submitted guess"""
guess = self.guess.value.strip().lower()
# Validate the guess
if len(guess) != 5:
await interaction.response.send_message("Please enter a 5-letter word.", ephemeral=True)
return
if not guess.isalpha():
await interaction.response.send_message("Your guess must contain only letters.", ephemeral=True)
return
# Process the guess - this is the only place where make_guess should be called
# to actually modify the game state
self.wordle_view.game.make_guess(guess)
# Update the game message
await self.wordle_view.update_message(interaction)
def load_word_list(file_path: str = "words.txt", word_length: int = 5) -> List[str]:
"""
Load and filter words from a file
Args:
file_path: Path to the words file
word_length: Length of words to filter (default: 5 for Wordle)
Returns:
List of words with the specified length
"""
try:
with open(file_path, "r") as file:
words = [word.strip().lower() for word in file if word.strip()]
# Filter words by length
filtered_words = [word for word in words if len(word) == word_length]
return filtered_words
except FileNotFoundError:
print(f"Word list file not found: {file_path}")
return []