first commit
This commit is contained in:
36
bot/games/button_games/__init__.py
Normal file
36
bot/games/button_games/__init__.py
Normal file
@@ -0,0 +1,36 @@
|
||||
"""This folder contains games that require discord.py v2.0.0 + to be used
|
||||
they utilize UI components such as buttons.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
#from .aki_buttons import BetaAkinator
|
||||
from .twenty_48_buttons import BetaTwenty48
|
||||
from .wordle_buttons import BetaWordle
|
||||
from .tictactoe_buttons import BetaTictactoe
|
||||
from .memory_game import MemoryGame
|
||||
from .rps_buttons import BetaRockPaperScissors
|
||||
#from .hangman_buttons import BetaHangman
|
||||
from .reaction_test_buttons import BetaReactionGame
|
||||
from .country_guess_buttons import BetaCountryGuesser
|
||||
from .chess_buttons import BetaChess
|
||||
from .battleship_buttons import BetaBattleShip
|
||||
from .number_slider import NumberSlider
|
||||
from .lights_out import LightsOut
|
||||
#from .boggle import Boggle
|
||||
from .connect_four_buttons import BetaConnectFour
|
||||
|
||||
|
||||
__all__: tuple[str, ...] = (
|
||||
"BetaConnectFour",
|
||||
"BetaTwenty48",
|
||||
"BetaWordle",
|
||||
"BetaTictactoe",
|
||||
"MemoryGame",
|
||||
"BetaRockPaperScissors",
|
||||
"BetaReactionGame",
|
||||
"BetaCountryGuesser",
|
||||
"BetaChess",
|
||||
"BetaBattleShip",
|
||||
"NumberSlider",
|
||||
"LightsOut",
|
||||
)
|
||||
503
bot/games/button_games/battleship_buttons.py
Normal file
503
bot/games/button_games/battleship_buttons.py
Normal file
@@ -0,0 +1,503 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Optional, Any, Coroutine, Union
|
||||
import asyncio
|
||||
import string
|
||||
|
||||
import discord
|
||||
from discord.ext import commands
|
||||
from utils.emoji import TARGET
|
||||
from utils.emoji import TARGET
|
||||
|
||||
from ..battleship import (
|
||||
BattleShip,
|
||||
SHIPS,
|
||||
Ship,
|
||||
Board,
|
||||
)
|
||||
|
||||
from .wordle_buttons import WordInputButton
|
||||
from ..utils import DiscordColor, DEFAULT_COLOR, BaseView
|
||||
|
||||
|
||||
class Player:
|
||||
def __init__(self, player: discord.User, *, game: BetaBattleShip) -> None:
|
||||
self.game = game
|
||||
self.player = player
|
||||
|
||||
self.embed = discord.Embed(title="Log", description="```\n\u200b\n```")
|
||||
|
||||
self._logs: list[str] = []
|
||||
self.log: str = ""
|
||||
|
||||
self.approves_cancel: bool = False
|
||||
|
||||
def update_log(self, log: str) -> None:
|
||||
self._logs.append(log)
|
||||
log_str = "\n\n".join(self._logs[-self.game.max_log_size :])
|
||||
|
||||
if len(self._logs) > self.game.max_log_size:
|
||||
log_str = "...\n\n" + log_str
|
||||
|
||||
self.embed.description = f"```diff\n{log_str}\n```"
|
||||
|
||||
def __getattribute__(self, name: str) -> Any:
|
||||
try:
|
||||
return super().__getattribute__(name)
|
||||
except AttributeError:
|
||||
return self.player.__getattribute__(name)
|
||||
|
||||
|
||||
class BattleshipInput(discord.ui.Modal, title="Input a coordinate"):
|
||||
def __init__(self, view: BattleshipView) -> None:
|
||||
super().__init__()
|
||||
self.view = view
|
||||
|
||||
self.coord = discord.ui.TextInput(
|
||||
label="Enter your target coordinate",
|
||||
placeholder="ex: a8",
|
||||
style=discord.TextStyle.short,
|
||||
required=True,
|
||||
min_length=2,
|
||||
max_length=3,
|
||||
)
|
||||
|
||||
self.add_item(self.coord)
|
||||
|
||||
async def on_submit(self, interaction: discord.Interaction) -> None:
|
||||
game = self.view.game
|
||||
content = self.coord.value
|
||||
content = content.strip().lower()
|
||||
|
||||
if not game.inputpat.fullmatch(content):
|
||||
return await interaction.response.send_message(
|
||||
f"`{content}` is not a valid coordinate!", ephemeral=True
|
||||
)
|
||||
else:
|
||||
raw, coords = game.get_coords(content)
|
||||
self.view.update_views()
|
||||
|
||||
if coords in self.view.player_board.moves:
|
||||
return await interaction.response.send_message(
|
||||
"You've attacked this coordinate before!", ephemeral=True
|
||||
)
|
||||
else:
|
||||
await interaction.response.defer()
|
||||
return await game.process_move(raw, coords)
|
||||
|
||||
|
||||
class BattleshipButton(WordInputButton):
|
||||
view: BattleshipView
|
||||
|
||||
async def callback(self, interaction: discord.Interaction) -> None:
|
||||
game = self.view.game
|
||||
|
||||
if self.label == "Cancel":
|
||||
player = self.view.player
|
||||
other_player = (
|
||||
game.player2
|
||||
if interaction.user == game.player1.player
|
||||
else game.player1
|
||||
)
|
||||
|
||||
if not player.approves_cancel:
|
||||
player.approves_cancel = True
|
||||
|
||||
await interaction.response.defer()
|
||||
|
||||
if not other_player.approves_cancel:
|
||||
await player.send("- Waiting for opponent to approve cancellation -")
|
||||
await other_player.send(
|
||||
"Opponent wants to cancel, press the `Cancel` button if you approve."
|
||||
)
|
||||
else:
|
||||
game.view1.disable_all()
|
||||
game.view2.disable_all()
|
||||
|
||||
await game.player1.send("**GAME OVER**, Cancelled")
|
||||
await game.player2.send("**GAME OVER**, Cancelled")
|
||||
|
||||
await game.message1.edit(view=game.view1)
|
||||
await game.message2.edit(view=game.view2)
|
||||
|
||||
game.view1.stop()
|
||||
return game.view2.stop()
|
||||
else:
|
||||
if interaction.user != game.turn.player:
|
||||
return await interaction.response.send_message(
|
||||
"It is not your turn yet!", ephemeral=True
|
||||
)
|
||||
else:
|
||||
return await interaction.response.send_modal(BattleshipInput(self.view))
|
||||
|
||||
|
||||
class CoordButton(discord.ui.Button["BattleshipView"]):
|
||||
def __init__(self, letter_or_num: Union[str, int]) -> None:
|
||||
super().__init__(
|
||||
label=str(letter_or_num),
|
||||
style=discord.ButtonStyle.green,
|
||||
)
|
||||
|
||||
async def callback(self, interaction: discord.Interaction) -> None:
|
||||
game = self.view.game
|
||||
|
||||
if self.label.isdigit():
|
||||
self.view.digit = int(self.label)
|
||||
|
||||
raw = self.view.alpha + str(self.view.digit)
|
||||
coords = (game.to_num(self.view.alpha), self.view.digit)
|
||||
await interaction.response.defer()
|
||||
|
||||
self.view.alpha = None
|
||||
self.view.digit = None
|
||||
|
||||
self.view.update_views()
|
||||
return await game.process_move(raw, coords)
|
||||
else:
|
||||
self.view.alpha = self.label.lower()
|
||||
self.view.initialize_view(clear=True)
|
||||
return await interaction.response.edit_message(view=self.view)
|
||||
|
||||
|
||||
class BattleshipView(BaseView):
|
||||
def __init__(self, game: BetaBattleShip, user: Player, *, timeout: float) -> None:
|
||||
super().__init__(timeout=timeout)
|
||||
|
||||
self.game = game
|
||||
self.player = user
|
||||
self.player_board = self.game.get_board(self.player)
|
||||
|
||||
self.initialize_view(start=True)
|
||||
|
||||
self.alpha: Optional[str] = None
|
||||
self.digit: Optional[int] = None
|
||||
|
||||
def disable(self) -> None:
|
||||
self.disable_all()
|
||||
self.children[-1].disabled = False
|
||||
|
||||
def update_views(self) -> None:
|
||||
game = self.game
|
||||
self.disable()
|
||||
|
||||
other_view = game.view1 if game.turn == game.player2 else game.view2
|
||||
|
||||
other_view.clear_items()
|
||||
other_view.initialize_view()
|
||||
|
||||
def initialize_view(self, *, clear: bool = False, start: bool = False) -> None:
|
||||
moves = self.player_board.moves
|
||||
|
||||
if clear:
|
||||
self.clear_items()
|
||||
for num in range(1, 11):
|
||||
button = CoordButton(num)
|
||||
coord = (self.game.to_num(self.alpha), num)
|
||||
if coord in moves:
|
||||
button.disabled = True
|
||||
self.add_item(button)
|
||||
else:
|
||||
for letter in string.ascii_uppercase[:10]:
|
||||
button = CoordButton(letter)
|
||||
if all(
|
||||
(self.game.to_num(letter.lower()), i) in moves for i in range(1, 11)
|
||||
):
|
||||
button.disabled = True
|
||||
self.add_item(button)
|
||||
|
||||
inpbutton = BattleshipButton()
|
||||
inpbutton.label = "\u200b"
|
||||
inpbutton.emoji = TARGET
|
||||
|
||||
self.add_item(inpbutton)
|
||||
self.add_item(BattleshipButton(cancel_button=True))
|
||||
|
||||
if start and self.player == self.game.player2:
|
||||
self.disable()
|
||||
|
||||
|
||||
class SetupInput(discord.ui.Modal):
|
||||
def __init__(self, button: SetupButton) -> None:
|
||||
self.button = button
|
||||
self.ship = self.button.label
|
||||
|
||||
super().__init__(title=f"{self.ship} Setup")
|
||||
|
||||
self.start_coord = discord.ui.TextInput(
|
||||
label=f"Enter the starting coordinate",
|
||||
placeholder="ex: a8",
|
||||
style=discord.TextStyle.short,
|
||||
required=True,
|
||||
min_length=2,
|
||||
max_length=3,
|
||||
)
|
||||
|
||||
self.is_vertical = discord.ui.TextInput(
|
||||
label=f"Do you want it to be vertical? (y/n)",
|
||||
placeholder='"y" or "n"',
|
||||
style=discord.TextStyle.short,
|
||||
required=True,
|
||||
min_length=1,
|
||||
max_length=1,
|
||||
)
|
||||
|
||||
self.add_item(self.start_coord)
|
||||
self.add_item(self.is_vertical)
|
||||
|
||||
async def on_submit(self, interaction: discord.Interaction) -> None:
|
||||
game = self.button.view.game
|
||||
|
||||
start = self.start_coord.value.strip().lower()
|
||||
vertical = self.is_vertical.value.strip().lower()
|
||||
|
||||
board = game.get_board(interaction.user)
|
||||
|
||||
if not game.inputpat.match(start):
|
||||
return await interaction.response.send_message(
|
||||
f"{start} is not a valid coordinate!", ephemeral=True
|
||||
)
|
||||
|
||||
if vertical not in ("y", "n"):
|
||||
return await interaction.response.send_message(
|
||||
f"Response for `vertical` must be either `y` or `n`", ephemeral=True
|
||||
)
|
||||
|
||||
vertical = vertical != "y"
|
||||
|
||||
_, start = game.get_coords(start)
|
||||
|
||||
new_ship = Ship(
|
||||
name=self.ship,
|
||||
size=self.button.ship_size,
|
||||
start=start,
|
||||
vertical=vertical,
|
||||
color=self.button.ship_color,
|
||||
)
|
||||
|
||||
if board._is_valid(new_ship):
|
||||
self.button.disabled = True
|
||||
board.ships.append(new_ship)
|
||||
|
||||
embed, file, _, _ = await game.get_file(interaction.user, hide=False)
|
||||
|
||||
await interaction.response.edit_message(
|
||||
attachments=[file], embed=embed, view=self.button.view
|
||||
)
|
||||
|
||||
if all(
|
||||
button.disabled
|
||||
for button in self.button.view.children
|
||||
if isinstance(button, discord.ui.Button)
|
||||
):
|
||||
await interaction.user.send(
|
||||
"**All setup!** (Game will soon start after the opponent finishes)"
|
||||
)
|
||||
return self.button.view.stop()
|
||||
else:
|
||||
return await interaction.response.send_message(
|
||||
"Ship placement was detected to be invalid, please try again.",
|
||||
ephemeral=True,
|
||||
)
|
||||
|
||||
|
||||
class SetupButton(discord.ui.Button["SetupView"]):
|
||||
def __init__(
|
||||
self, label: str, ship_size: int, ship_color: tuple[int, int, int]
|
||||
) -> None:
|
||||
super().__init__(
|
||||
label=label,
|
||||
style=discord.ButtonStyle.green,
|
||||
)
|
||||
|
||||
self.ship_size = ship_size
|
||||
self.ship_color = ship_color
|
||||
|
||||
async def callback(self, interaction: discord.Interaction) -> None:
|
||||
await interaction.response.send_modal(SetupInput(self))
|
||||
|
||||
|
||||
class SetupView(BaseView):
|
||||
def __init__(self, game: BetaBattleShip, timeout: float) -> None:
|
||||
super().__init__(timeout=timeout)
|
||||
|
||||
self.game = game
|
||||
|
||||
for ship, (size, color) in SHIPS.items():
|
||||
self.add_item(SetupButton(ship, size, color))
|
||||
|
||||
|
||||
class BetaBattleShip(BattleShip):
|
||||
"""
|
||||
BattleShip(buttons) Game
|
||||
"""
|
||||
|
||||
embed: discord.Embed
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
player1: discord.User,
|
||||
player2: discord.User,
|
||||
*,
|
||||
random: bool = True,
|
||||
) -> None:
|
||||
|
||||
super().__init__(player1, player2, random=random)
|
||||
|
||||
self.player1: Player = Player(player1, game=self)
|
||||
self.player2: Player = Player(player2, game=self)
|
||||
|
||||
self.turn: Player = self.player1
|
||||
|
||||
def get_board(self, player: discord.User, other: bool = False) -> Board:
|
||||
player = getattr(player, "player", player)
|
||||
if other:
|
||||
return (
|
||||
self.player2_board
|
||||
if player == self.player1.player
|
||||
else self.player1_board
|
||||
)
|
||||
else:
|
||||
return (
|
||||
self.player1_board
|
||||
if player == self.player1.player
|
||||
else self.player2_board
|
||||
)
|
||||
|
||||
async def get_ship_inputs(self, user: Player) -> Coroutine[Any, Any, bool]:
|
||||
embed, file, _, _ = await self.get_file(user)
|
||||
|
||||
embed1 = discord.Embed(
|
||||
description="**Press the buttons to place your ships!**",
|
||||
color=self.embed_color,
|
||||
)
|
||||
|
||||
view = SetupView(self, timeout=self.timeout)
|
||||
await user.send(file=file, embeds=[embed, embed1], view=view)
|
||||
|
||||
return view.wait()
|
||||
|
||||
async def process_move(self, raw: str, coords: tuple[int, int]):
|
||||
sunk, hit = self.place_move(self.turn, coords)
|
||||
next_turn = self.player2 if self.turn == self.player1 else self.player1
|
||||
|
||||
if hit and sunk:
|
||||
self.turn.update_log(
|
||||
f"+ ({raw}) was a hit!, you also sank one of their ships! :)"
|
||||
)
|
||||
next_turn.update_log(
|
||||
f"- They went for ({raw}), and it was a hit!\n- One of your ships also got sunk! :("
|
||||
)
|
||||
elif hit:
|
||||
self.turn.update_log(f"+ ({raw}) was a hit :)")
|
||||
next_turn.update_log(f"- They went for ({raw}), and it was a hit! :(")
|
||||
else:
|
||||
self.turn.update_log(f"- ({raw}) was a miss :(")
|
||||
next_turn.update_log(f"+ They went for ({raw}), and it was a miss! :)")
|
||||
|
||||
e1, f1, e2, f2 = await self.get_file(self.player1)
|
||||
e3, f3, e4, f4 = await self.get_file(self.player2)
|
||||
|
||||
self.turn = next_turn
|
||||
|
||||
self.player1.embed.set_field_at(
|
||||
0, name="\u200b", value=f"```yml\nturn: {self.turn.player}\n```"
|
||||
)
|
||||
self.player2.embed.set_field_at(
|
||||
0, name="\u200b", value=f"```yml\nturn: {self.turn.player}\n```"
|
||||
)
|
||||
|
||||
await self.message1.edit(
|
||||
view=self.view1,
|
||||
content="**Battleship**",
|
||||
embeds=[e2, e1, self.player1.embed],
|
||||
attachments=[f2, f1],
|
||||
)
|
||||
await self.message2.edit(
|
||||
view=self.view2,
|
||||
content="**Battleship**",
|
||||
embeds=[e4, e3, self.player2.embed],
|
||||
attachments=[f4, f3],
|
||||
)
|
||||
|
||||
if winner := self.who_won():
|
||||
await winner.send("Congrats, you won! :)")
|
||||
|
||||
other = self.player2 if winner == self.player1 else self.player1
|
||||
await other.send("You lost, better luck next time :(")
|
||||
|
||||
self.view1.stop()
|
||||
return self.view2.stop()
|
||||
|
||||
async def start(
|
||||
self,
|
||||
ctx: commands.Context[commands.Bot],
|
||||
*,
|
||||
max_log_size: int = 10,
|
||||
embed_color: DiscordColor = DEFAULT_COLOR,
|
||||
timeout: Optional[float] = None,
|
||||
) -> tuple[discord.Message, discord.Message]:
|
||||
"""
|
||||
starts the battleship(buttons) game
|
||||
|
||||
Parameters
|
||||
----------
|
||||
ctx : commands.Context[commands.Bot]
|
||||
the context of the invokation command
|
||||
max_log_size : int, optional
|
||||
indicates the length of the move log to show, by default 10
|
||||
embed_color : DiscordColor, optional
|
||||
the color of the game embed, by default DEFAULT_COLOR
|
||||
timeout : Optional[float], optional
|
||||
the timeout for the view, by default None
|
||||
|
||||
Returns
|
||||
-------
|
||||
tuple[discord.Message, discord.Message]
|
||||
returns the game messages respectively
|
||||
"""
|
||||
self.max_log_size = max_log_size
|
||||
self.timeout = timeout
|
||||
self.embed_color = embed_color
|
||||
|
||||
await ctx.send("**Game Started!**\nI've setup the boards in your dms!")
|
||||
|
||||
if not self.random:
|
||||
await asyncio.gather(
|
||||
await self.get_ship_inputs(self.player1),
|
||||
await self.get_ship_inputs(self.player2),
|
||||
)
|
||||
|
||||
self.player1.embed.color = self.embed_color
|
||||
self.player2.embed.color = self.embed_color
|
||||
|
||||
e1, f1, e2, f2 = await self.get_file(self.player1)
|
||||
e3, f3, e4, f4 = await self.get_file(self.player2)
|
||||
|
||||
self.view1 = BattleshipView(self, user=self.player1, timeout=timeout)
|
||||
self.view2 = BattleshipView(self, user=self.player2, timeout=timeout)
|
||||
|
||||
self.player1.embed.add_field(
|
||||
name="\u200b", value=f"```yml\nturn: {self.turn.player}\n```"
|
||||
)
|
||||
self.player2.embed.add_field(
|
||||
name="\u200b", value=f"```yml\nturn: {self.turn.player}\n```"
|
||||
)
|
||||
|
||||
self.message1 = await self.player1.send(
|
||||
content="**Game starting!**",
|
||||
view=self.view1,
|
||||
embeds=[e2, e1, self.player1.embed],
|
||||
files=[f2, f1],
|
||||
)
|
||||
self.message2 = await self.player2.send(
|
||||
content="**Game starting!**",
|
||||
view=self.view2,
|
||||
embeds=[e4, e3, self.player2.embed],
|
||||
files=[f4, f3],
|
||||
)
|
||||
|
||||
await asyncio.gather(
|
||||
self.view1.wait(),
|
||||
self.view2.wait(),
|
||||
)
|
||||
return self.message1, self.message2
|
||||
140
bot/games/button_games/chess_buttons.py
Normal file
140
bot/games/button_games/chess_buttons.py
Normal file
@@ -0,0 +1,140 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Optional
|
||||
import discord
|
||||
from discord.ext import commands
|
||||
|
||||
from ..chess_game import Chess
|
||||
from .wordle_buttons import WordInputButton
|
||||
from ..utils import DiscordColor, DEFAULT_COLOR, BaseView
|
||||
|
||||
|
||||
class ChessInput(discord.ui.Modal, title="Make your move"):
|
||||
def __init__(self, view: ChessView) -> None:
|
||||
super().__init__()
|
||||
self.view = view
|
||||
|
||||
self.move_from = discord.ui.TextInput(
|
||||
label="from coordinate",
|
||||
style=discord.TextStyle.short,
|
||||
required=True,
|
||||
min_length=2,
|
||||
max_length=2,
|
||||
)
|
||||
|
||||
self.move_to = discord.ui.TextInput(
|
||||
label="to coordinate",
|
||||
style=discord.TextStyle.short,
|
||||
required=True,
|
||||
min_length=2,
|
||||
max_length=2,
|
||||
)
|
||||
|
||||
self.add_item(self.move_from)
|
||||
self.add_item(self.move_to)
|
||||
|
||||
async def on_submit(self, interaction: discord.Interaction) -> discord.Message:
|
||||
game = self.view.game
|
||||
from_coord = self.move_from.value.strip().lower()
|
||||
to_coord = self.move_to.value.strip().lower()
|
||||
|
||||
uci = from_coord + to_coord
|
||||
|
||||
try:
|
||||
is_valid_uci = game.board.parse_uci(uci)
|
||||
except ValueError:
|
||||
is_valid_uci = False
|
||||
|
||||
if not is_valid_uci:
|
||||
return await interaction.response.send_message(
|
||||
f"Invalid coordinates for move: `{from_coord} -> {to_coord}`",
|
||||
ephemeral=True,
|
||||
)
|
||||
else:
|
||||
await game.place_move(uci)
|
||||
|
||||
if game.board.is_game_over():
|
||||
self.view.disable_all()
|
||||
embed = await game.fetch_results()
|
||||
self.view.stop()
|
||||
else:
|
||||
embed = await game.make_embed()
|
||||
|
||||
return await interaction.response.edit_message(embed=embed, view=self.view)
|
||||
|
||||
|
||||
class ChessButton(WordInputButton):
|
||||
view: ChessView
|
||||
|
||||
async def callback(self, interaction: discord.Interaction) -> None:
|
||||
game = self.view.game
|
||||
if interaction.user not in (game.black, game.white):
|
||||
return await interaction.response.send_message(
|
||||
"You are not part of this game!", ephemeral=True
|
||||
)
|
||||
else:
|
||||
if self.label == "Cancel":
|
||||
self.view.disable_all()
|
||||
await interaction.message.edit(view=self.view)
|
||||
await interaction.response.send_message(f"**Game Over!** Cancelled")
|
||||
return self.view.stop()
|
||||
else:
|
||||
if interaction.user != game.turn:
|
||||
return await interaction.response.send_message(
|
||||
"It is not your turn yet!", ephemeral=True
|
||||
)
|
||||
else:
|
||||
return await interaction.response.send_modal(ChessInput(self.view))
|
||||
|
||||
|
||||
class ChessView(BaseView):
|
||||
def __init__(self, game: BetaChess, *, timeout: float) -> None:
|
||||
super().__init__(timeout=timeout)
|
||||
|
||||
self.game = game
|
||||
|
||||
inpbutton = ChessButton()
|
||||
inpbutton.label = "Make your move!"
|
||||
|
||||
self.add_item(inpbutton)
|
||||
self.add_item(ChessButton(cancel_button=True))
|
||||
|
||||
|
||||
class BetaChess(Chess):
|
||||
"""
|
||||
Chess(buttons) Game
|
||||
"""
|
||||
|
||||
async def start(
|
||||
self,
|
||||
ctx: commands.Context[commands.Bot],
|
||||
*,
|
||||
embed_color: DiscordColor = DEFAULT_COLOR,
|
||||
timeout: Optional[float] = None,
|
||||
) -> discord.Message:
|
||||
"""
|
||||
starts the Chess(buttons) Game
|
||||
|
||||
Parameters
|
||||
----------
|
||||
ctx : commands.Context
|
||||
the context of the invokation command
|
||||
embed_color : DiscordColor, optional
|
||||
the color of the game embed, by default DEFAULT_COLOR
|
||||
timeout : Optional[float], optional
|
||||
the timeout for the view, by default None
|
||||
|
||||
Returns
|
||||
-------
|
||||
discord.Message
|
||||
returns the game message
|
||||
"""
|
||||
self.embed_color = discord.Color.random()
|
||||
|
||||
embed = await self.make_embed()
|
||||
self.view = ChessView(self, timeout=timeout)
|
||||
|
||||
self.message = await ctx.send(embed=embed, view=self.view)
|
||||
|
||||
await self.view.wait()
|
||||
return self.message
|
||||
113
bot/games/button_games/connect_four_buttons.py
Normal file
113
bot/games/button_games/connect_four_buttons.py
Normal file
@@ -0,0 +1,113 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Optional
|
||||
|
||||
import discord
|
||||
from discord.ext import commands
|
||||
|
||||
from ..connect_four import ConnectFour, BLANK
|
||||
from ..utils import *
|
||||
|
||||
|
||||
class ConnectFourButton(discord.ui.Button["ConnectFourView"]):
|
||||
def __init__(self, number: int, style: discord.ButtonStyle) -> None:
|
||||
self.number = number
|
||||
|
||||
super().__init__(
|
||||
label=str(self.number),
|
||||
style=style,
|
||||
)
|
||||
|
||||
async def callback(self, interaction: discord.Interaction) -> None:
|
||||
game = self.view.game
|
||||
|
||||
if interaction.user not in (game.red_player, game.blue_player):
|
||||
return await interaction.response.send_message(
|
||||
"You are not part of this game!", ephemeral=True
|
||||
)
|
||||
|
||||
if interaction.user != game.turn:
|
||||
return await interaction.response.send_message(
|
||||
"It is not your turn yet!", ephemeral=True
|
||||
)
|
||||
|
||||
if game.board[0][self.number - 1] != BLANK:
|
||||
return await interaction.response.send_message(
|
||||
"Selected column is full!", ephemeral=True
|
||||
)
|
||||
|
||||
game.place_move(self.number - 1, interaction.user)
|
||||
|
||||
status = game.is_game_over()
|
||||
|
||||
embed = game.make_embed(status=status)
|
||||
|
||||
if status:
|
||||
self.view.disable_all()
|
||||
self.view.stop()
|
||||
|
||||
return await interaction.response.edit_message(
|
||||
view=self.view,
|
||||
embed=embed,
|
||||
content=game.board_string(),
|
||||
)
|
||||
|
||||
|
||||
class ConnectFourView(BaseView):
|
||||
game: ConnectFour
|
||||
|
||||
def __init__(self, game: BetaConnectFour, timeout: float) -> None:
|
||||
super().__init__(timeout=timeout)
|
||||
|
||||
self.game = game
|
||||
|
||||
for i in range(1, 8):
|
||||
self.add_item(ConnectFourButton(i, self.game.button_style))
|
||||
|
||||
|
||||
class BetaConnectFour(ConnectFour):
|
||||
"""
|
||||
Connect-4(buttons) Game
|
||||
"""
|
||||
|
||||
async def start(
|
||||
self,
|
||||
ctx: commands.Context[commands.Bot],
|
||||
*,
|
||||
timeout: Optional[float] = None,
|
||||
button_style: discord.ButtonStyle = discord.ButtonStyle.blurple,
|
||||
embed_color: DiscordColor = DEFAULT_COLOR,
|
||||
) -> discord.Message:
|
||||
"""
|
||||
starts the Connect-4(buttons) game
|
||||
|
||||
Parameters
|
||||
----------
|
||||
ctx : commands.Context
|
||||
the context of the invokation command
|
||||
timeout : Optional[float], optional
|
||||
the timeout for when waiting, by default None
|
||||
button_style : discord.ButtonStyle, optional
|
||||
the primary button style to use, by default discord.ButtonStyle.red
|
||||
embed_color : DiscordColor, optional
|
||||
the color of the game embed, by default DEFAULT_COLOR
|
||||
|
||||
Returns
|
||||
-------
|
||||
discord.Message
|
||||
returns the game message
|
||||
"""
|
||||
self.embed_color = discord.Color.random()
|
||||
self.button_style = button_style
|
||||
|
||||
self.view = ConnectFourView(self, timeout=timeout)
|
||||
|
||||
embed = self.make_embed(status=False)
|
||||
self.message = await ctx.send(
|
||||
content=self.board_string(),
|
||||
view=self.view,
|
||||
embed=embed,
|
||||
)
|
||||
|
||||
await self.view.wait()
|
||||
return self.message
|
||||
166
bot/games/button_games/country_guess_buttons.py
Normal file
166
bot/games/button_games/country_guess_buttons.py
Normal file
@@ -0,0 +1,166 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Optional
|
||||
|
||||
import discord
|
||||
from discord.ext import commands
|
||||
|
||||
from ..country_guess import CountryGuesser
|
||||
from ..utils import DiscordColor, DEFAULT_COLOR, BaseView
|
||||
|
||||
|
||||
class CountryInput(discord.ui.Modal, title="Input your guess!"):
|
||||
def __init__(self, view: CountryView) -> None:
|
||||
super().__init__()
|
||||
self.view = view
|
||||
|
||||
self.guess = discord.ui.TextInput(
|
||||
label="Input your guess",
|
||||
style=discord.TextStyle.short,
|
||||
required=True,
|
||||
max_length=self.view.game.accepted_length,
|
||||
)
|
||||
|
||||
self.add_item(self.guess)
|
||||
|
||||
async def on_submit(self, interaction: discord.Interaction) -> None:
|
||||
guess = self.guess.value.strip().lower()
|
||||
game = self.view.game
|
||||
|
||||
if guess == game.country:
|
||||
game.update_guesslog("+ GAME OVER, you won! +")
|
||||
await interaction.response.send_message(
|
||||
f"That is correct! The country was `{game.country.title()}`"
|
||||
)
|
||||
|
||||
self.view.disable_all()
|
||||
game.embed.description = f"```fix\n{game.country.title()}\n```"
|
||||
await interaction.message.edit(view=self.view, embed=game.embed)
|
||||
return self.view.stop()
|
||||
else:
|
||||
game.guesses -= 1
|
||||
|
||||
if not game.guesses:
|
||||
self.view.disable_all()
|
||||
game.update_guesslog("- GAME OVER, you lost -")
|
||||
|
||||
await interaction.message.edit(embed=game.embed, view=self.view)
|
||||
await interaction.response.send_message(
|
||||
f"Game Over! you lost, The country was `{game.country.title()}`"
|
||||
)
|
||||
return self.view.stop()
|
||||
else:
|
||||
acc = game.get_accuracy(guess)
|
||||
game.update_guesslog(
|
||||
f"- [{guess}] was incorrect! but you are ({acc}%) of the way there!\n"
|
||||
f"+ You have {game.guesses} guesses left.\n"
|
||||
)
|
||||
|
||||
await interaction.response.edit_message(embed=game.embed)
|
||||
|
||||
|
||||
class CountryView(BaseView):
|
||||
def __init__(
|
||||
self, game: BetaCountryGuesser, *, user: discord.User, timeout: float
|
||||
) -> None:
|
||||
super().__init__(timeout=timeout)
|
||||
|
||||
self.game = game
|
||||
self.user = user
|
||||
|
||||
async def interaction_check(self, interaction: discord.Interaction) -> bool:
|
||||
if interaction.user != self.user:
|
||||
await interaction.response.send_message(
|
||||
f"This is not your game!", ephemeral=True
|
||||
)
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
@discord.ui.button(label="Make a guess!", style=discord.ButtonStyle.blurple)
|
||||
async def guess_button(self, interaction: discord.Interaction, _) -> None:
|
||||
return await interaction.response.send_modal(CountryInput(self))
|
||||
|
||||
@discord.ui.button(label="hint", style=discord.ButtonStyle.green)
|
||||
async def hint_button(
|
||||
self, interaction: discord.Interaction, button: discord.ui.Button
|
||||
) -> None:
|
||||
hint = self.game.get_hint()
|
||||
self.game.hints -= 1
|
||||
await interaction.response.send_message(
|
||||
f"Here is your hint: `{hint}`", ephemeral=True
|
||||
)
|
||||
|
||||
if not self.game.hints:
|
||||
button.disabled = True
|
||||
await interaction.message.edit(view=self)
|
||||
|
||||
@discord.ui.button(label="Cancel", style=discord.ButtonStyle.red)
|
||||
async def cancel_button(self, interaction: discord.Interaction, _) -> None:
|
||||
self.disable_all()
|
||||
|
||||
self.game.embed.description = f"```fix\n{self.game.country.title()}\n```"
|
||||
self.game.update_guesslog("- GAME OVER, CANCELLED -")
|
||||
|
||||
await interaction.response.send_message(
|
||||
f"Game Over! The country was `{self.game.country.title()}`"
|
||||
)
|
||||
await interaction.message.edit(view=self, embed=self.game.embed)
|
||||
return self.stop()
|
||||
|
||||
|
||||
class BetaCountryGuesser(CountryGuesser):
|
||||
"""
|
||||
Country Guesser(buttons) Game
|
||||
"""
|
||||
|
||||
guesslog: str = ""
|
||||
|
||||
def update_guesslog(self, entry: str) -> None:
|
||||
self.guesslog += entry + "\n"
|
||||
self.embed.set_field_at(
|
||||
1, name="Guess Log", value=f"```diff\n{self.guesslog}\n```"
|
||||
)
|
||||
|
||||
async def start(
|
||||
self,
|
||||
ctx: commands.Context[commands.Bot],
|
||||
*,
|
||||
embed_color: DiscordColor = DEFAULT_COLOR,
|
||||
ignore_diff_len: bool = False,
|
||||
timeout: Optional[float] = None,
|
||||
) -> discord.Message:
|
||||
"""
|
||||
starts the Country Guesser(buttons) Game
|
||||
|
||||
Parameters
|
||||
----------
|
||||
ctx : commands.Context
|
||||
the context of the invokation command
|
||||
embed_color : DiscordColor, optional
|
||||
the color of the game embed, by default DEFAULT_COLOR
|
||||
ignore_diff_len : bool, optional
|
||||
specifies whether or not to ignore guesses that are not of the same length as the correct answer, by default False
|
||||
timeout : Optional[float], optional
|
||||
the timeout for the view, by default None
|
||||
|
||||
Returns
|
||||
-------
|
||||
discord.Message
|
||||
returns the game message
|
||||
"""
|
||||
self.accepted_length = len(self.country) if ignore_diff_len else None
|
||||
|
||||
file = await self.get_country()
|
||||
|
||||
self.embed_color = discord.Color.random()
|
||||
self.embed = self.get_embed()
|
||||
self.embed.add_field(
|
||||
name="Guess Log", value="```diff\n\u200b\n```", inline=False
|
||||
)
|
||||
|
||||
self.view = CountryView(self, user=ctx.author, timeout=timeout)
|
||||
self.message = await ctx.send(embed=self.embed, file=file, view=self.view)
|
||||
|
||||
await self.view.wait()
|
||||
return self.message
|
||||
168
bot/games/button_games/lights_out.py
Normal file
168
bot/games/button_games/lights_out.py
Normal file
@@ -0,0 +1,168 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import random
|
||||
from typing import Any, TYPE_CHECKING, Optional, Literal, Final
|
||||
|
||||
import discord
|
||||
from discord.ext import commands
|
||||
|
||||
from .number_slider import SlideView
|
||||
from ..utils import *
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing_extensions import TypeAlias
|
||||
|
||||
Board: TypeAlias = list[list[Optional[Literal["💡"]]]]
|
||||
|
||||
BULB: Final[Literal["💡"]] = "💡"
|
||||
|
||||
|
||||
class LightsOutButton(discord.ui.Button["LightsOutView"]):
|
||||
def __init__(
|
||||
self, emoji: str, *, style: discord.ButtonStyle, row: int, col: int
|
||||
) -> None:
|
||||
super().__init__(
|
||||
emoji=emoji,
|
||||
label="\u200b",
|
||||
style=style,
|
||||
row=row,
|
||||
)
|
||||
|
||||
self.col = col
|
||||
|
||||
async def callback(self, interaction: discord.Interaction) -> None:
|
||||
game = self.view.game
|
||||
|
||||
if interaction.user != game.player:
|
||||
return await interaction.response.send_message(
|
||||
"This is not your game!", ephemeral=True
|
||||
)
|
||||
else:
|
||||
row, col = self.row, self.col
|
||||
|
||||
beside_item = game.beside_item(row, col)
|
||||
game.toggle(row, col)
|
||||
|
||||
for i, j in beside_item:
|
||||
game.toggle(i, j)
|
||||
|
||||
self.view.update_board(clear=True)
|
||||
|
||||
game.moves += 1
|
||||
game.embed.set_field_at(0, name="\u200b", value=f"Moves: `{game.moves}`")
|
||||
|
||||
if game.tiles == game.completed:
|
||||
self.view.disable_all()
|
||||
self.view.stop()
|
||||
game.embed.description = "**Congrats! You won!**"
|
||||
|
||||
return await interaction.response.edit_message(
|
||||
embed=game.embed, view=self.view
|
||||
)
|
||||
|
||||
|
||||
class LightsOutView(SlideView):
|
||||
game: LightsOut
|
||||
|
||||
def __init__(self, game: LightsOut, *, timeout: float) -> None:
|
||||
super().__init__(game, timeout=timeout)
|
||||
|
||||
def update_board(self, *, clear: bool = False) -> None:
|
||||
|
||||
if clear:
|
||||
self.clear_items()
|
||||
|
||||
for i, row in enumerate(self.game.tiles):
|
||||
for j, tile in enumerate(row):
|
||||
button = LightsOutButton(
|
||||
emoji=tile,
|
||||
style=self.game.button_style,
|
||||
row=i,
|
||||
col=j,
|
||||
)
|
||||
self.add_item(button)
|
||||
|
||||
|
||||
class LightsOut:
|
||||
"""
|
||||
Lights Out Game
|
||||
"""
|
||||
|
||||
def __init__(self, count: Literal[1, 2, 3, 4, 5] = 4) -> None:
|
||||
|
||||
if count not in range(1, 6):
|
||||
raise ValueError("Count must be an integer between 1 and 5")
|
||||
|
||||
self.moves: int = 0
|
||||
self.count = count
|
||||
|
||||
self.completed: Final[Board] = [[None] * self.count for _ in range(self.count)]
|
||||
self.tiles: Board = []
|
||||
|
||||
self.player: Optional[discord.User] = None
|
||||
self.button_style: discord.ButtonStyle = discord.ButtonStyle.green
|
||||
|
||||
def toggle(self, row: int, col: int) -> None:
|
||||
self.tiles[row][col] = BULB if self.tiles[row][col] is None else None
|
||||
|
||||
def beside_item(self, row: int, col: int) -> list[tuple[int, int]]:
|
||||
beside = [
|
||||
(row - 1, col),
|
||||
(row, col - 1),
|
||||
(row + 1, col),
|
||||
(row, col + 1),
|
||||
]
|
||||
|
||||
data = [
|
||||
(i, j)
|
||||
for i, j in beside
|
||||
if i in range(self.count) and j in range(self.count)
|
||||
]
|
||||
return data
|
||||
|
||||
async def start(
|
||||
self,
|
||||
ctx: commands.Context[commands.Bot],
|
||||
*,
|
||||
button_style: discord.ButtonStyle = discord.ButtonStyle.green,
|
||||
embed_color: DiscordColor = DEFAULT_COLOR,
|
||||
timeout: Optional[float] = None,
|
||||
) -> discord.Message:
|
||||
"""
|
||||
starts the Lights Out Game
|
||||
|
||||
Parameters
|
||||
----------
|
||||
ctx : commands.Context
|
||||
the context of the invokation command
|
||||
button_style : discord.ButtonStyle, optional
|
||||
the primary button style to use, by default discord.ButtonStyle.green
|
||||
embed_color : DiscordColor, optional
|
||||
the color of the game embed, by default DEFAULT_COLOR
|
||||
timeout : Optional[float], optional
|
||||
the timeout for the view, by default None
|
||||
|
||||
Returns
|
||||
-------
|
||||
discord.Message
|
||||
returns the game message
|
||||
"""
|
||||
self.button_style = button_style
|
||||
self.player = ctx.author
|
||||
|
||||
self.tiles = random.choices((None, BULB), k=self.count**2)
|
||||
self.tiles = chunk(self.tiles, count=self.count)
|
||||
|
||||
self.view = LightsOutView(self, timeout=timeout)
|
||||
self.embed = discord.Embed(
|
||||
description="Turn off all the tiles!", color=embed_color
|
||||
)
|
||||
self.embed.add_field(name="\u200b", value="Moves: `0`")
|
||||
|
||||
self.message = await ctx.send(embed=self.embed, view=self.view)
|
||||
|
||||
await double_wait(
|
||||
wait_for_delete(ctx, self.message),
|
||||
self.view.wait(),
|
||||
)
|
||||
return self.message
|
||||
181
bot/games/button_games/memory_game.py
Normal file
181
bot/games/button_games/memory_game.py
Normal file
@@ -0,0 +1,181 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Optional, ClassVar
|
||||
import random
|
||||
import asyncio
|
||||
|
||||
import discord
|
||||
from discord.ext import commands
|
||||
|
||||
from ..utils import *
|
||||
|
||||
|
||||
class MemoryButton(discord.ui.Button["MemoryView"]):
|
||||
def __init__(self, emoji: str, *, style: discord.ButtonStyle, row: int = 0) -> None:
|
||||
self.value = emoji
|
||||
|
||||
super().__init__(
|
||||
label="\u200b",
|
||||
style=style,
|
||||
row=row,
|
||||
)
|
||||
|
||||
async def callback(self, interaction: discord.Interaction) -> None:
|
||||
game = self.view.game
|
||||
|
||||
if opened := self.view.opened:
|
||||
game.moves += 1
|
||||
game.embed.set_field_at(0, name="\u200b", value=f"Moves: `{game.moves}`")
|
||||
|
||||
self.emoji = self.value
|
||||
self.disabled = True
|
||||
await interaction.response.edit_message(view=self.view)
|
||||
|
||||
if opened.value != self.value:
|
||||
await asyncio.sleep(self.view.pause_time)
|
||||
|
||||
opened.emoji = None
|
||||
opened.disabled = False
|
||||
|
||||
self.emoji = None
|
||||
self.disabled = False
|
||||
self.view.opened = None
|
||||
else:
|
||||
self.view.opened = None
|
||||
|
||||
if all(
|
||||
button.disabled
|
||||
for button in self.view.children
|
||||
if isinstance(button, discord.ui.Button)
|
||||
):
|
||||
await interaction.message.edit(
|
||||
content="Game Over, Congrats!", view=self.view
|
||||
)
|
||||
return self.view.stop()
|
||||
|
||||
return await interaction.message.edit(view=self.view, embed=game.embed)
|
||||
else:
|
||||
self.emoji = self.value
|
||||
self.view.opened = self
|
||||
self.disabled = True
|
||||
return await interaction.response.edit_message(view=self.view)
|
||||
|
||||
|
||||
class MemoryView(BaseView):
|
||||
board: list[list[str]]
|
||||
DEFAULT_ITEMS: ClassVar[list[str]] = [
|
||||
"🥝",
|
||||
"🍓",
|
||||
"🍹",
|
||||
"🍋",
|
||||
"🥭",
|
||||
"🍎",
|
||||
"🍊",
|
||||
"🍍",
|
||||
"🍑",
|
||||
"🍇",
|
||||
"🍉",
|
||||
"🥬",
|
||||
]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
game: MemoryGame,
|
||||
items: list[str],
|
||||
*,
|
||||
button_style: discord.ButtonStyle,
|
||||
pause_time: float,
|
||||
timeout: Optional[float] = None,
|
||||
) -> None:
|
||||
|
||||
super().__init__(timeout=timeout)
|
||||
|
||||
self.game = game
|
||||
|
||||
self.button_style = button_style
|
||||
self.pause_time = pause_time
|
||||
self.opened: Optional[MemoryButton] = None
|
||||
|
||||
if not items:
|
||||
items = self.DEFAULT_ITEMS[:]
|
||||
assert len(items) == 12
|
||||
|
||||
items *= 2
|
||||
random.shuffle(items)
|
||||
random.shuffle(items)
|
||||
items.insert(12, None)
|
||||
|
||||
self.board = chunk(items, count=5)
|
||||
|
||||
for i, row in enumerate(self.board):
|
||||
for item in row:
|
||||
button = MemoryButton(item, style=self.button_style, row=i)
|
||||
|
||||
if not item:
|
||||
button.disabled = True
|
||||
self.add_item(button)
|
||||
|
||||
|
||||
class MemoryGame:
|
||||
"""
|
||||
Memory Game
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.embed_color: Optional[DiscordColor] = None
|
||||
self.embed: Optional[discord.Embed] = None
|
||||
self.moves: int = 0
|
||||
|
||||
async def start(
|
||||
self,
|
||||
ctx: commands.Context[commands.Bot],
|
||||
*,
|
||||
embed_color: DiscordColor = DEFAULT_COLOR,
|
||||
items: list[str] = [],
|
||||
pause_time: float = 0.7,
|
||||
button_style: discord.ButtonStyle = discord.ButtonStyle.red,
|
||||
timeout: Optional[float] = None,
|
||||
) -> discord.Message:
|
||||
"""
|
||||
starts the memory game
|
||||
|
||||
Parameters
|
||||
----------
|
||||
ctx : commands.Context
|
||||
the context of the invokation command
|
||||
embed_color : DiscordColor, optional
|
||||
the color of the game embed, by default DEFAULT_COLOR
|
||||
items : list[str], optional
|
||||
items to use for the game tiles, by default []
|
||||
pause_time : float, optional
|
||||
specifies the duration to pause for before hiding the tiles again, by default 0.7
|
||||
button_style : discord.ButtonStyle, optional
|
||||
the primary button style to use, by default discord.ButtonStyle.red
|
||||
timeout : Optional[float], optional
|
||||
the timeout for the view, by default None
|
||||
|
||||
Returns
|
||||
-------
|
||||
discord.Message
|
||||
returns the game message
|
||||
"""
|
||||
self.embed_color = discord.Color.random()
|
||||
self.embed = discord.Embed(
|
||||
description="**Memory Game**", color=discord.Color.random()
|
||||
)
|
||||
self.embed.add_field(name="\u200b", value="Moves: `0`")
|
||||
|
||||
self.view = MemoryView(
|
||||
game=self,
|
||||
items=items,
|
||||
button_style=button_style,
|
||||
pause_time=pause_time,
|
||||
timeout=timeout,
|
||||
)
|
||||
self.message = await ctx.send(embed=self.embed, view=self.view)
|
||||
|
||||
await double_wait(
|
||||
wait_for_delete(ctx, self.message),
|
||||
self.view.wait(),
|
||||
)
|
||||
return self.message
|
||||
196
bot/games/button_games/number_slider.py
Normal file
196
bot/games/button_games/number_slider.py
Normal file
@@ -0,0 +1,196 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Optional, Literal
|
||||
import random
|
||||
|
||||
import discord
|
||||
from discord.ext import commands
|
||||
|
||||
from ..utils import *
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing_extensions import TypeAlias
|
||||
|
||||
Board: TypeAlias = list[list[Optional[int]]]
|
||||
|
||||
|
||||
class SlideButton(discord.ui.Button["SlideView"]):
|
||||
def __init__(self, label: str, *, style: discord.ButtonStyle, row: int) -> None:
|
||||
super().__init__(
|
||||
label=label,
|
||||
style=style,
|
||||
row=row,
|
||||
)
|
||||
|
||||
if label == "\u200b":
|
||||
self.disabled = True
|
||||
|
||||
async def callback(self, interaction: discord.Interaction) -> None:
|
||||
game = self.view.game
|
||||
|
||||
if interaction.user != game.player:
|
||||
return await interaction.response.send_message(
|
||||
"This is not your game!", ephemeral=True
|
||||
)
|
||||
else:
|
||||
num = int(self.label)
|
||||
|
||||
if num not in game.beside_blank():
|
||||
return await interaction.response.defer()
|
||||
else:
|
||||
ix, iy = game.get_item(num)
|
||||
nx, ny = game.get_item()
|
||||
|
||||
game.numbers[nx][ny], game.numbers[ix][iy] = (
|
||||
game.numbers[ix][iy],
|
||||
game.numbers[nx][ny],
|
||||
)
|
||||
|
||||
self.view.update_board(clear=True)
|
||||
|
||||
game.moves += 1
|
||||
game.embed.set_field_at(
|
||||
0, name="\u200b", value=f"Moves: `{game.moves}`"
|
||||
)
|
||||
|
||||
if game.numbers == game.completed:
|
||||
self.view.disable_all()
|
||||
self.view.stop()
|
||||
game.embed.description = "**Congrats! You won!**"
|
||||
|
||||
return await interaction.response.edit_message(
|
||||
embed=game.embed, view=self.view
|
||||
)
|
||||
|
||||
|
||||
class SlideView(BaseView):
|
||||
def __init__(self, game: NumberSlider, *, timeout: float) -> None:
|
||||
super().__init__(timeout=timeout)
|
||||
|
||||
self.game = game
|
||||
|
||||
self.update_board()
|
||||
|
||||
def update_board(self, *, clear: bool = False) -> None:
|
||||
|
||||
if clear:
|
||||
self.clear_items()
|
||||
|
||||
for i, row in enumerate(self.game.numbers):
|
||||
for j, number in enumerate(row):
|
||||
if number == self.game.completed[i][j]:
|
||||
style = self.game.correct_style
|
||||
else:
|
||||
style = self.game.wrong_style
|
||||
|
||||
button = SlideButton(
|
||||
label=number or "\u200b",
|
||||
style=style,
|
||||
row=i,
|
||||
)
|
||||
self.add_item(button)
|
||||
|
||||
|
||||
class NumberSlider:
|
||||
"""
|
||||
Number Slider Game
|
||||
"""
|
||||
|
||||
def __init__(self, count: Literal[1, 2, 3, 4, 5] = 4) -> None:
|
||||
|
||||
if count not in range(1, 6):
|
||||
raise ValueError("Count must be an integer between 1 and 5")
|
||||
|
||||
self.all_numbers = list(range(1, count**2))
|
||||
|
||||
self.player: Optional[discord.User] = None
|
||||
|
||||
self.moves: int = 0
|
||||
self.count = count
|
||||
self.numbers: Board = []
|
||||
self.completed: Board = []
|
||||
|
||||
self.wrong_style: discord.ButtonStyle = discord.ButtonStyle.gray
|
||||
self.correct_style: discord.ButtonStyle = discord.ButtonStyle.green
|
||||
|
||||
def get_item(self, obj: Optional[int] = None) -> tuple[int, int]:
|
||||
return next(
|
||||
(x, y)
|
||||
for x, row in enumerate(self.numbers)
|
||||
for y, item in enumerate(row)
|
||||
if item == obj
|
||||
)
|
||||
|
||||
def beside_blank(self) -> list[int]:
|
||||
nx, ny = self.get_item()
|
||||
|
||||
beside_item = [
|
||||
(nx - 1, ny),
|
||||
(nx, ny - 1),
|
||||
(nx + 1, ny),
|
||||
(nx, ny + 1),
|
||||
]
|
||||
|
||||
data = [
|
||||
self.numbers[i][j]
|
||||
for i, j in beside_item
|
||||
if i in range(self.count) and j in range(self.count)
|
||||
]
|
||||
return data
|
||||
|
||||
async def start(
|
||||
self,
|
||||
ctx: commands.Context[commands.Bot],
|
||||
*,
|
||||
wrong_style: discord.ButtonStyle = discord.ButtonStyle.gray,
|
||||
correct_style: discord.ButtonStyle = discord.ButtonStyle.green,
|
||||
embed_color: DiscordColor = DEFAULT_COLOR,
|
||||
timeout: Optional[float] = None,
|
||||
) -> discord.Message:
|
||||
"""
|
||||
starts the number slider game
|
||||
|
||||
Parameters
|
||||
----------
|
||||
ctx : commands.Context
|
||||
the context of the invokation command
|
||||
wrong_style : discord.ButtonStyle, optional
|
||||
the button style to use for tiles that are in the wrong spot, by default discord.ButtonStyle.gray
|
||||
correct_style : discord.ButtonStyle, optional
|
||||
the button style to use for tiles that are in the right spot, by default discord.ButtonStyle.green
|
||||
embed_color : DiscordColor, optional
|
||||
the game embedd color, by default DEFAULT_COLOR
|
||||
timeout : Optional[float], optional
|
||||
the timeout for the view, by default None
|
||||
|
||||
Returns
|
||||
-------
|
||||
discord.Message
|
||||
returns the game message
|
||||
"""
|
||||
self.player = ctx.author
|
||||
self.wrong_style = wrong_style
|
||||
self.correct_style = correct_style
|
||||
|
||||
numbers = self.all_numbers[:]
|
||||
random.shuffle(numbers)
|
||||
random.shuffle(numbers)
|
||||
|
||||
numbers.append(None)
|
||||
self.numbers = chunk(numbers, count=self.count)
|
||||
|
||||
self.completed = chunk(self.all_numbers + [None], count=self.count)
|
||||
|
||||
self.view = SlideView(self, timeout=timeout)
|
||||
self.embed = discord.Embed(
|
||||
description="Slide the tiles back in ascending order!", color=discord.Color.random()
|
||||
)
|
||||
self.embed.add_field(name="\u200b", value="Moves: `0`")
|
||||
|
||||
self.message = await ctx.send(embed=self.embed, view=self.view)
|
||||
|
||||
await double_wait(
|
||||
wait_for_delete(ctx, self.message),
|
||||
self.view.wait(),
|
||||
)
|
||||
return self.message
|
||||
132
bot/games/button_games/reaction_test_buttons.py
Normal file
132
bot/games/button_games/reaction_test_buttons.py
Normal file
@@ -0,0 +1,132 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Optional, Union
|
||||
import time
|
||||
import random
|
||||
import asyncio
|
||||
|
||||
import discord
|
||||
from discord.ext import commands
|
||||
|
||||
from ..utils import DiscordColor, DEFAULT_COLOR, BaseView
|
||||
|
||||
|
||||
class ReactionButton(discord.ui.Button["ReactionView"]):
|
||||
def __init__(self, style: discord.ButtonStyle) -> None:
|
||||
super().__init__(label="\u200b", style=style)
|
||||
|
||||
self.edited: bool = False
|
||||
self.clicked: bool = False
|
||||
|
||||
async def callback(self, interaction: discord.Interaction) -> None:
|
||||
game = self.view.game
|
||||
|
||||
if game.author_only and interaction.user != game.author:
|
||||
return await interaction.response.send_message(
|
||||
"This game is only for the author!", ephemeral=True
|
||||
)
|
||||
|
||||
if not self.edited or self.clicked:
|
||||
return await interaction.response.defer()
|
||||
else:
|
||||
end_time = time.perf_counter()
|
||||
elapsed = end_time - self.view.game.start_time
|
||||
|
||||
game.embed.description = (
|
||||
f"{interaction.user.mention} reacted first in `{elapsed:.2f}s` !"
|
||||
)
|
||||
await interaction.response.edit_message(embed=game.embed)
|
||||
|
||||
self.clicked = True
|
||||
return game.finished_event.set()
|
||||
|
||||
|
||||
class ReactionView(BaseView):
|
||||
game: BetaReactionGame
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
game: BetaReactionGame,
|
||||
*,
|
||||
button_style: discord.ButtonStyle,
|
||||
timeout: float,
|
||||
) -> None:
|
||||
|
||||
super().__init__(timeout=timeout)
|
||||
|
||||
self.game = game
|
||||
self.button_style = button_style
|
||||
self.button = ReactionButton(self.button_style)
|
||||
self.add_item(self.button)
|
||||
|
||||
|
||||
class BetaReactionGame:
|
||||
"""
|
||||
Reaction(buttons) game
|
||||
"""
|
||||
|
||||
async def start(
|
||||
self,
|
||||
ctx: commands.Context[commands.Bot],
|
||||
*,
|
||||
author_only: bool = False,
|
||||
pause_range: tuple[float, float] = (1.0, 5.0),
|
||||
start_button_style: discord.ButtonStyle = discord.ButtonStyle.blurple,
|
||||
end_button_style: Union[
|
||||
discord.ButtonStyle, tuple[discord.ButtonStyle, ...]
|
||||
] = (discord.ButtonStyle.green, discord.ButtonStyle.red),
|
||||
embed_color: DiscordColor = DEFAULT_COLOR,
|
||||
timeout: Optional[float] = None,
|
||||
) -> discord.Message:
|
||||
"""
|
||||
starts the Reaction(buttons) Game
|
||||
|
||||
Parameters
|
||||
----------
|
||||
ctx : commands.Context
|
||||
the context of the invokation command
|
||||
author_only : bool, optional
|
||||
specifies whether or not tjhe view is only limited to the author, by default False
|
||||
pause_range : tuple[float, float], optional
|
||||
the time range to randomly pause for, by default (1.0, 5.0)
|
||||
start_button_style : discord.ButtonStyle, optional
|
||||
specifies the button style to start with, by default discord.ButtonStyle.blurple
|
||||
end_button_style : Union[discord.ButtonStyle, tuple[discord.ButtonStyle, ...]], optional
|
||||
specifies the button styles(s) to change to, by default (discord.ButtonStyle.green, discord.ButtonStyle.red)
|
||||
embed_color : DiscordColor, optional
|
||||
the color of the game embed, by default DEFAULT_COLOR
|
||||
timeout : Optional[float], optional
|
||||
the timeout for the view, by default None
|
||||
|
||||
Returns
|
||||
-------
|
||||
discord.Message
|
||||
returns the game message
|
||||
"""
|
||||
self.finished_event = asyncio.Event()
|
||||
|
||||
self.author_only = author_only
|
||||
self.author = ctx.author
|
||||
|
||||
self.embed = discord.Embed(
|
||||
title="Reaction Game",
|
||||
description=f"Click the button below, when the button changes color!",
|
||||
color=discord.Color.random(),
|
||||
)
|
||||
self.view = ReactionView(self, button_style=start_button_style, timeout=timeout)
|
||||
self.message = await ctx.send(embed=self.embed, view=self.view)
|
||||
|
||||
pause = random.uniform(*pause_range)
|
||||
await asyncio.sleep(pause)
|
||||
|
||||
if isinstance(end_button_style, tuple):
|
||||
self.view.button.style = random.choice(end_button_style)
|
||||
else:
|
||||
self.view.button.style = end_button_style
|
||||
|
||||
await self.message.edit(view=self.view)
|
||||
self.start_time = time.perf_counter()
|
||||
self.view.button.edited = True
|
||||
|
||||
await self.finished_event.wait()
|
||||
return self.message
|
||||
174
bot/games/button_games/rps_buttons.py
Normal file
174
bot/games/button_games/rps_buttons.py
Normal file
@@ -0,0 +1,174 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Optional
|
||||
import random
|
||||
|
||||
import discord
|
||||
from utils.emoji import WARNING_ALT
|
||||
from discord.ext import commands
|
||||
|
||||
from ..rps import RockPaperScissors
|
||||
from ..utils import DiscordColor, DEFAULT_COLOR, BaseView
|
||||
|
||||
|
||||
class RPSButton(discord.ui.Button["RPSView"]):
|
||||
def __init__(self, emoji: str, *, style: discord.ButtonStyle) -> None:
|
||||
super().__init__(
|
||||
label="\u200b",
|
||||
emoji=emoji,
|
||||
style=style,
|
||||
)
|
||||
|
||||
def get_choice(self, user: discord.User, other: bool = False) -> Optional[str]:
|
||||
game = self.view.game
|
||||
if other:
|
||||
return game.player2_choice if user == game.player2 else game.player1_choice
|
||||
else:
|
||||
return game.player1_choice if user == game.player1 else game.player2_choice
|
||||
|
||||
async def callback(self, interaction: discord.Interaction) -> None:
|
||||
game = self.view.game
|
||||
players = (game.player1, game.player2) if game.player2 else (game.player1,)
|
||||
|
||||
if interaction.user not in players:
|
||||
return await interaction.response.send_message(
|
||||
"This is not your game!", ephemeral=True
|
||||
)
|
||||
else:
|
||||
if not game.player2:
|
||||
bot_choice = random.choice(game.OPTIONS)
|
||||
user_choice = self.emoji.name
|
||||
|
||||
if user_choice == bot_choice:
|
||||
game.embed.description = f"**Tie!**\nWe both picked {user_choice}"
|
||||
else:
|
||||
if game.check_win(bot_choice, user_choice):
|
||||
game.embed.description = f"**You Won!**\nYou picked {user_choice} and I picked {bot_choice}."
|
||||
else:
|
||||
game.embed.description = f"**You Lost!**\nI picked {bot_choice} and you picked {user_choice}."
|
||||
|
||||
self.view.disable_all()
|
||||
self.view.stop()
|
||||
|
||||
else:
|
||||
if self.get_choice(interaction.user):
|
||||
return await interaction.response.send_message(
|
||||
"You have already chosen!", ephemeral=True
|
||||
)
|
||||
|
||||
other_player_choice = self.get_choice(interaction.user, other=True)
|
||||
|
||||
if interaction.user == game.player1:
|
||||
game.player1_choice = self.emoji.name
|
||||
|
||||
if not other_player_choice:
|
||||
game.embed.description += f"\n\n{game.player1.mention} has chosen...\n*Waiting for {game.player2.mention} to choose...*"
|
||||
else:
|
||||
game.player2_choice = self.emoji.name
|
||||
|
||||
if not other_player_choice:
|
||||
game.embed.description += f"\n\n{game.player2.mention} has chosen...\n*Waiting for {game.player1.mention} to choose...*"
|
||||
|
||||
if game.player1_choice and game.player2_choice:
|
||||
if game.player1_choice == game.player2_choice:
|
||||
game.embed.description = f"**Tie!**\nBoth {game.player1.mention} and {game.player2.mention} picked {game.player1_choice}."
|
||||
else:
|
||||
who_won = (
|
||||
game.player1
|
||||
if game.check_win(game.player2_choice, game.player1_choice)
|
||||
else game.player2
|
||||
)
|
||||
|
||||
game.embed.description = (
|
||||
f"**{who_won.mention} Won!**"
|
||||
f"\n\n{game.player1.mention} chose {game.player1_choice}."
|
||||
f"\n{game.player2.mention} chose {game.player2_choice}."
|
||||
)
|
||||
|
||||
self.view.disable_all()
|
||||
self.view.stop()
|
||||
|
||||
return await interaction.response.edit_message(
|
||||
embed=game.embed, view=self.view
|
||||
)
|
||||
|
||||
|
||||
class RPSView(BaseView):
|
||||
game: BetaRockPaperScissors
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
game: BetaRockPaperScissors,
|
||||
*,
|
||||
button_style: discord.ButtonStyle,
|
||||
timeout: float,
|
||||
) -> None:
|
||||
|
||||
super().__init__(timeout=timeout)
|
||||
|
||||
self.button_style = button_style
|
||||
self.game = game
|
||||
|
||||
for option in self.game.OPTIONS:
|
||||
self.add_item(RPSButton(option, style=self.button_style))
|
||||
|
||||
|
||||
class BetaRockPaperScissors(RockPaperScissors):
|
||||
"""
|
||||
RockPaperScissors(buttons) game
|
||||
"""
|
||||
|
||||
player1: discord.User
|
||||
embed: discord.Embed
|
||||
|
||||
def __init__(self, other_player: Optional[discord.User] = None) -> None:
|
||||
self.player2 = other_player
|
||||
|
||||
if self.player2:
|
||||
self.player1_choice: Optional[str] = None
|
||||
self.player2_choice: Optional[str] = None
|
||||
|
||||
async def start(
|
||||
self,
|
||||
ctx: commands.Context[commands.Bot],
|
||||
*,
|
||||
button_style: discord.ButtonStyle = discord.ButtonStyle.blurple,
|
||||
embed_color: DiscordColor = DEFAULT_COLOR,
|
||||
timeout: Optional[float] = None,
|
||||
) -> discord.Message:
|
||||
if ctx.author == self.player2:
|
||||
embed = discord.Embed(title=f"{WARNING_ALT} Access Denied", description="You cannot play against yourself!", color=0x000000)
|
||||
return await ctx.reply(embed=embed)
|
||||
|
||||
"""
|
||||
Starts the Rock Paper Scissors (buttons) game.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
ctx : commands.Context
|
||||
The context of the invoking command.
|
||||
button_style : discord.ButtonStyle, optional
|
||||
The primary button style to use, by default discord.ButtonStyle.blurple.
|
||||
embed_color : DiscordColor, optional
|
||||
The color of the game embed, by default DEFAULT_COLOR.
|
||||
timeout : Optional[float], optional
|
||||
The timeout for the view, by default None.
|
||||
|
||||
Returns
|
||||
-------
|
||||
discord.Message
|
||||
Returns the game message.
|
||||
"""
|
||||
self.player1 = ctx.author
|
||||
|
||||
self.embed = discord.Embed(
|
||||
title="Rock Paper Scissors",
|
||||
description="Select a button to play!",
|
||||
color=discord.Color.random(),
|
||||
)
|
||||
|
||||
self.view = RPSView(self, button_style=button_style, timeout=timeout)
|
||||
self.message = await ctx.send(embed=self.embed, view=self.view)
|
||||
|
||||
await self.view.wait()
|
||||
return self.message
|
||||
128
bot/games/button_games/tictactoe_buttons.py
Normal file
128
bot/games/button_games/tictactoe_buttons.py
Normal file
@@ -0,0 +1,128 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import ClassVar, Optional
|
||||
|
||||
import discord
|
||||
from discord.ext import commands
|
||||
|
||||
from ..tictactoe import Tictactoe
|
||||
from ..utils import *
|
||||
|
||||
|
||||
class TTTButton(discord.ui.Button["TTTView"]):
|
||||
def __init__(self, label: str, style: discord.ButtonStyle, *, row: int, col: int):
|
||||
super().__init__(
|
||||
label=label,
|
||||
style=style,
|
||||
row=row,
|
||||
)
|
||||
|
||||
self.col = col
|
||||
|
||||
async def callback(self, interaction: discord.Interaction) -> None:
|
||||
user = interaction.user
|
||||
game = self.view.game
|
||||
|
||||
if user not in (game.cross, game.circle):
|
||||
return await interaction.response.send_message(
|
||||
"You are not part of this game!", ephemeral=True
|
||||
)
|
||||
|
||||
if user != game.turn:
|
||||
return await interaction.response.send_message(
|
||||
"it is not your turn!", ephemeral=True
|
||||
)
|
||||
|
||||
self.label = game.player_to_emoji[user]
|
||||
self.disabled = True
|
||||
|
||||
game.board[self.row][self.col] = self.label
|
||||
game.turn = game.circle if user == game.cross else game.cross
|
||||
|
||||
tie = all(button.disabled for button in self.view.children)
|
||||
|
||||
if game_over := game.is_game_over(tie=tie):
|
||||
if game.winning_indexes:
|
||||
self.view.disable_all()
|
||||
game.create_streak()
|
||||
self.view.stop()
|
||||
|
||||
embed = game.make_embed(game_over=game_over or tie)
|
||||
await interaction.response.edit_message(embed=embed, view=self.view)
|
||||
|
||||
|
||||
class TTTView(BaseView):
|
||||
def __init__(self, game: BetaTictactoe, *, timeout: float) -> None:
|
||||
super().__init__(timeout=timeout)
|
||||
|
||||
self.game = game
|
||||
|
||||
for x, row in enumerate(game.board):
|
||||
for y, square in enumerate(row):
|
||||
button = TTTButton(
|
||||
label=square,
|
||||
style=self.game.button_style,
|
||||
row=x,
|
||||
col=y,
|
||||
)
|
||||
self.add_item(button)
|
||||
|
||||
|
||||
class BetaTictactoe(Tictactoe):
|
||||
"""
|
||||
Tictactoe(buttons) game
|
||||
"""
|
||||
|
||||
BLANK: ClassVar[str] = "\u200b"
|
||||
CIRCLE: ClassVar[str] = "O"
|
||||
CROSS: ClassVar[str] = "X"
|
||||
|
||||
def create_streak(self) -> None:
|
||||
chunked = chunk(self.view.children, count=3)
|
||||
for row, col in self.winning_indexes:
|
||||
button: TTTButton = chunked[row][col]
|
||||
button.style = self.win_button_style
|
||||
|
||||
async def start(
|
||||
self,
|
||||
ctx: commands.Context[commands.Bot],
|
||||
button_style: discord.ButtonStyle = discord.ButtonStyle.green,
|
||||
*,
|
||||
embed_color: DiscordColor = DEFAULT_COLOR,
|
||||
win_button_style: discord.ButtonStyle = discord.ButtonStyle.red,
|
||||
timeout: Optional[float] = None,
|
||||
) -> discord.Message:
|
||||
"""
|
||||
starts the tictactoe(buttons) game
|
||||
|
||||
Parameters
|
||||
----------
|
||||
ctx : commands.Context
|
||||
the context of the invokation command
|
||||
button_style : discord.ButtonStyle, optional
|
||||
the primary button style to use, by default discord.ButtonStyle.green
|
||||
embed_color : DiscordColor, optional
|
||||
the color of the game embed, by default DEFAULT_COLOR
|
||||
win_button_style : discord.ButtonStyle, optional
|
||||
the button style to use to show the winning line, by default discord.ButtonStyle.red
|
||||
timeout : Optional[float], optional
|
||||
the timeout for the view, by default None
|
||||
|
||||
Returns
|
||||
-------
|
||||
discord.Message
|
||||
returns the game message
|
||||
"""
|
||||
self.embed_color = discord.Color.random()
|
||||
self.button_style = button_style
|
||||
self.win_button_style = win_button_style
|
||||
|
||||
self.view = TTTView(self, timeout=timeout)
|
||||
self.message = await ctx.send(embed=self.make_embed(), view=self.view)
|
||||
|
||||
await double_wait(
|
||||
wait_for_delete(ctx, self.message, user=(self.cross, self.circle)),
|
||||
self.view.wait(),
|
||||
)
|
||||
|
||||
return self.message
|
||||
137
bot/games/button_games/twenty_48_buttons.py
Normal file
137
bot/games/button_games/twenty_48_buttons.py
Normal file
@@ -0,0 +1,137 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Optional, Literal
|
||||
import random
|
||||
|
||||
import discord
|
||||
from discord.ext import commands
|
||||
|
||||
from ..twenty_48 import Twenty48
|
||||
from ..utils import DiscordColor, DEFAULT_COLOR, BaseView
|
||||
|
||||
|
||||
class Twenty48_Button(discord.ui.Button["BaseView"]):
|
||||
def __init__(self, game: BetaTwenty48, emoji: str) -> None:
|
||||
self.game = game
|
||||
|
||||
style = (
|
||||
discord.ButtonStyle.red if emoji == "⏹️" else discord.ButtonStyle.blurple
|
||||
)
|
||||
|
||||
super().__init__(
|
||||
style=style, emoji=discord.PartialEmoji(name=emoji), label="\u200b"
|
||||
)
|
||||
|
||||
async def callback(self, interaction: discord.Interaction) -> None:
|
||||
|
||||
if interaction.user != self.game.player:
|
||||
return await interaction.response.send_message(
|
||||
"This isn't your game!", ephemeral=True
|
||||
)
|
||||
|
||||
emoji = str(self.emoji)
|
||||
|
||||
if emoji == "⏹️":
|
||||
self.view.stop()
|
||||
return await interaction.message.delete()
|
||||
|
||||
elif emoji == "➡️":
|
||||
self.game.move_right()
|
||||
|
||||
elif emoji == "⬅️":
|
||||
self.game.move_left()
|
||||
|
||||
elif emoji == "⬇️":
|
||||
self.game.move_down()
|
||||
|
||||
elif emoji == "⬆️":
|
||||
self.game.move_up()
|
||||
|
||||
lost = self.game.spawn_new()
|
||||
won = self.game.check_win()
|
||||
|
||||
if won or lost:
|
||||
self.view.disable_all()
|
||||
self.view.stop()
|
||||
|
||||
if lost:
|
||||
self.game.embed = discord.Embed(
|
||||
description="Game Over! You lost.",
|
||||
color=self.game.embed_color,
|
||||
)
|
||||
|
||||
if self.game._render_image:
|
||||
image = await self.game.render_image()
|
||||
await interaction.response.edit_message(
|
||||
attachments=[image], embed=self.game.embed
|
||||
)
|
||||
else:
|
||||
board_string = self.game.number_to_emoji()
|
||||
await interaction.response.edit_message(
|
||||
content=board_string, embed=self.game.embed
|
||||
)
|
||||
|
||||
|
||||
class BetaTwenty48(Twenty48):
|
||||
view: discord.ui.View
|
||||
"""
|
||||
Twenty48(buttons) game
|
||||
"""
|
||||
|
||||
async def start(
|
||||
self,
|
||||
ctx: commands.Context[commands.Bot],
|
||||
*,
|
||||
win_at: Literal[2048, 4096, 8192] = 8192,
|
||||
timeout: Optional[float] = None,
|
||||
delete_button: bool = False,
|
||||
embed_color: DiscordColor = DEFAULT_COLOR,
|
||||
**kwargs,
|
||||
) -> discord.Message:
|
||||
"""
|
||||
starts the 2048(buttons) game
|
||||
|
||||
Parameters
|
||||
----------
|
||||
ctx : commands.Context
|
||||
the context of the invokation command
|
||||
win_at : Literal[2048, 4096, 8192], optional
|
||||
the tile to stop the game / win at, by default 8192
|
||||
timeout : Optional[float], optional
|
||||
the timeout for the view, by default None
|
||||
delete_button : bool, optional
|
||||
specifies whether or not to add a stop button, by default False
|
||||
embed_color : DiscordColor, optional
|
||||
the color of the game embed, by default DEFAULT_COLOR
|
||||
|
||||
Returns
|
||||
-------
|
||||
discord.Message
|
||||
returns the game message
|
||||
"""
|
||||
self.win_at = win_at
|
||||
self.embed_color = discord.Color.random()
|
||||
|
||||
self.player = ctx.author
|
||||
self.view = BaseView(timeout=timeout)
|
||||
|
||||
self.board[random.randrange(4)][random.randrange(4)] = 2
|
||||
self.board[random.randrange(4)][random.randrange(4)] = 2
|
||||
|
||||
if delete_button:
|
||||
self._controls.append("⏹️")
|
||||
|
||||
for button in self._controls:
|
||||
self.view.add_item(Twenty48_Button(self, button))
|
||||
|
||||
if self._render_image:
|
||||
image = await self.render_image()
|
||||
self.message = await ctx.send(file=image, view=self.view, **kwargs)
|
||||
else:
|
||||
board_string = self.number_to_emoji()
|
||||
self.message = await ctx.send(
|
||||
content=board_string, view=self.view, **kwargs
|
||||
)
|
||||
|
||||
await self.view.wait()
|
||||
return self.message
|
||||
139
bot/games/button_games/wordle_buttons.py
Normal file
139
bot/games/button_games/wordle_buttons.py
Normal file
@@ -0,0 +1,139 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Optional
|
||||
|
||||
import discord
|
||||
from discord.ext import commands
|
||||
|
||||
from ..wordle import Wordle
|
||||
from ..utils import DiscordColor, DEFAULT_COLOR, BaseView
|
||||
|
||||
|
||||
class WordInput(discord.ui.Modal, title="Word Input"):
|
||||
word = discord.ui.TextInput(
|
||||
label=f"Input your guess",
|
||||
style=discord.TextStyle.short,
|
||||
required=True,
|
||||
min_length=5,
|
||||
max_length=5,
|
||||
)
|
||||
|
||||
def __init__(self, view: WordleView) -> None:
|
||||
super().__init__()
|
||||
self.view = view
|
||||
|
||||
async def on_submit(self, interaction: discord.Interaction) -> None:
|
||||
content = self.word.value.lower()
|
||||
game = self.view.game
|
||||
|
||||
if content not in game._valid_words:
|
||||
return await interaction.response.send_message(
|
||||
"That is not a valid word!", ephemeral=True
|
||||
)
|
||||
else:
|
||||
won = game.parse_guess(content)
|
||||
buf = await game.render_image()
|
||||
|
||||
embed = discord.Embed(title="Wordle!", color=self.view.game.embed_color)
|
||||
embed.set_image(url="attachment://wordle.png")
|
||||
file = discord.File(buf, "wordle.png")
|
||||
|
||||
if won:
|
||||
await interaction.message.reply(
|
||||
"Game Over! You won!", mention_author=True
|
||||
)
|
||||
elif lost := len(game.guesses) >= 6:
|
||||
await interaction.message.reply(
|
||||
f"Game Over! You lose, the word was: **{game.word}**",
|
||||
mention_author=True,
|
||||
)
|
||||
|
||||
if won or lost:
|
||||
self.view.disable_all()
|
||||
self.view.stop()
|
||||
|
||||
return await interaction.response.edit_message(
|
||||
embed=embed, attachments=[file], view=self.view
|
||||
)
|
||||
|
||||
|
||||
class WordInputButton(discord.ui.Button["WordleView"]):
|
||||
def __init__(self, *, cancel_button: bool = False):
|
||||
super().__init__(
|
||||
label="Cancel" if cancel_button else "Make a guess!",
|
||||
style=discord.ButtonStyle.red
|
||||
if cancel_button
|
||||
else discord.ButtonStyle.blurple,
|
||||
)
|
||||
|
||||
async def callback(self, interaction: discord.Interaction) -> None:
|
||||
game = self.view.game
|
||||
if interaction.user != game.player:
|
||||
return await interaction.response.send_message(
|
||||
"This isn't your game!", ephemeral=True
|
||||
)
|
||||
else:
|
||||
if self.label == "Cancel":
|
||||
await interaction.response.send_message(
|
||||
f"Game Over! the word was: **{game.word}**"
|
||||
)
|
||||
await interaction.message.delete()
|
||||
return self.view.stop()
|
||||
else:
|
||||
return await interaction.response.send_modal(WordInput(self.view))
|
||||
|
||||
|
||||
class WordleView(BaseView):
|
||||
def __init__(self, game: BetaWordle, *, timeout: float):
|
||||
super().__init__(timeout=timeout)
|
||||
|
||||
self.game = game
|
||||
self.add_item(WordInputButton())
|
||||
self.add_item(WordInputButton(cancel_button=True))
|
||||
|
||||
|
||||
class BetaWordle(Wordle):
|
||||
player: discord.User
|
||||
"""
|
||||
Wordle(buttons) game
|
||||
"""
|
||||
|
||||
async def start(
|
||||
self,
|
||||
ctx: commands.Context[commands.Bot],
|
||||
*,
|
||||
embed_color: DiscordColor = DEFAULT_COLOR,
|
||||
timeout: Optional[float] = None,
|
||||
) -> discord.Message:
|
||||
"""
|
||||
starts the wordle(buttons) game
|
||||
|
||||
Parameters
|
||||
----------
|
||||
ctx : commands.Context
|
||||
the context of the invokation command
|
||||
embed_color : DiscordColor, optional
|
||||
the color of the game embed, by default DEFAULT_COLOR
|
||||
timeout : Optional[float], optional
|
||||
the timeout for the view, by default None
|
||||
|
||||
Returns
|
||||
-------
|
||||
discord.Message
|
||||
returns the game message
|
||||
"""
|
||||
self.embed_color = discord.Color.random()
|
||||
self.player = ctx.author
|
||||
|
||||
buf = await self.render_image()
|
||||
embed = discord.Embed(title="Wordle!", color=self.embed_color)
|
||||
embed.set_image(url="attachment://wordle.png")
|
||||
|
||||
self.view = WordleView(self, timeout=timeout)
|
||||
self.message = await ctx.send(
|
||||
embed=embed,
|
||||
file=discord.File(buf, "wordle.png"),
|
||||
view=self.view,
|
||||
)
|
||||
await self.view.wait()
|
||||
return self.message
|
||||
Reference in New Issue
Block a user