123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335 |
- from random import randint, gauss, shuffle, choices
- from datetime import datetime, timedelta
- import pickle
- from sudoku_py import SudokuGenerator, Sudoku
- from rollbot import as_plugin, with_help, with_startup, as_sender_singleton, as_group_singleton, RollbotFailure
- from rollbot.plugins.decorators.arg_wiring import Database, Config, ArgList, Singleton, Message
- @as_sender_singleton
- class RollcoinWallet:
- balance: float = 0.0
- @as_sender_singleton
- class RollcoinBegCooldown:
- cooldown: datetime = datetime(2000, 1, 1)
- @as_group_singleton
- class RollcoinBlockchain:
- puzzle: "binary"
- def get_puzzle(self):
- if self.puzzle is None:
- return None
- return pickle.loads(self.puzzle)
- def set_puzzle(self, new_puzzle):
- self.puzzle = pickle.dumps(new_puzzle)
-
- def clear_puzzle(self):
- self.puzzle = None
- @as_sender_singleton
- class RollcoinGamblingWallet:
- balance: float = 0.0
- @as_group_singleton
- class RollcoinMarket:
- state: str = "neutral"
- def setup_initial_balances(plugin, db):
- bot = plugin.bot
- bot.logger.info("Setting up rollcoin wallets")
- wallets = bot.config.get("rollcoin.wallets")
- initial = bot.config.get("rollcoin.initial")
- force = bot.config.get("rollcoin.force_initial")
- for k, v in initial.items():
- wallet_key = wallets.get(k, None)
- if wallet_key is None:
- continue
- wallet = db.query(RollcoinWallet).get(wallet_key)
- if wallet is None:
- wallet = RollcoinWallet.create_from_key(wallet_key)
- elif k not in force:
- continue
- wallet.balance = v
- db.add(wallet)
- bot.logger.info("Initial wallets created!")
- @with_startup(setup_initial_balances)
- @with_help("Check your balance of Rollcoins")
- @as_plugin
- def balance(wallet: Singleton(RollcoinWallet), holdings: Singleton(RollcoinGamblingWallet)):
- return f"You have {wallet.balance} Rollcoins in your wallet and {holdings.balance} Rollcoins in the market"
- SPECIAL_AMOUNTS = ("ALL", "FRAC", "-ALL", "-FRAC")
- def pop_amount_arg(args):
- raw_amount, *rest = args
- if (up_amount := raw_amount.upper()) in SPECIAL_AMOUNTS:
- return up_amount, rest
- try:
- amount = float(raw_amount)
- except ValueError:
- RollbotFailure.INVALID_ARGUMENTS.with_reason(f"Amount should be a number or ALL or FRAC - not {raw_amount}").raise_exc()
- return amount, rest
- def assert_positive(amount):
- if amount <= 0:
- RollbotFailure.INVALID_ARGUMENTS.with_reason(f"Amount should be positive").raise_exc()
- def fractional_part(number):
- return float("0." + str(float(number)).split(".")[1])
- @with_help("Tip someone else some Rollcoins: !tip target amount")
- @as_plugin
- def tip(args: ArgList,
- db: Database,
- sender_wallet: Singleton(RollcoinWallet),
- wallets: Config("rollcoin.wallets")):
- if len(args) < 2:
- RollbotFailure.INVALID_ARGUMENTS.with_reason("Tip requires 2 arguments - target and amount").raise_exc()
- target, *rest = args
- amount, _ = pop_amount_arg(rest)
- if amount in SPECIAL_AMOUNTS:
- if amount == "ALL":
- amount = sender_wallet.balance
- elif amount == "FRAC":
- amount = fractional_part(sender_wallet.balance)
- else:
- amount = -1
- assert_positive(amount)
- target_id = wallets.get(target.lower(), None)
- if target_id is None:
- RollbotFailure.INVALID_ARGUMENTS.with_reason(f"Could not find wallet-holder {target}").raise_exc()
- if target_id == sender_wallet.sender_id:
- return f"You can't tip yourself!"
- if sender_wallet.balance < amount:
- return f"Insufficient funds! You need {round(amount - sender_wallet.balance, 2)} more Rollcoins. Have you tried to !mine, !beg, or !gamble?"
-
- target_wallet = db.query(RollcoinWallet).get(target_id)
- if target_wallet is None:
- target_wallet = RollcoinWallet.create_from_key(target_id)
- db.add(target_wallet)
- sender_wallet.balance = sender_wallet.balance - amount
- target_wallet.balance = target_wallet.balance + amount
- return f"Done! {target} now has {target_wallet.balance} Rollcoins"
- @with_help("Donate money to be distributed evenly among all wallets")
- @as_plugin
- def donate(args: ArgList,
- db: Database,
- sender_wallet: Singleton(RollcoinWallet),
- wallets: Config("rollcoin.wallets")):
- if len(args) < 1:
- return RollbotFailure.INVALID_ARGUMENTS.with_reason("Need an amount to donate")
- amount, _ = pop_amount_arg(args)
- if amount in SPECIAL_AMOUNTS:
- if amount == "ALL":
- amount = sender_wallet.balance
- elif amount == "FRAC":
- amount = fractional_part(sender_wallet.balance)
- else:
- amount = -1
- assert_positive(amount)
- if sender_wallet.balance < amount:
- return f"Insufficient funds! You need {round(amount - sender_wallet.balance, 2)} more Rollcoins"
- wallets = [w for w in wallets.values() if w != sender_wallet.sender_id]
- per_person = amount / (len(wallets))
- for w in wallets:
- target_wallet = db.query(RollcoinWallet).get(w)
- if target_wallet is None:
- target_wallet = RollcoinWallet.create_from_key(w)
- db.add(target_wallet)
- target_wallet.balance = target_wallet.balance + per_person
- sender_wallet.balance = sender_wallet.balance - amount
- return f"Done! You have donated {per_person} to each of the {len(wallets)} other wallet(s)"
- @with_help("Receive a mining question you can answer to receive Rollcoins")
- @as_plugin
- def mine(msg: Message,
- blockchain: Singleton(RollcoinBlockchain),
- wallet: Singleton(RollcoinWallet)):
- skip = msg.raw_args is not None and msg.raw_args.strip().lower() == "skip" and msg.from_admin
- if skip:
- blockchain.clear_puzzle()
-
- puzzle = blockchain.get_puzzle()
- if puzzle is None:
- exchange = list(zip("abcdefghi", range(10)))
- shuffle(exchange)
- gen = SudokuGenerator(9)
- gen.generate(0)
- gen.board_exchange_values({k: v + 1 for k, v in exchange})
- gen.generate(randint(10, 50))
- puzzle = gen.board
- blockchain.set_puzzle(puzzle)
- if skip:
- return "Admin has skipped the previous mining challenge.\nThe current mining challenge is\n" + str(Sudoku(board=puzzle))
- if msg.raw_args is None:
- return "The current mining challenge is\n" + str(Sudoku(board=puzzle))
- parsed = []
- row = []
- for c in msg.raw_args:
- if not c.isdigit():
- continue
- row.append(int(c))
- if len(row) == 9:
- parsed.append(row)
- row = []
-
- try:
- guess = Sudoku(board=parsed, block_width=3, block_height=3)
- except:
- return "Sorry, I could not parse that solution!"
- for i, row in enumerate(puzzle):
- for j, cell in enumerate(row):
- if cell not in (0, parsed[i][j]):
- return "Sorry, that solution doesn't match the puzzle! The current mining challenge is\n" + str(Sudoku(board=puzzle))
-
- for row in parsed:
- if set(row) != set(range(1, 10)):
- return "Sorry, that solution isn't valid!"
-
- for i in range(9):
- if set(r[i] for r in parsed) != set(range(1, 10)):
- return "Sorry, that solution isn't valid!"
- for i in range(0, 9, 3):
- for j in range(0, 9, 3):
- if set(x for r in parsed[i:i+3] for x in r[j:j+3]) != set(range(1, 10)):
- return "Sorry, that solution isn't valid!"
- market_rate = round(abs(gauss(10, 1)), 2)
- wallet.balance = wallet.balance + market_rate
- blockchain.clear_puzzle()
- return f"Looks right to me! The current market rate is {market_rate} Rollcoins per Sudoku, so that brings your balance to {wallet.balance}"
- @with_help("Beg for some Rollcoins")
- @as_plugin
- def beg(wallet: Singleton(RollcoinWallet),
- beg_cooldown: Singleton(RollcoinBegCooldown),
- cooldown_time: Config("rollcoin.cooldown"),
- beg_chance: Config("rollcoin.beg_chance")):
- old_cooldown = beg_cooldown.cooldown
- beg_cooldown.cooldown = datetime.now() + timedelta(**cooldown_time)
- if datetime.now() < old_cooldown:
- return f"Don't beg so frequently - it's not a good look"
- if randint(0, 99) > beg_chance:
- return "What are you, poor?"
- increase = round(abs(gauss(1, 1)), 2)
- wallet.balance = wallet.balance + increase
- return f"Fine, I've given you {increase} Rollcoins, which brings your balance to {wallet.balance}"
- @with_help("Gamble some Rollcoins")
- @as_plugin
- def gamble(db: Database,
- args: ArgList,
- sender_wallet: Singleton(RollcoinWallet),
- sender_holdings: Singleton(RollcoinGamblingWallet),
- market: Singleton(RollcoinMarket),
- transitions: Config("rollcoin.market")):
- # pos moves money wallet -> gambling, neg moves money gambling -> wallet
- # market has a current state (neutral, bull, bear, boom, bust, collapse, etc.)
- # after a transfer is made, the market evolves based on
- # - direction of transfer (buy vs sell)
- # - current market state
- # - rng
- # market enters new state and affects all gambling wallets, multiplying their balances
- if len(args) != 1:
- RollbotFailure.INVALID_ARGUMENTS.with_reason("Gambling requires exactly one argument: the amount to transfer (can be positive or negative)").raise_exc()
- amount, _ = pop_amount_arg(args)
- if amount in SPECIAL_AMOUNTS:
- if amount == "ALL":
- amount = sender_wallet.balance
- elif amount == "FRAC":
- amount = fractional_part(sender_wallet.balance)
- elif amount == "-ALL":
- amount = -sender_holdings.balance
- elif amount == "-FRAC":
- amount = -fractional_part(sender_holdings.balance)
- else:
- raise ValueError(amount)
- if amount == 0:
- RollbotFailure.INVALID_ARGUMENTS.with_reason(f"Amount should be nonzero").raise_exc()
- buying = amount > 0
- abs_amount = abs(amount)
- if buying and abs_amount > sender_wallet.balance:
- return f"Insufficient funds! You need {round(abs_amount - sender_wallet.balance, 2)} more Rollcoins. Have you tried to !mine, !beg, or !gamble?"
- elif not buying and abs_amount > sender_holdings.balance:
- return f"Insufficient holdings! You would need {round(abs_amount - sender_holdings.balance, 2)} more Rollcoins in the market."
- options = transitions[market.state]["buy" if buying else "sell"]
- transition = choices(options, weights=tuple(x["weight"] for x in options))[0]
- multiplier = transition["multiplier"]
- market.state = transition["new_state"]
- sender_wallet.balance = sender_wallet.balance - amount
- sender_holdings.balance = multiplier * (sender_holdings.balance + amount)
- for holdings in db.query(RollcoinGamblingWallet
- ).filter(RollcoinGamblingWallet.sender_id != sender_holdings.sender_id
- ).all():
- holdings.balance = multiplier * holdings.balance
- return f"{transition['message']}\nYour current holdings: {sender_holdings.balance}"
- @with_help("View the whole blockchain (i.e., who has how many rollcoins)")
- @as_plugin
- def blockchain(db: Database, wallets: Config("rollcoin.wallets")):
- results = []
- for name, wallet_id in wallets.items():
- wallet = db.query(RollcoinWallet).get(wallet_id)
- market = db.query(RollcoinGamblingWallet).get(wallet_id)
- status = name.title() + "\n"
- total = 0
- if wallet is not None:
- status += f"\t\tWallet: {wallet.balance}\n"
- total += wallet.balance
- else:
- status += "\t\tHas not used their rollcoin wallet\n"
- if market is not None:
- status += f"\t\tMarket: {market.balance}\n"
- total += market.balance
- else:
- status += "\t\tHas not invested their rollcoins\n"
- if total != 0:
- status += f"\t\tTotal: {total}\n"
- results.append((total, name, status))
- return "\n".join(r for _, _, r in sorted(results, reverse=True))
|