322 lines
12 KiB
Python
322 lines
12 KiB
Python
# Standard Library
|
|
import calendar
|
|
from functools import wraps
|
|
|
|
# Casino
|
|
from typing import Optional
|
|
|
|
from redbot.core.utils.chat_formatting import humanize_number
|
|
|
|
from . import utils
|
|
from .data import Database
|
|
|
|
# Red
|
|
from redbot.core import bank
|
|
from redbot.core.errors import BalanceTooHigh
|
|
from redbot.core.i18n import Translator
|
|
|
|
# Discord
|
|
import discord
|
|
|
|
_ = Translator("Casino", __file__)
|
|
|
|
|
|
def game_engine(name=None, choice=None, choices=None):
|
|
def wrapper(coro):
|
|
@wraps(coro)
|
|
async def wrapped(*args, **kwargs):
|
|
try:
|
|
user_choice = args[3]
|
|
except IndexError:
|
|
user_choice = None
|
|
engine = GameEngine(name, user_choice, choice, args[1], args[2])
|
|
if await engine.check_conditions():
|
|
result = await coro(*args, **kwargs)
|
|
await engine.game_teardown(result)
|
|
|
|
return wrapped
|
|
|
|
return wrapper
|
|
|
|
|
|
class GameEngine(Database):
|
|
"""A class that handles setup and teardown for games.
|
|
|
|
This is a helper class to make games easier to create games and to
|
|
provide a level of consistency. This class is only to be used
|
|
in conjunction with the game_engine decorator.
|
|
|
|
You only need to specify the name, and depending on the game, a choice or
|
|
a list of choices to choose from. The decorater will obtain the rest of the
|
|
attributes.
|
|
|
|
Attributes
|
|
-----------
|
|
game: str
|
|
The name of the game.
|
|
choice: str
|
|
The decision the player chose for the game. When a decision is not
|
|
required, leave it None.
|
|
choices: list
|
|
A list of choices the player must pick from. If a list of choices is not
|
|
required, leave it None.
|
|
ctx: object
|
|
The Red context object necessary for sending/waiting for messages.
|
|
player: object
|
|
User or member object necessary for interacting with the player.
|
|
guild: object
|
|
The guild object from the Red Context object. This is used to pull data
|
|
from config.
|
|
bet: int
|
|
The amount the player has wagered.
|
|
|
|
"""
|
|
|
|
__slots__ = ("game", "choice", "choices", "ctx", "bet", "player", "guild")
|
|
|
|
def __init__(self, game, choice, choices, ctx, bet):
|
|
self.game = game
|
|
self.choice = choice
|
|
self.choices = choices
|
|
self.bet = bet
|
|
self.ctx = ctx
|
|
self.player = ctx.author
|
|
self.guild = ctx.guild
|
|
super().__init__()
|
|
|
|
async def check_conditions(self):
|
|
"""
|
|
|
|
Performs all the necessary checks for a game. Every game must validate under these specific
|
|
checks. The following conditions are checked:
|
|
|
|
- Checking to see if the casino is open
|
|
- Checking to see if the game is open
|
|
- Checking to see if the player has a high enough access level to play the game.
|
|
- Validating that the player's choice is in the list of declared choices.
|
|
- Checking that the bet is within the range of the set min and max.
|
|
- Checking to see that has enough currency in the bank account to cover the bet.
|
|
- Checking to see if the game is on cooldown.
|
|
|
|
Cooldowns must be checked last so that the game doesn't trigger a cooldown if another
|
|
condition has failed.
|
|
|
|
|
|
"""
|
|
settings, player_data = await super().get_all(self.ctx, self.player)
|
|
access = self.access_calculator(settings["Memberships"], player_data["Membership"]["Name"])
|
|
|
|
if not settings["Settings"]["Casino_Open"]:
|
|
error = _("The Casino is closed.")
|
|
|
|
elif not settings["Games"][self.game]["Open"]:
|
|
error = _("{} is closed.".format(self.game))
|
|
|
|
elif settings["Games"][self.game]["Access"] > access:
|
|
error = _(
|
|
"{} requires an access level of {}. Your current access level is {}. Obtain "
|
|
"a higher membership to play this game."
|
|
).format(self.game, settings["Games"][self.game]["Access"], access)
|
|
|
|
elif self.choices is not None and self.choice not in self.choices:
|
|
error = _("Incorrect response. Accepted responses are:\n{}.").format(utils.fmt_join(self.choices))
|
|
|
|
elif not self.bet_in_range(
|
|
settings["Games"][self.game]["Min"], settings["Games"][self.game]["Max"]
|
|
):
|
|
error = _(
|
|
"Your bet must be between "
|
|
"{} and {}.".format(
|
|
settings["Games"][self.game]["Min"], settings["Games"][self.game]["Max"]
|
|
)
|
|
)
|
|
|
|
elif not await bank.can_spend(self.player, self.bet):
|
|
error = _("You do not have enough credits to cover the bet.")
|
|
|
|
else:
|
|
error = await self.check_cooldown(settings["Games"][self.game], player_data)
|
|
|
|
if error:
|
|
await self.ctx.send(error)
|
|
return False
|
|
else:
|
|
await bank.withdraw_credits(self.player, self.bet)
|
|
await self.update_stats(stat="Played")
|
|
return True
|
|
|
|
async def update_stats(self, stat: str):
|
|
"""
|
|
|
|
:param stat: string
|
|
Must be Played or Won
|
|
:return: None
|
|
|
|
Updates either a player's win or played stat.
|
|
"""
|
|
instance = await self.get_data(self.ctx, player=self.player)
|
|
current = await instance.get_raw(stat, self.game)
|
|
await instance.set_raw(stat, self.game, value=current + 1)
|
|
|
|
async def check_cooldown(self, game_data, player_data):
|
|
"""
|
|
|
|
:param game_data: Dictionary
|
|
Contains all the data pertaining to a particular game.
|
|
:param player_data: Object
|
|
User or member Object
|
|
:return: String or None
|
|
Returns a string when a cooldown is remaining on a game, otherwise it will
|
|
return None
|
|
|
|
Checks the time a player last played a game, and compares it with the set cooldown
|
|
for that game. If a user is still on cooldown, then a string detailing the time
|
|
remaining will be returned. Otherwise this will update their cooldown, and return None.
|
|
|
|
"""
|
|
user_time = player_data["Cooldowns"][self.game]
|
|
now = calendar.timegm(self.ctx.message.created_at.utctimetuple())
|
|
base = game_data["Cooldown"]
|
|
membership = await super()._get_player_membership(self.ctx, self.player)
|
|
reduction = membership[1]["Reduction"]
|
|
if now >= user_time - reduction:
|
|
await super()._update_cooldown(self.ctx, self.game, now + base)
|
|
else:
|
|
seconds = int((user_time + reduction - now))
|
|
remaining = utils.time_formatter(seconds)
|
|
msg = _("{} is still on a cooldown. You still have: {} remaining.").format(self.game, remaining)
|
|
return msg
|
|
|
|
async def game_teardown(self, result):
|
|
data = await super().get_data(self.ctx)
|
|
settings = await data.all()
|
|
message_obj: Optional[discord.Message]
|
|
|
|
win, amount, msg, message_obj = result
|
|
|
|
if not win:
|
|
embed = await self.build_embed(msg, settings, win, total=amount, bonus="(+0)")
|
|
if (not await self.old_message_cache.get_guild(self.ctx.guild)) and message_obj:
|
|
return await message_obj.edit(content=self.player.mention, embed=embed, view=None)
|
|
else:
|
|
return await self.ctx.send(self.player.mention, embed=embed, view=None)
|
|
|
|
player_data = await super().get_data(self.ctx, player=self.player)
|
|
await self.update_stats(stat="Won")
|
|
if self.limit_check(settings, amount):
|
|
embed = await self.build_embed(msg, settings, win, total=amount, bonus="(+0)")
|
|
return await self.limit_handler(
|
|
embed,
|
|
amount,
|
|
player_data,
|
|
settings["Settings"]["Payout_Limit"],
|
|
message=message_obj,
|
|
)
|
|
|
|
total, bonus = await self.deposit_winnings(amount, player_data, settings)
|
|
embed = await self.build_embed(msg, settings, win, total=total, bonus=bonus)
|
|
if (not await self.old_message_cache.get_guild(self.ctx.guild)) and message_obj:
|
|
return await message_obj.edit(content=self.player.mention, embed=embed, view=None)
|
|
else:
|
|
return await self.ctx.send(self.player.mention, embed=embed, view=None)
|
|
|
|
async def limit_handler(self, embed, amount, player_instance, limit, message):
|
|
await player_instance.Pending_Credits.set(int(amount))
|
|
|
|
if (not await self.old_message_cache.get_guild(self.ctx.guild)) and message:
|
|
await message.edit(content=self.player.mention, embed=embed)
|
|
else:
|
|
await self.ctx.send(self.player.mention, embed=embed)
|
|
msg = _(
|
|
"{} Your winnings exceeded the maximum credit limit allowed ({}). The amount "
|
|
"of {} credits will be pending on your account until reviewed. Until an "
|
|
"Administrator or higher authority has released the pending currency, "
|
|
"**DO NOT** attempt to place a bet that will exceed the payout limit. You "
|
|
"may only have **ONE** pending payout at a "
|
|
"time."
|
|
).format(self.player.name, limit, amount)
|
|
|
|
await self.player.send(msg)
|
|
|
|
async def deposit_winnings(self, amount, player_instance, settings):
|
|
multiplier = settings["Games"][self.game]["Multiplier"]
|
|
if self.game == "Allin" or self.game == "Double":
|
|
try:
|
|
await bank.deposit_credits(self.player, amount)
|
|
return amount, "(+0)"
|
|
except BalanceTooHigh as e:
|
|
return await bank.set_balance(self.player, e.max_balance), "(+0)"
|
|
|
|
initial = round(amount * multiplier)
|
|
total, amt, msg = await self.calculate_bonus(initial, player_instance, settings)
|
|
try:
|
|
await bank.deposit_credits(self.player, total)
|
|
except BalanceTooHigh as e:
|
|
await bank.set_balance(self.player, e.max_balance)
|
|
return total, msg
|
|
|
|
def bet_in_range(self, minimum, maximum):
|
|
if self.game == "Allin":
|
|
return True
|
|
|
|
if minimum <= self.bet <= maximum:
|
|
return True
|
|
else:
|
|
return False
|
|
|
|
async def build_embed(self, msg, settings, win, total, bonus):
|
|
balance = await bank.get_balance(self.player)
|
|
currency = await bank.get_currency_name(self.guild)
|
|
bal_msg = _("**Remaining Balance:** {} {}").format(humanize_number(balance), currency)
|
|
embed = discord.Embed()
|
|
embed.title = _("{} Casino | {}").format(settings["Settings"]["Casino_Name"], self.game)
|
|
|
|
if isinstance(msg, discord.Embed):
|
|
for field in msg.fields:
|
|
embed.add_field(**field.__dict__)
|
|
else:
|
|
embed.description = msg
|
|
|
|
if win:
|
|
embed.colour = 0x00FF00
|
|
end = _("Congratulations, you just won {} {} {}!\n{}").format(
|
|
humanize_number(total), currency, bonus, bal_msg
|
|
)
|
|
else:
|
|
embed.colour = 0xFF0000
|
|
end = _("Sorry, you didn't win anything.\n{}").format(bal_msg)
|
|
embed.add_field(name="-" * 65, value=end)
|
|
return embed
|
|
|
|
@staticmethod
|
|
def access_calculator(memberships, user_membership):
|
|
if user_membership == "Basic":
|
|
return 0
|
|
|
|
try:
|
|
access = memberships[user_membership]["Access"]
|
|
except KeyError:
|
|
return 0
|
|
else:
|
|
return access
|
|
|
|
@staticmethod
|
|
async def calculate_bonus(amount, player_instance, settings):
|
|
membership = await player_instance.Membership.Name()
|
|
try:
|
|
bonus_multiplier = settings["Memberships"][membership]["Bonus"]
|
|
except KeyError:
|
|
bonus_multiplier = 1
|
|
total = round(amount * bonus_multiplier)
|
|
bonus = total - amount
|
|
return total, amount, "(+{})".format(humanize_number(bonus) if bonus_multiplier > 1 else 0)
|
|
|
|
@staticmethod
|
|
def limit_check(settings, amount):
|
|
if settings["Settings"]["Payout_Switch"]:
|
|
if amount > settings["Settings"]["Payout_Limit"]:
|
|
return True
|
|
else:
|
|
return False
|
|
else:
|
|
return False
|