rollcoin.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335
  1. from random import randint, gauss, shuffle, choices
  2. from datetime import datetime, timedelta
  3. import pickle
  4. from sudoku_py import SudokuGenerator, Sudoku
  5. from rollbot import as_plugin, with_help, with_startup, as_sender_singleton, as_group_singleton, RollbotFailure
  6. from rollbot.plugins.decorators.arg_wiring import Database, Config, ArgList, Singleton, Message
  7. @as_sender_singleton
  8. class RollcoinWallet:
  9. balance: float = 0.0
  10. @as_sender_singleton
  11. class RollcoinBegCooldown:
  12. cooldown: datetime = datetime(2000, 1, 1)
  13. @as_group_singleton
  14. class RollcoinBlockchain:
  15. puzzle: "binary"
  16. def get_puzzle(self):
  17. if self.puzzle is None:
  18. return None
  19. return pickle.loads(self.puzzle)
  20. def set_puzzle(self, new_puzzle):
  21. self.puzzle = pickle.dumps(new_puzzle)
  22. def clear_puzzle(self):
  23. self.puzzle = None
  24. @as_sender_singleton
  25. class RollcoinGamblingWallet:
  26. balance: float = 0.0
  27. @as_group_singleton
  28. class RollcoinMarket:
  29. state: str = "neutral"
  30. def setup_initial_balances(plugin, db):
  31. bot = plugin.bot
  32. bot.logger.info("Setting up rollcoin wallets")
  33. wallets = bot.config.get("rollcoin.wallets")
  34. initial = bot.config.get("rollcoin.initial")
  35. force = bot.config.get("rollcoin.force_initial")
  36. for k, v in initial.items():
  37. wallet_key = wallets.get(k, None)
  38. if wallet_key is None:
  39. continue
  40. wallet = db.query(RollcoinWallet).get(wallet_key)
  41. if wallet is None:
  42. wallet = RollcoinWallet.create_from_key(wallet_key)
  43. elif k not in force:
  44. continue
  45. wallet.balance = v
  46. db.add(wallet)
  47. bot.logger.info("Initial wallets created!")
  48. @with_startup(setup_initial_balances)
  49. @with_help("Check your balance of Rollcoins")
  50. @as_plugin
  51. def balance(wallet: Singleton(RollcoinWallet), holdings: Singleton(RollcoinGamblingWallet)):
  52. return f"You have {wallet.balance} Rollcoins in your wallet and {holdings.balance} Rollcoins in the market"
  53. SPECIAL_AMOUNTS = ("ALL", "FRAC", "-ALL", "-FRAC")
  54. def pop_amount_arg(args):
  55. raw_amount, *rest = args
  56. if (up_amount := raw_amount.upper()) in SPECIAL_AMOUNTS:
  57. return up_amount, rest
  58. try:
  59. amount = float(raw_amount)
  60. except ValueError:
  61. RollbotFailure.INVALID_ARGUMENTS.with_reason(f"Amount should be a number or ALL or FRAC - not {raw_amount}").raise_exc()
  62. return amount, rest
  63. def assert_positive(amount):
  64. if amount <= 0:
  65. RollbotFailure.INVALID_ARGUMENTS.with_reason(f"Amount should be positive").raise_exc()
  66. def fractional_part(number):
  67. return float("0." + str(float(number)).split(".")[1])
  68. @with_help("Tip someone else some Rollcoins: !tip target amount")
  69. @as_plugin
  70. def tip(args: ArgList,
  71. db: Database,
  72. sender_wallet: Singleton(RollcoinWallet),
  73. wallets: Config("rollcoin.wallets")):
  74. if len(args) < 2:
  75. RollbotFailure.INVALID_ARGUMENTS.with_reason("Tip requires 2 arguments - target and amount").raise_exc()
  76. target, *rest = args
  77. amount, _ = pop_amount_arg(rest)
  78. if amount in SPECIAL_AMOUNTS:
  79. if amount == "ALL":
  80. amount = sender_wallet.balance
  81. elif amount == "FRAC":
  82. amount = fractional_part(sender_wallet.balance)
  83. else:
  84. amount = -1
  85. assert_positive(amount)
  86. target_id = wallets.get(target.lower(), None)
  87. if target_id is None:
  88. RollbotFailure.INVALID_ARGUMENTS.with_reason(f"Could not find wallet-holder {target}").raise_exc()
  89. if target_id == sender_wallet.sender_id:
  90. return f"You can't tip yourself!"
  91. if sender_wallet.balance < amount:
  92. return f"Insufficient funds! You need {round(amount - sender_wallet.balance, 2)} more Rollcoins. Have you tried to !mine, !beg, or !gamble?"
  93. target_wallet = db.query(RollcoinWallet).get(target_id)
  94. if target_wallet is None:
  95. target_wallet = RollcoinWallet.create_from_key(target_id)
  96. db.add(target_wallet)
  97. sender_wallet.balance = sender_wallet.balance - amount
  98. target_wallet.balance = target_wallet.balance + amount
  99. return f"Done! {target} now has {target_wallet.balance} Rollcoins"
  100. @with_help("Donate money to be distributed evenly among all wallets")
  101. @as_plugin
  102. def donate(args: ArgList,
  103. db: Database,
  104. sender_wallet: Singleton(RollcoinWallet),
  105. wallets: Config("rollcoin.wallets")):
  106. if len(args) < 1:
  107. return RollbotFailure.INVALID_ARGUMENTS.with_reason("Need an amount to donate")
  108. amount, _ = pop_amount_arg(args)
  109. if amount in SPECIAL_AMOUNTS:
  110. if amount == "ALL":
  111. amount = sender_wallet.balance
  112. elif amount == "FRAC":
  113. amount = fractional_part(sender_wallet.balance)
  114. else:
  115. amount = -1
  116. assert_positive(amount)
  117. if sender_wallet.balance < amount:
  118. return f"Insufficient funds! You need {round(amount - sender_wallet.balance, 2)} more Rollcoins"
  119. wallets = [w for w in wallets.values() if w != sender_wallet.sender_id]
  120. per_person = amount / (len(wallets))
  121. for w in wallets:
  122. target_wallet = db.query(RollcoinWallet).get(w)
  123. if target_wallet is None:
  124. target_wallet = RollcoinWallet.create_from_key(w)
  125. db.add(target_wallet)
  126. target_wallet.balance = target_wallet.balance + per_person
  127. sender_wallet.balance = sender_wallet.balance - amount
  128. return f"Done! You have donated {per_person} to each of the {len(wallets)} other wallet(s)"
  129. @with_help("Receive a mining question you can answer to receive Rollcoins")
  130. @as_plugin
  131. def mine(msg: Message,
  132. blockchain: Singleton(RollcoinBlockchain),
  133. wallet: Singleton(RollcoinWallet)):
  134. skip = msg.raw_args is not None and msg.raw_args.strip().lower() == "skip" and msg.from_admin
  135. if skip:
  136. blockchain.clear_puzzle()
  137. puzzle = blockchain.get_puzzle()
  138. if puzzle is None:
  139. exchange = list(zip("abcdefghi", range(10)))
  140. shuffle(exchange)
  141. gen = SudokuGenerator(9)
  142. gen.generate(0)
  143. gen.board_exchange_values({k: v + 1 for k, v in exchange})
  144. gen.generate(randint(10, 50))
  145. puzzle = gen.board
  146. blockchain.set_puzzle(puzzle)
  147. if skip:
  148. return "Admin has skipped the previous mining challenge.\nThe current mining challenge is\n" + str(Sudoku(board=puzzle))
  149. if msg.raw_args is None:
  150. return "The current mining challenge is\n" + str(Sudoku(board=puzzle))
  151. parsed = []
  152. row = []
  153. for c in msg.raw_args:
  154. if not c.isdigit():
  155. continue
  156. row.append(int(c))
  157. if len(row) == 9:
  158. parsed.append(row)
  159. row = []
  160. try:
  161. guess = Sudoku(board=parsed, block_width=3, block_height=3)
  162. except:
  163. return "Sorry, I could not parse that solution!"
  164. for i, row in enumerate(puzzle):
  165. for j, cell in enumerate(row):
  166. if cell not in (0, parsed[i][j]):
  167. return "Sorry, that solution doesn't match the puzzle! The current mining challenge is\n" + str(Sudoku(board=puzzle))
  168. for row in parsed:
  169. if set(row) != set(range(1, 10)):
  170. return "Sorry, that solution isn't valid!"
  171. for i in range(9):
  172. if set(r[i] for r in parsed) != set(range(1, 10)):
  173. return "Sorry, that solution isn't valid!"
  174. for i in range(0, 9, 3):
  175. for j in range(0, 9, 3):
  176. if set(x for r in parsed[i:i+3] for x in r[j:j+3]) != set(range(1, 10)):
  177. return "Sorry, that solution isn't valid!"
  178. market_rate = round(abs(gauss(10, 1)), 2)
  179. wallet.balance = wallet.balance + market_rate
  180. blockchain.clear_puzzle()
  181. return f"Looks right to me! The current market rate is {market_rate} Rollcoins per Sudoku, so that brings your balance to {wallet.balance}"
  182. @with_help("Beg for some Rollcoins")
  183. @as_plugin
  184. def beg(wallet: Singleton(RollcoinWallet),
  185. beg_cooldown: Singleton(RollcoinBegCooldown),
  186. cooldown_time: Config("rollcoin.cooldown"),
  187. beg_chance: Config("rollcoin.beg_chance")):
  188. old_cooldown = beg_cooldown.cooldown
  189. beg_cooldown.cooldown = datetime.now() + timedelta(**cooldown_time)
  190. if datetime.now() < old_cooldown:
  191. return f"Don't beg so frequently - it's not a good look"
  192. if randint(0, 99) > beg_chance:
  193. return "What are you, poor?"
  194. increase = round(abs(gauss(1, 1)), 2)
  195. wallet.balance = wallet.balance + increase
  196. return f"Fine, I've given you {increase} Rollcoins, which brings your balance to {wallet.balance}"
  197. @with_help("Gamble some Rollcoins")
  198. @as_plugin
  199. def gamble(db: Database,
  200. args: ArgList,
  201. sender_wallet: Singleton(RollcoinWallet),
  202. sender_holdings: Singleton(RollcoinGamblingWallet),
  203. market: Singleton(RollcoinMarket),
  204. transitions: Config("rollcoin.market")):
  205. # pos moves money wallet -> gambling, neg moves money gambling -> wallet
  206. # market has a current state (neutral, bull, bear, boom, bust, collapse, etc.)
  207. # after a transfer is made, the market evolves based on
  208. # - direction of transfer (buy vs sell)
  209. # - current market state
  210. # - rng
  211. # market enters new state and affects all gambling wallets, multiplying their balances
  212. if len(args) != 1:
  213. RollbotFailure.INVALID_ARGUMENTS.with_reason("Gambling requires exactly one argument: the amount to transfer (can be positive or negative)").raise_exc()
  214. amount, _ = pop_amount_arg(args)
  215. if amount in SPECIAL_AMOUNTS:
  216. if amount == "ALL":
  217. amount = sender_wallet.balance
  218. elif amount == "FRAC":
  219. amount = fractional_part(sender_wallet.balance)
  220. elif amount == "-ALL":
  221. amount = -sender_holdings.balance
  222. elif amount == "-FRAC":
  223. amount = -fractional_part(sender_holdings.balance)
  224. else:
  225. raise ValueError(amount)
  226. if amount == 0:
  227. RollbotFailure.INVALID_ARGUMENTS.with_reason(f"Amount should be nonzero").raise_exc()
  228. buying = amount > 0
  229. abs_amount = abs(amount)
  230. if buying and abs_amount > sender_wallet.balance:
  231. return f"Insufficient funds! You need {round(abs_amount - sender_wallet.balance, 2)} more Rollcoins. Have you tried to !mine, !beg, or !gamble?"
  232. elif not buying and abs_amount > sender_holdings.balance:
  233. return f"Insufficient holdings! You would need {round(abs_amount - sender_holdings.balance, 2)} more Rollcoins in the market."
  234. options = transitions[market.state]["buy" if buying else "sell"]
  235. transition = choices(options, weights=tuple(x["weight"] for x in options))[0]
  236. multiplier = transition["multiplier"]
  237. market.state = transition["new_state"]
  238. sender_wallet.balance = sender_wallet.balance - amount
  239. sender_holdings.balance = multiplier * (sender_holdings.balance + amount)
  240. for holdings in db.query(RollcoinGamblingWallet
  241. ).filter(RollcoinGamblingWallet.sender_id != sender_holdings.sender_id
  242. ).all():
  243. holdings.balance = multiplier * holdings.balance
  244. return f"{transition['message']}\nYour current holdings: {sender_holdings.balance}"
  245. @with_help("View the whole blockchain (i.e., who has how many rollcoins)")
  246. @as_plugin
  247. def blockchain(db: Database, wallets: Config("rollcoin.wallets")):
  248. results = []
  249. for name, wallet_id in wallets.items():
  250. wallet = db.query(RollcoinWallet).get(wallet_id)
  251. market = db.query(RollcoinGamblingWallet).get(wallet_id)
  252. status = name.title() + "\n"
  253. total = 0
  254. if wallet is not None:
  255. status += f"\t\tWallet: {wallet.balance}\n"
  256. total += wallet.balance
  257. else:
  258. status += "\t\tHas not used their rollcoin wallet\n"
  259. if market is not None:
  260. status += f"\t\tMarket: {market.balance}\n"
  261. total += market.balance
  262. else:
  263. status += "\t\tHas not invested their rollcoins\n"
  264. if total != 0:
  265. status += f"\t\tTotal: {total}\n"
  266. results.append((total, name, status))
  267. return "\n".join(r for _, _, r in sorted(results, reverse=True))