1
Fork 0
mirror of https://github.com/wlinator/luminara.git synced 2024-10-02 20:23:12 +00:00

Merge pull request #37 from wlinator/economy-patch

Refactor blackjack
This commit is contained in:
wlinator 2024-08-22 17:36:49 +02:00
commit 4fbfc6e05d
5 changed files with 258 additions and 282 deletions

View file

@ -17,6 +17,7 @@ RUN pip install --no-cache-dir poetry && \
pip cache purge pip cache purge
COPY . . COPY . .
RUN rm -rf .venv
ENV LANG=en_US.UTF-8 ENV LANG=en_US.UTF-8
ENV LC_ALL=en_US.UTF-8 ENV LC_ALL=en_US.UTF-8

View file

@ -22,5 +22,9 @@
"flowers": "https://i.imgur.com/79XfsbS.png", "flowers": "https://i.imgur.com/79XfsbS.png",
"teapot": "https://i.imgur.com/wFsgSnr.png", "teapot": "https://i.imgur.com/wFsgSnr.png",
"muffin": "https://i.imgur.com/hSauh7K.png" "muffin": "https://i.imgur.com/hSauh7K.png"
},
"other": {
"cloud": "https://i.imgur.com/rc68c43.png",
"trophy": "https://i.imgur.com/dvIIr2G.png"
} }
} }

View file

@ -32,6 +32,23 @@
"birthday_upcoming_description_line": "🎂 {0} - {1}", "birthday_upcoming_description_line": "🎂 {0} - {1}",
"birthday_upcoming_no_birthdays": "there are no upcoming birthdays in this server.", "birthday_upcoming_no_birthdays": "there are no upcoming birthdays in this server.",
"birthday_upcoming_no_birthdays_author": "No Upcoming Birthdays", "birthday_upcoming_no_birthdays_author": "No Upcoming Birthdays",
"blackjack_bet": "Bet ${0}",
"blackjack_busted": "Busted..",
"blackjack_dealer_busted": "The dealer busted. You won!",
"blackjack_dealer_hand": "**Dealer**\nScore: {0}\n*Hand: {1}*",
"blackjack_dealer_hidden": "??",
"blackjack_deck_shuffled": "deck shuffled",
"blackjack_description": "You | Score: {0}\nDealer | Score: {1}",
"blackjack_error": "I.. don't know if you won?",
"blackjack_error_description": "This is an error, please report it.",
"blackjack_footer": "Game finished",
"blackjack_lost": "You lost **${0}**.",
"blackjack_lost_generic": "You lost..",
"blackjack_player_hand": "**You**\nScore: {0}\n*Hand: {1}*",
"blackjack_title": "BlackJack",
"blackjack_won_21": "You won with a score of 21!",
"blackjack_won_natural": "You won with a natural hand!",
"blackjack_won_payout": "You won **${0}**.",
"boost_default_description": "Thanks for boosting, **{0}**!!", "boost_default_description": "Thanks for boosting, **{0}**!!",
"boost_default_title": "New Booster", "boost_default_title": "New Booster",
"case_case_field": "Case:", "case_case_field": "Case:",
@ -127,6 +144,7 @@
"error_birthdays_disabled_author": "Birthdays Disabled", "error_birthdays_disabled_author": "Birthdays Disabled",
"error_birthdays_disabled_description": "birthdays are disabled in this server.", "error_birthdays_disabled_description": "birthdays are disabled in this server.",
"error_birthdays_disabled_footer": "Contact a mod to enable them.", "error_birthdays_disabled_footer": "Contact a mod to enable them.",
"error_blackjack_game_error": "something went wrong while playing blackjack.",
"error_boost_image_url_invalid": "the image URL must end with `.jpg` or `.png`.", "error_boost_image_url_invalid": "the image URL must end with `.jpg` or `.png`.",
"error_bot_missing_permissions_author": "Bot Missing Permissions", "error_bot_missing_permissions_author": "Bot Missing Permissions",
"error_bot_missing_permissions_description": "Lumi lacks the required permissions to run this command.", "error_bot_missing_permissions_description": "Lumi lacks the required permissions to run this command.",
@ -134,6 +152,7 @@
"error_command_cooldown_description": "try again in **{0:02d}:{1:02d}**.", "error_command_cooldown_description": "try again in **{0:02d}:{1:02d}**.",
"error_command_not_found": "No command called \"{0}\" found.", "error_command_not_found": "No command called \"{0}\" found.",
"error_image_url_invalid": "invalid image URL.", "error_image_url_invalid": "invalid image URL.",
"error_invalid_bet": "the bet you entered is invalid.",
"error_invalid_duration": "Invalid duration: {0}", "error_invalid_duration": "Invalid duration: {0}",
"error_invalid_duration_author": "Invalid Duration", "error_invalid_duration_author": "Invalid Duration",
"error_invalid_duration_description": "Please provide a valid duration between 1 minute and 30 days.", "error_invalid_duration_description": "Please provide a valid duration between 1 minute and 30 days.",
@ -145,6 +164,7 @@
"error_no_case_found_description": "no case found with that ID.", "error_no_case_found_description": "no case found with that ID.",
"error_no_private_message_author": "Guild Only", "error_no_private_message_author": "Guild Only",
"error_no_private_message_description": "this command can only be used in servers.", "error_no_private_message_description": "this command can only be used in servers.",
"error_not_enough_cash": "you don't have enough cash.",
"error_not_owner_author": "Owner Only", "error_not_owner_author": "Owner Only",
"error_not_owner_description": "this command requires Lumi ownership permissions.", "error_not_owner_description": "this command requires Lumi ownership permissions.",
"error_out_of_time": "you ran out of time.", "error_out_of_time": "you ran out of time.",
@ -262,4 +282,4 @@
"xp_level": "Level {0}", "xp_level": "Level {0}",
"xp_progress": "Progress to next level", "xp_progress": "Progress to next level",
"xp_server_rank": "Server Rank: #{0}" "xp_server_rank": "Server Rank: #{0}"
} }

