929 lines
36 KiB
Python
929 lines
36 KiB
Python
import discord
|
|
from discord.ext import commands
|
|
from discord import app_commands, ui
|
|
import random
|
|
import asyncio
|
|
from typing import Optional, List, Union
|
|
import chess
|
|
import chess.engine
|
|
import chess.pgn
|
|
import platform
|
|
import os
|
|
import io
|
|
import ast
|
|
|
|
# Import game implementations from separate files
|
|
from .games.chess_game import (
|
|
generate_board_image,
|
|
MoveInputModal,
|
|
ChessView,
|
|
ChessBotView,
|
|
get_stockfish_path,
|
|
)
|
|
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"""
|
|
|
|
def __init__(self, bot: commands.Bot):
|
|
self.bot = bot
|
|
# Store active bot game views to manage engine resources
|
|
self.active_chess_bot_views = {} # Store by message ID
|
|
self.ttt_games = {} # Store TicTacToe game instances by user ID
|
|
|
|
# Create the main command group for this cog
|
|
self.games_group = app_commands.Group(
|
|
name="fun", description="Play various games with the bot or other users"
|
|
)
|
|
|
|
# Register commands
|
|
self.register_commands()
|
|
|
|
# Add command group to the bot's tree
|
|
self.bot.tree.add_command(self.games_group)
|
|
|
|
def _array_to_fen(self, board_array: List[List[str]], turn: chess.Color) -> str:
|
|
"""Converts an 8x8 array representation to a basic FEN string."""
|
|
fen_rows = []
|
|
for rank_idx in range(8): # Iterate ranks 0-7 (corresponds to 8-1 in FEN)
|
|
rank_data = board_array[rank_idx]
|
|
fen_row = ""
|
|
empty_count = 0
|
|
for piece in rank_data: # Iterate files a-h
|
|
if piece == ".":
|
|
empty_count += 1
|
|
else:
|
|
if empty_count > 0:
|
|
fen_row += str(empty_count)
|
|
empty_count = 0
|
|
# Validate piece character if needed, assume valid for now
|
|
fen_row += piece
|
|
if empty_count > 0:
|
|
fen_row += str(empty_count)
|
|
fen_rows.append(fen_row)
|
|
|
|
piece_placement = "/".join(fen_rows)
|
|
turn_char = "w" if turn == chess.WHITE else "b"
|
|
# Default castling, no en passant, 0 halfmove, 1 fullmove for simplicity from array
|
|
fen = f"{piece_placement} {turn_char} - - 0 1"
|
|
return fen
|
|
|
|
def register_commands(self):
|
|
"""Register all commands for this cog"""
|
|
|
|
# --- Dice Commands ---
|
|
# Coinflip command
|
|
coinflip_command = app_commands.Command(
|
|
name="coinflip",
|
|
description="Flip a coin and get Heads or Tails",
|
|
callback=self.games_coinflip_callback,
|
|
parent=self.games_group,
|
|
)
|
|
self.games_group.add_command(coinflip_command)
|
|
|
|
# Roll command
|
|
roll_command = app_commands.Command(
|
|
name="roll",
|
|
description="Roll a dice and get a number between 1 and 6",
|
|
callback=self.games_roll_callback,
|
|
parent=self.games_group,
|
|
)
|
|
self.games_group.add_command(roll_command)
|
|
|
|
# Magic 8-ball command
|
|
magic8ball_command = app_commands.Command(
|
|
name="magic8ball",
|
|
description="Ask the magic 8 ball a question",
|
|
callback=self.games_magic8ball_callback,
|
|
parent=self.games_group,
|
|
)
|
|
self.games_group.add_command(magic8ball_command)
|
|
|
|
# --- RPS Commands ---
|
|
# RPS command
|
|
rps_command = app_commands.Command(
|
|
name="rps",
|
|
description="Play Rock-Paper-Scissors against the bot",
|
|
callback=self.games_rps_callback,
|
|
parent=self.games_group,
|
|
)
|
|
self.games_group.add_command(rps_command)
|
|
|
|
# RPS Challenge command
|
|
rpschallenge_command = app_commands.Command(
|
|
name="rpschallenge",
|
|
description="Challenge another user to a game of Rock-Paper-Scissors",
|
|
callback=self.games_rpschallenge_callback,
|
|
parent=self.games_group,
|
|
)
|
|
self.games_group.add_command(rpschallenge_command)
|
|
|
|
# --- Other Game Commands ---
|
|
# Guess command
|
|
guess_command = app_commands.Command(
|
|
name="guess",
|
|
description="Guess the number I'm thinking of (1-100)",
|
|
callback=self.games_guess_callback,
|
|
parent=self.games_group,
|
|
)
|
|
self.games_group.add_command(guess_command)
|
|
|
|
# Hangman command
|
|
hangman_command = app_commands.Command(
|
|
name="hangman",
|
|
description="Play a game of Hangman",
|
|
callback=self.games_hangman_callback,
|
|
parent=self.games_group,
|
|
)
|
|
self.games_group.add_command(hangman_command)
|
|
|
|
# --- TicTacToe Commands ---
|
|
# TicTacToe command
|
|
tictactoe_command = app_commands.Command(
|
|
name="tictactoe",
|
|
description="Challenge another user to a game of Tic-Tac-Toe",
|
|
callback=self.games_tictactoe_callback,
|
|
parent=self.games_group,
|
|
)
|
|
self.games_group.add_command(tictactoe_command)
|
|
|
|
# TicTacToe Bot command
|
|
tictactoebot_command = app_commands.Command(
|
|
name="tictactoebot",
|
|
description="Play a game of Tic-Tac-Toe against the bot",
|
|
callback=self.games_tictactoebot_callback,
|
|
parent=self.games_group,
|
|
)
|
|
self.games_group.add_command(tictactoebot_command)
|
|
|
|
# --- Chess Commands ---
|
|
# Chess command
|
|
chess_command = app_commands.Command(
|
|
name="chess",
|
|
description="Challenge another user to a game of chess",
|
|
callback=self.games_chess_callback,
|
|
parent=self.games_group,
|
|
)
|
|
self.games_group.add_command(chess_command)
|
|
|
|
# Chess Bot command
|
|
chessbot_command = app_commands.Command(
|
|
name="chessbot",
|
|
description="Play chess against the bot",
|
|
callback=self.games_chessbot_callback,
|
|
parent=self.games_group,
|
|
)
|
|
self.games_group.add_command(chessbot_command)
|
|
|
|
# Load Chess command
|
|
loadchess_command = app_commands.Command(
|
|
name="loadchess",
|
|
description="Load a chess game from FEN, PGN, or array representation",
|
|
callback=self.games_loadchess_callback,
|
|
parent=self.games_group,
|
|
)
|
|
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...")
|
|
# Create a copy of the dictionary items to avoid runtime errors during iteration
|
|
views_to_stop = list(self.active_chess_bot_views.values())
|
|
for view in views_to_stop:
|
|
await view.stop_engine()
|
|
view.stop() # Stop the view itself
|
|
self.active_chess_bot_views.clear()
|
|
print("GamesCog unloaded.")
|
|
|
|
# --- Command Callbacks ---
|
|
# Dice group callbacks
|
|
async def games_coinflip_callback(self, interaction: discord.Interaction):
|
|
"""Callback for /games dice coinflip command"""
|
|
result = flip_coin()
|
|
await interaction.response.send_message(f"The coin landed on **{result}**! 🪙")
|
|
|
|
async def games_roll_callback(self, interaction: discord.Interaction):
|
|
"""Callback for /games dice roll command"""
|
|
result = roll_dice()
|
|
await interaction.response.send_message(f"You rolled a **{result}**! 🎲")
|
|
|
|
async def games_magic8ball_callback(
|
|
self, interaction: discord.Interaction, question: str = None
|
|
):
|
|
"""Callback for /games dice magic8ball command"""
|
|
response = magic8ball_response()
|
|
await interaction.response.send_message(f"🎱 {response}")
|
|
|
|
# Games group callbacks
|
|
async def games_rps_callback(
|
|
self, interaction: discord.Interaction, choice: app_commands.Choice[str]
|
|
):
|
|
"""Callback for /games rps command"""
|
|
choices = ["Rock", "Paper", "Scissors"]
|
|
bot_choice = random.choice(choices)
|
|
user_choice = choice.value # Get value from choice
|
|
|
|
if user_choice == bot_choice:
|
|
result = "It's a tie!"
|
|
elif (
|
|
(user_choice == "Rock" and bot_choice == "Scissors")
|
|
or (user_choice == "Paper" and bot_choice == "Rock")
|
|
or (user_choice == "Scissors" and bot_choice == "Paper")
|
|
):
|
|
result = "You win! 🎉"
|
|
else:
|
|
result = "You lose! 😢"
|
|
|
|
emojis = {"Rock": "🪨", "Paper": "📄", "Scissors": "✂️"}
|
|
|
|
await interaction.response.send_message(
|
|
f"You chose **{user_choice}** {emojis[user_choice]}\n"
|
|
f"I chose **{bot_choice}** {emojis[bot_choice]}\n\n"
|
|
f"{result}"
|
|
)
|
|
|
|
async def games_rpschallenge_callback(
|
|
self, interaction: discord.Interaction, opponent: discord.User
|
|
):
|
|
"""Callback for /games rpschallenge command"""
|
|
initiator = interaction.user
|
|
|
|
if opponent == initiator:
|
|
await interaction.response.send_message(
|
|
"You cannot challenge yourself!", ephemeral=True
|
|
)
|
|
return
|
|
if opponent.bot:
|
|
await interaction.response.send_message(
|
|
"You cannot challenge a bot!", ephemeral=True
|
|
)
|
|
return
|
|
|
|
view = RockPaperScissorsView(initiator, opponent)
|
|
initial_message = f"Rock Paper Scissors: {initiator.mention} vs {opponent.mention}\n\nChoose your move!"
|
|
await interaction.response.send_message(initial_message, view=view)
|
|
message = await interaction.original_response()
|
|
view.message = message
|
|
|
|
async def games_guess_callback(self, interaction: discord.Interaction, guess: int):
|
|
"""Callback for /games guess command"""
|
|
# Simple implementation: generate number per guess (no state needed)
|
|
number_to_guess = random.randint(1, 100)
|
|
|
|
if guess < 1 or guess > 100:
|
|
await interaction.response.send_message(
|
|
"Please guess a number between 1 and 100.", ephemeral=True
|
|
)
|
|
return
|
|
|
|
if guess == number_to_guess:
|
|
await interaction.response.send_message(
|
|
f"🎉 Correct! The number was **{number_to_guess}**."
|
|
)
|
|
elif guess < number_to_guess:
|
|
await interaction.response.send_message(
|
|
f"Too low! The number was {number_to_guess}."
|
|
)
|
|
else:
|
|
await interaction.response.send_message(
|
|
f"Too high! The number was {number_to_guess}."
|
|
)
|
|
|
|
async def games_hangman_callback(self, interaction: discord.Interaction):
|
|
"""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_alpha.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)
|
|
|
|
# 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.",
|
|
file=initial_board_image,
|
|
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.User
|
|
):
|
|
"""Callback for /games tictactoe play command"""
|
|
initiator = interaction.user
|
|
|
|
if opponent == initiator:
|
|
await interaction.response.send_message(
|
|
"You cannot challenge yourself!", ephemeral=True
|
|
)
|
|
return
|
|
if opponent.bot:
|
|
await interaction.response.send_message(
|
|
"You cannot challenge a bot! Use `/games tictactoe bot` instead.",
|
|
ephemeral=True,
|
|
)
|
|
return
|
|
|
|
view = TicTacToeView(initiator, opponent)
|
|
initial_message = f"Tic Tac Toe: {initiator.mention} (X) vs {opponent.mention} (O)\n\nTurn: **{initiator.mention} (X)**"
|
|
await interaction.response.send_message(initial_message, view=view)
|
|
message = await interaction.original_response()
|
|
view.message = message # Store message for timeout handling
|
|
|
|
async def games_tictactoebot_callback(
|
|
self,
|
|
interaction: discord.Interaction,
|
|
difficulty: app_commands.Choice[str] = None,
|
|
):
|
|
"""Callback for /games tictactoe bot command"""
|
|
# Use default if no choice is made (discord.py handles default value assignment)
|
|
difficulty_value = difficulty.value if difficulty else "minimax"
|
|
|
|
# Ensure tictactoe module is importable
|
|
try:
|
|
import sys
|
|
import os
|
|
|
|
parent_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
if parent_dir not in sys.path:
|
|
sys.path.append(parent_dir)
|
|
from tictactoe import (
|
|
TicTacToe,
|
|
) # Assuming tictactoe.py is in the parent directory
|
|
except ImportError:
|
|
await interaction.response.send_message(
|
|
"Error: TicTacToe game engine module not found.", ephemeral=True
|
|
)
|
|
return
|
|
except Exception as e:
|
|
await interaction.response.send_message(
|
|
f"Error importing TicTacToe module: {e}", ephemeral=True
|
|
)
|
|
return
|
|
|
|
# Create a new game instance
|
|
try:
|
|
game = TicTacToe(ai_player="O", ai_difficulty=difficulty_value)
|
|
except Exception as e:
|
|
await interaction.response.send_message(
|
|
f"Error initializing TicTacToe game: {e}", ephemeral=True
|
|
)
|
|
return
|
|
|
|
# Create a view for the user interface
|
|
view = BotTicTacToeView(game, interaction.user)
|
|
await interaction.response.send_message(
|
|
f"Tic Tac Toe: {interaction.user.mention} (X) vs Bot (O) - Difficulty: {difficulty_value.capitalize()}\n\nYour turn!",
|
|
view=view,
|
|
)
|
|
view.message = await interaction.original_response()
|
|
|
|
# Chess group callbacks
|
|
async def games_chess_callback(
|
|
self, interaction: discord.Interaction, opponent: discord.User
|
|
):
|
|
"""Callback for /games chess play command"""
|
|
initiator = interaction.user
|
|
|
|
if opponent == initiator:
|
|
await interaction.response.send_message(
|
|
"You cannot challenge yourself!", ephemeral=True
|
|
)
|
|
return
|
|
if opponent.bot:
|
|
await interaction.response.send_message(
|
|
"You cannot challenge a bot! Use `/games chess bot` instead.",
|
|
ephemeral=True,
|
|
)
|
|
return
|
|
|
|
# Initiator is white, opponent is black
|
|
view = ChessView(initiator, opponent)
|
|
initial_status = f"Turn: **{initiator.mention}** (White)"
|
|
initial_message = f"Chess: {initiator.mention} (White) vs {opponent.mention} (Black)\n\n{initial_status}"
|
|
board_image = generate_board_image(view.board) # Generate initial board image
|
|
|
|
await interaction.response.send_message(
|
|
initial_message, file=board_image, view=view
|
|
)
|
|
message = await interaction.original_response()
|
|
view.message = message
|
|
|
|
# Send initial DMs
|
|
asyncio.create_task(view._send_or_update_dm(view.white_player))
|
|
asyncio.create_task(view._send_or_update_dm(view.black_player))
|
|
|
|
async def games_chessbot_callback(
|
|
self,
|
|
interaction: discord.Interaction,
|
|
color: app_commands.Choice[str] = None,
|
|
variant: app_commands.Choice[str] = None,
|
|
skill_level: int = 10,
|
|
think_time: float = 1.0,
|
|
):
|
|
"""Callback for /games chess bot command"""
|
|
player = interaction.user
|
|
player_color_str = color.value if color else "white"
|
|
variant_str = variant.value if variant else "standard"
|
|
player_color = chess.WHITE if player_color_str == "white" else chess.BLACK
|
|
|
|
# Validate inputs
|
|
skill_level = max(0, min(20, skill_level))
|
|
think_time = max(0.1, min(5.0, think_time))
|
|
|
|
# Check if variant is supported (currently standard and chess960)
|
|
supported_variants = ["standard", "chess960"]
|
|
if variant_str not in supported_variants:
|
|
await interaction.response.send_message(
|
|
f"Sorry, the variant '{variant_str}' is not currently supported. Choose from: {', '.join(supported_variants)}",
|
|
ephemeral=True,
|
|
)
|
|
return
|
|
|
|
# Defer response as engine start might take a moment
|
|
await interaction.response.defer()
|
|
|
|
view = ChessBotView(player, player_color, variant_str, skill_level, think_time)
|
|
|
|
# Start the engine asynchronously
|
|
# Store interaction temporarily for potential error reporting during init
|
|
view._interaction = interaction
|
|
await view.start_engine()
|
|
del view._interaction # Remove temporary attribute
|
|
|
|
if (
|
|
view.engine is None or view.is_finished()
|
|
): # Check if engine failed or view stopped during init
|
|
# Error message should have been sent by start_engine or view stopped itself
|
|
# Ensure we don't try to send another response if already handled
|
|
# No need to send another message here, start_engine handles it.
|
|
print("ChessBotView: Engine failed to start, stopping command execution.")
|
|
return # Stop if engine failed
|
|
|
|
# Determine initial message based on who moves first
|
|
initial_status_prefix = (
|
|
"Your turn." if player_color == chess.WHITE else "Bot is thinking..."
|
|
)
|
|
initial_message_content = view.get_board_message(initial_status_prefix)
|
|
board_image = generate_board_image(
|
|
view.board, perspective_white=(player_color == chess.WHITE)
|
|
)
|
|
|
|
# Send the initial game state using followup
|
|
message = await interaction.followup.send(
|
|
initial_message_content, file=board_image, view=view, wait=True
|
|
)
|
|
view.message = message
|
|
self.active_chess_bot_views[message.id] = view # Track the view
|
|
|
|
# Send initial DM to player
|
|
asyncio.create_task(view._send_or_update_dm())
|
|
|
|
# If bot moves first (player chose black), trigger its move
|
|
if player_color == chess.BLACK:
|
|
# Don't await this, let it run in the background
|
|
asyncio.create_task(view.make_bot_move())
|
|
|
|
async def games_loadchess_callback(
|
|
self,
|
|
interaction: discord.Interaction,
|
|
state: str,
|
|
turn: Optional[app_commands.Choice[str]] = None,
|
|
opponent: Optional[discord.User] = None,
|
|
color: Optional[app_commands.Choice[str]] = None,
|
|
skill_level: int = 10,
|
|
think_time: float = 1.0,
|
|
):
|
|
"""Callback for /games chess load command"""
|
|
await interaction.response.defer()
|
|
initiator = interaction.user
|
|
board = None
|
|
load_error = None
|
|
loaded_pgn_game = None # To store the loaded PGN game object if parsed
|
|
|
|
# --- Input Validation ---
|
|
if not opponent and not color:
|
|
await interaction.followup.send(
|
|
"The 'color' parameter is required when playing against the bot.",
|
|
ephemeral=True,
|
|
)
|
|
return
|
|
|
|
# --- Parsing Logic ---
|
|
state_trimmed = state.strip()
|
|
|
|
# 1. Try parsing as PGN
|
|
if state_trimmed.startswith("[Event") or (
|
|
"." in state_trimmed
|
|
and (
|
|
"O-O" in state_trimmed
|
|
or "x" in state_trimmed
|
|
or state_trimmed[0].isdigit()
|
|
)
|
|
):
|
|
try:
|
|
pgn_io = io.StringIO(state_trimmed)
|
|
loaded_pgn_game = chess.pgn.read_game(pgn_io)
|
|
if loaded_pgn_game is None:
|
|
raise ValueError("Could not parse PGN data.")
|
|
# Get the board state from the end of the main line
|
|
board = loaded_pgn_game.end().board()
|
|
print("[Debug] Parsed as PGN.")
|
|
except Exception as e:
|
|
load_error = f"Could not parse as PGN: {e}. Trying other formats."
|
|
print(f"[Debug] PGN parsing failed: {e}")
|
|
loaded_pgn_game = None # Reset if PGN parsing failed
|
|
|
|
# 2. Try parsing as FEN (if not already parsed as PGN)
|
|
if (
|
|
board is None
|
|
and "/" in state_trimmed
|
|
and (" w " in state_trimmed or " b " in state_trimmed)
|
|
):
|
|
try:
|
|
board = chess.Board(fen=state_trimmed)
|
|
print(f"[Debug] Parsed as FEN: {state_trimmed}")
|
|
except ValueError as e:
|
|
load_error = f"Invalid FEN string: {e}. Trying array format."
|
|
print(f"[Error] FEN parsing failed: {e}")
|
|
except Exception as e:
|
|
load_error = f"Unexpected FEN parsing error: {e}. Trying array format."
|
|
print(f"[Error] Unexpected FEN parsing error: {e}")
|
|
|
|
# 3. Try parsing as Array (if not parsed as PGN or FEN)
|
|
if board is None:
|
|
try:
|
|
# Check if it looks like a list before eval
|
|
if not state_trimmed.startswith("[") or not state_trimmed.endswith("]"):
|
|
raise ValueError("Input does not look like a list array.")
|
|
|
|
board_array = ast.literal_eval(state_trimmed)
|
|
print("[Debug] Attempting to parse as array...")
|
|
|
|
if (
|
|
not isinstance(board_array, list)
|
|
or len(board_array) != 8
|
|
or not all(
|
|
isinstance(row, list) and len(row) == 8 for row in board_array
|
|
)
|
|
):
|
|
raise ValueError("Invalid array structure. Must be 8x8 list.")
|
|
|
|
if not turn:
|
|
load_error = (
|
|
"The 'turn' parameter is required when providing a board array."
|
|
)
|
|
else:
|
|
turn_color = chess.WHITE if turn.value == "white" else chess.BLACK
|
|
fen = self._array_to_fen(board_array, turn_color)
|
|
print(f"[Debug] Converted array to FEN: {fen}")
|
|
board = chess.Board(fen=fen)
|
|
|
|
except (ValueError, SyntaxError, TypeError) as e:
|
|
# If PGN/FEN failed, this is the final error message
|
|
load_error = f"Invalid state format. Could not parse as PGN, FEN, or Python list array. Error: {e}"
|
|
print(f"[Error] Array parsing failed: {e}")
|
|
except Exception as e:
|
|
load_error = f"Error parsing array state: {e}"
|
|
print(f"[Error] Unexpected array parsing error: {e}")
|
|
|
|
# --- Final Check and Error Handling ---
|
|
if board is None:
|
|
final_error = (
|
|
load_error or "Failed to load board state from the provided input."
|
|
)
|
|
await interaction.followup.send(final_error, ephemeral=True)
|
|
return
|
|
|
|
# --- Game Setup ---
|
|
if opponent:
|
|
# Player vs Player
|
|
if opponent == initiator:
|
|
await interaction.followup.send(
|
|
"You cannot challenge yourself!", ephemeral=True
|
|
)
|
|
return
|
|
if opponent.bot:
|
|
await interaction.followup.send(
|
|
"You cannot challenge a bot! Use `/games chess bot` or load without opponent.",
|
|
ephemeral=True,
|
|
)
|
|
return
|
|
|
|
white_player = initiator if board.turn == chess.WHITE else opponent
|
|
black_player = opponent if board.turn == chess.WHITE else initiator
|
|
|
|
view = ChessView(
|
|
white_player, black_player, board=board
|
|
) # Pass loaded board
|
|
# If loaded from PGN, set the game object in the view
|
|
if loaded_pgn_game:
|
|
view.game_pgn = loaded_pgn_game
|
|
view.pgn_node = loaded_pgn_game.end() # Start from the end node
|
|
|
|
current_player_mention = (
|
|
white_player.mention
|
|
if board.turn == chess.WHITE
|
|
else black_player.mention
|
|
)
|
|
turn_color_name = "White" if board.turn == chess.WHITE else "Black"
|
|
initial_status = f"Turn: **{current_player_mention}** ({turn_color_name})"
|
|
if board.is_check():
|
|
initial_status += " **Check!**"
|
|
initial_message = f"Loaded Chess Game: {white_player.mention} (White) vs {black_player.mention} (Black)\n\n{initial_status}"
|
|
perspective_white = board.turn == chess.WHITE
|
|
board_image = generate_board_image(
|
|
view.board, perspective_white=perspective_white
|
|
)
|
|
|
|
message = await interaction.followup.send(
|
|
initial_message, file=board_image, view=view, wait=True
|
|
)
|
|
view.message = message
|
|
|
|
# Send initial DMs
|
|
asyncio.create_task(view._send_or_update_dm(view.white_player))
|
|
asyncio.create_task(view._send_or_update_dm(view.black_player))
|
|
|
|
else:
|
|
# Player vs Bot
|
|
player = initiator
|
|
# Color is now required, checked at the start
|
|
player_color = chess.WHITE if color.value == "white" else chess.BLACK
|
|
|
|
skill_level = max(0, min(20, skill_level))
|
|
think_time = max(0.1, min(5.0, think_time))
|
|
variant_str = "chess960" if board.chess960 else "standard"
|
|
|
|
view = ChessBotView(
|
|
player, player_color, variant_str, skill_level, think_time, board=board
|
|
) # Pass loaded board
|
|
# If loaded from PGN, set the game object in the view
|
|
if loaded_pgn_game:
|
|
view.game_pgn = loaded_pgn_game
|
|
view.pgn_node = loaded_pgn_game.end() # Start from the end node
|
|
|
|
view._interaction = interaction # For error reporting during start
|
|
await view.start_engine()
|
|
if hasattr(view, "_interaction"):
|
|
del view._interaction
|
|
|
|
# --- Legacy Commands (kept for backward compatibility) ---
|
|
|
|
# --- Prefix Commands (Legacy Support) ---
|
|
|
|
@commands.command(name="coinflipbet", add_to_app_commands=False)
|
|
async def coinflipbet_prefix(self, ctx: commands.Context, opponent: discord.User):
|
|
"""(Prefix) Challenge another user to a coin flip game."""
|
|
initiator = ctx.author
|
|
|
|
if opponent.bot:
|
|
await ctx.send("You cannot challenge a bot!")
|
|
return
|
|
|
|
view = CoinFlipView(initiator, opponent)
|
|
initial_message = f"{initiator.mention} has challenged {opponent.mention} to a coin flip game! {initiator.mention}, choose your side:"
|
|
message = await ctx.send(initial_message, view=view)
|
|
view.message = message
|
|
|
|
@commands.command(name="coinflip", add_to_app_commands=False)
|
|
async def coinflip_prefix(self, ctx: commands.Context):
|
|
"""(Prefix) Flip a coin."""
|
|
result = flip_coin()
|
|
await ctx.send(f"The coin landed on **{result}**! 🪙")
|
|
|
|
@commands.command(name="roll", add_to_app_commands=False)
|
|
async def roll_prefix(self, ctx: commands.Context):
|
|
"""(Prefix) Roll a dice."""
|
|
result = roll_dice()
|
|
await ctx.send(f"You rolled a **{result}**! 🎲")
|
|
|
|
@commands.command(name="magic8ball", add_to_app_commands=False)
|
|
async def magic8ball_prefix(self, ctx: commands.Context, *, question: str):
|
|
"""(Prefix) Ask the magic 8 ball."""
|
|
# Note: question parameter is required for UX but not used in the response
|
|
response = magic8ball_response()
|
|
await ctx.send(f"🎱 {response}")
|
|
|
|
@commands.command(name="tictactoe", add_to_app_commands=False)
|
|
async def tictactoe_prefix(self, ctx: commands.Context, opponent: discord.User):
|
|
"""(Prefix) Challenge another user to Tic-Tac-Toe."""
|
|
initiator = ctx.author
|
|
|
|
if opponent.bot:
|
|
await ctx.send("You cannot challenge a bot! Use `!tictactoebot` instead.")
|
|
return
|
|
|
|
view = TicTacToeView(initiator, opponent)
|
|
initial_message = f"Tic Tac Toe: {initiator.mention} (X) vs {opponent.mention} (O)\n\nTurn: **{initiator.mention} (X)**"
|
|
message = await ctx.send(initial_message, view=view)
|
|
view.message = message
|
|
|
|
@commands.command(name="tictactoebot", add_to_app_commands=False)
|
|
async def tictactoebot_prefix(
|
|
self, ctx: commands.Context, difficulty: str = "minimax"
|
|
):
|
|
"""(Prefix) Play Tic-Tac-Toe against the bot."""
|
|
difficulty_value = difficulty.lower()
|
|
valid_difficulties = ["random", "rule", "minimax"]
|
|
if difficulty_value not in valid_difficulties:
|
|
await ctx.send(
|
|
f"Invalid difficulty! Choose from: {', '.join(valid_difficulties)}"
|
|
)
|
|
return
|
|
|
|
try:
|
|
import sys
|
|
import os
|
|
|
|
parent_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
if parent_dir not in sys.path:
|
|
sys.path.append(parent_dir)
|
|
from tictactoe import TicTacToe
|
|
except ImportError:
|
|
await ctx.send("Error: TicTacToe game engine module not found.")
|
|
return
|
|
except Exception as e:
|
|
await ctx.send(f"Error importing TicTacToe module: {e}")
|
|
return
|
|
|
|
try:
|
|
game = TicTacToe(ai_player="O", ai_difficulty=difficulty_value)
|
|
except Exception as e:
|
|
await ctx.send(f"Error initializing TicTacToe game: {e}")
|
|
return
|
|
|
|
view = BotTicTacToeView(game, ctx.author)
|
|
message = await ctx.send(
|
|
f"Tic Tac Toe: {ctx.author.mention} (X) vs Bot (O) - Difficulty: {difficulty_value.capitalize()}\n\nYour turn!",
|
|
view=view,
|
|
)
|
|
view.message = message
|
|
|
|
@commands.command(name="rpschallenge", add_to_app_commands=False)
|
|
async def rpschallenge_prefix(self, ctx: commands.Context, opponent: discord.User):
|
|
"""(Prefix) Challenge another user to Rock-Paper-Scissors."""
|
|
initiator = ctx.author
|
|
|
|
if opponent.bot:
|
|
await ctx.send("You cannot challenge a bot!")
|
|
return
|
|
|
|
view = RockPaperScissorsView(initiator, opponent)
|
|
initial_message = f"Rock Paper Scissors: {initiator.mention} vs {opponent.mention}\n\nChoose your move!"
|
|
message = await ctx.send(initial_message, view=view)
|
|
view.message = message
|
|
|
|
@commands.command(name="rps", add_to_app_commands=False)
|
|
async def rps_prefix(self, ctx: commands.Context, choice: str):
|
|
"""(Prefix) Play Rock-Paper-Scissors against the bot."""
|
|
choices = ["Rock", "Paper", "Scissors"]
|
|
bot_choice = random.choice(choices)
|
|
user_choice = choice.capitalize()
|
|
|
|
if user_choice not in choices:
|
|
await ctx.send("Invalid choice! Please choose Rock, Paper, or Scissors.")
|
|
return
|
|
|
|
# Identical logic to slash command, just using ctx.send
|
|
if user_choice == bot_choice:
|
|
result = "It's a tie!"
|
|
elif (
|
|
(user_choice == "Rock" and bot_choice == "Scissors")
|
|
or (user_choice == "Paper" and bot_choice == "Rock")
|
|
or (user_choice == "Scissors" and bot_choice == "Paper")
|
|
):
|
|
result = "You win! 🎉"
|
|
else:
|
|
result = "You lose! 😢"
|
|
|
|
emojis = {"Rock": "🪨", "Paper": "📄", "Scissors": "✂️"}
|
|
await ctx.send(
|
|
f"You chose **{user_choice}** {emojis[user_choice]}\n"
|
|
f"I chose **{bot_choice}** {emojis[bot_choice]}\n\n"
|
|
f"{result}"
|
|
)
|
|
|
|
@commands.command(name="chess", add_to_app_commands=False)
|
|
async def chess_prefix(self, ctx: commands.Context, opponent: discord.User):
|
|
"""(Prefix) Start a game of chess with another user."""
|
|
initiator = ctx.author
|
|
|
|
if opponent.bot:
|
|
await ctx.send("You cannot challenge a bot! Use `!chessbot` instead.")
|
|
return
|
|
|
|
view = ChessView(initiator, opponent)
|
|
initial_status = f"Turn: **{initiator.mention}** (White)"
|
|
initial_message = f"Chess: {initiator.mention} (White) vs {opponent.mention} (Black)\n\n{initial_status}"
|
|
board_image = generate_board_image(view.board)
|
|
|
|
message = await ctx.send(initial_message, file=board_image, view=view)
|
|
view.message = message
|
|
|
|
# Send initial DMs
|
|
asyncio.create_task(view._send_or_update_dm(view.white_player))
|
|
asyncio.create_task(view._send_or_update_dm(view.black_player))
|
|
|
|
@commands.command(name="hangman", add_to_app_commands=False)
|
|
async def hangman_prefix(self, ctx: commands.Context):
|
|
"""(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_alpha.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)
|
|
|
|
# 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.",
|
|
file=initial_board_image,
|
|
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."""
|
|
number_to_guess = random.randint(1, 100)
|
|
|
|
if guess < 1 or guess > 100:
|
|
await ctx.send("Please guess a number between 1 and 100.")
|
|
return
|
|
|
|
if guess == number_to_guess:
|
|
await ctx.send(f"🎉 Correct! The number was **{number_to_guess}**.")
|
|
elif guess < number_to_guess:
|
|
await ctx.send(f"Too low! The number was {number_to_guess}.")
|
|
else:
|
|
await ctx.send(f"Too high! The number was {number_to_guess}.")
|
|
|
|
|
|
async def setup(bot: commands.Bot):
|
|
"""Set up the GamesCog with the bot."""
|
|
print("Setting up GamesCog...")
|
|
cog = GamesCog(bot)
|
|
await bot.add_cog(cog)
|
|
print(
|
|
f"GamesCog setup complete with command group: {[cmd.name for cmd in bot.tree.get_commands() if cmd.name == 'games']}"
|
|
)
|
|
print(
|
|
f"Available commands: {[cmd.name for cmd in cog.games_group.walk_commands() if isinstance(cmd, app_commands.Command)]}"
|
|
)
|