rollcoin.py 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253
  1. from dataclasses import dataclass, field
  2. from typing import Optional
  3. import random
  4. from rollbot import as_command, initialize_data, RollbotFailure
  5. from rollbot.injection import Data, Sender, Config, Const, Arg, Reply, OriginAdmin
  6. # View
  7. # !wallet - shows your number of rollcoins, NFTs (Non-Functional Tamagotchis), and market balance
  8. # !blockchain - shows the contents of all wallets and the rollcoins in the treasury
  9. # Generate
  10. # !mine - provides a sudoku that can be solved for rollcoins, which also adds to the treasury
  11. # !appraise - rollbot will purchase a post, based indirectly on number of likes received
  12. # Spend
  13. # !tip - transfer rollcoins from one person to another
  14. # !gacha - insert a rollcoin (which enters the treasury), receive a random NFT
  15. # !donate - split an amount and donate it to everyone
  16. # Market
  17. # !bet - put some amount of rollcoins into the market, some portion of which will be put in the treasury, evolves market
  18. # !cash - take some amount of rollcoins from the market, up to investment + the number of coins in the treasury, evolves market
  19. # Admin
  20. # !deflate - deflate all coins by a power of 10
  21. # !brrr - print some number of coins into the treasury
  22. # !mine !clear - clear the current mining puzzle
  23. # !market !force - force the market to transition to a given state
  24. @initialize_data
  25. @dataclass
  26. class RollcoinState:
  27. treasury: float
  28. mining_puzzle: Optional[list[list[int]]]
  29. market_state: int
  30. @initialize_data
  31. @dataclass
  32. class RollcoinWallet:
  33. balance: float = 10
  34. holdings: float = 0
  35. cost_basis: float = 0
  36. nfts: list[str] = field(default_factory=list)
  37. def __str__(self):
  38. s = f"Wallet: {self.balance}\n"
  39. if self.holdings > 0 or self.cost_basis > 0:
  40. s += f"Investments: {self.holdings}\n\t(cost basis {self.cost_basis})\n"
  41. if len(self.nfts) > 0:
  42. s += f"NFTs:\n"
  43. for nft in self.nfts:
  44. s += f"\t{nft}\n"
  45. return s.strip()
  46. SPECIAL_AMOUNTS = {
  47. "all": lambda limit: limit,
  48. "half": lambda limit: limit / 2,
  49. "frac": lambda limit: limit - int(limit),
  50. }
  51. def convert_amount(amount):
  52. if (conv := SPECIAL_AMOUNTS.get(amount.lower(), None)) is not None:
  53. return conv
  54. return float(amount)
  55. GLOBAL_STATE_KEY = "ROLLCOIN_GLOBAL_STATE"
  56. # injection values
  57. State = Data(RollcoinState).For(Const(GLOBAL_STATE_KEY), treasury=100, mining_puzzle=None, market_state=0)
  58. SenderWallet = Data(RollcoinWallet).For(Sender)
  59. WalletLookup = Config("rollcoin.wallet_names")
  60. MarketTransitions = Config("rollcoin.market.transitions")
  61. MarketMultipliers = Config("rollcoin.market.multipliers")
  62. MarketMessages = Config("rollcoin.market.messages")
  63. @as_command
  64. def wallet(sender_wallet: SenderWallet, reply: Reply):
  65. return f"You currently own...\n{sender_wallet}", reply
  66. @as_command
  67. async def blockchain(wallets: Data(RollcoinWallet), wallet_lookup: WalletLookup, state: State):
  68. response = f"Blockchain:\n\tTreasury: {state.treasury}\n\n"
  69. names = {v: k.title() for k, v in wallet_lookup.items()}
  70. async for (sender_id, wallet) in wallets.all():
  71. response += names.get(sender_id, f"Unnamed wallet {sender_id}") + ":\n"
  72. response += "\n".join("\t" + s for s in str(wallet).split("\n")) + "\n\n"
  73. return response.strip()
  74. @as_command
  75. async def tip(
  76. wallets: Data(RollcoinWallet),
  77. sender_id: Sender,
  78. sender_wallet: SenderWallet,
  79. wallet_lookup: WalletLookup,
  80. target_name: Arg(0, missing_msg="You must tell me who to tip!"),
  81. amount: Arg(1, convert=convert_amount, missing_msg="You must provide an amount to tip!", fail_msg="Could not parse {} as value"),
  82. ):
  83. if not isinstance(amount, float):
  84. # handle special converters
  85. amount = amount(sender_wallet.balance)
  86. if sender_wallet.balance == 0:
  87. return "Sorry! You don't have any rollcoins right now - try mining!"
  88. if amount > sender_wallet.balance:
  89. return f"Sorry! You only have {sender_wallet.balance} RollCoins available - try mining for more!"
  90. if amount <= 0:
  91. RollbotFailure.INVALID_ARGUMENTS.raise_exc(f"Amount must be positive, not {amount}")
  92. if (target_id := wallet_lookup.get(target_name.lower(), None)) is None:
  93. RollbotFailure.INVALID_ARGUMENTS.raise_exc(f"Cannot find wallet belonging to {target_name}")
  94. target_wallet = await wallets.load_or(target_id)
  95. sender_wallet.balance -= amount
  96. target_wallet.balance += amount
  97. await wallets.save(target_id, target_wallet)
  98. await wallets.save(sender_id, sender_wallet)
  99. return f"Done! {target_name} now has {target_wallet.balance} RollCoins!"
  100. async def evolve_market(state, transitions, multipliers, wallet_store, state_store):
  101. state.market_state = random.choices(range(len(transitions)), weights=transitions[state.market_state], k=1)[0]
  102. await state_store.save(GLOBAL_STATE_KEY, state)
  103. multiplier = multipliers[state.market_state]
  104. async for (wallet_id, wallet) in wallet_store.all():
  105. wallet.holdings *= multiplier
  106. await wallet_store.save(wallet_id, wallet)
  107. @as_command
  108. async def bet(
  109. sender_id: Sender,
  110. sender_wallet: SenderWallet,
  111. wallet_store: Data(RollcoinWallet),
  112. state: State,
  113. state_store: Data(RollcoinState),
  114. transitions: MarketTransitions,
  115. multipliers: MarketMultipliers,
  116. messages: MarketMessages,
  117. amount: Arg(0, convert=convert_amount, missing_msg="You must provide an amount to bet!", fail_msg="Could not parse {} as value"),
  118. reply: Reply,
  119. ):
  120. if not isinstance(amount, float):
  121. # handle special converters
  122. amount = amount(sender_wallet.balance)
  123. if sender_wallet.balance == 0:
  124. yield "Sorry! You don't have any rollcoins right now - try mining!"
  125. return
  126. if amount > sender_wallet.balance:
  127. yield f"Sorry! You only have {sender_wallet.balance} RollCoins available - try mining for more!"
  128. return
  129. if amount <= 0:
  130. RollbotFailure.INVALID_ARGUMENTS.raise_exc(f"Amount must be positive, not {amount}")
  131. sender_wallet.balance -= amount
  132. sender_wallet.holdings += amount
  133. sender_wallet.cost_basis += amount
  134. await wallet_store.save(sender_id, sender_wallet)
  135. # coins enter treasury, allow evolve_market to save state
  136. state.treasury += amount
  137. await evolve_market(state, transitions, multipliers, wallet_store, state_store)
  138. yield f"Trade complete!\n{await wallet_store.load(sender_id)}", reply
  139. yield f"Market status: {messages[state.market_state]}"
  140. @as_command
  141. async def cash(
  142. sender_id: Sender,
  143. sender_wallet: SenderWallet,
  144. wallet_store: Data(RollcoinWallet),
  145. state: State,
  146. state_store: Data(RollcoinState),
  147. transitions: MarketTransitions,
  148. multipliers: MarketMultipliers,
  149. messages: MarketMessages,
  150. amount: Arg(0, convert=convert_amount, missing_msg="You must provide an amount to bet!", fail_msg="Could not parse {} as value"),
  151. reply: Reply,
  152. ):
  153. if not isinstance(amount, float):
  154. # handle special converters
  155. amount = amount(sender_wallet.holdings)
  156. if sender_wallet.holdings == 0:
  157. yield "Sorry! You don't have any rollcoins invested right now - try betting!"
  158. return
  159. if amount > sender_wallet.holdings:
  160. yield f"Sorry! You only have {sender_wallet.holdings} RollCoins in the market - try betting more first!"
  161. return
  162. if amount <= 0:
  163. RollbotFailure.INVALID_ARGUMENTS.raise_exc(f"Amount must be positive, not {amount}")
  164. if state.treasury == 0:
  165. yield f"Sorry! The treasury is actually empty right now so uh... no one can sell..."
  166. return
  167. if amount <= state.treasury:
  168. response = "Successfully sold!"
  169. actual_amount = amount
  170. else:
  171. response = f"Funny story, I'm actually out of coins! I gave you what I could, draining the treasury, which was {state.treasury}."
  172. actual_amount = state.treasury
  173. sender_wallet.balance += actual_amount
  174. sender_wallet.holdings -= actual_amount
  175. sender_wallet.cost_basis = max(sender_wallet.cost_basis - actual_amount, 0)
  176. await wallet_store.save(sender_id, sender_wallet)
  177. # coins exit treasury, allow evolve_market to save state
  178. state.treasury -= actual_amount
  179. await evolve_market(state, transitions, multipliers, wallet_store, state_store)
  180. yield f"{response}\n{await wallet_store.load(sender_id)}", reply
  181. yield f"Market status: {messages[state.market_state]}"
  182. # ADMIN COMMANDS
  183. @as_command
  184. async def deflate(
  185. origin_admin: OriginAdmin,
  186. power: Arg(0, convert=int, missing_msg="Must provide power to deflate by", fail_msg="Power to deflate by must be an integer"),
  187. wallet_store: Data(RollcoinWallet),
  188. state_store: Data(RollcoinState),
  189. ):
  190. if not origin_admin:
  191. RollbotFailure.PERMISSIONS.raise_exc("Only admins can deflate the currency")
  192. factor = 10 ** power
  193. state = await state_store.load(GLOBAL_STATE_KEY)
  194. state.treasury /= factor
  195. await state_store.save(GLOBAL_STATE_KEY, state)
  196. async for (wallet_id, wallet) in wallet_store.all():
  197. wallet.balance /= factor
  198. wallet.holdings /= factor
  199. wallet.cost_basis /= factor
  200. await wallet_store.save(wallet_id, wallet)
  201. return f"Economy deflated by a factor of 10^{power}"