Ruby-Cogs/casino/games.py

592 lines
21 KiB
Python

from __future__ import annotations
# Standard Library
import asyncio
import random
# Casino
from .deck import Deck
from .engine import game_engine
# Red
from redbot.core import bank
from redbot.core.i18n import Translator
from redbot.core.errors import BalanceTooHigh
from redbot.core.utils.chat_formatting import box
from redbot.core.utils.predicates import MessagePredicate
# Discord
import discord
_ = Translator("Casino", __file__)
deck = Deck()
# Any game created must return a tuple of 3 arguments.
# The outcome (True or False)
# The final bet or amount (int)
# A msg that is either a string or an embed
# If the msg is a string it is added to the description of the final embed.
# If the msg is an embed, it's fields are added to the final embed.
class Core:
"""
A simple class to hold the basic original Casino mini games.
Games
-----------
Allin
Bet all your credits. All or nothing gamble.
Coin
Coin flip game. Pick heads or tails.
Cups
Three cups are shuffled. Pick the one covering the ball.
Dice
Roll a pair of die. 2, 7, 11, or 12 wins.
Hilo
Guess if the dice result will be high, low, or 7.
Craps
Win with a comeout roll of 7 or 11, lose on 2, 3, or 12.
If you roll any other number you must match it on your
second roll to win.
"""
def __init__(self, old_message_cache):
self.old_message_cache = old_message_cache
@game_engine("Allin")
async def play_allin(self, ctx, bet, multiplier):
message = await ctx.send(
_("You put all your chips into the machine and pull the lever...")
)
await asyncio.sleep(3)
outcome = random.randint(0, multiplier + 1)
if outcome == 0:
msg = "▂▃▅▇█▓▒░ [♠] [♥] [♦] [♣] ░▒▓█▇▅▃▂\n"
msg += _(" CONGRATULATIONS YOU WON\n")
msg += _("░▒▓█▇▅▃▂ ⚅ J A C K P O T ⚅ ▂▃▅▇█▓▒░")
msg = box(msg, lang="py")
bet *= multiplier
else:
msg = _("Nothing happens. You stare at the machine contemplating your decision.")
return outcome == 0, bet, msg, message
@game_engine("Coin", (_("heads"), _("tails")))
async def play_coin(self, ctx, bet, choice):
message = await ctx.send(_("The coin flips into the air..."))
await asyncio.sleep(2)
outcome = random.choice((_("heads"), _("tails")))
msg = _("The coin landed on {}!").format(outcome)
return choice.lower() in outcome, bet, msg, message
@game_engine("Cups", ("1", "2", "3"))
async def play_cups(self, ctx, bet, choice):
message = await ctx.send(_("The cups start shuffling along the table..."))
await asyncio.sleep(3)
outcome = random.randint(1, 3)
msg = _("The coin was under cup {}!").format(outcome)
return int(choice) == outcome, bet, msg, message
@game_engine("Dice")
async def play_dice(self, ctx, bet):
message = await ctx.send(
_("The dice strike the back of the table and begin to tumble into place...")
)
await asyncio.sleep(2)
die_one, die_two = self.roll_dice()
outcome = die_one + die_two
msg = _("The dice landed on {} and {} ({}).").format(die_one, die_two, outcome)
return outcome in (2, 7, 11, 12), bet, msg, message
@game_engine("Hilo", (_("low"), _("lo"), _("high"), _("hi"), _("seven"), _("7")))
async def play_hilo(self, ctx, bet, choice):
message = await ctx.send(_("The dice hit the table and slowly fall into place..."))
await asyncio.sleep(2)
result = sum(self.roll_dice())
if result < 7:
outcome = (_("low"), _("lo"))
elif result > 7:
outcome = (_("high"), _("hi"))
else:
outcome = (_("seven"), "7")
msg = _("The outcome was {} ({[0]})!").format(result, outcome)
if result == 7 and outcome[1] == "7":
bet *= 5
return choice.lower() in outcome, bet, msg, message
@game_engine(name="Craps")
async def play_craps(self, ctx, bet):
return await self._craps_game(ctx, bet)
async def _craps_game(self, ctx, bet, comeout=False, message=None):
msg1 = _("The dice strike against the back of the table...")
if (not await self.old_message_cache.get_guild(ctx.guild)) and message:
await asyncio.sleep(3)
await message.edit(content=msg1)
else:
message = await ctx.send(msg1)
await asyncio.sleep(2)
d1, d2 = self.roll_dice()
result = d1 + d2
msg = _("You rolled a {} and {}.")
if comeout:
if result == comeout:
return True, bet, msg.format(d1, d2), message
return False, bet, msg.format(d1, d2), message
if result == 7:
bet *= 3
return True, bet, msg.format(d1, d2), message
elif result == 11:
return True, bet, msg.format(d1, d2), message
elif result in (2, 3, 12):
return False, bet, msg.format(d1, d2), message
msg2 = _(
"{}\nI'll roll the dice one more time. This time you will need exactly {} to win."
).format(msg.format(d1, d2), d1 + d2)
if not await self.old_message_cache.get_guild(ctx.guild):
await message.edit(content=msg2)
else:
await ctx.send(msg2)
return await self._craps_game(ctx, bet, comeout=result, message=message)
@staticmethod
def roll_dice():
return random.randint(1, 6), random.randint(1, 6)
class BlackjackView(discord.ui.View):
def __init__(self, ctx, include_double: bool):
self.ctx = ctx
super().__init__()
self.result = None
if include_double is False:
self.double_button.disabled = True
@discord.ui.button(label="Hit", emoji="\N{RAISED FIST}")
async def hit_button(self, interaction: discord.Interaction, button):
self.result = "hit"
await interaction.response.defer()
self.stop()
@discord.ui.button(label="Stay", emoji="\N{RAISED HAND}")
async def stay_button(self, interaction: discord.Interaction, button):
self.result = "stay"
await interaction.response.defer()
self.stop()
@discord.ui.button(label="Double", emoji="\N{VICTORY HAND}\N{VARIATION SELECTOR-16}")
async def double_button(self, interaction: discord.Interaction, button):
self.result = "double"
await interaction.response.defer()
self.stop()
async def interaction_check(self, interaction: discord.Interaction):
if interaction.user.id != self.ctx.author.id:
await interaction.response.send_message(
"You are not authorized to interact with this.", ephemeral=True
)
return False
return True
class Blackjack:
"""A simple class to hold the game logic for Blackjack.
Blackjack requires inheritance from data to verify the user
can double down.
"""
def __init__(self, old_message_cache):
self.old_message_cache = old_message_cache
super().__init__()
@game_engine(name="Blackjack")
async def play(self, ctx, bet):
ph, dh, amt, msg = await self.blackjack_game(ctx, bet)
result = await self.blackjack_results(ctx, amt, ph, dh, message=msg)
return result
@game_engine(name="Blackjack")
async def mock(self, ctx, bet, ph, dh):
result = await self.blackjack_results(ctx, bet, ph, dh)
return result
async def blackjack_game(self, ctx, amount):
ph = deck.deal(num=2)
ph_count = deck.bj_count(ph)
dh = deck.deal(num=2)
# End game if player has 21
if ph_count == 21:
return ph, dh, amount, None
embed = self.bj_embed(ctx, ph, dh, ph_count, initial=True)
view = BlackjackView(ctx, include_double=True)
msg = await ctx.send(ctx.author.mention, embed=embed, view=view)
await view.wait()
if view.result == "stay":
dh = self.dealer(dh)
return ph, dh, amount, msg
if view.result == "double":
return await self.double_down(ctx, ph, dh, amount, message=msg)
else:
ph, dh, message = await self.bj_loop(ctx, ph, dh, ph_count, message=msg)
dh = self.dealer(dh)
return ph, dh, amount, msg
async def double_down(self, ctx, ph, dh, amount, message):
try:
await bank.withdraw_credits(ctx.author, amount)
except ValueError:
await ctx.send(
_("{} You can not cover the bet. Please choose hit or stay.").format(
ctx.author.mention
)
)
view = BlackjackView(ctx, include_double=False)
ph_count = deck.bj_count(ph)
embed = self.bj_embed(ctx, ph, dh, ph_count, initial=False)
if not await self.old_message_cache.get_guild(ctx.guild):
await message.edit(content=ctx.author.mention, embed=embed, view=view)
else:
await ctx.send(content=ctx.author.mention, embed=embed, view=view)
await view.wait()
if view.result == "stay":
dh = self.dealer(dh)
return ph, dh, amount, message
elif view.result == "hit":
ph, dh, message = await self.bj_loop(
ctx, ph, dh, deck.bj_count(ph), message=message
)
dh = self.dealer(dh)
return ph, dh, amount, message
else:
deck.deal(hand=ph)
dh = self.dealer(dh)
amount *= 2
return ph, dh, amount, message
async def blackjack_results(self, ctx, amount, ph, dh, message=None):
dc = deck.bj_count(dh)
pc = deck.bj_count(ph)
if dc > 21 >= pc or dc < pc <= 21:
outcome = _("Winner!")
result = True
elif pc > 21:
outcome = _("BUST!")
result = False
elif dc == pc <= 21:
outcome = _("Pushed")
try:
await bank.deposit_credits(ctx.author, amount)
except BalanceTooHigh as e:
await bank.set_balance(ctx.author, e.max_balance)
result = False
else:
outcome = _("House Wins!")
result = False
embed = self.bj_embed(ctx, ph, dh, pc, outcome=outcome)
return result, amount, embed, message
async def bj_loop(self, ctx, ph, dh, count, message: discord.Message):
while count < 21:
ph = deck.deal(hand=ph)
count = deck.bj_count(hand=ph)
if count >= 21:
break
embed = self.bj_embed(ctx, ph, dh, count)
view = BlackjackView(ctx, include_double=False)
if not await self.old_message_cache.get_guild(ctx.guild):
await message.edit(content=ctx.author.mention, embed=embed, view=view)
else:
await ctx.send(content=ctx.author.mention, embed=embed, view=view)
await view.wait()
if view.result == "stay":
break
await asyncio.sleep(1)
# Return player hand & dealer hand when count >= 21 or the player picks stay.
return ph, dh, message
@staticmethod
def dealer(dh):
count = deck.bj_count(dh)
# forces hit if ace in first two cards without 21
if deck.hand_check(dh, "Ace") and count != 21:
deck.deal(hand=dh)
count = deck.bj_count(dh)
# defines maximum hit score X
while count < 17:
deck.deal(hand=dh)
count = deck.bj_count(dh)
return dh
@staticmethod
def bj_embed(ctx, ph, dh, count1, initial=False, outcome=None):
hand = _("{}\n**Score:** {}")
footer = _("Cards in Deck: {}")
start = _("**Options:** hit, stay, or double")
after = _("**Options:** hit or stay")
options = "**Outcome:** " + outcome if outcome else start if initial else after
count2 = deck.bj_count(dh, hole=True) if not outcome else deck.bj_count(dh)
hole = " ".join(deck.fmt_hand([dh[0]]))
dealer_hand = hole if not outcome else ", ".join(deck.fmt_hand(dh))
embed = discord.Embed(colour=0xFF0000)
embed.add_field(
name=_("{}'s Hand").format(ctx.author.name),
value=hand.format(", ".join(deck.fmt_hand(ph)), count1),
)
embed.add_field(
name=_("{}'s Hand").format(ctx.bot.user.name), value=hand.format(dealer_hand, count2)
)
embed.add_field(name="\u200b", value=options, inline=False)
embed.set_footer(text=footer.format(len(deck)))
return embed
class WarView(discord.ui.View):
def __init__(self, ctx):
self.ctx = ctx
super().__init__()
self.result = None
@discord.ui.button(label=_("War"))
async def war_button(self, interaction: discord.Interaction, button):
self.result = "war"
await interaction.response.defer()
self.stop()
@discord.ui.button(label=_("Surrender"), emoji="\N{WAVING WHITE FLAG}\N{VARIATION SELECTOR-16}")
async def surrender_button(self, interaction: discord.Interaction, button):
self.result = "surrender"
await interaction.response.defer()
self.stop()
async def interaction_check(self, interaction: discord.Interaction):
if interaction.user.id != self.ctx.author.id:
await interaction.response.send_message(
"You are not authorized to interact with this.", ephemeral=True
)
return False
return True
class War:
"""A simple class for the war card game."""
def __init__(self, old_message_cache):
self.old_message_cache = old_message_cache
@game_engine("War")
async def play(self, ctx, bet):
outcome, player_card, dealer_card, amount, msg = await self.war_game(ctx, bet)
return await self.war_results(outcome, player_card, dealer_card, amount, message=msg)
async def war_game(self, ctx, bet):
player_card, dealer_card, pc, dc = self.war_draw()
message = await ctx.send(
_(
"The dealer shuffles the deck and deals 2 cards face down. One for the "
"player and one for the dealer..."
)
)
await asyncio.sleep(2)
text = _("**FLIP!**")
if not await self.old_message_cache.get_guild(ctx.guild):
await message.edit(content=text)
else:
await ctx.send(text)
await asyncio.sleep(1)
if pc != dc:
if pc >= dc:
outcome = "Win"
else:
outcome = "Loss"
return outcome, player_card, dealer_card, bet, message
content = _(
"The player and dealer are both showing a **{}**!\nTHIS MEANS "
"WAR! You may choose to surrender and forfeit half your bet, or "
"you can go to war.\nIf you go to war your bet will be doubled, "
"but the multiplier is only applied to your original bet, the rest will "
"be pushed."
).format(deck.fmt_card(player_card))
view = WarView(ctx)
if not await self.old_message_cache.get_guild(ctx.guild):
await message.edit(content=content, view=view)
else:
await ctx.send(content=content, view=view)
await view.wait()
choice = view.result
if choice is None or choice.title() in (_("Surrender"), _("Ffs")):
outcome = "Surrender"
bet /= 2
return outcome, player_card, dealer_card, bet, message
else:
player_card, dealer_card, pc, dc = self.burn_and_draw()
msg1 = _("The dealer burns three cards and deals two cards face down...")
msg2 = _("**FLIP!**")
if not await self.old_message_cache.get_guild(ctx.guild):
action = message.edit
else:
action = ctx.send
await action(content=msg1)
await asyncio.sleep(3)
await action(content=msg2)
if pc >= dc:
outcome = "Win"
else:
outcome = "Loss"
return outcome, player_card, dealer_card, bet, message
@staticmethod
async def war_results(outcome, player_card, dealer_card, amount, message=None):
msg = _("**Player Card:** {}\n**Dealer Card:** {}\n").format(
deck.fmt_card(player_card), deck.fmt_card(dealer_card)
)
if outcome == "Win":
msg += _("**Result**: Winner")
return True, amount, msg, message
elif outcome == "Loss":
msg += _("**Result**: Loser")
return False, amount, msg, message
else:
msg += _("**Result**: Surrendered")
return False, amount, msg, message
@staticmethod
def get_count(pc, dc):
return deck.war_count(pc), deck.war_count(dc)
def war_draw(self):
player_card, dealer_card = deck.deal(num=2)
pc, dc = self.get_count(player_card, dealer_card)
return player_card, dealer_card, pc, dc
def burn_and_draw(self):
deck.burn(3)
player_card, dealer_card = deck.deal(num=2)
pc, dc = self.get_count(player_card, dealer_card)
return player_card, dealer_card, pc, dc
class DoubleView(discord.ui.View):
def __init__(self, ctx):
self.ctx = ctx
super().__init__()
self.result = None
@discord.ui.button(label=_("Double"))
async def war_button(self, interaction: discord.Interaction, button):
self.result = "double"
await interaction.response.defer()
self.stop()
@discord.ui.button(label=_("Cash out"), emoji="\N{BANKNOTE WITH DOLLAR SIGN}")
async def surrender_button(self, interaction: discord.Interaction, button):
self.result = None
# set this to None so we exit even if the user doesn't interact
await interaction.response.defer()
self.stop()
async def interaction_check(self, interaction: discord.Interaction):
if interaction.user.id != self.ctx.author.id:
await interaction.response.send_message(
"You are not authorized to interact with this.", ephemeral=True
)
return False
return True
class Double:
"""A simple class for the Double Or Nothing game."""
def __init__(self, old_message_cache):
self.old_message_cache = old_message_cache
@game_engine("Double")
async def play(self, ctx, bet):
count, amount, message = await self.double_game(ctx, bet)
return await self.double_results(ctx, count, amount, message=message)
async def double_game(self, ctx, bet):
count = 0
message = None
while bet > 0:
count += 1
flip = random.randint(0, 1)
if flip == 0:
bet = 0
break
else:
bet *= 2
view = DoubleView(ctx)
embed = self.double_embed(ctx, count, bet)
if (not await self.old_message_cache.get_guild(ctx.guild)) and message:
await message.edit(content=ctx.author.mention, embed=embed, view=view)
else:
message = await ctx.send(ctx.author.mention, embed=embed, view=view)
await view.wait()
if not view.result:
break
await asyncio.sleep(1)
return count, bet, message
async def double_results(self, ctx, count, amount, message=None):
if amount > 0:
outcome = _("Cashed Out!")
result = True
else:
outcome = _("You Lost It All!")
result = False
embed = self.double_embed(ctx, count, amount, outcome=outcome)
return result, amount, embed, message
@staticmethod
def double_embed(ctx, count, amount, outcome=None):
double = _("{}\n**DOUBLE!:** x{}")
zero = _("{}\n**NOTHING!**")
choice = _("**Options:** double or cash out")
options = "**Outcome:** " + outcome if outcome else choice
if amount == 0:
score = zero.format(amount)
else:
score = double.format(amount, count)
embed = discord.Embed(colour=0xFF0000)
embed.add_field(name=_("{}'s Score").format(ctx.author.name), value=score)
embed.add_field(name="\u200b", value=options, inline=False)
if not outcome:
embed.add_field(
name="\u200b", value="Remember, you can cash out at anytime.", inline=False
)
embed.set_footer(text="Try again and test your luck!")
return embed