import utils from discord.ext import commands import asyncio import math class Blackjack(commands.Cog): """Pretty self-explanatory""" def __init__(self, bot): self.bot = bot self.games = {} def cog_unload(self): # Simply cancel every task for game in self.games.values(): game.task.cancel() def create_game(self, message): # When we're done with the game, we need to delete the game itself and remove it's instance from games # To do this, it needs to be able to access this instance of Blackjack game = Game(self.bot, message, self) self.games[message.guild.id] = game @commands.group(aliases=["bj"], invoke_without_command=True) @commands.guild_only() @utils.can_run(send_messages=True) async def blackjack(self, ctx): """Creates a game/joins the current running game of blackjack EXAMPLE: !blackjack RESULT: A new game of blackjack!""" # Get this guild's game if it exists game = self.games.get(ctx.message.guild.id) # If it doesn't, start one if game is None: self.create_game(ctx.message) else: # Try to join the game status = game.join(ctx.message.author) # If it worked, they're ready to play if status: fmt = "{} has joined the game of blackjack, and will be able to play next round!" await ctx.send(fmt.format(ctx.message.author.display_name)) else: # Otherwise, lets check *why* they couldn't join if game.playing(ctx.message.author): await ctx.send("You are already playing! Wait for your turn!") else: await ctx.send( "There are already a max number of players playing/waiting to play!" ) @blackjack.command(name="leave", aliases=["quit"]) @commands.guild_only() @utils.can_run(send_messages=True) async def blackjack_leave(self, ctx): """Leaves the current game of blackjack EXAMPLE: !blackjack leave RESULT: You stop losing money in blackjack""" # Get this guild's game if it exists game = self.games.get(ctx.message.guild.id) if game is None: await ctx.send("There are currently no games of Blackjack running!") return status = game.leave(ctx.message.author) if status: await ctx.send( "You have left the game, and will be removed at the end of this round" ) else: await ctx.send( "Either you have already bet, or you are not even playing right now!" ) @blackjack.command(name="forcestop", aliases=["stop"]) @commands.guild_only() @utils.can_run(manage_guild=True) async def blackjack_stop(self, ctx): """Forces the game to stop, mostly for use if someone has gone afk EXAMPLE: !blackjack forcestop RESULT: No more blackjack spam""" # Get this guild's game if it exists game = self.games.get(ctx.message.guild.id) if game is None: await ctx.send("There are currently no games of Blackjack running!") return game.task.cancel() del self.games[ctx.message.guild.id] await ctx.send("The blackjack game running here has just ended") def FOIL(a, b): """Uses FOIL to calculate a new possible total (who knew math would come in handy?!) For our purposes, we are adding, not multiplying, so this is not the *same* as using normal FOIL So no built in method will work""" # This whole thing may *look* like it may add a lot of entries # However, we are going to be using pretty short lists, and duplicate entries will be common # So this isn't actually as bad as it looks new_totals = [] for i in a: for x in b: total = i + x if total not in new_totals: new_totals.append(total) # The list comprehension is to ensure we don't care about totals over 21 new_totals = [x for x in new_totals if x < 22] return new_totals class Player: def __init__(self, member): self.member = member self.hand = utils.Deck(prefill=False) # The chips a player starts with self.chips = 1000 @property def bust(self): for total in self.count: if total <= 21: return False return True @property def count(self): """The current count of our hand""" total = [0] for card in self.hand: # Order is suit, face...so we want the second value value = card.value.value face = card.value.name if face in ["queen", "king", "jack"]: for index, t in enumerate(total): total[index] += 10 elif face == "ace": total = FOIL(total, [1, 11]) else: for index, t in enumerate(total): total[index] += int(value) # If we have more than one possible total (there is at least one ace) then we do not care about one if it is # over 21 if len(total) > 1: new_total = [x for x in total if x < 22] # However, if the ace is there and both possibilities cause a bust, we want the lowest one to be our count if len(new_total) == 0: new_total = [min(total)] total = new_total return total def __eq__(self, other): if isinstance(other, Player): if hasattr(other, "member") and other.member == self.member: return True return False def __str__(self): # We only care about our hand, for printing wise fmt = "Hand:\n" fmt += "\n".join(str(card) for card in self.hand) fmt += "\n(Total: {})".format(self.count) return fmt class Game: def __init__(self, bot, message, bj): player = Player(message.author) self.bj = bj self.bot = bot self.players = [{"status": "playing", "player": player}] # Our buffer for players who want to join # People cannot join in the middle of a game, so we'll add them at the end self._added_players = [] # Our list of players who lost/left self._removed_players = [] # The channel we'll send messages to self.channel = message.channel # People can join in on this game, but lets make sure we don't go over the limit however self._max_players = 10 # Lets create our main deck, and shuffle it self.deck = utils.Deck() # So apparently, it is possible, with 10 players and nearly everyone/everyone busting # To actually deplete the deck, and cause it to return None, and mess up later # Due to this, lets make put 2 decks in here _deck2 = utils.Deck() self.deck.insert(list(_deck2.draw(52))) del _deck2 self.deck.shuffle() # The dealer self.dealer = Player("Dealer") self.min_bet = 5 self.max_bet = 500 self.bet = 0 self.task = self.bot.loop.create_task(self.game_task()) async def game_task(self): """The task to handle the entire game""" while len(self.players) > 0: await self.channel.send("A new round has started!!") # First wait for bets await self.bet_task() # To allow people to leave correctly during the betting phase # (i.e. they left before they bet) we need to do a cleanup here self.player_cleanup() # Then deal to every player self.deal() # Now wait for our round to finish await self.round_task() # Now it's the dealers turn # This is the only loop we have to worry about still occurring if there are no more players # This is due to the fact the dealer has nothing to do with players during his "turn" # We need an extra check here to stop this from occurring if len(self.players) > 0: await self.dealer_task() # Then clean everything up, including: # Put everyone's hand inside the deck # Shuffle the deck # Payout/pickup bets # Include any new players # Remove players leaving/ran out of chips await self.cleanup() # If we reach the end of this loop, that means there are no more players del self.bj.games[self.channel.guild.id] async def dealer_task(self): """The task handling the dealer's play after all players have stood""" fmt = "It is the dealer's turn to play\n\n{}".format(self.dealer) msg = await self.channel.send(fmt) while True: await asyncio.sleep(1) if self.dealer.bust: fmt = msg.content + "\n\nDealer has busted!!" await msg.edit(content=fmt) return for num in self.dealer.count: if num > 16: return self.hit(self.dealer) fmt = "It is the dealer's turn to play\n\n{}".format(self.dealer) await msg.edit(content=fmt) async def round_task(self): """The task handling the round itself, asking each person to hit or stand""" # This task is going to be called at the beginning of a game # It is purely for asking for hit or stand from each player, till this round is over # A differen task will handle if a player hit blackjack to start (so they would not be 'playing') # Our check to make sure a valid 'command' was provided def check(m): if m.channel == self.channel and m.author == player.member: return m.content.lower() in ["hit", "stand", "double"] else: return False # First lets handle the blackjacks for entry in [p for p in self.players if p["status"] == "blackjack"]: player = entry["player"] fmt = "You got a blackjack {0.member.mention}!\n\n{0}".format(player) await self.channel.send(fmt) # Loop through each player (as long as their status is playing) and they have bet chips for entry in [ p for p in self.players if p["status"] == "playing" and hasattr(p["player"], "bet") ]: player = entry["player"] # Let them know it's their turn to play fmt = "It is your turn to play {0.member.mention}\n\n{0}".format(player) await self.channel.send(fmt) first = True # If they're not playing anymore (i.e. they busted, are standing, etc.) then we don't want to keep asking # them to hit or stand while entry["status"] not in ["stand", "bust"]: # Ask if they want to hit or stand if first: fmt = "Hit, stand, or double?" else: fmt = "Hit or stand?" await self.channel.send(fmt) try: msg = await self.bot.wait_for("message", timeout=60, check=check) except asyncio.TimeoutError: await self.channel.send("Took to long! You're standing!") entry["status"] = "stand" else: # If they want to hit if "hit" in msg.content.lower(): self.hit(player) await self.channel.send(player) # If they want to stand elif "stand" in msg.content.lower(): self.stand(player) elif "double" in msg.content.lower() and first: self.double(player) await self.channel.send(player) # TODO: Handle double, split first = False async def bet_task(self): """Performs the task of betting""" # There is one situation that we want to allow that means we cannot loop through players like normally would # be the case: Betting has started; while one person is betting, another joins This means our list has # changed, and neither based on the length or looping through the list itself will handle this To handle # this, we'll loop 'infinitely', get a list of players who haven't bet yet, and then use the first person in # that list while True: players = [p for p in self.players if p["status"] == "playing"] # If everyone has bet/there is no one playing anymore if len(players) == 0: break entry = players[0] player = entry["player"] def check(_msg): """Makes sure the message provided is within the min and max bets""" if _msg.channel == self.channel and _msg.author == player.member: try: msg_length = int(_msg.content) return self.min_bet <= msg_length <= self.max_bet except ValueError: return _msg.content.lower() == "skip" else: return False fmt = ( "Your turn to bet {0.member.mention}, your current chips are: {0.chips}\n" "Current min bet is {1}, current max bet is {2}\n" "Place your bet now (please provide only the number;" "'skip' if you would like to leave this game)".format( player, self.min_bet, self.max_bet ) ) await self.channel.send(fmt) try: msg = await self.bot.wait_for("message", timeout=60, check=check) except asyncio.TimeoutError: await self.channel.send( "You took too long! You're sitting this round out" ) entry["status"] = "stand" else: if msg.content.lower() == "skip": await self.channel.send( "Alright, you've been removed from the game" ) self.leave(player.member) else: num = int(msg.content) # Set the new bet, and remove it from this players chip total if num <= player.chips: player.bet = num player.chips -= num entry["status"] = "bet" else: await self.channel.send("You can't bet more than you have!!") # Call this so that we can correct the list, if someone has left or join self.player_cleanup() async def cleanup(self): """Performs the tasks done after the game has completed""" dealer_count = max(self.dealer.count) # A list of losers, winners, and people who tied losers = [] ties = [] winners = [] blackjack = [] for entry in self.players: player = entry["player"] # Quick check here to ensure the player isn't someone who got added # Specifically right after the betting phase if not hasattr(player, "bet"): continue hand = player.hand count = max(player.count) # First, lets handle bets # TODO: Handle blackjacks # First if is to check If we can possibly win (a bust is an automatic loss, no matter what) # Payouts for wins are 2 times the bet if entry["status"] == "blackjack": if dealer_count != 21: player.chips += math.floor(player.bet * 2.5) blackjack.append(player) else: # A push, the player gets their chips back player.chips += player.bet ties.append(player) elif not player.bust: # If the dealer busts, we win if dealer_count > 21: player.chips += player.bet * 2 winners.append(player) else: # If the dealer hasn't busted, we need to compare if count == dealer_count: # A push, the player gets their chips back player.chips += player.bet ties.append(player) elif count > dealer_count: # The player won, they get their payout player.chips += player.bet * 2 winners.append(player) else: # The player lost losers.append(player) else: # Otherwise, they lost, nothing we need to do losers.append(player) player.bet = 0 # "Draw" the remaining cards so that the hand is emptied, as well as returned cards = list(hand.draw(hand.count)) # Now add them back into the deck self.deck.insert(cards) # While we're already looping, another task we need to complete is removing people who lost # So if their chips are 0 (or less which shouldn't happen) add them to the _removed_players list if player.chips <= 0: self._removed_players.append(player) entry["status"] = "playing" # Now that we've looped through everyone, send the message regarding the outcome fmt = "Round stats:\n" if len(winners) > 0: # First get a string of all the winners _fmt = " ".join(m.member.display_name for m in winners) # Add that to the main string fmt += "Winners: {}\n".format(_fmt) if len(losers) > 0: # First get a string of all the losers _fmt = " ".join(m.member.display_name for m in losers) # Add that to the main string fmt += "Losers: {}\n".format(_fmt) if len(ties) > 0: # First get a string of all the ties _fmt = " ".join(m.member.display_name for m in ties) # Add that to the main string fmt += "Ties: {}\n".format(_fmt) if len(blackjack) > 0: # First get a string of all the blackjacks _fmt = " ".join(m.member.display_name for m in blackjack) # Add that to the main string fmt += "Blackjacks: {}\n".format(_fmt) await self.channel.send(fmt) # Do the same for the dealers hand cards = list(self.dealer.hand.draw(self.dealer.hand.count)) self.deck.insert(cards) self.player_cleanup() # Now that's complete, shuffle the deck self.deck.shuffle() def player_cleanup(self): """Handles the cleanup of adding and removing waiting players""" # We need this method separate from our other cleanup processes # Because people can leave/join in the middle of betting (if they haven't already bet) # Include the new players self.players.extend(self._added_players) self._added_players.clear() # What we want to do is remove any players that are in the game and have left the guild for entry in self.players: m = entry["player"].member if m not in self.channel.guild.members: self._removed_players.append(entry["player"]) # Remove the players who left self.players = [ p for p in self.players if p["player"] not in self._removed_players ] self._removed_players.clear() def _get_player_index(self, player): """Provides the index of a certain player""" for i, entry in enumerate(self.players): if entry["player"] == player: return i def get_player(self, member): """Returns the player object for the discord member provided""" for entry in self.players: if entry["player"].member == member: return entry["player"] def playing(self, member): """Returns true if the member provided is currently in this game""" for entry in self.players: if member == entry["player"].member: return True return False def deal(self): """Deals to all players playing""" # I would like this to simulate a real "deal", so need to deal to each member in order for i in range(2): for entry in self.players: card = list(self.deck.draw()) entry["player"].hand.insert(card) # Make sure we detect blackjack here, as this is when it matters if 21 in entry["player"].count: entry["status"] = "blackjack" else: entry["status"] = "playing" # Also add a card to the dealer's hand card = list(self.deck.draw()) self.dealer.hand.insert(card) def join(self, member): """Adds the member to the game playing""" if self.playing(member): return False if len(self.players) + len(self._added_players) >= self._max_players: return False player = Player(member) entry = {"status": "playing", "player": player} self._added_players.append(entry) return True def leave(self, member): player = self.get_player(member) # If they are playing, then add them to the _removed_players list so that they can be removed at the end of # the round if player: # We need to make sure they haven't already bet index = self._get_player_index(player) if self.players[index]["status"] == "bet": return False else: self.players[index]["status"] = "left" self._removed_players.append(player) return True else: return False def double(self, player): """Doubles down on the current hand""" # First double the bet player.chips -= player.bet player.bet *= 2 # Then hit self.hit(player) # After doubling down, they are not allowed to play again, make them stand self.stand(player) def stand(self, player): """Causes a player to stand""" for entry in self.players: if entry["player"] == player: entry["status"] = "stand" return def hit(self, player): """Hits a player""" # Draw one card card = list(self.deck.draw()) # Add it to the player's hand player.hand.insert(card) # If this is the dealer, we don't need to care about this if player == self.dealer: return if player.bust: index = self._get_player_index(player) self.players[index]["status"] = "bust" elif 21 in player.count: index = self._get_player_index(player) self.players[index]["status"] = "stand" def setup(bot): bot.add_cog(Blackjack(bot))