Ruby-Cogs/casino/engine.py

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