feat: add connect four
This commit is contained in:
parent
ca8dc84693
commit
65beb2bd53
2 changed files with 209 additions and 0 deletions
113
src/exts/games/connectfour.py
Normal file
113
src/exts/games/connectfour.py
Normal file
|
|
@ -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))
|
||||||
96
src/games/connect4.py
Normal file
96
src/games/connect4.py
Normal file
|
|
@ -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
|
||||||
Loading…
Add table
Add a link
Reference in a new issue