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))