Draper 3c1b6ae4cf [Utils] Add humanize_number() function to chat formatting (#2836)
This adds babel as a dependency, and also includes `redbot.core.i18n.get_babel_locale()`
2019-08-28 08:44:52 +10:00

710 lines
27 KiB
Python

import calendar
import logging
import random
from collections import defaultdict, deque
from enum import Enum
from typing import cast, Iterable
import discord
from redbot.cogs.bank import check_global_setting_guildowner, check_global_setting_admin
from redbot.core import Config, bank, commands, errors
from redbot.core.i18n import Translator, cog_i18n
from redbot.core.utils.chat_formatting import box, humanize_number
from redbot.core.utils.menus import menu, DEFAULT_CONTROLS
from redbot.core.bot import Red
T_ = Translator("Economy", __file__)
logger = logging.getLogger("red.economy")
NUM_ENC = "\N{COMBINING ENCLOSING KEYCAP}"
class SMReel(Enum):
cherries = "\N{CHERRIES}"
cookie = "\N{COOKIE}"
two = "\N{DIGIT TWO}" + NUM_ENC
flc = "\N{FOUR LEAF CLOVER}"
cyclone = "\N{CYCLONE}"
sunflower = "\N{SUNFLOWER}"
six = "\N{DIGIT SIX}" + NUM_ENC
mushroom = "\N{MUSHROOM}"
heart = "\N{HEAVY BLACK HEART}"
snowflake = "\N{SNOWFLAKE}"
_ = lambda s: s
PAYOUTS = {
(SMReel.two, SMReel.two, SMReel.six): {
"payout": lambda x: x * 50,
"phrase": _("JACKPOT! 226! Your bid has been multiplied * 50!"),
},
(SMReel.flc, SMReel.flc, SMReel.flc): {
"payout": lambda x: x * 25,
"phrase": _("4LC! Your bid has been multiplied * 25!"),
},
(SMReel.cherries, SMReel.cherries, SMReel.cherries): {
"payout": lambda x: x * 20,
"phrase": _("Three cherries! Your bid has been multiplied * 20!"),
},
(SMReel.two, SMReel.six): {
"payout": lambda x: x * 4,
"phrase": _("2 6! Your bid has been multiplied * 4!"),
},
(SMReel.cherries, SMReel.cherries): {
"payout": lambda x: x * 3,
"phrase": _("Two cherries! Your bid has been multiplied * 3!"),
},
"3 symbols": {
"payout": lambda x: x * 10,
"phrase": _("Three symbols! Your bid has been multiplied * 10!"),
},
"2 symbols": {
"payout": lambda x: x * 2,
"phrase": _("Two consecutive symbols! Your bid has been multiplied * 2!"),
},
}
SLOT_PAYOUTS_MSG = _(
"Slot machine payouts:\n"
"{two.value} {two.value} {six.value} Bet * 50\n"
"{flc.value} {flc.value} {flc.value} Bet * 25\n"
"{cherries.value} {cherries.value} {cherries.value} Bet * 20\n"
"{two.value} {six.value} Bet * 4\n"
"{cherries.value} {cherries.value} Bet * 3\n\n"
"Three symbols: Bet * 10\n"
"Two symbols: Bet * 2"
).format(**SMReel.__dict__)
_ = T_
def guild_only_check():
async def pred(ctx: commands.Context):
if await bank.is_global():
return True
elif not await bank.is_global() and ctx.guild is not None:
return True
else:
return False
return commands.check(pred)
class SetParser:
def __init__(self, argument):
allowed = ("+", "-")
self.sum = int(argument)
if argument and argument[0] in allowed:
if self.sum < 0:
self.operation = "withdraw"
elif self.sum > 0:
self.operation = "deposit"
else:
raise RuntimeError
self.sum = abs(self.sum)
elif argument.isdigit():
self.operation = "set"
else:
raise RuntimeError
@cog_i18n(_)
class Economy(commands.Cog):
"""Get rich and have fun with imaginary currency!"""
default_guild_settings = {
"PAYDAY_TIME": 300,
"PAYDAY_CREDITS": 120,
"SLOT_MIN": 5,
"SLOT_MAX": 100,
"SLOT_TIME": 5,
"REGISTER_CREDITS": 0,
}
default_global_settings = default_guild_settings
default_member_settings = {"next_payday": 0, "last_slot": 0}
default_role_settings = {"PAYDAY_CREDITS": 0}
default_user_settings = default_member_settings
def __init__(self, bot: Red):
super().__init__()
self.bot = bot
self.file_path = "data/economy/settings.json"
self.config = Config.get_conf(self, 1256844281)
self.config.register_guild(**self.default_guild_settings)
self.config.register_global(**self.default_global_settings)
self.config.register_member(**self.default_member_settings)
self.config.register_user(**self.default_user_settings)
self.config.register_role(**self.default_role_settings)
self.slot_register = defaultdict(dict)
@guild_only_check()
@commands.group(name="bank")
async def _bank(self, ctx: commands.Context):
"""Manage the bank."""
pass
@_bank.command()
async def balance(self, ctx: commands.Context, user: discord.Member = None):
"""Show the user's account balance.
Defaults to yours."""
if user is None:
user = ctx.author
bal = await bank.get_balance(user)
currency = await bank.get_currency_name(ctx.guild)
await ctx.send(
_("{user}'s balance is {num} {currency}").format(
user=user.display_name, num=humanize_number(bal), currency=currency
)
)
@_bank.command()
async def transfer(self, ctx: commands.Context, to: discord.Member, amount: int):
"""Transfer currency to other users."""
from_ = ctx.author
currency = await bank.get_currency_name(ctx.guild)
try:
await bank.transfer_credits(from_, to, amount)
except (ValueError, errors.BalanceTooHigh) as e:
return await ctx.send(str(e))
await ctx.send(
_("{user} transferred {num} {currency} to {other_user}").format(
user=from_.display_name,
num=humanize_number(amount),
currency=currency,
other_user=to.display_name,
)
)
@_bank.command(name="set")
@check_global_setting_admin()
async def _set(self, ctx: commands.Context, to: discord.Member, creds: SetParser):
"""Set the balance of user's bank account.
Passing positive and negative values will add/remove currency instead.
Examples:
- `[p]bank set @Twentysix 26` - Sets balance to 26
- `[p]bank set @Twentysix +2` - Increases balance by 2
- `[p]bank set @Twentysix -6` - Decreases balance by 6
"""
author = ctx.author
currency = await bank.get_currency_name(ctx.guild)
try:
if creds.operation == "deposit":
await bank.deposit_credits(to, creds.sum)
msg = _("{author} added {num} {currency} to {user}'s account.").format(
author=author.display_name,
num=humanize_number(creds.sum),
currency=currency,
user=to.display_name,
)
elif creds.operation == "withdraw":
await bank.withdraw_credits(to, creds.sum)
msg = _("{author} removed {num} {currency} from {user}'s account.").format(
author=author.display_name,
num=humanize_number(creds.sum),
currency=currency,
user=to.display_name,
)
else:
await bank.set_balance(to, creds.sum)
msg = _("{author} set {user}'s account balance to {num} {currency}.").format(
author=author.display_name,
num=humanize_number(creds.sum),
currency=currency,
user=to.display_name,
)
except (ValueError, errors.BalanceTooHigh) as e:
await ctx.send(str(e))
else:
await ctx.send(msg)
@_bank.command()
@check_global_setting_guildowner()
async def reset(self, ctx, confirmation: bool = False):
"""Delete all bank accounts."""
if confirmation is False:
await ctx.send(
_(
"This will delete all bank accounts for {scope}.\nIf you're sure, type "
"`{prefix}bank reset yes`"
).format(
scope=self.bot.user.name if await bank.is_global() else _("this server"),
prefix=ctx.prefix,
)
)
else:
await bank.wipe_bank(guild=ctx.guild)
await ctx.send(
_("All bank accounts for {scope} have been deleted.").format(
scope=self.bot.user.name if await bank.is_global() else _("this server")
)
)
@guild_only_check()
@commands.command()
async def payday(self, ctx: commands.Context):
"""Get some free currency."""
author = ctx.author
guild = ctx.guild
cur_time = calendar.timegm(ctx.message.created_at.utctimetuple())
credits_name = await bank.get_currency_name(ctx.guild)
if await bank.is_global(): # Role payouts will not be used
next_payday = await self.config.user(author).next_payday()
if cur_time >= next_payday:
try:
await bank.deposit_credits(author, await self.config.PAYDAY_CREDITS())
except errors.BalanceTooHigh as exc:
await bank.set_balance(author, exc.max_balance)
await ctx.send(
_(
"You've reached the maximum amount of {currency}!"
"Please spend some more \N{GRIMACING FACE}\n\n"
"You currently have {new_balance} {currency}."
).format(
currency=credits_name, new_balance=humanize_number(exc.max_balance)
)
)
return
next_payday = cur_time + await self.config.PAYDAY_TIME()
await self.config.user(author).next_payday.set(next_payday)
pos = await bank.get_leaderboard_position(author)
await ctx.send(
_(
"{author.mention} Here, take some {currency}. "
"Enjoy! (+{amount} {currency}!)\n\n"
"You currently have {new_balance} {currency}.\n\n"
"You are currently #{pos} on the global leaderboard!"
).format(
author=author,
currency=credits_name,
amount=humanize_number(await self.config.PAYDAY_CREDITS()),
new_balance=humanize_number(await bank.get_balance(author)),
pos=humanize_number(pos) if pos else pos,
)
)
else:
dtime = self.display_time(next_payday - cur_time)
await ctx.send(
_(
"{author.mention} Too soon. For your next payday you have to wait {time}."
).format(author=author, time=dtime)
)
else:
next_payday = await self.config.member(author).next_payday()
if cur_time >= next_payday:
credit_amount = await self.config.guild(guild).PAYDAY_CREDITS()
for role in author.roles:
role_credits = await self.config.role(
role
).PAYDAY_CREDITS() # Nice variable name
if role_credits > credit_amount:
credit_amount = role_credits
try:
await bank.deposit_credits(author, credit_amount)
except errors.BalanceTooHigh as exc:
await bank.set_balance(author, exc.max_balance)
await ctx.send(
_(
"You've reached the maximum amount of {currency}! "
"Please spend some more \N{GRIMACING FACE}\n\n"
"You currently have {new_balance} {currency}."
).format(
currency=credits_name, new_balance=humanize_number(exc.max_balance)
)
)
return
next_payday = cur_time + await self.config.guild(guild).PAYDAY_TIME()
await self.config.member(author).next_payday.set(next_payday)
pos = await bank.get_leaderboard_position(author)
await ctx.send(
_(
"{author.mention} Here, take some {currency}. "
"Enjoy! (+{amount} {currency}!)\n\n"
"You currently have {new_balance} {currency}.\n\n"
"You are currently #{pos} on the global leaderboard!"
).format(
author=author,
currency=credits_name,
amount=humanize_number(credit_amount),
new_balance=humanize_number(await bank.get_balance(author)),
pos=humanize_number(pos) if pos else pos,
)
)
else:
dtime = self.display_time(next_payday - cur_time)
await ctx.send(
_(
"{author.mention} Too soon. For your next payday you have to wait {time}."
).format(author=author, time=dtime)
)
@commands.command()
@guild_only_check()
async def leaderboard(self, ctx: commands.Context, top: int = 10, show_global: bool = False):
"""Print the leaderboard.
Defaults to top 10.
"""
guild = ctx.guild
author = ctx.author
if top < 1:
top = 10
if await bank.is_global() and show_global:
# show_global is only applicable if bank is global
bank_sorted = await bank.get_leaderboard(positions=top, guild=None)
else:
bank_sorted = await bank.get_leaderboard(positions=top, guild=guild)
try:
bal_len = len(humanize_number(bank_sorted[0][1]["balance"]))
# first user is the largest we'll see
except IndexError:
return await ctx.send(_("There are no accounts in the bank."))
pound_len = len(str(len(bank_sorted)))
header = "{pound:{pound_len}}{score:{bal_len}}{name:2}\n".format(
pound="#",
name=_("Name"),
score=_("Score"),
bal_len=bal_len + 6,
pound_len=pound_len + 3,
)
highscores = []
pos = 1
temp_msg = header
for acc in bank_sorted:
try:
name = guild.get_member(acc[0]).display_name
except AttributeError:
user_id = ""
if await ctx.bot.is_owner(ctx.author):
user_id = f"({str(acc[0])})"
name = f"{acc[1]['name']} {user_id}"
balance = humanize_number(acc[1]["balance"])
if acc[0] != author.id:
temp_msg += (
f"{f'{humanize_number(pos)}.': <{pound_len+2}} "
f"{balance: <{bal_len + 5}} {name}\n"
)
else:
temp_msg += (
f"{f'{humanize_number(pos)}.': <{pound_len+2}} "
f"{balance: <{bal_len + 5}} "
f"<<{author.display_name}>>\n"
)
if pos % 10 == 0:
highscores.append(box(temp_msg, lang="md"))
temp_msg = header
pos += 1
if temp_msg != header:
highscores.append(box(temp_msg, lang="md"))
if highscores:
await menu(ctx, highscores, DEFAULT_CONTROLS)
@commands.command()
@guild_only_check()
async def payouts(self, ctx: commands.Context):
"""Show the payouts for the slot machine."""
await ctx.author.send(SLOT_PAYOUTS_MSG)
@commands.command()
@guild_only_check()
async def slot(self, ctx: commands.Context, bid: int):
"""Use the slot machine."""
author = ctx.author
guild = ctx.guild
channel = ctx.channel
if await bank.is_global():
valid_bid = await self.config.SLOT_MIN() <= bid <= await self.config.SLOT_MAX()
slot_time = await self.config.SLOT_TIME()
last_slot = await self.config.user(author).last_slot()
else:
valid_bid = (
await self.config.guild(guild).SLOT_MIN()
<= bid
<= await self.config.guild(guild).SLOT_MAX()
)
slot_time = await self.config.guild(guild).SLOT_TIME()
last_slot = await self.config.member(author).last_slot()
now = calendar.timegm(ctx.message.created_at.utctimetuple())
if (now - last_slot) < slot_time:
await ctx.send(_("You're on cooldown, try again in a bit."))
return
if not valid_bid:
await ctx.send(_("That's an invalid bid amount, sorry :/"))
return
if not await bank.can_spend(author, bid):
await ctx.send(_("You ain't got enough money, friend."))
return
if await bank.is_global():
await self.config.user(author).last_slot.set(now)
else:
await self.config.member(author).last_slot.set(now)
await self.slot_machine(author, channel, bid)
@staticmethod
async def slot_machine(author, channel, bid):
default_reel = deque(cast(Iterable, SMReel))
reels = []
for i in range(3):
default_reel.rotate(random.randint(-999, 999)) # weeeeee
new_reel = deque(default_reel, maxlen=3) # we need only 3 symbols
reels.append(new_reel) # for each reel
rows = (
(reels[0][0], reels[1][0], reels[2][0]),
(reels[0][1], reels[1][1], reels[2][1]),
(reels[0][2], reels[1][2], reels[2][2]),
)
slot = "~~\n~~" # Mobile friendly
for i, row in enumerate(rows): # Let's build the slot to show
sign = " "
if i == 1:
sign = ">"
slot += "{}{} {} {}\n".format(
sign, *[c.value for c in row] # pylint: disable=no-member
)
payout = PAYOUTS.get(rows[1])
if not payout:
# Checks for two-consecutive-symbols special rewards
payout = PAYOUTS.get((rows[1][0], rows[1][1]), PAYOUTS.get((rows[1][1], rows[1][2])))
if not payout:
# Still nothing. Let's check for 3 generic same symbols
# or 2 consecutive symbols
has_three = rows[1][0] == rows[1][1] == rows[1][2]
has_two = (rows[1][0] == rows[1][1]) or (rows[1][1] == rows[1][2])
if has_three:
payout = PAYOUTS["3 symbols"]
elif has_two:
payout = PAYOUTS["2 symbols"]
pay = 0
if payout:
then = await bank.get_balance(author)
pay = payout["payout"](bid)
now = then - bid + pay
try:
await bank.set_balance(author, now)
except errors.BalanceTooHigh as exc:
await bank.set_balance(author, exc.max_balance)
await channel.send(
_(
"You've reached the maximum amount of {currency}! "
"Please spend some more \N{GRIMACING FACE}\n{old_balance} -> {new_balance}!"
).format(
currency=await bank.get_currency_name(getattr(channel, "guild", None)),
old_balance=humanize_number(then),
new_balance=humanize_number(exc.max_balance),
)
)
return
phrase = T_(payout["phrase"])
else:
then = await bank.get_balance(author)
await bank.withdraw_credits(author, bid)
now = then - bid
phrase = _("Nothing!")
await channel.send(
(
"{slot}\n{author.mention} {phrase}\n\n"
+ _("Your bid: {bid}")
+ _("\n{old_balance} - {bid} (Your bid) + {pay} (Winnings) → {new_balance}!")
).format(
slot=slot,
author=author,
phrase=phrase,
bid=humanize_number(bid),
old_balance=humanize_number(then),
new_balance=humanize_number(now),
pay=humanize_number(pay),
)
)
@commands.group()
@guild_only_check()
@check_global_setting_admin()
async def economyset(self, ctx: commands.Context):
"""Manage Economy settings."""
guild = ctx.guild
if ctx.invoked_subcommand is None:
if await bank.is_global():
conf = self.config
else:
conf = self.config.guild(ctx.guild)
await ctx.send(
box(
_(
"----Economy Settings---\n"
"Minimum slot bid: {slot_min}\n"
"Maximum slot bid: {slot_max}\n"
"Slot cooldown: {slot_time}\n"
"Payday amount: {payday_amount}\n"
"Payday cooldown: {payday_time}\n"
"Amount given at account registration: {register_amount}"
).format(
slot_min=humanize_number(await conf.SLOT_MIN()),
slot_max=humanize_number(await conf.SLOT_MAX()),
slot_time=humanize_number(await conf.SLOT_TIME()),
payday_time=humanize_number(await conf.PAYDAY_TIME()),
payday_amount=humanize_number(await conf.PAYDAY_CREDITS()),
register_amount=humanize_number(await bank.get_default_balance(guild)),
)
)
)
@economyset.command()
async def slotmin(self, ctx: commands.Context, bid: int):
"""Set the minimum slot machine bid."""
if bid < 1:
await ctx.send(_("Invalid min bid amount."))
return
guild = ctx.guild
if await bank.is_global():
await self.config.SLOT_MIN.set(bid)
else:
await self.config.guild(guild).SLOT_MIN.set(bid)
credits_name = await bank.get_currency_name(guild)
await ctx.send(
_("Minimum bid is now {bid} {currency}.").format(
bid=humanize_number(bid), currency=credits_name
)
)
@economyset.command()
async def slotmax(self, ctx: commands.Context, bid: int):
"""Set the maximum slot machine bid."""
slot_min = await self.config.SLOT_MIN()
if bid < 1 or bid < slot_min:
await ctx.send(
_("Invalid maximum bid amount. Must be greater than the minimum amount.")
)
return
guild = ctx.guild
credits_name = await bank.get_currency_name(guild)
if await bank.is_global():
await self.config.SLOT_MAX.set(bid)
else:
await self.config.guild(guild).SLOT_MAX.set(bid)
await ctx.send(
_("Maximum bid is now {bid} {currency}.").format(
bid=humanize_number(bid), currency=credits_name
)
)
@economyset.command()
async def slottime(self, ctx: commands.Context, seconds: int):
"""Set the cooldown for the slot machine."""
guild = ctx.guild
if await bank.is_global():
await self.config.SLOT_TIME.set(seconds)
else:
await self.config.guild(guild).SLOT_TIME.set(seconds)
await ctx.send(_("Cooldown is now {num} seconds.").format(num=seconds))
@economyset.command()
async def paydaytime(self, ctx: commands.Context, seconds: int):
"""Set the cooldown for payday."""
guild = ctx.guild
if await bank.is_global():
await self.config.PAYDAY_TIME.set(seconds)
else:
await self.config.guild(guild).PAYDAY_TIME.set(seconds)
await ctx.send(
_("Value modified. At least {num} seconds must pass between each payday.").format(
num=seconds
)
)
@economyset.command()
async def paydayamount(self, ctx: commands.Context, creds: int):
"""Set the amount earned each payday."""
guild = ctx.guild
if creds <= 0 or creds > bank.MAX_BALANCE:
await ctx.send(_("Har har so funny."))
return
credits_name = await bank.get_currency_name(guild)
if await bank.is_global():
await self.config.PAYDAY_CREDITS.set(creds)
else:
await self.config.guild(guild).PAYDAY_CREDITS.set(creds)
await ctx.send(
_("Every payday will now give {num} {currency}.").format(
num=humanize_number(creds), currency=credits_name
)
)
@economyset.command()
async def rolepaydayamount(self, ctx: commands.Context, role: discord.Role, creds: int):
"""Set the amount earned each payday for a role."""
guild = ctx.guild
if creds <= 0 or creds > bank.MAX_BALANCE:
await ctx.send(_("Har har so funny."))
return
credits_name = await bank.get_currency_name(guild)
if await bank.is_global():
await ctx.send(_("The bank must be per-server for per-role paydays to work."))
else:
await self.config.role(role).PAYDAY_CREDITS.set(creds)
await ctx.send(
_(
"Every payday will now give {num} {currency} "
"to people with the role {role_name}."
).format(num=humanize_number(creds), currency=credits_name, role_name=role.name)
)
@economyset.command()
async def registeramount(self, ctx: commands.Context, creds: int):
"""Set the initial balance for new bank accounts."""
guild = ctx.guild
if creds < 0:
creds = 0
credits_name = await bank.get_currency_name(guild)
await bank.set_default_balance(creds, guild)
await ctx.send(
_("Registering an account will now give {num} {currency}.").format(
num=humanize_number(creds), currency=credits_name
)
)
# What would I ever do without stackoverflow?
@staticmethod
def display_time(seconds, granularity=2):
intervals = ( # Source: http://stackoverflow.com/a/24542445
(_("weeks"), 604800), # 60 * 60 * 24 * 7
(_("days"), 86400), # 60 * 60 * 24
(_("hours"), 3600), # 60 * 60
(_("minutes"), 60),
(_("seconds"), 1),
)
result = []
for name, count in intervals:
value = seconds // count
if value:
seconds -= value * count
if value == 1:
name = name.rstrip("s")
result.append("{} {}".format(value, name))
return ", ".join(result[:granularity])