aboutsummaryrefslogtreecommitdiffstats
path: root/bot/exts/evergreen/connect_four.py
diff options
context:
space:
mode:
Diffstat (limited to 'bot/exts/evergreen/connect_four.py')
-rw-r--r--bot/exts/evergreen/connect_four.py452
1 files changed, 0 insertions, 452 deletions
diff --git a/bot/exts/evergreen/connect_four.py b/bot/exts/evergreen/connect_four.py
deleted file mode 100644
index 647bb2b7..00000000
--- a/bot/exts/evergreen/connect_four.py
+++ /dev/null
@@ -1,452 +0,0 @@
-import asyncio
-import random
-from functools import partial
-from typing import Optional, Union
-
-import discord
-import emojis
-from discord.ext import commands
-from discord.ext.commands import guild_only
-
-from bot.bot import Bot
-from bot.constants import Emojis
-
-NUMBERS = list(Emojis.number_emojis.values())
-CROSS_EMOJI = Emojis.incident_unactioned
-
-Coordinate = Optional[tuple[int, int]]
-EMOJI_CHECK = Union[discord.Emoji, str]
-
-
-class Game:
- """A Connect 4 Game."""
-
- def __init__(
- self,
- bot: Bot,
- channel: discord.TextChannel,
- player1: discord.Member,
- player2: Optional[discord.Member],
- tokens: list[str],
- size: int = 7
- ):
- self.bot = bot
- self.channel = channel
- self.player1 = player1
- self.player2 = player2 or AI(self.bot, game=self)
- self.tokens = tokens
-
- self.grid = self.generate_board(size)
- self.grid_size = size
-
- self.unicode_numbers = NUMBERS[:self.grid_size]
-
- self.message = None
-
- self.player_active = None
- self.player_inactive = None
-
- @staticmethod
- def generate_board(size: int) -> list[list[int]]:
- """Generate the connect 4 board."""
- return [[0 for _ in range(size)] for _ in range(size)]
-
- async def print_grid(self) -> None:
- """Formats and outputs the Connect Four grid to the channel."""
- title = (
- f"Connect 4: {self.player1.display_name}"
- f" VS {self.bot.user.display_name if isinstance(self.player2, AI) else self.player2.display_name}"
- )
-
- rows = [" ".join(self.tokens[s] for s in row) for row in self.grid]
- first_row = " ".join(x for x in NUMBERS[:self.grid_size])
- formatted_grid = "\n".join([first_row] + rows)
- embed = discord.Embed(title=title, description=formatted_grid)
-
- if self.message:
- await self.message.edit(embed=embed)
- else:
- self.message = await self.channel.send(content="Loading...")
- for emoji in self.unicode_numbers:
- await self.message.add_reaction(emoji)
- await self.message.add_reaction(CROSS_EMOJI)
- await self.message.edit(content=None, embed=embed)
-
- async def game_over(self, action: str, player1: discord.user, player2: discord.user) -> None:
- """Announces to public chat."""
- if action == "win":
- await self.channel.send(f"Game Over! {player1.mention} won against {player2.mention}")
- elif action == "draw":
- await self.channel.send(f"Game Over! {player1.mention} {player2.mention} It's A Draw :tada:")
- elif action == "quit":
- await self.channel.send(f"{self.player1.mention} surrendered. Game over!")
- await self.print_grid()
-
- async def start_game(self) -> None:
- """Begins the game."""
- self.player_active, self.player_inactive = self.player1, self.player2
-
- while True:
- await self.print_grid()
-
- if isinstance(self.player_active, AI):
- coords = self.player_active.play()
- if not coords:
- await self.game_over(
- "draw",
- self.bot.user if isinstance(self.player_active, AI) else self.player_active,
- self.bot.user if isinstance(self.player_inactive, AI) else self.player_inactive,
- )
- else:
- coords = await self.player_turn()
-
- if not coords:
- return
-
- if self.check_win(coords, 1 if self.player_active == self.player1 else 2):
- await self.game_over(
- "win",
- self.bot.user if isinstance(self.player_active, AI) else self.player_active,
- self.bot.user if isinstance(self.player_inactive, AI) else self.player_inactive,
- )
- return
-
- self.player_active, self.player_inactive = self.player_inactive, self.player_active
-
- def predicate(self, reaction: discord.Reaction, user: discord.Member) -> bool:
- """The predicate to check for the player's reaction."""
- return (
- reaction.message.id == self.message.id
- and user.id == self.player_active.id
- and str(reaction.emoji) in (*self.unicode_numbers, CROSS_EMOJI)
- )
-
- async def player_turn(self) -> Coordinate:
- """Initiate the player's turn."""
- message = await self.channel.send(
- f"{self.player_active.mention}, it's your turn! React with the column you want to place your token in."
- )
- player_num = 1 if self.player_active == self.player1 else 2
- while True:
- try:
- reaction, user = await self.bot.wait_for("reaction_add", check=self.predicate, timeout=30.0)
- except asyncio.TimeoutError:
- await self.channel.send(f"{self.player_active.mention}, you took too long. Game over!")
- return
- else:
- await message.delete()
- if str(reaction.emoji) == CROSS_EMOJI:
- await self.game_over("quit", self.player_active, self.player_inactive)
- return
-
- await self.message.remove_reaction(reaction, user)
-
- column_num = self.unicode_numbers.index(str(reaction.emoji))
- column = [row[column_num] for row in self.grid]
-
- for row_num, square in reversed(list(enumerate(column))):
- if not square:
- self.grid[row_num][column_num] = player_num
- return row_num, column_num
- message = await self.channel.send(f"Column {column_num + 1} is full. Try again")
-
- def check_win(self, coords: Coordinate, player_num: int) -> bool:
- """Check that placing a counter here would cause the player to win."""
- vertical = [(-1, 0), (1, 0)]
- horizontal = [(0, 1), (0, -1)]
- forward_diag = [(-1, 1), (1, -1)]
- backward_diag = [(-1, -1), (1, 1)]
- axes = [vertical, horizontal, forward_diag, backward_diag]
-
- for axis in axes:
- counters_in_a_row = 1 # The initial counter that is compared to
- for (row_incr, column_incr) in axis:
- row, column = coords
- row += row_incr
- column += column_incr
-
- while 0 <= row < self.grid_size and 0 <= column < self.grid_size:
- if self.grid[row][column] == player_num:
- counters_in_a_row += 1
- row += row_incr
- column += column_incr
- else:
- break
- if counters_in_a_row >= 4:
- return True
- return False
-
-
-class AI:
- """The Computer Player for Single-Player games."""
-
- def __init__(self, bot: Bot, game: Game):
- self.game = game
- self.mention = bot.user.mention
-
- def get_possible_places(self) -> list[Coordinate]:
- """Gets all the coordinates where the AI could possibly place a counter."""
- possible_coords = []
- for column_num in range(self.game.grid_size):
- column = [row[column_num] for row in self.game.grid]
- for row_num, square in reversed(list(enumerate(column))):
- if not square:
- possible_coords.append((row_num, column_num))
- break
- return possible_coords
-
- def check_ai_win(self, coord_list: list[Coordinate]) -> Optional[Coordinate]:
- """
- Check AI win.
-
- Check if placing a counter in any possible coordinate would cause the AI to win
- with 10% chance of not winning and returning None
- """
- if random.randint(1, 10) == 1:
- return
- for coords in coord_list:
- if self.game.check_win(coords, 2):
- return coords
-
- def check_player_win(self, coord_list: list[Coordinate]) -> Optional[Coordinate]:
- """
- Check Player win.
-
- Check if placing a counter in possible coordinates would stop the player
- from winning with 25% of not blocking them and returning None.
- """
- if random.randint(1, 4) == 1:
- return
- for coords in coord_list:
- if self.game.check_win(coords, 1):
- return coords
-
- @staticmethod
- def random_coords(coord_list: list[Coordinate]) -> Coordinate:
- """Picks a random coordinate from the possible ones."""
- return random.choice(coord_list)
-
- def play(self) -> Union[Coordinate, bool]:
- """
- Plays for the AI.
-
- Gets all possible coords, and determins the move:
- 1. coords where it can win.
- 2. coords where the player can win.
- 3. Random coord
- The first possible value is choosen.
- """
- possible_coords = self.get_possible_places()
-
- if not possible_coords:
- return False
-
- coords = (
- self.check_ai_win(possible_coords)
- or self.check_player_win(possible_coords)
- or self.random_coords(possible_coords)
- )
-
- row, column = coords
- self.game.grid[row][column] = 2
- return coords
-
-
-class ConnectFour(commands.Cog):
- """Connect Four. The Classic Vertical Four-in-a-row Game!"""
-
- def __init__(self, bot: Bot):
- self.bot = bot
- self.games: list[Game] = []
- self.waiting: list[discord.Member] = []
-
- self.tokens = [":white_circle:", ":blue_circle:", ":red_circle:"]
-
- self.max_board_size = 9
- self.min_board_size = 5
-
- async def check_author(self, ctx: commands.Context, board_size: int) -> bool:
- """Check if the requester is free and the board size is correct."""
- if self.already_playing(ctx.author):
- await ctx.send("You're already playing a game!")
- return False
-
- if ctx.author in self.waiting:
- await ctx.send("You've already sent out a request for a player 2")
- return False
-
- if not self.min_board_size <= board_size <= self.max_board_size:
- await ctx.send(
- f"{board_size} is not a valid board size. A valid board size is "
- f"between `{self.min_board_size}` and `{self.max_board_size}`."
- )
- return False
-
- return True
-
- def get_player(
- 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) == Emojis.hand_raised
- 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.player1, game.player2) for game in self.games)
-
- @staticmethod
- def check_emojis(
- e1: EMOJI_CHECK, e2: EMOJI_CHECK
- ) -> tuple[bool, Optional[str]]:
- """Validate the emojis, the user put."""
- if isinstance(e1, str) and emojis.count(e1) != 1:
- return False, e1
- if isinstance(e2, str) and emojis.count(e2) != 1:
- return False, e2
- return True, None
-
- async def _play_game(
- self,
- ctx: commands.Context,
- user: Optional[discord.Member],
- board_size: int,
- emoji1: str,
- emoji2: str
- ) -> None:
- """Helper for playing a game of connect four."""
- self.tokens = [":white_circle:", str(emoji1), str(emoji2)]
- game = None # if game fails to intialize in try...except
-
- try:
- game = Game(self.bot, ctx.channel, ctx.author, user, self.tokens, size=board_size)
- self.games.append(game)
- await game.start_game()
- self.games.remove(game)
- except Exception:
- # End the game in the event of an unforeseen error so the players aren't stuck in a game
- await ctx.send(f"{ctx.author.mention} {user.mention if user else ''} An error occurred. Game failed.")
- if game in self.games:
- self.games.remove(game)
- raise
-
- @guild_only()
- @commands.group(
- invoke_without_command=True,
- aliases=("4inarow", "connect4", "connectfour", "c4"),
- case_insensitive=True
- )
- async def connect_four(
- self,
- ctx: commands.Context,
- board_size: int = 7,
- emoji1: EMOJI_CHECK = "\U0001f535",
- emoji2: EMOJI_CHECK = "\U0001f534"
- ) -> None:
- """
- Play the classic game of Connect Four with someone!
-
- Sets up a message waiting for someone else to react and play along.
- The game will start once someone has reacted.
- All inputs will be through reactions.
- """
- check, emoji = self.check_emojis(emoji1, emoji2)
- if not check:
- raise commands.EmojiNotFound(emoji)
-
- check_author_result = await self.check_author(ctx, board_size)
- if not check_author_result:
- return
-
- announcement = await ctx.send(
- "**Connect Four**: A new game is about to start!\n"
- f"Press {Emojis.hand_raised} to play against {ctx.author.mention}!\n"
- f"(Cancel the game with {CROSS_EMOJI}.)"
- )
- self.waiting.append(ctx.author)
- await announcement.add_reaction(Emojis.hand_raised)
- await announcement.add_reaction(CROSS_EMOJI)
-
- try:
- reaction, user = await self.bot.wait_for(
- "reaction_add",
- check=partial(self.get_player, 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. "
- f"Use `{ctx.prefix}{ctx.invoked_with} ai` to play against a computer."
- )
- 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
-
- await self._play_game(ctx, user, board_size, str(emoji1), str(emoji2))
-
- @guild_only()
- @connect_four.command(aliases=("bot", "computer", "cpu"))
- async def ai(
- self,
- ctx: commands.Context,
- board_size: int = 7,
- emoji1: EMOJI_CHECK = "\U0001f535",
- emoji2: EMOJI_CHECK = "\U0001f534"
- ) -> None:
- """Play Connect Four against a computer player."""
- check, emoji = self.check_emojis(emoji1, emoji2)
- if not check:
- raise commands.EmojiNotFound(emoji)
-
- check_author_result = await self.check_author(ctx, board_size)
- if not check_author_result:
- return
-
- await self._play_game(ctx, None, board_size, str(emoji1), str(emoji2))
-
-
-def setup(bot: Bot) -> None:
- """Load ConnectFour Cog."""
- bot.add_cog(ConnectFour(bot))