Update configuration and README for improved setup and security; disable default API and emoji sync, and enhance Discord permission checks in API routes.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
451
bot/games/battleship.py
Normal file
451
bot/games/battleship.py
Normal file
@@ -0,0 +1,451 @@
|
||||
# ╔══════════════════════════════════════════════════════════════════╗
|
||||
# ║ ║
|
||||
# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║
|
||||
# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║
|
||||
# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║
|
||||
# ║ ║
|
||||
# ║ © 2026 CodeX Devs — All Rights Reserved ║
|
||||
# ║ ║
|
||||
# ║ discord ── https://discord.gg/codexdev ║
|
||||
# ║ youtube ── https://youtube.com/@CodeXDevs ║
|
||||
# ║ github ── https://github.com/RayExo ║
|
||||
# ║ ║
|
||||
# ╚══════════════════════════════════════════════════════════════════╝
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Optional, Union, ClassVar
|
||||
from io import BytesIO
|
||||
import asyncio
|
||||
import pathlib
|
||||
import random
|
||||
import re
|
||||
|
||||
import discord
|
||||
from discord.ext import commands
|
||||
from PIL import Image, ImageDraw
|
||||
|
||||
from .utils import *
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing_extensions import TypeAlias
|
||||
|
||||
Coords: TypeAlias = tuple[int, int]
|
||||
|
||||
SHIPS: dict[str, tuple[int, tuple[int, int, int]]] = {
|
||||
"carrier": (5, (52, 152, 219)),
|
||||
"battleship": (4, (246, 246, 112)),
|
||||
"destroyer": (3, (14, 146, 150)),
|
||||
"submarine": (3, (95, 245, 80)),
|
||||
"patrol boat": (2, (190, 190, 190)),
|
||||
}
|
||||
|
||||
|
||||
class Ship:
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
size: int,
|
||||
start: Coords,
|
||||
color: tuple[int, int, int],
|
||||
vertical: bool = False,
|
||||
) -> None:
|
||||
|
||||
self.name: str = name
|
||||
self.size: int = size
|
||||
|
||||
self.start: Coords = start
|
||||
self.vertical: bool = vertical
|
||||
self.color: tuple[int, int, int] = color
|
||||
|
||||
self.end: Coords = (
|
||||
(self.start[0], self.start[1] + self.size - 1)
|
||||
if self.vertical
|
||||
else (self.start[0] + self.size - 1, self.start[1])
|
||||
)
|
||||
|
||||
self.span: list[Coords] = (
|
||||
[(self.start[0], i) for i in range(self.start[1], self.end[1] + 1)]
|
||||
if self.vertical
|
||||
else [(i, self.start[1]) for i in range(self.start[0], self.end[0] + 1)]
|
||||
)
|
||||
|
||||
self.hits: list[bool] = [False] * self.size
|
||||
|
||||
|
||||
class Board:
|
||||
def __init__(self, player: discord.User, random: bool = True) -> None:
|
||||
|
||||
self.player: discord.User = player
|
||||
self.ships: list[Ship] = []
|
||||
|
||||
self.my_hits: list[Coords] = []
|
||||
self.my_misses: list[Coords] = []
|
||||
|
||||
self.op_hits: list[Coords] = []
|
||||
self.op_misses: list[Coords] = []
|
||||
|
||||
if random:
|
||||
self._place_ships()
|
||||
|
||||
@property
|
||||
def moves(self) -> list[Coords]:
|
||||
return self.my_hits + self.my_misses
|
||||
|
||||
def _is_valid(self, ship: Ship) -> bool:
|
||||
|
||||
if ship.end[0] > 10 or ship.end[1] > 10:
|
||||
return False
|
||||
|
||||
for existing in self.ships:
|
||||
if any(c in existing.span for c in ship.span):
|
||||
return False
|
||||
return True
|
||||
|
||||
def _place_ships(self) -> None:
|
||||
def place_ship(ship: str, size: int, color: tuple[int, int, int]) -> None:
|
||||
start = random.randint(1, 10), random.randint(1, 10)
|
||||
vertical = bool(random.randint(0, 1))
|
||||
|
||||
new_ship = Ship(
|
||||
name=ship,
|
||||
size=size,
|
||||
start=start,
|
||||
vertical=vertical,
|
||||
color=color,
|
||||
)
|
||||
|
||||
if self._is_valid(new_ship):
|
||||
self.ships.append(new_ship)
|
||||
else:
|
||||
place_ship(ship, size, color)
|
||||
|
||||
for ship, (size, color) in SHIPS.items():
|
||||
place_ship(ship, size, color)
|
||||
|
||||
def won(self) -> bool:
|
||||
return all(all(ship.hits) for ship in self.ships)
|
||||
|
||||
def draw_dot(
|
||||
self, cur: ImageDraw.Draw, x: int, y: int, fill: Union[int, tuple[int, ...]]
|
||||
) -> None:
|
||||
x1, y1 = x - 10, y - 10
|
||||
x2, y2 = x + 10, y + 10
|
||||
cur.ellipse((x1, y1, x2, y2), fill=fill)
|
||||
|
||||
def draw_sq(
|
||||
self, cur: ImageDraw.Draw, x: int, y: int, *, coord: Coords, ship: Ship
|
||||
) -> None:
|
||||
vertical = ship.vertical
|
||||
left_end = ship.span.index(coord) == 0
|
||||
right_end = ship.span.index(coord) == ship.size - 1
|
||||
|
||||
if vertical and left_end:
|
||||
diffs = (18, 18, 25, 18)
|
||||
elif vertical and right_end:
|
||||
diffs = (25, 18, 18, 18)
|
||||
elif not vertical and left_end:
|
||||
diffs = (18, 18, 18, 25)
|
||||
elif not vertical and right_end:
|
||||
diffs = (18, 25, 18, 18)
|
||||
elif vertical:
|
||||
diffs = (25, 18, 25, 18)
|
||||
else:
|
||||
diffs = (18, 25, 18, 25)
|
||||
|
||||
d1, d2, d3, d4 = diffs
|
||||
x1, y1 = x - d1, y - d2
|
||||
x2, y2 = x + d3, y + d4
|
||||
cur.rounded_rectangle((x1, y1, x2, y2), radius=5, fill=ship.color)
|
||||
|
||||
def get_ship(self, coord: Coords) -> Optional[Ship]:
|
||||
if s := [ship for ship in self.ships if coord in ship.span]:
|
||||
return s[0]
|
||||
|
||||
@executor()
|
||||
def to_image(self, hide: bool = False) -> BytesIO:
|
||||
RED = (255, 0, 0)
|
||||
GRAY = (128, 128, 128)
|
||||
|
||||
with Image.open(pathlib.Path(__file__).parent / "assets/battleship.png") as img:
|
||||
cur = ImageDraw.Draw(img)
|
||||
|
||||
for i, y in zip(range(1, 11), range(75, 530, 50)):
|
||||
for j, x in zip(range(1, 11), range(75, 530, 50)):
|
||||
coord = (i, j)
|
||||
if coord in self.op_misses:
|
||||
self.draw_dot(cur, x, y, fill=GRAY)
|
||||
|
||||
elif coord in self.op_hits:
|
||||
if hide:
|
||||
self.draw_dot(cur, x, y, fill=RED)
|
||||
else:
|
||||
ship = self.get_ship(coord)
|
||||
self.draw_sq(cur, x, y, coord=coord, ship=ship)
|
||||
self.draw_dot(cur, x, y, fill=RED)
|
||||
|
||||
elif ship := self.get_ship(coord):
|
||||
if not hide:
|
||||
self.draw_sq(cur, x, y, coord=coord, ship=ship)
|
||||
buffer = BytesIO()
|
||||
img.save(buffer, "PNG")
|
||||
|
||||
buffer.seek(0)
|
||||
del img
|
||||
return buffer
|
||||
|
||||
|
||||
class BattleShip:
|
||||
"""
|
||||
BattleShip Game
|
||||
"""
|
||||
|
||||
inputpat: ClassVar[re.Pattern] = re.compile(r"([a-j])(10|[1-9])")
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
player1: discord.User,
|
||||
player2: discord.User,
|
||||
*,
|
||||
random: bool = True,
|
||||
) -> None:
|
||||
|
||||
self.embed_color: Optional[DiscordColor] = None
|
||||
|
||||
self.player1: discord.User = player1
|
||||
self.player2: discord.User = player2
|
||||
|
||||
self.random: bool = random
|
||||
|
||||
self.player1_board: Board = Board(player1, random=self.random)
|
||||
self.player2_board: Board = Board(player2, random=self.random)
|
||||
|
||||
self.turn: discord.User = self.player1
|
||||
self.timeout: Optional[int] = None
|
||||
|
||||
self.message1: Optional[discord.Message] = None
|
||||
self.message2: Optional[discord.Message] = None
|
||||
|
||||
def get_board(self, player: discord.User, other: bool = False) -> Board:
|
||||
if other:
|
||||
return self.player2_board if player == self.player1 else self.player1_board
|
||||
else:
|
||||
return self.player1_board if player == self.player1 else self.player2_board
|
||||
|
||||
def place_move(self, player: discord.User, coords: Coords) -> tuple[bool, bool]:
|
||||
board = self.get_board(player)
|
||||
op_board = self.get_board(player, other=True)
|
||||
|
||||
for i, ship in enumerate(op_board.ships):
|
||||
for j, coord in enumerate(ship.span):
|
||||
if coords == coord:
|
||||
op_board.ships[i].hits[j] = True
|
||||
board.my_hits.append(coords)
|
||||
op_board.op_hits.append(coords)
|
||||
return all(op_board.ships[i].hits), True
|
||||
|
||||
board.my_misses.append(coords)
|
||||
op_board.op_misses.append(coords)
|
||||
return False, False
|
||||
|
||||
async def get_file(
|
||||
self, player: discord.User, *, hide: bool = True
|
||||
) -> tuple[discord.Embed, discord.File, discord.Embed, discord.File]:
|
||||
|
||||
board = self.get_board(player)
|
||||
image1 = await board.to_image()
|
||||
|
||||
board2 = self.get_board(player, other=True)
|
||||
image2 = await board2.to_image(hide=hide)
|
||||
|
||||
file1 = discord.File(image1, "board1.png")
|
||||
file2 = discord.File(image2, "board2.png")
|
||||
|
||||
embed1 = discord.Embed(color=self.embed_color)
|
||||
embed2 = discord.Embed(color=self.embed_color)
|
||||
|
||||
embed1.set_image(url="attachment://board1.png")
|
||||
embed2.set_image(url="attachment://board2.png")
|
||||
|
||||
return embed1, file1, embed2, file2
|
||||
|
||||
def to_num(self, alpha: str) -> int:
|
||||
return ord(alpha) % 96
|
||||
|
||||
def get_coords(self, inp: str) -> tuple[str, Coords]:
|
||||
inp = re.sub(r"\s+", "", inp).lower()
|
||||
match = self.inputpat.match(inp)
|
||||
x, y = match.group(1), match.group(2)
|
||||
return (inp, (self.to_num(x), int(y)))
|
||||
|
||||
def who_won(self) -> Optional[discord.User]:
|
||||
if self.player1_board.won():
|
||||
return self.player2
|
||||
elif self.player2_board.won():
|
||||
return self.player1
|
||||
else:
|
||||
return None
|
||||
|
||||
async def get_ship_inputs(
|
||||
self, ctx: commands.Context[commands.Bot], user: discord.User
|
||||
) -> bool:
|
||||
|
||||
board = self.get_board(user)
|
||||
|
||||
async def place_ship(ship: str, size: int, color: tuple[int, int, int]) -> bool:
|
||||
embed, file, _, _ = await self.get_file(user)
|
||||
await user.send(
|
||||
f"Where do you want to place your `{ship}`?\nSend the start coordinate... e.g. (`a1`)",
|
||||
embed=embed,
|
||||
file=file,
|
||||
)
|
||||
|
||||
def check(msg: discord.Message) -> bool:
|
||||
if not msg.guild and msg.author == user:
|
||||
content = re.sub(r"\s+", "", message.content).lower()
|
||||
return bool(self.inputpat.match(content))
|
||||
|
||||
try:
|
||||
message: discord.Message = await ctx.bot.wait_for(
|
||||
"message", check=check, timeout=self.timeout
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
await user.send(
|
||||
f"The timeout of {self.timeout} seconds, has been reached. Aborting..."
|
||||
)
|
||||
return False
|
||||
|
||||
_, start = self.get_coords(message.content)
|
||||
|
||||
await user.send("Do you want it to be vertical?\nSay `yes` or `no`")
|
||||
|
||||
def check(msg: discord.Message) -> bool:
|
||||
if not msg.guild and msg.author == user:
|
||||
content = msg.content.replace(" ", "").lower()
|
||||
return content in ("yes", "no")
|
||||
|
||||
try:
|
||||
message: discord.Message = await ctx.bot.wait_for(
|
||||
"message", check=check, timeout=self.timeout
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
await user.send(
|
||||
f"The timeout of {self.timeout} seconds, has been reached. Aborting..."
|
||||
)
|
||||
return False
|
||||
|
||||
vertical = message.content.replace(" ", "").lower() != "yes"
|
||||
|
||||
new_ship = Ship(
|
||||
name=ship,
|
||||
size=size,
|
||||
start=start,
|
||||
vertical=vertical,
|
||||
color=color,
|
||||
)
|
||||
|
||||
if board._is_valid(new_ship):
|
||||
board.ships.append(new_ship)
|
||||
else:
|
||||
await user.send("That is a not a valid location, please try again")
|
||||
await place_ship(ship, size, color)
|
||||
|
||||
for ship, (size, color) in SHIPS.items():
|
||||
await place_ship(ship, size, color)
|
||||
|
||||
await user.send("All setup! (Game will soon start after the opponent finishes)")
|
||||
return True
|
||||
|
||||
async def start(
|
||||
self, ctx: commands.Context[commands.Bot], *, timeout: Optional[float] = None
|
||||
) -> tuple[discord.Message, discord.Message]:
|
||||
"""
|
||||
starts the battleship game
|
||||
|
||||
Parameters
|
||||
----------
|
||||
ctx : commands.Context
|
||||
the context of the invokation command
|
||||
timeout : Optional[float], optional
|
||||
the timeout for when waiting, by default None
|
||||
|
||||
Returns
|
||||
-------
|
||||
tuple[discord.Message, discord.Message]
|
||||
returns both player's messages respectively
|
||||
"""
|
||||
|
||||
await ctx.send("**Game Started!**\nI've setup the boards in your dms!")
|
||||
|
||||
if not self.random:
|
||||
await asyncio.gather(
|
||||
self.get_ship_inputs(ctx, self.player1),
|
||||
self.get_ship_inputs(ctx, self.player2),
|
||||
)
|
||||
|
||||
_, f1, _, f2 = await self.get_file(self.player1)
|
||||
_, f3, _, f4 = await self.get_file(self.player2)
|
||||
|
||||
self.message1 = await self.player1.send("**Game starting!**", files=[f2, f1])
|
||||
self.message2 = await self.player2.send("**Game starting!**", files=[f4, f3])
|
||||
self.timeout = timeout
|
||||
|
||||
while not ctx.bot.is_closed():
|
||||
|
||||
def check(msg: discord.Message) -> bool:
|
||||
if not msg.guild and msg.author == self.turn:
|
||||
content = msg.content.replace(" ", "").lower()
|
||||
return bool(self.inputpat.match(content))
|
||||
|
||||
try:
|
||||
message: discord.Message = await ctx.bot.wait_for(
|
||||
"message", check=check, timeout=self.timeout
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
await ctx.send(
|
||||
f"The timeout of {timeout} seconds, has been reached. Aborting..."
|
||||
)
|
||||
break
|
||||
|
||||
raw, coords = self.get_coords(message.content)
|
||||
|
||||
if coords in self.get_board(self.turn):
|
||||
await self.turn.send("You've attacked this coordinate before!")
|
||||
|
||||
else:
|
||||
sunk, hit = self.place_move(self.turn, coords)
|
||||
next_turn: discord.User = (
|
||||
self.player2 if self.turn == self.player1 else self.player1
|
||||
)
|
||||
|
||||
if hit and sunk:
|
||||
await self.turn.send(
|
||||
f"`{raw}` was a hit!, you also sank one of their ships! :)"
|
||||
)
|
||||
await next_turn.send(
|
||||
f"They went for `{raw}`, and it was a hit!\nOne of your ships also got sunk! :("
|
||||
)
|
||||
elif hit:
|
||||
await self.turn.send(f"`{raw}` was a hit :)")
|
||||
await next_turn.send(f"They went for `{raw}`, and it was a hit! :(")
|
||||
else:
|
||||
await self.turn.send(f"`{raw}` was a miss :(")
|
||||
await next_turn.send(
|
||||
f"They went for `{raw}`, and it was a miss! :)"
|
||||
)
|
||||
|
||||
_, f1, _, f2 = await self.get_file(self.player1)
|
||||
_, f3, _, f4 = await self.get_file(self.player2)
|
||||
|
||||
await self.player1.send(files=[f2, f1])
|
||||
await self.player2.send(files=[f4, f3])
|
||||
self.turn = next_turn
|
||||
|
||||
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 :(")
|
||||
break
|
||||
|
||||
return self.message1, self.message2
|
||||
Reference in New Issue
Block a user