View file

@ -84,6 +84,10 @@ class Constants:
TEAPOT_ART = ART["juicybblue"]["teapot"] TEAPOT_ART = ART["juicybblue"]["teapot"]
MUFFIN_ART = ART["juicybblue"]["muffin"] MUFFIN_ART = ART["juicybblue"]["muffin"]
# other art
CLOUD_ART = ART["other"]["cloud"]
TROPHY_ART = ART["other"]["trophy"]
# birthdays # birthdays
BIRTHDAY_MESSAGES = JsonCache.read_json("birthday")["birthday_messages"] BIRTHDAY_MESSAGES = JsonCache.read_json("birthday")["birthday_messages"]
BIRTHDAY_MONTHS = JsonCache.read_json("birthday")["months"] BIRTHDAY_MONTHS = JsonCache.read_json("birthday")["months"]

View file

@ -1,332 +1,279 @@
import random import random
from datetime import datetime from typing import List, Tuple
import discord import discord
import pytz import pytz
from discord.ext import commands from discord.ext import commands
from loguru import logger
from lib import interaction from lib import interaction
from lib.constants import CONST from lib.constants import CONST
from lib.exceptions.LumiExceptions import LumiException from lib.exceptions.LumiExceptions import LumiException
from services.currency_service import Currency from services.currency_service import Currency
from services.stats_service import BlackJackStats from services.stats_service import BlackJackStats
from lib.embed_builder import EmbedBuilder
est = pytz.timezone("US/Eastern") EST = pytz.timezone("US/Eastern")
active_blackjack_games = {} ACTIVE_BLACKJACK_GAMES: dict[int, bool] = {}
Card = str
Hand = List[Card]
async def cmd(ctx, bet: int): async def cmd(ctx: commands.Context, bet: int) -> None:
""" if ctx.author.id in ACTIVE_BLACKJACK_GAMES:
status states:
0 = game start
1 = player busted
2 = player won with 21 (after hit)
3 = dealer busted
4 = dealer won
5 = player won with 21 (blackjack)
"""
# check if the player already has an active blackjack going
if ctx.author.id in active_blackjack_games:
raise LumiException(CONST.STRINGS["error_already_playing_blackjack"]) raise LumiException(CONST.STRINGS["error_already_playing_blackjack"])
# Currency handler currency = Currency(ctx.author.id)
ctx_currency = Currency(ctx.author.id) if bet > currency.balance:
raise LumiException(CONST.STRINGS["error_not_enough_cash"])
if bet <= 0:
raise LumiException(CONST.STRINGS["error_invalid_bet"])
# check if the user has enough cash ACTIVE_BLACKJACK_GAMES[ctx.author.id] = True
player_balance = ctx_currency.balance
if bet > player_balance:
raise commands.BadArgument("you don't have enough cash.")
elif bet <= 0:
raise commands.BadArgument("the bet you entered is invalid.")
active_blackjack_games[ctx.author.id] = True
try: try:
deck = get_new_deck() await play_blackjack(ctx, currency, bet)
multiplier = float(CONST.BLACKJACK["reward_multiplier"])
player_hand = [deal_card(deck), deal_card(deck)]
dealer_hand = [deal_card(deck)]
# calculate initial hands
player_hand_value = calculate_hand_value(player_hand)
dealer_hand_value = calculate_hand_value(dealer_hand)
status = 0 if player_hand_value != 21 else 5
view = interaction.BlackJackButtons(ctx)
playing_embed = False
while status == 0:
if not playing_embed:
await ctx.respond(
embed=blackjack_show(
ctx,
Currency.format_human(bet),
player_hand,
dealer_hand,
player_hand_value,
dealer_hand_value,
),
view=view,
content=ctx.author.mention,
)
playing_embed = True
await view.wait()
if view.clickedHit:
# player draws a card & value is calculated
player_hand.append(deal_card(deck))
player_hand_value = calculate_hand_value(player_hand)
if player_hand_value > 21:
status = 1
break
elif player_hand_value == 21:
status = 2
break
elif view.clickedStand:
# player stands, dealer draws cards until he wins OR busts
while dealer_hand_value <= player_hand_value:
dealer_hand.append(deal_card(deck))
dealer_hand_value = calculate_hand_value(dealer_hand)
status = 3 if dealer_hand_value > 21 else 4
break
else:
# timed out
ctx_currency.take_balance(bet)
ctx_currency.push()
raise LumiException(CONST.STRINGS["error_out_of_time_economy"])
# refresh
view = interaction.BlackJackButtons(ctx)
embed = blackjack_show(
ctx,
Currency.format_human(bet),
player_hand,
dealer_hand,
player_hand_value,
dealer_hand_value,
)
await ctx.edit(embed=embed, view=view, content=ctx.author.mention)
"""
At this point the game has concluded, generate a final output & backend
"""
payout = bet * multiplier if status != 5 else bet * 2
is_won = status not in [1, 4]
embed = blackjack_finished(
ctx,
Currency.format_human(bet),
player_hand_value,
dealer_hand_value,
Currency.format_human(payout),
status,
)
if playing_embed:
await ctx.edit(embed=embed, view=None, content=ctx.author.mention)
else:
await ctx.respond(embed=embed, view=None, content=ctx.author.mention)
# change balance
# if status == 1 or status == 4:
if not is_won:
ctx_currency.take_balance(bet)
ctx_currency.push()
# push stats (low priority)
stats = BlackJackStats(
user_id=ctx.author.id,
is_won=False,
bet=bet,
payout=0,
hand_player=player_hand,
hand_dealer=dealer_hand,
)
else:
ctx_currency.add_balance(payout)
ctx_currency.push()
# push stats (low priority)
stats = BlackJackStats(
user_id=ctx.author.id,
is_won=True,
bet=bet,
payout=payout,
hand_player=player_hand,
hand_dealer=dealer_hand,
)
stats.push()
except Exception as e: except Exception as e:
# await ctx.respond(embed=GenericErrors.default_exception(ctx)) raise LumiException(CONST.STRINGS["error_blackjack_game_error"]) from e
logger.error("Something went wrong in the blackjack command: ", e)
finally: finally:
# remove player from active games list del ACTIVE_BLACKJACK_GAMES[ctx.author.id]
del active_blackjack_games[ctx.author.id]
def blackjack_show( async def play_blackjack(ctx: commands.Context, currency: Currency, bet: int) -> None:
ctx, deck = get_new_deck()
bet, player_hand, dealer_hand = initial_deal(deck)
player_hand, multiplier = float(CONST.BLACKJACK["reward_multiplier"])
dealer_hand,
player_hand_value,
dealer_hand_value,
):
current_time = datetime.now(est).strftime("%I:%M %p")
embed = discord.Embed(
title="BlackJack",
color=discord.Color.dark_orange(),
)
embed.description = ( player_value = calculate_hand_value(player_hand)
f"**You**\n" status = 5 if player_value == 21 else 0
f"Score: {player_hand_value}\n" view = interaction.BlackJackButtons(ctx)
f"*Hand: {' + '.join(player_hand)}*\n\n" playing_embed = False
)
if len(dealer_hand) < 2: while status == 0:
embed.description += ( dealer_value = calculate_hand_value(dealer_hand)
f"**Dealer**\n"
f"Score: {dealer_hand_value}\n" embed = create_game_embed(
f"*Hand: {dealer_hand[0]} + ??*" ctx,
bet,
player_hand,
dealer_hand,
player_value,
dealer_value,
) )
if not playing_embed:
await ctx.respond(embed=embed, view=view, content=ctx.author.mention)
playing_embed = True
else:
await ctx.edit(embed=embed, view=view)
await view.wait()
if view.clickedHit:
player_hand.append(deal_card(deck))
player_value = calculate_hand_value(player_hand)
if player_value > 21:
status = 1
break
elif player_value == 21:
status = 2
break
elif view.clickedStand:
status = dealer_play(deck, dealer_hand, player_value)
break
else:
currency.take_balance(bet)
currency.push()
raise LumiException(CONST.STRINGS["error_out_of_time_economy"])
view = interaction.BlackJackButtons(ctx)
await handle_game_end(
ctx,
currency,
bet,
player_hand,
dealer_hand,
status,
multiplier,
playing_embed,
)
def initial_deal(deck: List[Card]) -> Tuple[Hand, Hand]:
return [deal_card(deck) for _ in range(2)], [deal_card(deck)]
def dealer_play(deck: List[Card], dealer_hand: Hand, player_value: int) -> int:
while calculate_hand_value(dealer_hand) <= player_value:
dealer_hand.append(deal_card(deck))
return 3 if calculate_hand_value(dealer_hand) > 21 else 4
async def handle_game_end(
ctx: commands.Context,
currency: Currency,
bet: int,
player_hand: Hand,
dealer_hand: Hand,
status: int,
multiplier: float,
playing_embed: bool,
) -> None:
player_value = calculate_hand_value(player_hand)
dealer_value = calculate_hand_value(dealer_hand)
payout = bet * (2 if status == 5 else multiplier)
is_won = status not in [1, 4]
embed = create_end_game_embed(ctx, bet, player_value, dealer_value, payout, status)
if playing_embed:
await ctx.edit(embed=embed, view=None)
else: else:
embed.description += ( await ctx.respond(embed=embed, view=None, content=ctx.author.mention)
f"**Dealer | Score: {dealer_hand_value}**\n"
f"*Hand: {' + '.join(dealer_hand)}*"
)
embed.set_author(name=ctx.author.name, icon_url=ctx.author.avatar.url) currency.add_balance(payout) if is_won else currency.take_balance(bet)
embed.set_footer( currency.push()
text=f"Bet ${bet} • deck shuffled • Today at {current_time}",
icon_url="https://i.imgur.com/96jPPXO.png", BlackJackStats(
user_id=ctx.author.id,
is_won=is_won,
bet=bet,
payout=payout if is_won else 0,
hand_player=player_hand,
hand_dealer=dealer_hand,
).push()
def create_game_embed(
ctx: commands.Context,
bet: int,
player_hand: Hand,
dealer_hand: Hand,
player_value: int,
dealer_value: int,
) -> discord.Embed:
player_hand_str = " + ".join(player_hand)
dealer_hand_str = f"{dealer_hand[0]} + " + (
CONST.STRINGS["blackjack_dealer_hidden"]
if len(dealer_hand) < 2
else " + ".join(dealer_hand[1:])
) )
if thumbnail_url := None: description = (
embed.set_thumbnail(url=thumbnail_url) f"{CONST.STRINGS['blackjack_player_hand'].format(player_value, player_hand_str)}\n\n"
f"{CONST.STRINGS['blackjack_dealer_hand'].format(dealer_value, dealer_hand_str)}"
return embed
def blackjack_finished(ctx, bet, player_hand_value, dealer_hand_value, payout, status):
current_time = datetime.now(est).strftime("%I:%M %p")
thumbnail_url = None
embed = discord.Embed(
title="BlackJack",
)
embed.description = (
f"You | Score: {player_hand_value}\n" f"Dealer | Score: {dealer_hand_value}"
)
embed.set_author(name=ctx.author.name, icon_url=ctx.author.avatar.url)
embed.set_footer(
text=f"Game finished • Today at {current_time}",
icon_url="https://i.imgur.com/96jPPXO.png",
) )
if status == 1: footer_text = (
name = "Busted.." f"{CONST.STRINGS['blackjack_bet'].format(Currency.format_human(bet))}"
value = f"You lost **${bet}**." f"{CONST.STRINGS['blackjack_deck_shuffled']}"
thumbnail_url = "https://i.imgur.com/rc68c43.png" )
color = discord.Color.red()
elif status == 2: return EmbedBuilder.create_embed(
name = "You won with a score of 21!" ctx,
value = f"You won **${payout}**." title=CONST.STRINGS["blackjack_title"],
thumbnail_url = "https://i.imgur.com/dvIIr2G.png" color=discord.Colour.embed_background(),
color = discord.Color.green() description=description,
footer_text=footer_text,
footer_icon_url=CONST.MUFFIN_ART,
show_name=False,
hide_timestamp=True,
)
elif status == 3:
name = "The dealer busted. You won!"
value = f"You won **${payout}**."
thumbnail_url = "https://i.imgur.com/dvIIr2G.png"
color = discord.Color.green()
elif status == 4: def create_end_game_embed(
name = "You lost.." ctx: commands.Context,
value = f"You lost **${bet}**." bet: int,
thumbnail_url = "https://i.imgur.com/rc68c43.png" player_value: int,
color = discord.Color.red() dealer_value: int,
payout: int,
status: int,
) -> discord.Embed:
embed = EmbedBuilder.create_embed(
ctx,
title=CONST.STRINGS["blackjack_title"],
color=discord.Colour.embed_background(),
description=CONST.STRINGS["blackjack_description"].format(
player_value,
dealer_value,
),
footer_text=CONST.STRINGS["blackjack_footer"],
footer_icon_url=CONST.MUFFIN_ART,
show_name=False,
)
elif status == 5: result = {
name = "You won with a natural hand!" 1: (
value = f"You won **${payout}**." CONST.STRINGS["blackjack_busted"],
thumbnail_url = "https://i.imgur.com/dvIIr2G.png" CONST.STRINGS["blackjack_lost"].format(Currency.format_human(bet)),
color = discord.Color.green() discord.Color.red(),
CONST.CLOUD_ART,
else: ),
name = "I.. don't know if you won?" 2: (
value = "This is an error, please report it." CONST.STRINGS["blackjack_won_21"],
color = discord.Color.red() CONST.STRINGS["blackjack_won_payout"].format(Currency.format_human(payout)),
discord.Color.green(),
CONST.TROPHY_ART,
),
3: (
CONST.STRINGS["blackjack_dealer_busted"],
CONST.STRINGS["blackjack_won_payout"].format(Currency.format_human(payout)),
discord.Color.green(),
CONST.TROPHY_ART,
),
4: (
CONST.STRINGS["blackjack_lost_generic"],
CONST.STRINGS["blackjack_lost"].format(Currency.format_human(bet)),
discord.Color.red(),
CONST.CLOUD_ART,
),
5: (
CONST.STRINGS["blackjack_won_natural"],
CONST.STRINGS["blackjack_won_payout"].format(Currency.format_human(payout)),
discord.Color.green(),
CONST.TROPHY_ART,
),
}.get(
status,
(
CONST.STRINGS["blackjack_error"],
CONST.STRINGS["blackjack_error_description"],
discord.Color.red(),
None,
),
)
name, value, color, thumbnail_url = result
embed.add_field(name=name, value=value, inline=False)
embed.colour = color
if thumbnail_url: if thumbnail_url:
embed.set_thumbnail(url=thumbnail_url) embed.set_thumbnail(url=thumbnail_url)
embed.add_field(
name=name,
value=value,
inline=False,
)
embed.colour = color
return embed return embed
def get_new_deck(): def get_new_deck() -> List[Card]:
suits = CONST.BLACKJACK["deck_suits"] deck = [
ranks = CONST.BLACKJACK["deck_ranks"] rank + suit
deck = [] for suit in CONST.BLACKJACK["deck_suits"]
for suit in suits: for rank in CONST.BLACKJACK["deck_ranks"]
for rank in ranks: ]
deck.append(rank + suit)
random.shuffle(deck) random.shuffle(deck)
return deck return deck
def deal_card(deck): def deal_card(deck: List[Card]) -> Card:
return deck.pop() return deck.pop()
def calculate_hand_value(hand): def calculate_hand_value(hand: Hand) -> int:
value = 0 value = sum(
has_ace = False 10 if rank in "JQK" else 11 if rank == "A" else int(rank)
aces_count = 0 for card in hand
for rank in card[:-1]
for card in hand: )
if card is None: aces = sum(card[0] == "A" for card in hand)
continue while value > 21 and aces:
value -= 10
rank = card[:-1] aces -= 1
if rank.isdigit():
value += int(rank)
elif rank in ["J", "Q", "K"]:
value += 10
elif rank == "A":
value += 11
has_ace = True
aces_count += 1
"""
An Ace will have a value of 11 unless that would give a player
or the dealer a score in excess of 21; in which case, it has a value of 1
"""
if value > 21 and has_ace:
value -= 10 * aces_count
return value return value