From 65beb2bd530a936dc58d35cffcaa1223a9cf6cc6 Mon Sep 17 00:00:00 2001 From: teesh3rt Date: Wed, 19 Mar 2025 11:00:31 +0200 Subject: [PATCH] feat: add connect four --- src/exts/games/connectfour.py | 113 ++++++++++++++++++++++++++++++++++ src/games/connect4.py | 96 +++++++++++++++++++++++++++++ 2 files changed, 209 insertions(+) create mode 100644 src/exts/games/connectfour.py create mode 100644 src/games/connect4.py diff --git a/src/exts/games/connectfour.py b/src/exts/games/connectfour.py new file mode 100644 index 0000000..6c0ee1f --- /dev/null +++ b/src/exts/games/connectfour.py @@ -0,0 +1,113 @@ +import games.connect4 as c4 +import games.discord as dgames +from discord.ext import commands +import discord +from discord import app_commands + + +class View(discord.ui.View): + def __init__(self, lobby: dgames.Lobby): + super().__init__() + self.lobby = lobby + self.game = c4.Connect4(player_count=len(lobby.joined)) + self.players = list(lobby.joined) + self.current_column = 0 + + def get_player_for_turn(self) -> int: + return self.players[self.game.current_turn_idx] + + def show_embed(self): + if self.game.checkpoint.win_state != c4.WinState.NoOne: + desc = "" + field = [[a.value for a in row] for row in self.game.playfield] + for row in field: + desc += "".join(row) + "\n" + + embed = discord.Embed( + title=self.game.checkpoint.win_state.value, + description=desc, + color=discord.Color.red(), + ) + return embed + + desc = "" + field = [[a.value for a in row] for row in self.game.playfield] + marker_row = [ + "⬇️" if i == self.current_column else "⚫" for i in range(self.game.width) + ] + field.insert(0, marker_row) + + for row in field: + desc += "".join(row) + "\n" + + title = f"It's {self.game.players[self.game.current_turn_idx].value}'s turn" + embed = discord.Embed(title=title, description=desc, color=discord.Color.red()) + return embed + + async def interaction_check(self, interaction: discord.Interaction): + if interaction.user.id not in self.players: + await interaction.response.send_message( + content="You are not in this game!", ephemeral=True + ) + return False + + if interaction.user.id != self.get_player_for_turn(): + await interaction.response.send_message( + content="Wait your turn!", ephemeral=True + ) + return False + + return True + + @discord.ui.button(emoji="⬅️") + async def go_left( + self, interaction: discord.Interaction, button: discord.ui.Button + ): + self.current_column = (self.current_column - 1) % self.game.width + await interaction.response.edit_message(embed=self.show_embed(), view=self) + + @discord.ui.button(emoji="➡️") + async def go_right( + self, interaction: discord.Interaction, button: discord.ui.Button + ): + self.current_column = (self.current_column + 1) % self.game.width + await interaction.response.edit_message(embed=self.show_embed(), view=self) + + @discord.ui.button(emoji="✅") + async def confirm( + self, interaction: discord.Interaction, button: discord.ui.Button + ): + try: + checkpoint = self.game.play_move(self.current_column) + if checkpoint.win_state != c4.WinState.NoOne: + await interaction.response.edit_message( + embed=self.show_embed(), view=None + ) + return + await interaction.response.edit_message(embed=self.show_embed(), view=self) + except ValueError as e: + await interaction.response.send_message(content=str(e), ephemeral=True) + + +class Lobby(dgames.LobbyView): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.to_play = "Connect 4" + + async def on_start(self, interaction: discord.Interaction, lobby: dgames.Lobby): + view = View(lobby) + await interaction.channel.send(embed=view.show_embed(), view=view) + + +class Connect4(commands.Cog): + def __init__(self, bot: commands.Bot): + self.bot = bot + + @app_commands.command(name="connectfour", description="Play a game of Connect 4!") + async def connect4(self, interaction: discord.Interaction): + view = Lobby(min_players=2, max_players=4) + await interaction.response.send_message(embed=view.make_embed(), view=view) + + +async def setup(bot: commands.Bot): + await bot.add_cog(Connect4(bot)) diff --git a/src/games/connect4.py b/src/games/connect4.py new file mode 100644 index 0000000..f6dbe35 --- /dev/null +++ b/src/games/connect4.py @@ -0,0 +1,96 @@ +from enum import Enum +from pydantic import BaseModel + + +class Players(Enum): + Red = "🔴" + Yellow = "🟡" + Green = "🟢" + Blue = "🔵" + Nothing = "⚫" + + +class WinState(Enum): + Red = "🔴 won!" + Yellow = "🟡 won!" + Green = "🟢 won!" + Blue = "🔵 won!" + NoOne = "⚫" + Draw = "It's a draw! 🔴🟡🟢🔵" + + +class Checkpoint(BaseModel): + win_state: WinState = WinState.NoOne + playfield: list[list[Players]] + to_play: Players + + +class Connect4: + def __init__(self, width: int = 7, height: int = 6, player_count: int = 2): + if player_count not in [2, 3, 4]: + raise ValueError("Connect4 supports only 2, 3, or 4 players") + + self.width: int = width + self.height: int = height + self.players = list(Players)[:player_count] + self.current_turn_idx: int = 0 + self.playfield: list[list[Players]] = [ + [Players.Nothing for _ in range(width)] for _ in range(height) + ] + self.checkpoint: Checkpoint = Checkpoint( + win_state=WinState.NoOne, + playfield=self.playfield, + to_play=self.players[self.current_turn_idx], + ) + + def switch_turns(self) -> None: + self.current_turn_idx = (self.current_turn_idx + 1) % len(self.players) + + def play_move(self, column: int) -> Checkpoint: + if column < 0 or column >= self.width: + raise ValueError("Column outside of available playfield") + + for row in reversed(self.playfield): + if row[column] == Players.Nothing: + row[column] = self.players[self.current_turn_idx] + self.switch_turns() + self.checkpoint = Checkpoint( + win_state=self.check_for_win(), + playfield=self.playfield, + to_play=self.players[self.current_turn_idx], + ) + return self.checkpoint + + raise ValueError("Column is full") + + def check_for_win(self) -> WinState: + def check_direction(x, y, dx, dy, player): + count = 0 + for _ in range(4): + if ( + 0 <= x < self.width + and 0 <= y < self.height + and self.playfield[y][x] == player + ): + count += 1 + else: + break + x += dx + y += dy + return count == 4 + + for y in range(self.height): + for x in range(self.width): + if self.playfield[y][x] == Players.Nothing: + continue + player = self.playfield[y][x] + if any( + check_direction(x, y, dx, dy, player) + for dx, dy in [(1, 0), (0, 1), (1, 1), (1, -1)] + ): + return WinState[player.name] + + if all(cell != Players.Nothing for row in self.playfield for cell in row): + return WinState.Draw + + return WinState.NoOne