aboutsummaryrefslogtreecommitdiffstats
path: root/bot/exts/evergreen/battleship.py
diff options
context:
space:
mode:
Diffstat (limited to 'bot/exts/evergreen/battleship.py')
-rw-r--r--bot/exts/evergreen/battleship.py448
1 files changed, 0 insertions, 448 deletions
diff --git a/bot/exts/evergreen/battleship.py b/bot/exts/evergreen/battleship.py
deleted file mode 100644
index f4351954..00000000
--- a/bot/exts/evergreen/battleship.py
+++ /dev/null
@@ -1,448 +0,0 @@
-import asyncio
-import logging
-import random
-import re
-from dataclasses import dataclass
-from functools import partial
-from typing import Optional
-
-import discord
-from discord.ext import commands
-
-from bot.bot import Bot
-from bot.constants import Colours
-
-log = logging.getLogger(__name__)
-
-
-@dataclass
-class Square:
- """Each square on the battleship grid - if they contain a boat and if they've been aimed at."""
-
- boat: Optional[str]
- aimed: bool
-
-
-Grid = list[list[Square]]
-EmojiSet = dict[tuple[bool, bool], str]
-
-
-@dataclass
-class Player:
- """Each player in the game - their messages for the boards and their current grid."""
-
- user: Optional[discord.Member]
- board: Optional[discord.Message]
- opponent_board: discord.Message
- grid: Grid
-
-
-# The name of the ship and its size
-SHIPS = {
- "Carrier": 5,
- "Battleship": 4,
- "Cruiser": 3,
- "Submarine": 3,
- "Destroyer": 2,
-}
-
-
-# For these two variables, the first boolean is whether the square is a ship (True) or not (False).
-# The second boolean is whether the player has aimed for that square (True) or not (False)
-
-# This is for the player's own board which shows the location of their own ships.
-SHIP_EMOJIS = {
- (True, True): ":fire:",
- (True, False): ":ship:",
- (False, True): ":anger:",
- (False, False): ":ocean:",
-}
-
-# This is for the opposing player's board which only shows aimed locations.
-HIDDEN_EMOJIS = {
- (True, True): ":red_circle:",
- (True, False): ":black_circle:",
- (False, True): ":white_circle:",
- (False, False): ":black_circle:",
-}
-
-# For the top row of the board
-LETTERS = (
- ":stop_button::regional_indicator_a::regional_indicator_b::regional_indicator_c::regional_indicator_d:"
- ":regional_indicator_e::regional_indicator_f::regional_indicator_g::regional_indicator_h:"
- ":regional_indicator_i::regional_indicator_j:"
-)
-
-# For the first column of the board
-NUMBERS = [
- ":one:",
- ":two:",
- ":three:",
- ":four:",
- ":five:",
- ":six:",
- ":seven:",
- ":eight:",
- ":nine:",
- ":keycap_ten:",
-]
-
-CROSS_EMOJI = "\u274e"
-HAND_RAISED_EMOJI = "\U0001f64b"
-
-
-class Game:
- """A Battleship Game."""
-
- def __init__(
- self,
- bot: Bot,
- channel: discord.TextChannel,
- player1: discord.Member,
- player2: discord.Member
- ):
-
- self.bot = bot
- self.public_channel = channel
-
- self.p1 = Player(player1, None, None, self.generate_grid())
- self.p2 = Player(player2, None, None, self.generate_grid())
-
- self.gameover: bool = False
-
- self.turn: Optional[discord.Member] = None
- self.next: Optional[discord.Member] = None
-
- self.match: Optional[re.Match] = None
- self.surrender: bool = False
-
- self.setup_grids()
-
- @staticmethod
- def generate_grid() -> Grid:
- """Generates a grid by instantiating the Squares."""
- return [[Square(None, False) for _ in range(10)] for _ in range(10)]
-
- @staticmethod
- def format_grid(player: Player, emojiset: EmojiSet) -> str:
- """
- Gets and formats the grid as a list into a string to be output to the DM.
-
- Also adds the Letter and Number indexes.
- """
- grid = [
- [emojiset[bool(square.boat), square.aimed] for square in row]
- for row in player.grid
- ]
-
- rows = ["".join([number] + row) for number, row in zip(NUMBERS, grid)]
- return "\n".join([LETTERS] + rows)
-
- @staticmethod
- def get_square(grid: Grid, square: str) -> Square:
- """Grabs a square from a grid with an inputted key."""
- index = ord(square[0].upper()) - ord("A")
- number = int(square[1:])
-
- return grid[number-1][index] # -1 since lists are indexed from 0
-
- async def game_over(
- self,
- *,
- winner: discord.Member,
- loser: discord.Member
- ) -> None:
- """Removes games from list of current games and announces to public chat."""
- await self.public_channel.send(f"Game Over! {winner.mention} won against {loser.mention}")
-
- for player in (self.p1, self.p2):
- grid = self.format_grid(player, SHIP_EMOJIS)
- await self.public_channel.send(f"{player.user}'s Board:\n{grid}")
-
- @staticmethod
- def check_sink(grid: Grid, boat: str) -> bool:
- """Checks if all squares containing a given boat have sunk."""
- return all(square.aimed for row in grid for square in row if square.boat == boat)
-
- @staticmethod
- def check_gameover(grid: Grid) -> bool:
- """Checks if all boats have been sunk."""
- return all(square.aimed for row in grid for square in row if square.boat)
-
- def setup_grids(self) -> None:
- """Places the boats on the grids to initialise the game."""
- for player in (self.p1, self.p2):
- for name, size in SHIPS.items():
- while True: # Repeats if about to overwrite another boat
- ship_collision = False
- coords = []
-
- coord1 = random.randint(0, 9)
- coord2 = random.randint(0, 10 - size)
-
- if random.choice((True, False)): # Vertical or Horizontal
- x, y = coord1, coord2
- xincr, yincr = 0, 1
- else:
- x, y = coord2, coord1
- xincr, yincr = 1, 0
-
- for i in range(size):
- new_x = x + (xincr * i)
- new_y = y + (yincr * i)
- if player.grid[new_x][new_y].boat: # Check if there's already a boat
- ship_collision = True
- break
- coords.append((new_x, new_y))
- if not ship_collision: # If not overwriting any other boat spaces, break loop
- break
-
- for x, y in coords:
- player.grid[x][y].boat = name
-
- async def print_grids(self) -> None:
- """Prints grids to the DM channels."""
- # Convert squares into Emoji
-
- boards = [
- self.format_grid(player, emojiset)
- for emojiset in (HIDDEN_EMOJIS, SHIP_EMOJIS)
- for player in (self.p1, self.p2)
- ]
-
- locations = (
- (self.p2, "opponent_board"), (self.p1, "opponent_board"),
- (self.p1, "board"), (self.p2, "board")
- )
-
- for board, location in zip(boards, locations):
- player, attr = location
- if getattr(player, attr):
- await getattr(player, attr).edit(content=board)
- else:
- setattr(player, attr, await player.user.send(board))
-
- def predicate(self, message: discord.Message) -> bool:
- """Predicate checking the message typed for each turn."""
- if message.author == self.turn.user and message.channel == self.turn.user.dm_channel:
- if message.content.lower() == "surrender":
- self.surrender = True
- return True
- self.match = re.fullmatch("([A-J]|[a-j]) ?((10)|[1-9])", message.content.strip())
- if not self.match:
- self.bot.loop.create_task(message.add_reaction(CROSS_EMOJI))
- return bool(self.match)
-
- async def take_turn(self) -> Optional[Square]:
- """Lets the player who's turn it is choose a square."""
- square = None
- turn_message = await self.turn.user.send(
- "It's your turn! Type the square you want to fire at. Format it like this: A1\n"
- "Type `surrender` to give up."
- )
- await self.next.user.send("Their turn", delete_after=3.0)
- while True:
- try:
- await self.bot.wait_for("message", check=self.predicate, timeout=60.0)
- except asyncio.TimeoutError:
- await self.turn.user.send("You took too long. Game over!")
- await self.next.user.send(f"{self.turn.user} took too long. Game over!")
- await self.public_channel.send(
- f"Game over! {self.turn.user.mention} timed out so {self.next.user.mention} wins!"
- )
- self.gameover = True
- break
- else:
- if self.surrender:
- await self.next.user.send(f"{self.turn.user} surrendered. Game over!")
- await self.public_channel.send(
- f"Game over! {self.turn.user.mention} surrendered to {self.next.user.mention}!"
- )
- self.gameover = True
- break
- square = self.get_square(self.next.grid, self.match.string)
- if square.aimed:
- await self.turn.user.send("You've already aimed at this square!", delete_after=3.0)
- else:
- break
- await turn_message.delete()
- return square
-
- async def hit(self, square: Square, alert_messages: list[discord.Message]) -> None:
- """Occurs when a player successfully aims for a ship."""
- await self.turn.user.send("Hit!", delete_after=3.0)
- alert_messages.append(await self.next.user.send("Hit!"))
- if self.check_sink(self.next.grid, square.boat):
- await self.turn.user.send(f"You've sunk their {square.boat} ship!", delete_after=3.0)
- alert_messages.append(await self.next.user.send(f"Oh no! Your {square.boat} ship sunk!"))
- if self.check_gameover(self.next.grid):
- await self.turn.user.send("You win!")
- await self.next.user.send("You lose!")
- self.gameover = True
- await self.game_over(winner=self.turn.user, loser=self.next.user)
-
- async def start_game(self) -> None:
- """Begins the game."""
- await self.p1.user.send(f"You're playing battleship with {self.p2.user}.")
- await self.p2.user.send(f"You're playing battleship with {self.p1.user}.")
-
- alert_messages = []
-
- self.turn = self.p1
- self.next = self.p2
-
- while True:
- await self.print_grids()
-
- if self.gameover:
- return
-
- square = await self.take_turn()
- if not square:
- return
- square.aimed = True
-
- for message in alert_messages:
- await message.delete()
-
- alert_messages = []
- alert_messages.append(await self.next.user.send(f"{self.turn.user} aimed at {self.match.string}!"))
-
- if square.boat:
- await self.hit(square, alert_messages)
- if self.gameover:
- return
- else:
- await self.turn.user.send("Miss!", delete_after=3.0)
- alert_messages.append(await self.next.user.send("Miss!"))
-
- self.turn, self.next = self.next, self.turn
-
-
-class Battleship(commands.Cog):
- """Play the classic game Battleship!"""
-
- def __init__(self, bot: Bot):
- self.bot = bot
- self.games: list[Game] = []
- self.waiting: list[discord.Member] = []
-
- def predicate(
- self,
- ctx: commands.Context,
- announcement: discord.Message,
- reaction: discord.Reaction,
- user: discord.Member
- ) -> bool:
- """Predicate checking the criteria for the announcement message."""
- if self.already_playing(ctx.author): # If they've joined a game since requesting a player 2
- return True # Is dealt with later on
- if (
- user.id not in (ctx.me.id, ctx.author.id)
- and str(reaction.emoji) == HAND_RAISED_EMOJI
- and reaction.message.id == announcement.id
- ):
- if self.already_playing(user):
- self.bot.loop.create_task(ctx.send(f"{user.mention} You're already playing a game!"))
- self.bot.loop.create_task(announcement.remove_reaction(reaction, user))
- return False
-
- if user in self.waiting:
- self.bot.loop.create_task(ctx.send(
- f"{user.mention} Please cancel your game first before joining another one."
- ))
- self.bot.loop.create_task(announcement.remove_reaction(reaction, user))
- return False
-
- return True
-
- if (
- user.id == ctx.author.id
- and str(reaction.emoji) == CROSS_EMOJI
- and reaction.message.id == announcement.id
- ):
- return True
- return False
-
- def already_playing(self, player: discord.Member) -> bool:
- """Check if someone is already in a game."""
- return any(player in (game.p1.user, game.p2.user) for game in self.games)
-
- @commands.group(invoke_without_command=True)
- @commands.guild_only()
- async def battleship(self, ctx: commands.Context) -> None:
- """
- Play a game of Battleship with someone else!
-
- This will set up a message waiting for someone else to react and play along.
- The game takes place entirely in DMs.
- Make sure you have your DMs open so that the bot can message you.
- """
- if self.already_playing(ctx.author):
- await ctx.send("You're already playing a game!")
- return
-
- if ctx.author in self.waiting:
- await ctx.send("You've already sent out a request for a player 2.")
- return
-
- announcement = await ctx.send(
- "**Battleship**: A new game is about to start!\n"
- f"Press {HAND_RAISED_EMOJI} to play against {ctx.author.mention}!\n"
- f"(Cancel the game with {CROSS_EMOJI}.)"
- )
- self.waiting.append(ctx.author)
- await announcement.add_reaction(HAND_RAISED_EMOJI)
- await announcement.add_reaction(CROSS_EMOJI)
-
- try:
- reaction, user = await self.bot.wait_for(
- "reaction_add",
- check=partial(self.predicate, ctx, announcement),
- timeout=60.0
- )
- except asyncio.TimeoutError:
- self.waiting.remove(ctx.author)
- await announcement.delete()
- await ctx.send(f"{ctx.author.mention} Seems like there's no one here to play...")
- return
-
- if str(reaction.emoji) == CROSS_EMOJI:
- self.waiting.remove(ctx.author)
- await announcement.delete()
- await ctx.send(f"{ctx.author.mention} Game cancelled.")
- return
-
- await announcement.delete()
- self.waiting.remove(ctx.author)
- if self.already_playing(ctx.author):
- return
- game = Game(self.bot, ctx.channel, ctx.author, user)
- self.games.append(game)
- try:
- await game.start_game()
- self.games.remove(game)
- except discord.Forbidden:
- await ctx.send(
- f"{ctx.author.mention} {user.mention} "
- "Game failed. This is likely due to you not having your DMs open. Check and try again."
- )
- self.games.remove(game)
- except Exception:
- # End the game in the event of an unforseen error so the players aren't stuck in a game
- await ctx.send(f"{ctx.author.mention} {user.mention} An error occurred. Game failed.")
- self.games.remove(game)
- raise
-
- @battleship.command(name="ships", aliases=("boats",))
- async def battleship_ships(self, ctx: commands.Context) -> None:
- """Lists the ships that are found on the battleship grid."""
- embed = discord.Embed(colour=Colours.blue)
- embed.add_field(name="Name", value="\n".join(SHIPS))
- embed.add_field(name="Size", value="\n".join(str(size) for size in SHIPS.values()))
- await ctx.send(embed=embed)
-
-
-def setup(bot: Bot) -> None:
- """Load the Battleship Cog."""
- bot.add_cog(Battleship(bot))