feat: Add Wordle game implementation with command integration
This commit is contained in:
parent
62014d00be
commit
60cb29a62e
279
cogs/games/wordle_game.py
Normal file
279
cogs/games/wordle_game.py
Normal file
@ -0,0 +1,279 @@
|
||||
import discord
|
||||
from discord import ui
|
||||
import random
|
||||
import os
|
||||
import asyncio
|
||||
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
|
||||
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:
|
||||
result = self.game.make_guess(guess)
|
||||
# Skip re-processing the guess, just get the result
|
||||
self.game.attempts -= 1
|
||||
|
||||
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:
|
||||
# Check the status of this letter in the most recent guess
|
||||
status = "absent" # Default to absent
|
||||
for guess in self.game.guesses:
|
||||
for i, char in enumerate(guess):
|
||||
if char == letter_lower:
|
||||
if char == self.game.word[i]:
|
||||
status = "correct"
|
||||
break
|
||||
elif char in self.game.word:
|
||||
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, button: 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
|
||||
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 []
|
@ -21,6 +21,7 @@ from .games.coinflip_game import CoinFlipView
|
||||
from .games.tictactoe_game import TicTacToeView, BotTicTacToeView
|
||||
from .games.rps_game import RockPaperScissorsView
|
||||
from .games.basic_games import roll_dice, flip_coin, magic8ball_response, play_hangman
|
||||
from .games.wordle_game import WordleView, load_word_list
|
||||
|
||||
class GamesCog(commands.Cog, name="Games"):
|
||||
"""Cog for game-related commands"""
|
||||
@ -185,6 +186,15 @@ class GamesCog(commands.Cog, name="Games"):
|
||||
)
|
||||
self.games_group.add_command(loadchess_command)
|
||||
|
||||
# Wordle command
|
||||
wordle_command = app_commands.Command(
|
||||
name="wordle",
|
||||
description="Play a game of Wordle - guess the 5-letter word",
|
||||
callback=self.games_wordle_callback,
|
||||
parent=self.games_group
|
||||
)
|
||||
self.games_group.add_command(wordle_command)
|
||||
|
||||
async def cog_unload(self):
|
||||
"""Clean up resources when the cog is unloaded."""
|
||||
print("Unloading GamesCog, closing active chess engines...")
|
||||
@ -278,6 +288,30 @@ class GamesCog(commands.Cog, name="Games"):
|
||||
"""Callback for /games hangman command"""
|
||||
await play_hangman(self.bot, interaction.channel, interaction.user)
|
||||
|
||||
async def games_wordle_callback(self, interaction: discord.Interaction):
|
||||
"""Callback for /games wordle command"""
|
||||
# Load 5-letter words from the words.txt file
|
||||
word_list = load_word_list("words.txt", 5)
|
||||
|
||||
if not word_list:
|
||||
await interaction.response.send_message("Error: Could not load word list or no 5-letter words found.", ephemeral=True)
|
||||
return
|
||||
|
||||
# Select a random word
|
||||
target_word = random.choice(word_list)
|
||||
|
||||
# Create the Wordle game view
|
||||
view = WordleView(interaction.user, target_word)
|
||||
|
||||
# Send the initial game message
|
||||
await interaction.response.send_message(
|
||||
"# Wordle Game\n\nGuess the 5-letter word. You have 6 attempts.\n\nNo guesses yet. Use the button below to make a guess!",
|
||||
view=view
|
||||
)
|
||||
|
||||
# Store the message for later updates
|
||||
view.message = await interaction.original_response()
|
||||
|
||||
# TicTacToe group callbacks
|
||||
async def games_tictactoe_callback(self, interaction: discord.Interaction, opponent: discord.Member):
|
||||
"""Callback for /games tictactoe play command"""
|
||||
@ -1125,6 +1159,31 @@ class GamesCog(commands.Cog, name="Games"):
|
||||
"""(Prefix) Play a game of Hangman."""
|
||||
await play_hangman(self.bot, ctx.channel, ctx.author)
|
||||
|
||||
@commands.command(name="wordle", add_to_app_commands=False)
|
||||
async def wordle_prefix(self, ctx: commands.Context):
|
||||
"""(Prefix) Play a game of Wordle."""
|
||||
# Load 5-letter words from the words.txt file
|
||||
word_list = load_word_list("words.txt", 5)
|
||||
|
||||
if not word_list:
|
||||
await ctx.send("Error: Could not load word list or no 5-letter words found.")
|
||||
return
|
||||
|
||||
# Select a random word
|
||||
target_word = random.choice(word_list)
|
||||
|
||||
# Create the Wordle game view
|
||||
view = WordleView(ctx.author, target_word)
|
||||
|
||||
# Send the initial game message
|
||||
message = await ctx.send(
|
||||
"# Wordle Game\n\nGuess the 5-letter word. You have 6 attempts.\n\nNo guesses yet. Use the button below to make a guess!",
|
||||
view=view
|
||||
)
|
||||
|
||||
# Store the message for later updates
|
||||
view.message = message
|
||||
|
||||
@commands.command(name="guess", add_to_app_commands=False)
|
||||
async def guess_prefix(self, ctx: commands.Context, guess: int):
|
||||
"""(Prefix) Guess a number between 1 and 100."""
|
||||
|
Loading…
x
Reference in New Issue
Block a user