import asyncio import os import random from typing import List, Tuple import discord from discord.ext import commands from PIL import Image from discord import app_commands from bot import InsufficientFundsException from database import Database """NOTE: This code was found somewhere on GitHub a long time ago. I changed it a bit to work for discord.py 2.0 and for my needs. If anyone knows who wrote this, please let me know so I can give them credit.""" Entry = Tuple[int, int] class Card: suits = ["clubs", "diamonds", "hearts", "spades"] def __init__(self, suit: str, value: int, down=False): self.suit = suit self.value = value self.down = down self.symbol = self.name[0].upper() @property def name(self) -> str: """The name of the card value.""" if self.value <= 10: return str(self.value) else: return { 11: 'jack', 12: 'queen', 13: 'king', 14: 'ace', }[self.value] @property def image(self): return ( f"{self.symbol if self.name != '10' else '10'}"\ f"{self.suit[0].upper()}.png" \ if not self.down else "red_back.png" ) def flip(self): self.down = not self.down return self def __str__(self) -> str: return f'{self.name.title()} of {self.suit.title()}' def __repr__(self) -> str: return str(self) class Blackjack(commands.Cog): def __init__(self, bot: commands.Bot): self.bot = bot self.economy = Database(bot) async def check_bet( self, interaction: discord.Interaction, bet ): bet = int(bet) if bet <= 0: raise commands.errors.BadArgument() current = (await self.economy.get_entry(interaction.user.id))[1] if bet > current: raise InsufficientFundsException() @staticmethod def hand_to_images(hand: List[Card]) -> List[Image.Image]: return ([ Image.open(f'./code/utils/cards/{card.image}') for card in hand ]) @staticmethod def center(*hands: Tuple[Image.Image]) -> Image.Image: """Creates blackjack table with cards placed""" bg: Image.Image = Image.open('./code/utils/table.png') bg_center_x = bg.size[0] // 2 bg_center_y = bg.size[1] // 2 img_w = hands[0][0].size[0] img_h = hands[0][0].size[1] start_y = bg_center_y - (((len(hands)*img_h) + \ ((len(hands) - 1) * 15)) // 2) for hand in hands: start_x = bg_center_x - (((len(hand)*img_w) + \ ((len(hand) - 1) * 10)) // 2) for card in hand: bg.alpha_composite(card, (start_x, start_y)) start_x += img_w + 10 start_y += img_h + 15 return bg def output(self, name, *hands: Tuple[List[Card]]) -> None: self.center(*map(self.hand_to_images, hands)).save(f'./code/players/tables/{name}.png') @staticmethod def calc_hand(hand: List[List[Card]]) -> int: """Calculates the sum of the card values and accounts for aces""" non_aces = [c for c in hand if c.symbol != 'A'] aces = [c for c in hand if c.symbol == 'A'] sum = 0 for card in non_aces: if not card.down: if card.symbol in 'JQK': sum += 10 else: sum += card.value for card in aces: if not card.down: if sum <= 10: sum += 11 else: sum += 1 return sum @app_commands.command() @app_commands.describe(bet='Amount of money to bet') async def blackjack( self, interaction: discord.Interaction, bet: app_commands.Range[int, 1, None] ): "Bet your money on a blackjack game vs. the dealer" if f"{interaction.user.id}.png" in os.listdir("./code/players/tables"): await interaction.response.send_message(f"{interaction.user.mention}, It appears you have a game already running in this server or another, please finish that game before starting a new one.", ephemeral=True) else: await self.check_bet(interaction, bet) deck = [Card(suit, num) for num in range(2,15) for suit in Card.suits] random.shuffle(deck) # Generate deck and shuffle it player_hand: List[Card] = [] dealer_hand: List[Card] = [] player_hand.append(deck.pop()) dealer_hand.append(deck.pop()) player_hand.append(deck.pop()) dealer_hand.append(deck.pop().flip()) player_score = self.calc_hand(player_hand) dealer_score = self.calc_hand(dealer_hand) async def out_table(**kwargs) -> discord.Interaction: self.output(f'{interaction.user.id}', dealer_hand, player_hand) embed = discord.Embed(**kwargs) file = discord.File( f"./code/players/tables/{interaction.user.id}.png", filename=f"{interaction.user.id}.png" ) embed.set_image(url=f"attachment://{interaction.user.id}.png") try: msg = await interaction.response.send_message(file=file, embed=embed) except: msg = await interaction.edit_original_response(attachments=[file], embed=embed) reac = await interaction.original_response() await reac.clear_reactions() return msg standing = False while True: player_score = self.calc_hand(player_hand) dealer_score = self.calc_hand(dealer_hand) if player_score == 21: # win condition await self.economy.add_money(interaction.user.id, bet*2) result = (f"Blackjack! - you win ${bet*2:,}", 'won') break elif player_score > 21: # losing condition await self.economy.add_money(interaction.user.id, bet*-1) result = (f"Player busts - you lose ${bet:,}", 'lost') break msg = await out_table( title="Your Turn", description=f"Your hand: {player_score}\n" \ f"Dealer's hand: {dealer_score}" ) reac = await interaction.original_response() await reac.add_reaction("🇭") await reac.add_reaction("🇸") buttons = {"🇭", "🇸"} try: reaction, _ = await self.bot.wait_for( 'reaction_add', check=lambda reaction, user:user == interaction.user and reaction.emoji in buttons, timeout=60 ) except asyncio.TimeoutError: os.remove(f'./code/players/tables/{interaction.user.id}.png') await interaction.followup.send(f"{interaction.user.mention} your game timed out. No money was lost or gained.") return if str(reaction.emoji) == "🇭": player_hand.append(deck.pop()) continue elif str(reaction.emoji) == "🇸": standing = True break if standing: dealer_hand[1].flip() player_score = self.calc_hand(player_hand) dealer_score = self.calc_hand(dealer_hand) while dealer_score < 17: # dealer draws until 17 or greater dealer_hand.append(deck.pop()) dealer_score = self.calc_hand(dealer_hand) if dealer_score == 21: # winning/losing conditions await self.economy.add_money(interaction.user.id, bet*-1) result = (f"Dealer blackjack - you lose ${bet:,}", 'lost') elif dealer_score > 21: await self.economy.add_money(interaction.user.id, bet*2) result = (f"Dealer busts - you win ${bet*2:,}", 'won') elif dealer_score == player_score: result = (f"Tie - you keep your money", 'kept') elif dealer_score > player_score: await self.economy.add_money(interaction.user.id, bet*-1) result = (f"You lose ${bet:,}", 'lost') elif dealer_score < player_score: await self.economy.add_money(interaction.user.id, bet*2) result = (f"You win ${bet*2:,}", 'won') color = ( discord.Color.red() if result[1] == 'lost' else discord.Color.green() if result[1] == 'won' else discord.Color.blue() ) if result[1] == 'won': description=( f"**You won ${bet*2:,}**\nYour hand: {player_score}\n" + f"Dealer's hand: {dealer_score}" ) elif result[1] == 'lost': description=( f"**You lost ${bet:,}**\nYour hand: {player_score}\n" + f"Dealer's hand: {dealer_score}" ) msg = await out_table( title=result[0], color=color, ) os.remove(f'./code/players/tables/{interaction.user.id}.png') async def setup(bot: commands.Bot): await bot.add_cog(Blackjack(bot))