palmtree5 4923ffe98a [Economy] [WIP] rewrite (#781)
* [Economy][Bank] redo branch

* WIP

WIP

* Implement all current bank commands API calls

* Set dunder all and put into bot

* make core change to economy

* Add is_global method to bank

WIP

* Add extra bank API commands

* Update bank UI

Update some imports

Remove bank UI errors file

Typing thing

* Update bank get_global_accounts and touch up economy some more

Do some more economy updates

* Remove bank from bot

* Another passing test

FINALLY

* Fixy type things

Last fixes for now

Fix arg to toggle global

RJM

Invalid bid amount handler

cooldown msg

currency name fix

Fix fun bug

ANother bug

And payday limit

* PEP8 stuff

* Docstring change

* Fix this thing

* [Economy][Bank] redo branch

* [Economy][Bank] modify guild owner or bot owner check, add admin or bot owner check for global vs local bank

* [Economy] apply admin or bot owner check to [p]economyset

* Make some public things private

* [Economy] lots of refactoring for conditional permission checks and guild checks + supporting global economy

* And working stuff

* Fix Kowlin's bug

* Fix slot bugs
2017-08-09 17:23:41 -08:00

529 lines
20 KiB
Python

import calendar
import logging
import random
from collections import defaultdict, deque
from enum import Enum
import discord
from discord.ext import commands
from core import checks, Config, bank
from core.utils.chat_formatting import pagify, box
from core.bot import Red
from cogs.bank import check_global_setting_guildowner, check_global_setting_admin
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}"
PAYOUTS = {
(SMReel.two, SMReel.two, SMReel.six): {
"payout": lambda x: x * 2500 + x,
"phrase": "JACKPOT! 226! Your bid has been multiplied * 2500!"
},
(SMReel.flc, SMReel.flc, SMReel.flc): {
"payout": lambda x: x + 1000,
"phrase": "4LC! +1000!"
},
(SMReel.cherries, SMReel.cherries, SMReel.cherries): {
"payout": lambda x: x + 800,
"phrase": "Three cherries! +800!"
},
(SMReel.two, SMReel.six): {
"payout": lambda x: x * 4 + x,
"phrase": "2 6! Your bid has been multiplied * 4!"
},
(SMReel.cherries, SMReel.cherries): {
"payout": lambda x: x * 3 + x,
"phrase": "Two cherries! Your bid has been multiplied * 3!"
},
"3 symbols": {
"payout": lambda x: x + 500,
"phrase": "Three symbols! +500!"
},
"2 symbols": {
"payout": lambda x: x * 2 + x,
"phrase": "Two consecutive symbols! Your bid has been multiplied * 2!"
},
}
SLOT_PAYOUTS_MSG = ("Slot machine payouts:\n"
"{two.value} {two.value} {six.value} Bet * 2500\n"
"{flc.value} {flc.value} {flc.value} +1000\n"
"{cherries.value} {cherries.value} {cherries.value} +800\n"
"{two.value} {six.value} Bet * 4\n"
"{cherries.value} {cherries.value} Bet * 3\n\n"
"Three symbols: +500\n"
"Two symbols: Bet * 2".format(**SMReel.__dict__))
def guild_only_check():
async def pred(ctx: commands.Context):
if bank.is_global():
return True
elif not 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
class Economy:
"""Economy
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": 0,
"REGISTER_CREDITS": 0
}
default_global_settings = default_guild_settings
default_member_settings = {
"next_payday": 0,
"last_slot": 0
}
default_user_settings = default_member_settings
def __init__(self, bot: Red):
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.slot_register = defaultdict(dict)
@commands.group(name="bank")
async def _bank(self, ctx: commands.Context):
"""Bank operations"""
if ctx.invoked_subcommand is None:
await self.bot.send_cmd_help(ctx)
@_bank.command()
async def balance(self, ctx: commands.Context, user: discord.Member = None):
"""Shows balance of user.
Defaults to yours."""
if user is None:
user = ctx.author
bal = bank.get_balance(user)
currency = bank.get_currency_name(ctx.guild)
await ctx.send("{}'s balance is {} {}".format(
user.display_name, bal, currency))
@_bank.command()
async def transfer(self, ctx: commands.Context, to: discord.Member, amount: int):
"""Transfer currency to other users"""
from_ = ctx.author
currency = bank.get_currency_name(ctx.guild)
try:
await bank.transfer_credits(from_, to, amount)
except ValueError as e:
await ctx.send(str(e))
await ctx.send("{} transferred {} {} to {}".format(
from_.display_name, amount, currency, to.display_name
))
@_bank.command(name="set")
@check_global_setting_admin()
async def _set(self, ctx: commands.Context, to: discord.Member, creds: SetParser):
"""Sets balance of user's bank account. See help for more operations
Passing positive and negative values will add/remove currency instead
Examples:
bank set @Twentysix 26 - Sets balance to 26
bank set @Twentysix +2 - Increases balance by 2
bank set @Twentysix -6 - Decreases balance by 6"""
author = ctx.author
currency = bank.get_currency_name(ctx.guild)
if creds.operation == "deposit":
await bank.deposit_credits(to, creds.sum)
await ctx.send("{} added {} {} to {}'s account.".format(
author.display_name, creds.sum, currency, to.display_name
))
elif creds.operation == "withdraw":
await bank.withdraw_credits(to, creds.sum)
await ctx.send("{} removed {} {} from {}'s account.".format(
author.display_name, creds.sum, currency, to.display_name
))
else:
await bank.set_balance(to, creds.sum)
await ctx.send("{} set {}'s account to {} {}.".format(
author.display_name, to.display_name, creds.sum, currency
))
@_bank.command()
@guild_only_check()
@check_global_setting_guildowner()
async def reset(self, ctx, confirmation: bool = False):
"""Deletes all guild's bank accounts"""
if confirmation is False:
await ctx.send(
"This will delete all bank accounts for {}.\nIf you're sure, type "
"{}bank reset yes".format(
self.bot.user.name if bank.is_global() else "this guild",
ctx.prefix
)
)
else:
if bank.is_global():
# Bank being global means that the check would cause only
# the owner and any co-owners to be able to run the command
# so if we're in the function, it's safe to assume that the
# author is authorized to use owner-only commands
user = ctx.author
else:
user = ctx.guild.owner
success = await bank.wipe_bank(user)
if success:
await ctx.send("All bank accounts of this guild have been "
"deleted.")
@commands.command()
@guild_only_check()
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 = bank.get_currency_name(ctx.guild)
if bank.is_global():
next_payday = self.config.user(author).next_payday()
if cur_time >= next_payday:
await bank.deposit_credits(author, self.config.PAYDAY_CREDITS())
next_payday = cur_time + self.config.PAYDAY_TIME()
await self.config.user(author).next_payday.set(next_payday)
await ctx.send(
"{} Here, take some {}. Enjoy! (+{}"
" {}!)".format(
author.mention, credits_name,
str(self.config.PAYDAY_CREDITS()),
credits_name
)
)
else:
dtime = self.display_time(next_payday - cur_time)
await ctx.send(
"{} Too soon. For your next payday you have to"
" wait {}.".format(author.mention, dtime)
)
else:
next_payday = self.config.member(author).next_payday()
if cur_time >= next_payday:
await bank.deposit_credits(author, self.config.guild(guild).PAYDAY_CREDITS())
next_payday = cur_time + self.config.guild(guild).PAYDAY_TIME()
await self.config.member(author).next_payday.set(next_payday)
await ctx.send(
"{} Here, take some {}. Enjoy! (+{}"
" {}!)".format(
author.mention, credits_name,
str(self.config.guild(guild).PAYDAY_CREDITS()),
credits_name))
else:
dtime = self.display_time(next_payday - cur_time)
await ctx.send(
"{} Too soon. For your next payday you have to"
" wait {}.".format(author.mention, dtime))
@commands.command()
@guild_only_check()
async def leaderboard(self, ctx: commands.Context, top: int = 10):
"""Prints out the leaderboard
Defaults to top 10"""
# Originally coded by Airenkun - edited by irdumb, rewritten by Palm__ for v3
guild = ctx.guild
if top < 1:
top = 10
if bank.is_global():
bank_sorted = sorted(bank.get_global_accounts(ctx.author),
key=lambda x: x.balance, reverse=True)
else:
bank_sorted = sorted(bank.get_guild_accounts(guild),
key=lambda x: x.balance, reverse=True)
if len(bank_sorted) < top:
top = len(bank_sorted)
topten = bank_sorted[:top]
highscore = ""
place = 1
for acc in topten:
dname = str(acc.name)
if len(dname) >= 23 - len(str(acc.balance)):
dname = dname[:(23 - len(str(acc.balance))) - 3]
dname += "... "
highscore += str(place).ljust(len(str(top)) + 1)
highscore += dname.ljust(23 - len(str(acc.balance)))
highscore += str(acc.balance) + "\n"
place += 1
if highscore != "":
for page in pagify(highscore, shorten_by=12):
await ctx.send(box(page, lang="py"))
else:
await ctx.send("There are no accounts in the bank.")
@commands.command()
@guild_only_check()
async def payouts(self, ctx: commands.Context):
"""Shows slot machine payouts"""
await ctx.author.send(SLOT_PAYOUTS_MSG)
@commands.command()
@guild_only_check()
async def slot(self, ctx: commands.Context, bid: int):
"""Play the slot machine"""
author = ctx.author
guild = ctx.guild
channel = ctx.channel
if bank.is_global():
valid_bid = self.config.SLOT_MIN() <= bid <= self.config.SLOT_MAX()
slot_time = self.config.SLOT_TIME()
last_slot = self.config.user(author).last_slot()
else:
valid_bid = self.config.guild(guild).SLOT_MIN() <= bid <= self.config.guild(guild).SLOT_MAX()
slot_time = self.config.guild(guild).SLOT_TIME()
last_slot = 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 bank.can_spend(author, bid):
await ctx.send("You ain't got enough money, friend.")
return
if 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)
async def slot_machine(self, author, channel, bid):
default_reel = deque(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])
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"]
if payout:
then = bank.get_balance(author)
pay = payout["payout"](bid)
now = then - bid + pay
await bank.set_balance(author, now)
await channel.send("{}\n{} {}\n\nYour bid: {}\n{}{}!"
"".format(slot, author.mention,
payout["phrase"], bid, then, now))
else:
then = bank.get_balance(author)
await bank.withdraw_credits(author, bid)
now = then - bid
await channel.send("{}\n{} Nothing!\nYour bid: {}\n{}{}!"
"".format(slot, author.mention, bid, then, now))
@commands.group()
@guild_only_check()
@check_global_setting_admin()
async def economyset(self, ctx: commands.Context):
"""Changes economy module settings"""
guild = ctx.guild
if ctx.invoked_subcommand is None:
await self.bot.send_cmd_help(ctx)
if bank.is_global():
slot_min = self.config.SLOT_MIN()
slot_max = self.config.SLOT_MAX()
slot_time = self.config.SLOT_TIME()
payday_time = self.config.PAYDAY_TIME()
payday_amount = self.config.PAYDAY_CREDITS()
else:
slot_min = self.config.guild(guild).SLOT_MIN()
slot_max = self.config.guild(guild).SLOT_MAX()
slot_time = self.config.guild(guild).SLOT_TIME()
payday_time = self.config.guild(guild).PAYDAY_TIME()
payday_amount = self.config.guild(guild).PAYDAY_CREDITS()
register_amount = bank.get_default_balance(guild)
msg = box(
"Minimum slot bid: {}\n"
"Maximum slot bid: {}\n"
"Slot cooldown: {}\n"
"Payday amount: {}\n"
"Payday cooldown: {}\n"
"Amount given at account registration: {}"
"".format(
slot_min, slot_max, slot_time,
payday_amount, payday_time, register_amount
),
"Current Economy settings:"
)
await ctx.send(msg)
@economyset.command()
async def slotmin(self, ctx: commands.Context, bid: int):
"""Minimum slot machine bid"""
if bid < 1:
await ctx.send('Invalid min bid amount.')
return
guild = ctx.guild
if bank.is_global():
await self.config.SLOT_MIN.set(bid)
else:
await self.config.guild(guild).SLOT_MIN.set(bid)
credits_name = bank.get_currency_name(guild)
await ctx.send("Minimum bid is now {} {}.".format(bid, credits_name))
@economyset.command()
async def slotmax(self, ctx: commands.Context, bid: int):
"""Maximum slot machine bid"""
slot_min = self.config.SLOT_MIN()
if bid < 1 or bid < slot_min:
await ctx.send('Invalid slotmax bid amount. Must be greater'
' than slotmin.')
return
guild = ctx.guild
credits_name = bank.get_currency_name(guild)
if 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 {} {}.".format(bid, credits_name))
@economyset.command()
async def slottime(self, ctx: commands.Context, seconds: int):
"""Seconds between each slots use"""
guild = ctx.guild
if 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 {} seconds.".format(seconds))
@economyset.command()
async def paydaytime(self, ctx: commands.Context, seconds: int):
"""Seconds between each payday"""
guild = ctx.guild
if 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 {} seconds must pass "
"between each payday.".format(seconds))
@economyset.command()
async def paydayamount(self, ctx: commands.Context, creds: int):
"""Amount earned each payday"""
guild = ctx.guild
credits_name = bank.get_currency_name(guild)
if creds <= 0:
await ctx.send("Har har so funny.")
return
if 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 {} {}."
"".format(creds, credits_name))
@economyset.command()
async def registeramount(self, ctx: commands.Context, creds: int):
"""Amount given on registering an account"""
guild = ctx.guild
if creds < 0:
creds = 0
credits_name = bank.get_currency_name(guild)
await bank.set_default_balance(creds, guild)
await ctx.send("Registering an account will now give {} {}."
"".format(creds, credits_name))
# What would I ever do without stackoverflow?
def display_time(self, 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])