Files
TheOnlyMace b4110c3d66
Some checks failed
CI / Bot (Python) (push) Failing after 49s
CI / Dashboard (Next.js) (push) Failing after 11s
Update project configuration files, and API routes accordingly. Add SQLite runtime file ignores to .gitignore for better file management.
2026-07-21 17:11:38 +02:00

517 lines
17 KiB
Python

# ╔══════════════════════════════════════════════════════════════════╗
# ║ ║
# ║ +-+-+-+-+-+-+-+-+ ║
# ║ |H|e|x|a|H|o|s|t| ║
# ║ +-+-+-+-+-+-+-+-+ ║
# ║ ║
# ║ © 2026 HexaHost — All Rights Reserved ║
# ║ ║
# ║ discord ── https://discord.gg/hexahost ║
# ║ github ── https://github.com/theoneandonlymace ║
# ║ ║
# ╚══════════════════════════════════════════════════════════════════╝
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