feat: Generate initial Wordle game board image for game start
This commit is contained in:
parent
65624a5670
commit
dd334c5f24
@ -1,6 +1,9 @@
|
||||
import discord
|
||||
from discord import ui
|
||||
from typing import List, Dict, Optional, Set
|
||||
import io
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
import os
|
||||
|
||||
class WordleGame:
|
||||
"""Class to handle Wordle game logic"""
|
||||
@ -91,6 +94,231 @@ class WordleGame:
|
||||
|
||||
return result
|
||||
|
||||
def generate_board_image(game: WordleGame, used_letters: Set[str] = None) -> discord.File:
|
||||
"""
|
||||
Generate an image of the Wordle game board
|
||||
|
||||
Args:
|
||||
game: The WordleGame instance
|
||||
used_letters: Set of letters that have been used in guesses
|
||||
|
||||
Returns:
|
||||
discord.File: The image file to send
|
||||
"""
|
||||
# Define colors and dimensions
|
||||
CORRECT_COLOR = (106, 170, 100) # Green
|
||||
PRESENT_COLOR = (201, 180, 88) # Yellow
|
||||
ABSENT_COLOR = (120, 124, 126) # Gray
|
||||
UNUSED_COLOR = (211, 214, 218) # Light gray
|
||||
BACKGROUND_COLOR = (255, 255, 255) # White
|
||||
TEXT_COLOR = (0, 0, 0) # Black
|
||||
|
||||
SQUARE_SIZE = 60
|
||||
SQUARE_MARGIN = 5
|
||||
KEYBOARD_MARGIN_TOP = 30
|
||||
KEYBOARD_SQUARE_SIZE = 40
|
||||
KEYBOARD_SQUARE_MARGIN = 4
|
||||
|
||||
# Calculate board dimensions
|
||||
word_length = len(game.word)
|
||||
max_attempts = game.max_attempts
|
||||
|
||||
# Calculate total width and height
|
||||
board_width = (SQUARE_SIZE + SQUARE_MARGIN) * word_length + SQUARE_MARGIN
|
||||
board_height = (SQUARE_SIZE + SQUARE_MARGIN) * max_attempts + SQUARE_MARGIN
|
||||
|
||||
# Add space for keyboard
|
||||
keyboard_rows = ["QWERTYUIOP", "ASDFGHJKL", "ZXCVBNM"]
|
||||
keyboard_width = max(len(row) for row in keyboard_rows) * (KEYBOARD_SQUARE_SIZE + KEYBOARD_SQUARE_MARGIN) + KEYBOARD_SQUARE_MARGIN
|
||||
keyboard_height = len(keyboard_rows) * (KEYBOARD_SQUARE_SIZE + KEYBOARD_SQUARE_MARGIN) + KEYBOARD_SQUARE_MARGIN
|
||||
|
||||
# Add space for status text
|
||||
status_height = 40
|
||||
|
||||
# Total image dimensions
|
||||
total_width = max(board_width, keyboard_width) + 40 # Add padding
|
||||
total_height = board_height + KEYBOARD_MARGIN_TOP + keyboard_height + status_height + 40 # Add padding
|
||||
|
||||
# Create image
|
||||
img = Image.new("RGB", (total_width, total_height), BACKGROUND_COLOR)
|
||||
draw = ImageDraw.Draw(img)
|
||||
|
||||
# Try to load font
|
||||
font = None
|
||||
try:
|
||||
# Construct path relative to this script file
|
||||
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
PROJECT_ROOT = os.path.dirname(os.path.dirname(SCRIPT_DIR)) # Go up two levels from games dir
|
||||
FONT_DIR_NAME = "dejavusans" # Directory specified by user
|
||||
FONT_FILE_NAME = "DejaVuSans.ttf"
|
||||
font_path = os.path.join(PROJECT_ROOT, FONT_DIR_NAME, FONT_FILE_NAME)
|
||||
|
||||
# Try to load the font
|
||||
font = ImageFont.truetype(font_path, size=int(SQUARE_SIZE * 0.6))
|
||||
small_font = ImageFont.truetype(font_path, size=int(KEYBOARD_SQUARE_SIZE * 0.6))
|
||||
status_font = ImageFont.truetype(font_path, size=int(status_height * 0.6))
|
||||
except Exception as e:
|
||||
print(f"Error loading font: {e}")
|
||||
# Fallback to default font
|
||||
font = ImageFont.load_default()
|
||||
small_font = ImageFont.load_default()
|
||||
status_font = ImageFont.load_default()
|
||||
|
||||
# Calculate starting positions to center the board
|
||||
start_x = (total_width - board_width) // 2
|
||||
start_y = 20 # Top padding
|
||||
|
||||
# Draw the board
|
||||
for row in range(max_attempts):
|
||||
for col in range(word_length):
|
||||
# Calculate square position
|
||||
x = start_x + col * (SQUARE_SIZE + SQUARE_MARGIN) + SQUARE_MARGIN
|
||||
y = start_y + row * (SQUARE_SIZE + SQUARE_MARGIN) + SQUARE_MARGIN
|
||||
|
||||
# Default to empty square
|
||||
square_color = UNUSED_COLOR
|
||||
letter = ""
|
||||
|
||||
# If there's a guess for this row, fill in the square
|
||||
if row < len(game.guesses):
|
||||
guess = game.guesses[row]
|
||||
if col < len(guess):
|
||||
letter = guess[col].upper()
|
||||
|
||||
# Get the status of this letter
|
||||
result = game.evaluate_guess(guess)
|
||||
status = result[col]["status"]
|
||||
|
||||
if status == "correct":
|
||||
square_color = CORRECT_COLOR
|
||||
elif status == "present":
|
||||
square_color = PRESENT_COLOR
|
||||
else: # absent
|
||||
square_color = ABSENT_COLOR
|
||||
|
||||
# Draw the square
|
||||
draw.rectangle([x, y, x + SQUARE_SIZE, y + SQUARE_SIZE], fill=square_color, outline=(0, 0, 0), width=2)
|
||||
|
||||
# Draw the letter if there is one
|
||||
if letter:
|
||||
# Center the text in the square
|
||||
try:
|
||||
bbox = draw.textbbox((0, 0), letter, font=font)
|
||||
text_width = bbox[2] - bbox[0]
|
||||
text_height = bbox[3] - bbox[1]
|
||||
except AttributeError:
|
||||
# Fallback for older Pillow versions
|
||||
try:
|
||||
text_width, text_height = draw.textsize(letter, font=font)
|
||||
except:
|
||||
text_width, text_height = font.getsize(letter)
|
||||
|
||||
text_x = x + (SQUARE_SIZE - text_width) // 2
|
||||
text_y = y + (SQUARE_SIZE - text_height) // 2
|
||||
|
||||
# White text on colored backgrounds
|
||||
text_color = (255, 255, 255) if square_color != UNUSED_COLOR else TEXT_COLOR
|
||||
draw.text((text_x, text_y), letter, fill=text_color, font=font)
|
||||
|
||||
# Draw the keyboard
|
||||
if used_letters:
|
||||
keyboard_start_y = start_y + board_height + KEYBOARD_MARGIN_TOP
|
||||
|
||||
for row_idx, row in enumerate(keyboard_rows):
|
||||
# Center this row of keys
|
||||
row_width = len(row) * (KEYBOARD_SQUARE_SIZE + KEYBOARD_SQUARE_MARGIN) + KEYBOARD_SQUARE_MARGIN
|
||||
row_start_x = (total_width - row_width) // 2
|
||||
|
||||
for col_idx, key in enumerate(row):
|
||||
key_lower = key.lower()
|
||||
x = row_start_x + col_idx * (KEYBOARD_SQUARE_SIZE + KEYBOARD_SQUARE_MARGIN) + KEYBOARD_SQUARE_MARGIN
|
||||
y = keyboard_start_y + row_idx * (KEYBOARD_SQUARE_SIZE + KEYBOARD_SQUARE_MARGIN) + KEYBOARD_SQUARE_MARGIN
|
||||
|
||||
# Default color for unused keys
|
||||
key_color = UNUSED_COLOR
|
||||
|
||||
if key_lower in 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 game.guesses:
|
||||
result = game.evaluate_guess(guess)
|
||||
for i, char in enumerate(guess):
|
||||
if char == key_lower:
|
||||
if result[i]["status"] == "correct":
|
||||
correct_position = True
|
||||
break
|
||||
elif result[i]["status"] == "present":
|
||||
present_position = True
|
||||
|
||||
if correct_position:
|
||||
key_color = CORRECT_COLOR
|
||||
elif present_position:
|
||||
key_color = PRESENT_COLOR
|
||||
else:
|
||||
key_color = ABSENT_COLOR
|
||||
|
||||
# Draw the key
|
||||
draw.rectangle([x, y, x + KEYBOARD_SQUARE_SIZE, y + KEYBOARD_SQUARE_SIZE],
|
||||
fill=key_color, outline=(0, 0, 0), width=1)
|
||||
|
||||
# Draw the letter
|
||||
try:
|
||||
bbox = draw.textbbox((0, 0), key, font=small_font)
|
||||
text_width = bbox[2] - bbox[0]
|
||||
text_height = bbox[3] - bbox[1]
|
||||
except AttributeError:
|
||||
# Fallback for older Pillow versions
|
||||
try:
|
||||
text_width, text_height = draw.textsize(key, font=small_font)
|
||||
except:
|
||||
text_width, text_height = small_font.getsize(key)
|
||||
|
||||
text_x = x + (KEYBOARD_SQUARE_SIZE - text_width) // 2
|
||||
text_y = y + (KEYBOARD_SQUARE_SIZE - text_height) // 2
|
||||
|
||||
# White text on colored backgrounds (except unused)
|
||||
text_color = (255, 255, 255) if key_color != UNUSED_COLOR else TEXT_COLOR
|
||||
draw.text((text_x, text_y), key, fill=text_color, font=small_font)
|
||||
|
||||
# Draw game status
|
||||
status_y = keyboard_start_y + keyboard_height + 10 if used_letters else start_y + board_height + 10
|
||||
attempts_left = game.max_attempts - game.attempts
|
||||
status_text = f"Attempts: {game.attempts}/{game.max_attempts} ({attempts_left} left)"
|
||||
|
||||
# Add game result if game is over
|
||||
if game.game_over:
|
||||
if game.won:
|
||||
status_text += f" - You won! The word was {game.word.upper()}."
|
||||
else:
|
||||
status_text += f" - Game over! The word was {game.word.upper()}."
|
||||
|
||||
# Center the status text
|
||||
try:
|
||||
bbox = draw.textbbox((0, 0), status_text, font=status_font)
|
||||
text_width = bbox[2] - bbox[0]
|
||||
text_height = bbox[3] - bbox[1]
|
||||
except AttributeError:
|
||||
# Fallback for older Pillow versions
|
||||
try:
|
||||
text_width, text_height = draw.textsize(status_text, font=status_font)
|
||||
except:
|
||||
text_width, text_height = status_font.getsize(status_text)
|
||||
|
||||
text_x = (total_width - text_width) // 2
|
||||
draw.text((text_x, status_y), status_text, fill=TEXT_COLOR, font=status_font)
|
||||
|
||||
# Save image to a bytes buffer
|
||||
img_byte_arr = io.BytesIO()
|
||||
img.save(img_byte_arr, format='PNG')
|
||||
img_byte_arr.seek(0)
|
||||
|
||||
return discord.File(fp=img_byte_arr, filename="wordle_board.png")
|
||||
|
||||
class WordleView(ui.View):
|
||||
"""Discord UI View for the Wordle game"""
|
||||
|
||||
@ -122,122 +350,38 @@ class WordleView(ui.View):
|
||||
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)
|
||||
# Track used letters when processing guesses
|
||||
def track_used_letters(self, guess: str) -> None:
|
||||
"""Track letters that have been used in guesses"""
|
||||
for letter in guess:
|
||||
self.used_letters.add(letter)
|
||||
|
||||
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"
|
||||
# Generate the board image
|
||||
board_image = generate_board_image(self.game, self.used_letters)
|
||||
|
||||
# Add keyboard
|
||||
keyboard = self.format_keyboard()
|
||||
if keyboard:
|
||||
content += f"{keyboard}\n\n"
|
||||
# Create a simple text content
|
||||
content = f"# Wordle Game"
|
||||
|
||||
# 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 game is over, add result to content
|
||||
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()}**."
|
||||
content += f"\n\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()}**."
|
||||
content += f"\n\n⏰ Time's up! The word was **{self.game.word.upper()}**."
|
||||
else:
|
||||
content += f"\n❌ Game over! The word was **{self.game.word.upper()}**."
|
||||
content += f"\n\n❌ Game over! The word was **{self.game.word.upper()}**."
|
||||
|
||||
# Update the message
|
||||
# Update the message with the image
|
||||
if interaction:
|
||||
await interaction.response.edit_message(content=content, view=self)
|
||||
await interaction.response.edit_message(content=content, attachments=[board_image], view=self)
|
||||
else:
|
||||
await self.message.edit(content=content, view=self)
|
||||
await self.message.edit(content=content, attachments=[board_image], view=self)
|
||||
|
||||
@ui.button(label="Make a Guess", style=discord.ButtonStyle.primary)
|
||||
async def guess_button(self, interaction: discord.Interaction, _: ui.Button):
|
||||
@ -278,6 +422,9 @@ class WordleGuessModal(ui.Modal, title="Enter your guess"):
|
||||
# to actually modify the game state
|
||||
self.wordle_view.game.make_guess(guess)
|
||||
|
||||
# Track used letters for the keyboard display
|
||||
self.wordle_view.track_used_letters(guess)
|
||||
|
||||
# Update the game message
|
||||
await self.wordle_view.update_message(interaction)
|
||||
|
||||
|
@ -303,9 +303,14 @@ class GamesCog(commands.Cog, name="Games"):
|
||||
# Create the Wordle game view
|
||||
view = WordleView(interaction.user, target_word)
|
||||
|
||||
# Send the initial game message
|
||||
# Generate the initial board image
|
||||
from .games.wordle_game import generate_board_image
|
||||
initial_board_image = generate_board_image(view.game, view.used_letters)
|
||||
|
||||
# Send the initial game message with the image
|
||||
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!",
|
||||
"# Wordle Game\n\nGuess the 5-letter word. You have 6 attempts.",
|
||||
file=initial_board_image,
|
||||
view=view
|
||||
)
|
||||
|
||||
@ -766,9 +771,14 @@ class GamesCog(commands.Cog, name="Games"):
|
||||
# Create the Wordle game view
|
||||
view = WordleView(ctx.author, target_word)
|
||||
|
||||
# Send the initial game message
|
||||
# Generate the initial board image
|
||||
from .games.wordle_game import generate_board_image
|
||||
initial_board_image = generate_board_image(view.game, view.used_letters)
|
||||
|
||||
# Send the initial game message with the image
|
||||
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!",
|
||||
"# Wordle Game\n\nGuess the 5-letter word. You have 6 attempts.",
|
||||
file=initial_board_image,
|
||||
view=view
|
||||
)
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user