Refactor Shop Cog to improve inventory management and trading functionalities. Enhance shop management commands for better item handling and user interaction. Update documentation for clarity on new features and usage.
Some checks are pending
Run pre-commit / Run pre-commit (push) Waiting to run
Some checks are pending
Run pre-commit / Run pre-commit (push) Waiting to run
This commit is contained in:
parent
7fc2053951
commit
d507065184
9 changed files with 3006 additions and 0 deletions
9
casino/__init__.py
Normal file
9
casino/__init__.py
Normal file
|
@ -0,0 +1,9 @@
|
|||
from .casino import Casino
|
||||
|
||||
__red_end_user_data_statement__ = "This cog stores discord IDs as needed for operation."
|
||||
|
||||
|
||||
async def setup(bot):
|
||||
cog = Casino(bot)
|
||||
await bot.add_cog(cog)
|
||||
await cog.initialise()
|
30
casino/cache.py
Normal file
30
casino/cache.py
Normal file
|
@ -0,0 +1,30 @@
|
|||
from typing import Dict, Optional
|
||||
|
||||
import discord
|
||||
from redbot.core import Config
|
||||
|
||||
|
||||
class OldMessageTypeManager:
|
||||
def __init__(self, config: Config, enable_cache: bool = True):
|
||||
self._config: Config = config
|
||||
self.enable_cache = enable_cache
|
||||
self._cached_guild: Dict[int, bool] = {}
|
||||
|
||||
async def get_guild(self, guild: discord.Guild) -> bool:
|
||||
ret: bool
|
||||
gid: int = guild.id
|
||||
if self.enable_cache and gid in self._cached_guild:
|
||||
ret = self._cached_guild[gid]
|
||||
else:
|
||||
ret = await self._config.guild_from_id(gid).use_old_style()
|
||||
self._cached_guild[gid] = ret
|
||||
return ret
|
||||
|
||||
async def set_guild(self, guild: discord.Guild, set_to: Optional[bool]) -> None:
|
||||
gid: int = guild.id
|
||||
if set_to is not None:
|
||||
await self._config.guild_from_id(gid).use_old_style.set(set_to)
|
||||
self._cached_guild[gid] = set_to
|
||||
else:
|
||||
await self._config.guild_from_id(gid).use_old_style.clear()
|
||||
self._cached_guild[gid] = self._config.defaults["GUILD"]["use_old_style"]
|
1382
casino/casino.py
Normal file
1382
casino/casino.py
Normal file
File diff suppressed because it is too large
Load diff
413
casino/data.py
Normal file
413
casino/data.py
Normal file
|
@ -0,0 +1,413 @@
|
|||
import asyncio
|
||||
import logging
|
||||
from copy import deepcopy
|
||||
|
||||
import discord
|
||||
from redbot.core import Config, bank
|
||||
from collections import namedtuple
|
||||
|
||||
from .cache import OldMessageTypeManager
|
||||
from .utils import is_input_unsupported, min_int, max_int
|
||||
|
||||
user_defaults = {
|
||||
"Pending_Credits": 0,
|
||||
"Membership": {"Name": "Basic", "Assigned": False},
|
||||
"Played": {
|
||||
"Allin": 0,
|
||||
"Blackjack": 0,
|
||||
"Coin": 0,
|
||||
"Craps": 0,
|
||||
"Cups": 0,
|
||||
"Dice": 0,
|
||||
"Hilo": 0,
|
||||
"War": 0,
|
||||
"Double": 0,
|
||||
},
|
||||
"Won": {
|
||||
"Allin": 0,
|
||||
"Blackjack": 0,
|
||||
"Coin": 0,
|
||||
"Craps": 0,
|
||||
"Cups": 0,
|
||||
"Dice": 0,
|
||||
"Hilo": 0,
|
||||
"War": 0,
|
||||
"Double": 0,
|
||||
},
|
||||
"Cooldowns": {
|
||||
"Allin": 0,
|
||||
"Blackjack": 0,
|
||||
"Coin": 0,
|
||||
"Craps": 0,
|
||||
"Cups": 0,
|
||||
"Dice": 0,
|
||||
"Hilo": 0,
|
||||
"War": 0,
|
||||
"Double": 0,
|
||||
},
|
||||
}
|
||||
|
||||
guild_defaults = {
|
||||
"use_old_style": False,
|
||||
"Memberships": {},
|
||||
"Settings": {
|
||||
"Global": False,
|
||||
"Casino_Name": "Redjumpman's",
|
||||
"Casino_Open": True,
|
||||
"Payout_Switch": False,
|
||||
"Payout_Limit": 10000,
|
||||
},
|
||||
"Games": {
|
||||
"Allin": {
|
||||
"Access": 0,
|
||||
"Cooldown": 43200,
|
||||
"Min": None,
|
||||
"Max": None,
|
||||
"Multiplier": None,
|
||||
"Open": True,
|
||||
},
|
||||
"Blackjack": {
|
||||
"Access": 0,
|
||||
"Cooldown": 5,
|
||||
"Min": 50,
|
||||
"Max": 500,
|
||||
"Multiplier": 2.0,
|
||||
"Open": True,
|
||||
},
|
||||
"Coin": {
|
||||
"Access": 0,
|
||||
"Cooldown": 5,
|
||||
"Max": 10,
|
||||
"Min": 10,
|
||||
"Multiplier": 1.5,
|
||||
"Open": True,
|
||||
},
|
||||
"Craps": {
|
||||
"Access": 0,
|
||||
"Cooldown": 5,
|
||||
"Max": 500,
|
||||
"Min": 50,
|
||||
"Multiplier": 2.0,
|
||||
"Open": True,
|
||||
},
|
||||
"Cups": {
|
||||
"Access": 0,
|
||||
"Cooldown": 5,
|
||||
"Max": 100,
|
||||
"Min": 25,
|
||||
"Multiplier": 1.8,
|
||||
"Open": True,
|
||||
},
|
||||
"Dice": {
|
||||
"Access": 0,
|
||||
"Cooldown": 5,
|
||||
"Max": 100,
|
||||
"Min": 25,
|
||||
"Multiplier": 1.8,
|
||||
"Open": True,
|
||||
},
|
||||
"Hilo": {
|
||||
"Access": 0,
|
||||
"Cooldown": 5,
|
||||
"Min": 25,
|
||||
"Max": 75,
|
||||
"Multiplier": 1.7,
|
||||
"Open": True,
|
||||
},
|
||||
"Double": {
|
||||
"Access": 0,
|
||||
"Cooldown": 5,
|
||||
"Min": 10,
|
||||
"Max": 250,
|
||||
"Multiplier": None,
|
||||
"Open": True,
|
||||
},
|
||||
"War": {"Access": 0, "Cooldown": 5, "Min": 25, "Max": 75, "Multiplier": 1.5, "Open": True},
|
||||
},
|
||||
}
|
||||
|
||||
member_defaults = deepcopy(user_defaults)
|
||||
global_defaults = deepcopy(guild_defaults)
|
||||
global_defaults["Settings"]["Global"] = True
|
||||
|
||||
|
||||
_DataNamedTuple = namedtuple("Casino", "foo")
|
||||
_DataObj = _DataNamedTuple(foo=None)
|
||||
|
||||
|
||||
log = logging.getLogger("red.jumper-plugins.casino")
|
||||
|
||||
|
||||
class Database:
|
||||
|
||||
config: Config = Config.get_conf(_DataObj, 5074395001, force_registration=True)
|
||||
|
||||
def __init__(self):
|
||||
self.config.register_guild(**guild_defaults)
|
||||
self.config.register_global(schema_version=1, **global_defaults)
|
||||
self.config.register_member(**member_defaults)
|
||||
self.config.register_user(**user_defaults)
|
||||
self.old_message_cache = OldMessageTypeManager(config=self.config, enable_cache=True)
|
||||
self.migration_task: asyncio.Task = None
|
||||
self.cog_ready_event: asyncio.Event = asyncio.Event()
|
||||
|
||||
async def data_schema_migration(self, from_version: int, to_version: int):
|
||||
if from_version == to_version:
|
||||
self.cog_ready_event.set()
|
||||
return
|
||||
if from_version < 2 <= to_version:
|
||||
try:
|
||||
async with self.config.all() as casino_data:
|
||||
temp = deepcopy(casino_data)
|
||||
global_payout = casino_data["Settings"]["Payout_Limit"]
|
||||
if is_input_unsupported(global_payout):
|
||||
casino_data["Settings"]["Payout_Limit"] = await bank.get_max_balance()
|
||||
for g, g_data in temp["Games"].items():
|
||||
for g_data_key, g_data_value in g_data.items():
|
||||
if g_data_key in ["Access", "Cooldown", "Max", "Min", "Multiplier"]:
|
||||
if is_input_unsupported(g_data_value):
|
||||
if g_data_value < min_int:
|
||||
g_data_value_new = min_int
|
||||
else:
|
||||
g_data_value_new = max_int
|
||||
casino_data["Games"][g][g_data_key] = g_data_value_new
|
||||
async with self.config._get_base_group(self.config.GUILD).all() as casino_data:
|
||||
temp = deepcopy(casino_data)
|
||||
for guild_id, guild_data in temp.items():
|
||||
if (
|
||||
"Settings" in temp[guild_id]
|
||||
and "Payout_Limit" in temp[guild_id]["Settings"]
|
||||
):
|
||||
guild_payout = casino_data[guild_id]["Settings"]["Payout_Limit"]
|
||||
if is_input_unsupported(guild_payout):
|
||||
casino_data[guild_id]["Settings"][
|
||||
"Payout_Limit"
|
||||
] = await bank.get_max_balance(
|
||||
guild_payout, guild=discord.Object(id=int(guild_id))
|
||||
)
|
||||
if "Games" in temp[guild_id]:
|
||||
for g, g_data in temp[guild_id]["Games"].items():
|
||||
for g_data_key, g_data_value in g_data.items():
|
||||
if g_data_key in [
|
||||
"Access",
|
||||
"Cooldown",
|
||||
"Max",
|
||||
"Min",
|
||||
"Multiplier",
|
||||
]:
|
||||
if is_input_unsupported(g_data_value):
|
||||
if g_data_value < min_int:
|
||||
g_data_value_new = min_int
|
||||
else:
|
||||
g_data_value_new = max_int
|
||||
casino_data[guild_id]["Games"][g][
|
||||
g_data_key
|
||||
] = g_data_value_new
|
||||
await self.config.schema_version.set(2)
|
||||
except Exception as e:
|
||||
log.exception(
|
||||
"Fatal Exception during Data migration to Scheme 2, Casino cog will not be loaded.",
|
||||
exc_info=e,
|
||||
)
|
||||
raise
|
||||
self.cog_ready_event.set()
|
||||
|
||||
async def casino_is_global(self):
|
||||
"""Checks to see if the casino is storing data on
|
||||
a per server basis or globally."""
|
||||
return await self.config.Settings.Global()
|
||||
|
||||
async def get_data(self, ctx, player=None):
|
||||
"""
|
||||
|
||||
:param ctx: Context object
|
||||
:param player: Member or user object
|
||||
:return: Database that corresponds to the given data.
|
||||
|
||||
Returns the appropriate config category based on the given
|
||||
data, and wheater or not the casino is global.
|
||||
"""
|
||||
if await self.casino_is_global():
|
||||
if player is None:
|
||||
return self.config
|
||||
else:
|
||||
return self.config.user(player)
|
||||
else:
|
||||
if player is None:
|
||||
return self.config.guild(ctx.guild)
|
||||
else:
|
||||
return self.config.member(player)
|
||||
|
||||
async def get_all(self, ctx, player):
|
||||
"""
|
||||
|
||||
:param ctx: Context Object
|
||||
:param player: Member or user object
|
||||
:return: Tuple with two dictionaries
|
||||
|
||||
Returns a dictionary representation of casino's settings data
|
||||
and the player data.
|
||||
"""
|
||||
settings = await self.get_data(ctx)
|
||||
player_data = await self.get_data(ctx, player=player)
|
||||
return await settings.all(), await player_data.all()
|
||||
|
||||
async def _wipe_casino(self, ctx):
|
||||
"""
|
||||
Wipes all the casino data available
|
||||
|
||||
:param ctx: context object
|
||||
:return: None
|
||||
|
||||
This wipes everything, including member/user data.
|
||||
"""
|
||||
await self.config.clear_all()
|
||||
msg = "{0.name} ({0.id}) wiped all casino data.".format(ctx.author)
|
||||
await ctx.send(msg)
|
||||
|
||||
async def _reset_settings(self, ctx):
|
||||
"""
|
||||
Resets only the settings data.
|
||||
"""
|
||||
data = await self.get_data(ctx)
|
||||
await data.Settings.clear()
|
||||
msg = ("{0.name} ({0.id}) reset all casino settings.").format(ctx.author)
|
||||
await ctx.send(msg)
|
||||
|
||||
async def _reset_memberships(self, ctx):
|
||||
"""
|
||||
Resets all the information pertaining to memberships
|
||||
"""
|
||||
data = await self.get_data(ctx)
|
||||
await data.Memberships.clear()
|
||||
msg = ("{0.name} ({0.id}) cleared all casino memberships.").format(ctx.author)
|
||||
await ctx.send(msg)
|
||||
|
||||
async def _reset_games(self, ctx):
|
||||
"""
|
||||
Resets all game settings, such as multipliers and bets.
|
||||
"""
|
||||
data = await self.get_data(ctx)
|
||||
await data.Games.clear()
|
||||
msg = ("{0.name} ({0.id}) restored casino games to default settings.").format(ctx.author)
|
||||
await ctx.send(msg)
|
||||
|
||||
async def _reset_all_settings(self, ctx):
|
||||
"""
|
||||
Resets all settings, but retains all player data.
|
||||
"""
|
||||
await self._reset_settings(ctx)
|
||||
await self._reset_memberships(ctx)
|
||||
await self._reset_games(ctx)
|
||||
await self._reset_cooldowns(ctx)
|
||||
|
||||
async def _reset_player_stats(self, ctx, player):
|
||||
"""
|
||||
:param ctx: Context object
|
||||
:param player: user or member object
|
||||
:return: None
|
||||
|
||||
Resets a player's win / played stats.
|
||||
|
||||
"""
|
||||
data = await self.get_data(ctx, player=player)
|
||||
await data.Played.clear()
|
||||
await data.Won.clear()
|
||||
|
||||
msg = ("{0.name} ({0.id}) reset all stats for {1.name} ({1.id}).").format(ctx.author, player)
|
||||
await ctx.send(msg)
|
||||
|
||||
async def _reset_player_all(self, ctx, player):
|
||||
"""
|
||||
|
||||
:param ctx: context object
|
||||
:param player: user or member object
|
||||
:return: None
|
||||
|
||||
Resets all data belonging to the user, including stats and memberships.
|
||||
"""
|
||||
data = await self.get_data(ctx, player=player)
|
||||
await data.clear()
|
||||
|
||||
msg = ("{0.name} ({0.id}) reset all data for {1.name} ({1.id}).").format(ctx.author, player)
|
||||
await ctx.send(msg)
|
||||
|
||||
async def _reset_player_cooldowns(self, ctx, player):
|
||||
"""
|
||||
|
||||
:param ctx: context object
|
||||
:param player: user or member object
|
||||
:return: None
|
||||
|
||||
Resets all game cooldowns for a player.
|
||||
"""
|
||||
data = await self.get_data(ctx, player=player)
|
||||
await data.Cooldowns.clear()
|
||||
|
||||
msg = ("{0.name} ({0.id}) reset all cooldowns for {1.name} ({1.id}).").format(ctx.author, player)
|
||||
await ctx.send(msg)
|
||||
|
||||
async def _reset_cooldowns(self, ctx):
|
||||
"""
|
||||
Resets all game cooldowns for every player in the database.
|
||||
"""
|
||||
if await self.casino_is_global():
|
||||
for player in await self.config.all_users():
|
||||
user = discord.Object(id=player)
|
||||
await self.config.user(user).Cooldowns.clear()
|
||||
msg = ("{0.name} ({0.id}) reset all global cooldowns.").format(ctx.author)
|
||||
else:
|
||||
for player in await self.config.all_members(ctx.guild):
|
||||
user = discord.Object(id=player)
|
||||
await self.config.member(user).Cooldowns.clear()
|
||||
msg = ("{0.name} ({0.id}) reset all cooldowns on {1.name}.").format(ctx.author, ctx.guild)
|
||||
|
||||
await ctx.send(msg)
|
||||
|
||||
async def change_mode(self, mode):
|
||||
"""
|
||||
|
||||
:param mode: String, must be local or global.
|
||||
:return: None
|
||||
|
||||
Toggles how data is stored for casino between local and global.
|
||||
When switching modes, all perviously stored data will be deleted.
|
||||
"""
|
||||
if mode == "global":
|
||||
await self.config.clear_all_members()
|
||||
await self.config.clear_all_guilds()
|
||||
await self.config.Settings.Global.set(True)
|
||||
else:
|
||||
await self.config.clear_all_users()
|
||||
await self.config.clear_all_globals()
|
||||
await self.config.Settings.Global.set(False)
|
||||
|
||||
async def _update_cooldown(self, ctx, game, time):
|
||||
player_data = await self.get_data(ctx, player=ctx.author)
|
||||
await player_data.set_raw("Cooldowns", game, value=time)
|
||||
|
||||
async def _get_player_membership(self, ctx, player):
|
||||
"""
|
||||
|
||||
:param ctx: context object
|
||||
:param player: user or member object
|
||||
:return: Membership name and a dictionary with the perks
|
||||
|
||||
Performs a lookup on the user and the created memberhips for casino.
|
||||
If the user has a memberhip that was deleted, it will return the
|
||||
default basic membership. It will also set their new membership to the
|
||||
default.
|
||||
"""
|
||||
basic = {"Reduction": 0, "Access": 0, "Color": "grey", "Bonus": 1}
|
||||
player_data = await self.get_data(ctx, player=player)
|
||||
name = await player_data.Membership.Name()
|
||||
if name == "Basic":
|
||||
return name, basic
|
||||
|
||||
data = await self.get_data(ctx)
|
||||
memberships = await data.Memberships.all()
|
||||
try:
|
||||
return name, memberships[name]
|
||||
except KeyError:
|
||||
await player_data.Membership.set({"Name": "Basic", "Assigned": False})
|
||||
return "Basic", basic
|
117
casino/deck.py
Normal file
117
casino/deck.py
Normal file
|
@ -0,0 +1,117 @@
|
|||
import random
|
||||
from collections import deque
|
||||
from itertools import product, chain
|
||||
|
||||
|
||||
class Deck:
|
||||
"""Creates a Deck of playing cards."""
|
||||
|
||||
suites = (":clubs:", ":diamonds:", ":hearts:", ":spades:")
|
||||
face_cards = ("King", "Queen", "Jack", "Ace")
|
||||
bj_vals = {"Jack": 10, "Queen": 10, "King": 10, "Ace": 1}
|
||||
war_values = {"Jack": 11, "Queen": 12, "King": 13, "Ace": 14}
|
||||
|
||||
def __init__(self):
|
||||
self._deck = deque()
|
||||
|
||||
def __len__(self):
|
||||
return len(self._deck)
|
||||
|
||||
def __str__(self):
|
||||
return "Standard deck of cards with {} cards remaining.".format(len(self._deck))
|
||||
|
||||
def __repr__(self):
|
||||
return "Deck{!r}".format(self._deck)
|
||||
|
||||
@property
|
||||
def deck(self):
|
||||
if len(self._deck) < 1:
|
||||
self.new()
|
||||
return self._deck
|
||||
|
||||
def shuffle(self):
|
||||
random.shuffle(self._deck)
|
||||
|
||||
def war_count(self, card):
|
||||
try:
|
||||
return self.war_values[card[1]]
|
||||
except KeyError:
|
||||
return card[1]
|
||||
|
||||
def bj_count(self, hand: list, hole=False):
|
||||
hand = self._hand_type(hand)
|
||||
if hole:
|
||||
card = hand[0][1]
|
||||
count = self.bj_vals[card] if isinstance(card, str) else card
|
||||
return count if count > 1 else 11
|
||||
|
||||
count = sum([self.bj_vals[y] if isinstance(y, str) else y for x, y in hand])
|
||||
if any("Ace" in pair for pair in hand) and count <= 11:
|
||||
count += 10
|
||||
return count
|
||||
|
||||
@staticmethod
|
||||
def fmt_hand(hand: list):
|
||||
return ["{} {}".format(y, x) for x, y in hand]
|
||||
|
||||
@staticmethod
|
||||
def fmt_card(card):
|
||||
return "{1} {0}".format(*card)
|
||||
|
||||
@staticmethod
|
||||
def hand_check(hand: list, card):
|
||||
return any(x[1] == card for x in hand)
|
||||
|
||||
def split(self, position: int):
|
||||
self._deck.rotate(-position)
|
||||
|
||||
@staticmethod
|
||||
def _true_hand(hand: list):
|
||||
return [x.split(" ") for x in hand]
|
||||
|
||||
def draw(self, top=True):
|
||||
self._check()
|
||||
|
||||
if top:
|
||||
card = self._deck.popleft()
|
||||
else:
|
||||
card = self._deck.pop()
|
||||
return card
|
||||
|
||||
def _check(self, num=1):
|
||||
if num > 52:
|
||||
raise ValueError("Can not exceed deck limit.")
|
||||
if len(self._deck) < num:
|
||||
self.new()
|
||||
|
||||
def _hand_type(self, hand: list):
|
||||
if isinstance(hand[0], tuple):
|
||||
return hand
|
||||
|
||||
try:
|
||||
return self._true_hand(hand)
|
||||
except ValueError:
|
||||
raise ValueError("Invalid hand input.")
|
||||
|
||||
def deal(self, num=1, top=True, hand=None):
|
||||
self._check(num=num)
|
||||
|
||||
if hand is None:
|
||||
hand = []
|
||||
for x in range(0, num):
|
||||
if top:
|
||||
hand.append(self._deck.popleft())
|
||||
else:
|
||||
hand.append(self._deck.pop())
|
||||
|
||||
return hand
|
||||
|
||||
def burn(self, num):
|
||||
self._check(num=num)
|
||||
for x in range(0, num):
|
||||
del self._deck[0]
|
||||
|
||||
def new(self):
|
||||
cards = product(self.suites, chain(range(2, 11), ("King", "Queen", "Jack", "Ace")))
|
||||
self._deck = deque(cards)
|
||||
self.shuffle()
|
322
casino/engine.py
Normal file
322
casino/engine.py
Normal file
|
@ -0,0 +1,322 @@
|
|||
# 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
|
592
casino/games.py
Normal file
592
casino/games.py
Normal file
|
@ -0,0 +1,592 @@
|
|||
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
|
13
casino/info.json
Normal file
13
casino/info.json
Normal file
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"author" : ["Redjumpman (Redjumpman#1337)"],
|
||||
"install_msg" : "Thank you for installing casino. Be sure to check out the wiki here: https://github.com/Redjumpman/Jumper-Plugins/wiki/Casino-RedV3\nThis cog may put a heavy load on your bot if used with 10k users or more.",
|
||||
"name" : "Casino",
|
||||
"short" : "Casino style mini games.",
|
||||
"requirements" : ["tabulate"],
|
||||
"description" : "Play up to 7 unique games and earn currency.",
|
||||
"permissions" : ["Manage Messages", "Embed Links"],
|
||||
"tags" : ["Games", "Economy", "Fun", "Casino"],
|
||||
"min_python_version": [3, 8, 1],
|
||||
"min_bot_version": "3.2.0",
|
||||
"end_user_data_statement": "This cog stores discord IDs as needed for operation."
|
||||
}
|
128
casino/utils.py
Normal file
128
casino/utils.py
Normal file
|
@ -0,0 +1,128 @@
|
|||
import re
|
||||
import math
|
||||
|
||||
from typing import Union, Dict, List, Sequence
|
||||
|
||||
utf8_re = re.compile(r"^[\U00000000-\U0010FFFF]*$")
|
||||
min_int, max_int = 1 - (2 ** 64), (2 ** 64) - 1
|
||||
|
||||
|
||||
def is_input_unsupported(data: Union[Dict, List, str, int, float]):
|
||||
if type(data) is dict:
|
||||
for k, v in data.items():
|
||||
if is_input_unsupported(k) or is_input_unsupported(v):
|
||||
return True
|
||||
if type(data) is list:
|
||||
for i in data:
|
||||
if is_input_unsupported(i):
|
||||
return True
|
||||
if type(data) is str and not utf8_re.match(data):
|
||||
return True
|
||||
if type(data) is int:
|
||||
if not (min_int <= data <= max_int):
|
||||
return True
|
||||
if type(data) is float:
|
||||
if math.isnan(data) or math.isinf(data):
|
||||
return True
|
||||
if not (min_int <= data <= max_int):
|
||||
return True
|
||||
|
||||
|
||||
class PluralDict(dict):
|
||||
"""This class is used to plural strings
|
||||
|
||||
You can plural strings based on the value input when using this class as a dictionary.
|
||||
"""
|
||||
|
||||
def __missing__(self, key):
|
||||
if "(" in key and key.endswith(")"):
|
||||
key, rest = key.split("(", 1)
|
||||
value = super().__getitem__(key)
|
||||
suffix = rest.rstrip(")").split(",")
|
||||
if len(suffix) == 1:
|
||||
suffix.insert(0, "")
|
||||
return suffix[0] if value <= 1 else suffix[1]
|
||||
raise KeyError(key)
|
||||
|
||||
|
||||
def time_converter(units):
|
||||
return sum(int(x) * 60 ** i for i, x in enumerate(reversed(units.split(":"))))
|
||||
|
||||
|
||||
def color_lookup(color="grey"):
|
||||
colors = {
|
||||
"blue": 0x3366FF,
|
||||
"red": 0xFF0000,
|
||||
"green": 0x00CC33,
|
||||
"orange": 0xFF6600,
|
||||
"purple": 0xA220BD,
|
||||
"yellow": 0xFFFF00,
|
||||
"teal": 0x009999,
|
||||
"magenta": 0xBA2586,
|
||||
"turquoise": 0x00FFFF,
|
||||
"grey": 0x666666,
|
||||
"pink": 0xFE01D1,
|
||||
"white": 0xFFFFFF,
|
||||
}
|
||||
color = colors[color]
|
||||
return color
|
||||
|
||||
|
||||
def fmt_join(words: Sequence, ending: str = "or"):
|
||||
if not words:
|
||||
return ""
|
||||
elif len(words) == 1:
|
||||
return words[0]
|
||||
else:
|
||||
return "{} {} {}".format(", ".join(map(str, words[:-1])), ending, words[-1])
|
||||
|
||||
|
||||
def cooldown_formatter(seconds, custom_msg="0"):
|
||||
m, s = divmod(seconds, 60)
|
||||
h, m = divmod(m, 60)
|
||||
|
||||
if h > 0:
|
||||
msg = "{0}h"
|
||||
if m > 0 and s > 0:
|
||||
msg += ", {1}m, and {2}s"
|
||||
elif s > 0 and m == 0:
|
||||
msg += " and {2}s"
|
||||
elif s == 0 and m == 0:
|
||||
pass
|
||||
else:
|
||||
msg += " and {1}m"
|
||||
elif h == 0 and m > 0:
|
||||
msg = "{1}m" if s == 0 else "{1}m and {2}s"
|
||||
elif m == 0 and h == 0 and s > 0:
|
||||
msg = "{2}s"
|
||||
else:
|
||||
msg = custom_msg
|
||||
return msg.format(h, m, s)
|
||||
|
||||
|
||||
def time_formatter(seconds):
|
||||
# Calculate the time and input into a dict to plural the strings later.
|
||||
m, s = divmod(seconds, 60)
|
||||
h, m = divmod(m, 60)
|
||||
data = PluralDict({"hour": h, "minute": m, "second": s})
|
||||
|
||||
# Determine the remaining time.
|
||||
if h > 0:
|
||||
fmt = "{hour} hour{hour(s)}"
|
||||
if data["minute"] > 0 and data["second"] > 0:
|
||||
fmt += ", {minute} minute{minute(s)}, and {second} second{second(s)}"
|
||||
if data["second"] > 0 == data["minute"]:
|
||||
fmt += ", and {second} second{second(s)}"
|
||||
msg = fmt.format_map(data)
|
||||
elif h == 0 and m > 0:
|
||||
if data["second"] == 0:
|
||||
fmt = "{minute} minute{minute(s)}"
|
||||
else:
|
||||
fmt = "{minute} minute{minute(s)}, and {second} second{second(s)}"
|
||||
msg = fmt.format_map(data)
|
||||
elif m == 0 and h == 0 and s > 0:
|
||||
fmt = "{second} second{second(s)}"
|
||||
msg = fmt.format_map(data)
|
||||
else:
|
||||
msg = "None"
|
||||
return msg
|
Loading…
Add table
Reference in a new issue