rollcoin.py 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596
  1. from dataclasses import dataclass, field
  2. from typing import Optional
  3. from logging import Logger
  4. import random
  5. import json
  6. from sudoku_py import SudokuGenerator, Sudoku
  7. from rollbot import as_command, initialize_data, RollbotFailure, Attachment
  8. from rollbot.injection import Data, Sender, Config, Const, Arg, Reply, OriginAdmin, Args, Attachments, Origin, Request
  9. # View
  10. # !wallet - shows your number of rollcoins, NFTs (Non-Functional Tamagotchis), and market balance
  11. # !blockchain - shows the contents of all wallets and the rollcoins in the treasury
  12. # Generate
  13. # !mine - provides a sudoku that can be solved for rollcoins, which also adds to the treasury
  14. # !appraise - rollbot will purchase a post, based indirectly on number of likes received
  15. # Spend
  16. # !tip - transfer rollcoins from one person to another
  17. # !gacha - insert a rollcoin (which enters the treasury), receive a random NFT
  18. # !donate - split an amount and donate it to everyone
  19. # Market
  20. # !bet - put some amount of rollcoins into the market, some portion of which will be put in the treasury, evolves market
  21. # !cash - take some amount of rollcoins from the market, up to investment + the number of coins in the treasury, evolves market
  22. # Admin
  23. # !deflate - deflate all coins by a power of 10 (negative to inflate economy)
  24. # !brrr - print some number of coins into the treasury (negative to delete coins)
  25. # !tax - take some number of coins from a wallet and put it in the treasury (uses wallet id, negative to give coins)
  26. # !mine skip - skip the current mining puzzle
  27. # !market - force the market to transition to a given state
  28. @initialize_data
  29. @dataclass
  30. class RollcoinState:
  31. treasury: float
  32. mining_puzzle: Optional[list[list[int]]]
  33. market_state: int
  34. appraised: list[str] = field(default_factory=list)
  35. @initialize_data
  36. @dataclass
  37. class RollcoinWallet:
  38. balance: float = 0
  39. holdings: float = 0
  40. cost_basis: float = 0
  41. nfts: list[str] = field(default_factory=list)
  42. def __str__(self):
  43. s = f"Wallet: {self.balance}\n"
  44. if self.holdings > 0 or self.cost_basis > 0:
  45. s += f"Investments: {self.holdings}\n\t(cost basis {self.cost_basis})\n"
  46. if (num_nfts := len(self.nfts)) > 0:
  47. s += f"NFTs: {num_nfts}\n"
  48. # writing out all of them produces a TON of spam
  49. # for nft in self.nfts:
  50. # s += f"\t{nft}\n"
  51. return s.strip()
  52. SPECIAL_AMOUNTS = {
  53. "all": lambda limit: limit,
  54. "half": lambda limit: limit / 2,
  55. "frac": lambda limit: limit - int(limit),
  56. }
  57. def convert_amount(amount):
  58. if (conv := SPECIAL_AMOUNTS.get(amount.lower(), None)) is not None:
  59. return conv
  60. return float(amount)
  61. # constants
  62. GLOBAL_STATE_KEY = "ROLLCOIN_GLOBAL_STATE"
  63. NAME_URL = "https://randommer.io/api/Name?nameType=firstname&quantity=1"
  64. ICON_URL = "https://app.pixelencounter.com/api/basic/svgmonsters/image/png?size=256&fillType=2"
  65. NFT_COLORS = ["red", "blue", "yellow", "green", "orange", "purple", "brown", "gray"]
  66. NFT_UNCOMMON = "silver"
  67. NFT_RARE = "gold"
  68. NFT_ULTRA = "black"
  69. NFT_RARITY_LOOKUP = {
  70. NFT_UNCOMMON: 1,
  71. NFT_RARE: 2,
  72. NFT_ULTRA: 3,
  73. }
  74. NFT_RARITY = ["common", "uncommon", "rare!", "ultra rare!"]
  75. # injection values
  76. State = Data(RollcoinState).For(Const(GLOBAL_STATE_KEY), treasury=0, mining_puzzle=None, market_state=0)
  77. SenderWallet = Data(RollcoinWallet).For(Sender, balance=10) # new wallets start with 10 coins
  78. WalletLookup = Config("rollcoin.wallet_names")
  79. MarketTransitions = Config("rollcoin.market.transitions")
  80. MarketMultipliers = Config("rollcoin.market.multipliers")
  81. MarketMessages = Config("rollcoin.market.messages")
  82. NameAPIKey = Config("rollcoin.gacha.name_api_key")
  83. @as_command
  84. def wallet(sender_wallet: SenderWallet, reply: Reply):
  85. """
  86. View the contents of your wallet. Using this command once will initialize your wallet.
  87. """
  88. return f"You currently own...\n{sender_wallet}", reply
  89. @as_command
  90. async def blockchain(wallets: Data(RollcoinWallet), wallet_lookup: WalletLookup, state: State):
  91. """
  92. View the contents of everyone's wallets
  93. """
  94. response = f"Blockchain:\n\tTreasury: {state.treasury}\n\n"
  95. names = {v: k.title() for k, v in wallet_lookup.items()}
  96. async for (sender_id, wallet) in wallets.all():
  97. response += names.get(sender_id, f"Unnamed wallet {sender_id}") + ":\n"
  98. response += "\n".join("\t" + s for s in str(wallet).split("\n")) + "\n\n"
  99. return response.strip()
  100. @as_command
  101. async def tip(
  102. wallets: Data(RollcoinWallet),
  103. sender_id: Sender,
  104. sender_wallet: SenderWallet,
  105. wallet_lookup: WalletLookup,
  106. target_name: Arg(0, missing_msg="You must tell me who to tip!"),
  107. amount: Arg(1, convert=convert_amount, missing_msg="You must provide an amount to tip!", fail_msg="Could not parse {} as value"),
  108. ):
  109. """
  110. Send RollCoins to another person, as in !tip [person] [amount]
  111. """
  112. if not isinstance(amount, float):
  113. # handle special converters
  114. amount = amount(sender_wallet.balance)
  115. if sender_wallet.balance == 0:
  116. return "Sorry! You don't have any rollcoins right now - try mining!"
  117. if amount > sender_wallet.balance:
  118. return f"Sorry! You only have {sender_wallet.balance} RollCoins available - try mining for more!"
  119. if amount <= 0:
  120. RollbotFailure.INVALID_ARGUMENTS.raise_exc(f"Amount must be positive, not {amount}")
  121. if (target_id := wallet_lookup.get(target_name.lower(), None)) is None:
  122. RollbotFailure.INVALID_ARGUMENTS.raise_exc(f"Cannot find wallet belonging to {target_name}")
  123. target_wallet = await wallets.load_or(target_id)
  124. sender_wallet.balance -= amount
  125. target_wallet.balance += amount
  126. await wallets.save(target_id, target_wallet)
  127. await wallets.save(sender_id, sender_wallet)
  128. return f"Done! {target_name} now has {target_wallet.balance} RollCoins!"
  129. @as_command
  130. async def donate(
  131. wallets: Data(RollcoinWallet),
  132. sender_id: Sender,
  133. sender_wallet: SenderWallet,
  134. wallet_store: Data(RollcoinWallet),
  135. amount: Arg(0, convert=convert_amount, missing_msg="You must provide an amount to donate!", fail_msg="Could not parse {} as value"),
  136. ):
  137. """
  138. Donate RollCoins to everyone who has a wallet, the number of coins you specify will be divided amongst everyone else.
  139. """
  140. if not isinstance(amount, float):
  141. # handle special converters
  142. amount = amount(sender_wallet.balance)
  143. if sender_wallet.balance == 0:
  144. return "Sorry! You don't have any rollcoins right now - try mining!"
  145. if amount > sender_wallet.balance:
  146. return f"Sorry! You only have {sender_wallet.balance} RollCoins available - try mining for more!"
  147. if amount <= 0:
  148. RollbotFailure.INVALID_ARGUMENTS.raise_exc(f"Amount must be positive, not {amount}")
  149. sender_wallet.balance -= amount
  150. await wallet_store.save(sender_id, sender_wallet)
  151. wallets = [(wid, w) async for wid, w in wallet_store.all() if wid != sender_id]
  152. to_donate = amount / len(wallets)
  153. for (wallet_id, wallet) in wallets:
  154. wallet.balance += to_donate
  155. await wallet_store.save(wallet_id, wallet)
  156. return f"Done! Donated {to_donate} to each other wallet, leaving you with {sender_wallet.balance} RollCoins!"
  157. async def evolve_market(state, transitions, multipliers, wallet_store, state_store):
  158. state.market_state = random.choices(range(len(transitions)), weights=transitions[state.market_state], k=1)[0]
  159. await state_store.save(GLOBAL_STATE_KEY, state)
  160. multiplier = multipliers[state.market_state]
  161. async for (wallet_id, wallet) in wallet_store.all():
  162. wallet.holdings *= multiplier
  163. await wallet_store.save(wallet_id, wallet)
  164. @as_command
  165. async def bet(
  166. sender_id: Sender,
  167. sender_wallet: SenderWallet,
  168. wallet_store: Data(RollcoinWallet),
  169. state: State,
  170. state_store: Data(RollcoinState),
  171. transitions: MarketTransitions,
  172. multipliers: MarketMultipliers,
  173. messages: MarketMessages,
  174. amount: Arg(0, convert=convert_amount, missing_msg="You must provide an amount to bet!", fail_msg="Could not parse {} as value"),
  175. reply: Reply,
  176. ):
  177. """
  178. Bet some number of RollCoins on the market. These coins will leave your wallet.
  179. """
  180. if not isinstance(amount, float):
  181. # handle special converters
  182. amount = amount(sender_wallet.balance)
  183. if sender_wallet.balance == 0:
  184. yield "Sorry! You don't have any rollcoins right now - try mining!"
  185. return
  186. if amount > sender_wallet.balance:
  187. yield f"Sorry! You only have {sender_wallet.balance} RollCoins available - try mining for more!"
  188. return
  189. if amount <= 0:
  190. RollbotFailure.INVALID_ARGUMENTS.raise_exc(f"Amount must be positive, not {amount}")
  191. sender_wallet.balance -= amount
  192. sender_wallet.holdings += amount
  193. sender_wallet.cost_basis += amount
  194. await wallet_store.save(sender_id, sender_wallet)
  195. # coins enter treasury, allow evolve_market to save state
  196. state.treasury += amount
  197. await evolve_market(state, transitions, multipliers, wallet_store, state_store)
  198. yield f"Trade complete!\n{await wallet_store.load(sender_id)}", reply
  199. yield f"Market status: {messages[state.market_state]}"
  200. @as_command
  201. async def cash(
  202. sender_id: Sender,
  203. sender_wallet: SenderWallet,
  204. wallet_store: Data(RollcoinWallet),
  205. state: State,
  206. state_store: Data(RollcoinState),
  207. transitions: MarketTransitions,
  208. multipliers: MarketMultipliers,
  209. messages: MarketMessages,
  210. amount: Arg(0, convert=convert_amount, missing_msg="You must provide an amount to bet!", fail_msg="Could not parse {} as value"),
  211. reply: Reply,
  212. ):
  213. """
  214. Cash some number of RollCoins out of the market.
  215. Because RollCoins are the one true fiat currency, you can only cash out up to the number of coins available in the treasury.
  216. """
  217. if not isinstance(amount, float):
  218. # handle special converters
  219. amount = amount(sender_wallet.holdings)
  220. if sender_wallet.holdings == 0:
  221. yield "Sorry! You don't have any rollcoins invested right now - try betting!"
  222. return
  223. if amount > sender_wallet.holdings:
  224. yield f"Sorry! You only have {sender_wallet.holdings} RollCoins in the market - try betting more first!"
  225. return
  226. if amount <= 0:
  227. RollbotFailure.INVALID_ARGUMENTS.raise_exc(f"Amount must be positive, not {amount}")
  228. if state.treasury == 0:
  229. yield f"Sorry! The treasury is actually empty right now so uh... no one can sell..."
  230. return
  231. if amount <= state.treasury:
  232. response = "Successfully sold!"
  233. actual_amount = amount
  234. else:
  235. response = f"Funny story, I'm actually out of coins! I gave you what I could, draining the treasury, which was {state.treasury}."
  236. actual_amount = state.treasury
  237. sender_wallet.balance += actual_amount
  238. sender_wallet.holdings -= actual_amount
  239. sender_wallet.cost_basis = max(sender_wallet.cost_basis - actual_amount, 0)
  240. await wallet_store.save(sender_id, sender_wallet)
  241. # coins exit treasury, allow evolve_market to save state
  242. state.treasury -= actual_amount
  243. await evolve_market(state, transitions, multipliers, wallet_store, state_store)
  244. yield f"{response}\n{await wallet_store.load(sender_id)}", reply
  245. yield f"Market status: {messages[state.market_state]}"
  246. @as_command
  247. async def mine(
  248. args: Args,
  249. admin: OriginAdmin,
  250. state: State,
  251. sender_id: Sender,
  252. sender_wallet: SenderWallet,
  253. wallet_store: Data(RollcoinWallet),
  254. state_store: Data(RollcoinState),
  255. reply: Reply,
  256. ):
  257. """
  258. Trade solved Sudokus for RollCoins. Admins can skip the current puzzle with !mine skip
  259. """
  260. async def generate_puzzle():
  261. exchange = list(zip("abcdefghi", range(10)))
  262. random.shuffle(exchange)
  263. gen = SudokuGenerator(9)
  264. gen.generate(0)
  265. gen.board_exchange_values({k: v + 1 for k, v in exchange})
  266. gen.generate(random.randint(10, 50))
  267. state.mining_puzzle = gen.board
  268. await state_store.save(GLOBAL_STATE_KEY, state)
  269. if args is None or len(args) == 0:
  270. if state.mining_puzzle is None:
  271. await generate_puzzle()
  272. yield f"The current mining challenge is\n{Sudoku(board=state.mining_puzzle)}"
  273. return
  274. if "skip" in args.strip(" !/").lower():
  275. if not admin:
  276. RollbotFailure.PERMISSIONS.raise_exc("Only admins can skip the current mining challenge")
  277. await generate_puzzle()
  278. yield f"Admin has skipped the previous mining challenge. The new challenge is:\n{Sudoku(board=state.mining_puzzle)}"
  279. return
  280. # best effort parse input into a sudoku grid
  281. parsed = []
  282. row = []
  283. for c in args:
  284. if not c.isdigit():
  285. continue
  286. row.append(int(c))
  287. if len(row) == 9:
  288. parsed.append(row)
  289. row = []
  290. try:
  291. Sudoku(board=parsed, block_width=3, block_height=3)
  292. except:
  293. RollbotFailure.INVALID_ARGUMENTS.raise_exc("Could not parse that solution")
  294. for i, row in enumerate(state.mining_puzzle):
  295. for j, cell in enumerate(row):
  296. if cell not in (0, parsed[i][j]):
  297. yield "Sorry, that solution doesn't match the puzzle!", reply
  298. yield f"The current mining challenge is\n{Sudoku(board=state.mining_puzzle)}"
  299. return
  300. for row in parsed:
  301. if set(row) != set(range(1, 10)):
  302. yield "Sorry, that solution isn't valid!", reply
  303. yield f"The current mining challenge is\n{Sudoku(board=state.mining_puzzle)}"
  304. return
  305. for i in range(9):
  306. if set(r[i] for r in parsed) != set(range(1, 10)):
  307. yield "Sorry, that solution isn't valid!", reply
  308. yield f"The current mining challenge is\n{Sudoku(board=state.mining_puzzle)}"
  309. return
  310. for i in range(0, 9, 3):
  311. for j in range(0, 9, 3):
  312. if set(x for r in parsed[i:i+3] for x in r[j:j+3]) != set(range(1, 10)):
  313. yield "Sorry, that solution isn't valid!", reply
  314. yield f"The current mining challenge is\n{Sudoku(board=state.mining_puzzle)}"
  315. return
  316. zeroes = len([y for x in state.mining_puzzle for y in x if y == 0])
  317. difficulty = (zeroes - 10) / 4
  318. value = round(abs(random.gauss(difficulty * 200, 25)), 2)
  319. sender_wallet.balance += value
  320. await wallet_store.save(sender_id, sender_wallet)
  321. # allow generate_puzzle to save the state
  322. state.treasury += value / 10
  323. await generate_puzzle()
  324. yield f"Looks right to me! You earned {value} RollCoins (and generated some for the treasury). That brings your balance to {sender_wallet.balance}", reply
  325. yield f"The current mining challenge is\n{Sudoku(board=state.mining_puzzle)}"
  326. @as_command
  327. async def appraise(
  328. logger: Logger,
  329. origin: Origin,
  330. attachments: Attachments,
  331. sender_id: Sender,
  332. sender_wallet: SenderWallet,
  333. wallet_store: Data(RollcoinWallet),
  334. state: State,
  335. state_store: Data(RollcoinState),
  336. reply: Reply,
  337. ):
  338. """
  339. Use this command while replying to a popular post, and I will purchase it for some RollCoins!
  340. """
  341. if origin == "GROUPME":
  342. try:
  343. reply_attachment = next(a for a in attachments if a.name == "reply")
  344. target_msg = json.loads(await reply_attachment.body())
  345. appraisal_id = f"GROUPME-{target_msg['group_id']}-{target_msg['id']}"
  346. score_base = len(target_msg["favorited_by"])
  347. except:
  348. logger.exception("Failed appraisal, logging for debugging")
  349. RollbotFailure.INVALID_ARGUMENTS.raise_exc("Reply to a message to have it appraised, I could not read that one")
  350. # other origins an be handled here
  351. else:
  352. RollbotFailure.INVALID_COMMAND.raise_exc(f"Message appraisal is not implemented in this platform (origin was {origin})")
  353. if appraisal_id in state.appraised:
  354. return "Sorry, I've already purchased that message!", reply
  355. if score_base <= 2:
  356. return "Sorry, I don't think that message is worth very much...", reply
  357. value = round(abs(random.gauss(score_base * 50, 15)), 2)
  358. sender_wallet.balance += value
  359. await wallet_store.save(sender_id, sender_wallet)
  360. state.appraised.append(appraisal_id)
  361. await state_store.save(GLOBAL_STATE_KEY, state)
  362. return f"That post is very interesting! I will purchase it for {value} RollCoins! That brings your balance to {sender_wallet.balance}", reply
  363. def pull_gacha_color():
  364. pull = random.randint(1, 100)
  365. if pull == 100: # 1%
  366. return NFT_ULTRA
  367. if pull >= 90: # 10%
  368. return NFT_RARE
  369. if pull >= 70: # 20%
  370. return NFT_UNCOMMON
  371. return random.choice(NFT_COLORS) # nice%
  372. @as_command
  373. async def gacha(
  374. sender_wallet: SenderWallet,
  375. sender_id: Sender,
  376. wallet_store: Data(RollcoinWallet),
  377. logger: Logger,
  378. req: Request,
  379. name_api_key: NameAPIKey,
  380. reply: Reply,
  381. ):
  382. """
  383. Spend one RollCoin on a Non-Functional Tamagotchi! You could get a rare one!
  384. """
  385. if sender_wallet.balance < 1:
  386. return f"You only have {sender_wallet.balance} RollCoins available, and gacha pulls cost one each!", reply
  387. color1 = pull_gacha_color()
  388. color2 = pull_gacha_color()
  389. try:
  390. async with req.get(NAME_URL, headers={"X-Api-Key": name_api_key}) as res:
  391. name = (await res.json())[0]
  392. async with req.get(ICON_URL + f"&primaryColor={color1}&secondaryColor={color2}") as res:
  393. img = Attachment(name="image", body=await res.read())
  394. except:
  395. logger.exception("Failed gacha, logging for debugging")
  396. RollbotFailure.SERVICE_DOWN.raise_exc("Failed to pull from gachapon, your coin has not been deducted")
  397. # calculate rarity
  398. rarity = (NFT_RARITY_LOOKUP.get(color1, 0) + NFT_RARITY_LOOKUP.get(color2, 0)) // 2
  399. if color1 == color2:
  400. color_info = f"double {color1}!"
  401. rarity += 1
  402. else:
  403. color_info = f"{color1} and {color2}"
  404. rarity_info = "legendary!" if rarity >= len(NFT_RARITY) else NFT_RARITY[rarity]
  405. info = f"{name} ({color_info}) ({rarity_info})"
  406. sender_wallet.balance -= 1
  407. sender_wallet.nfts.append(info)
  408. await wallet_store.save(sender_id, sender_wallet)
  409. return f"You received...\n\t{info}", img, reply
  410. # ADMIN COMMANDS
  411. @as_command
  412. async def deflate(
  413. origin_admin: OriginAdmin,
  414. power: Arg(0, convert=int, missing_msg="Must provide power to deflate by", fail_msg="Power to deflate by must be an integer"),
  415. wallet_store: Data(RollcoinWallet),
  416. state_store: Data(RollcoinState),
  417. ):
  418. """
  419. Admins only: Deflate the value of the RollCoin by a factor of 10 to the given power, which can be negative to inflate.
  420. """
  421. if not origin_admin:
  422. RollbotFailure.PERMISSIONS.raise_exc("Only admins can deflate the currency")
  423. factor = 10 ** power
  424. state = await state_store.load(GLOBAL_STATE_KEY)
  425. state.treasury /= factor
  426. await state_store.save(GLOBAL_STATE_KEY, state)
  427. async for (wallet_id, wallet) in wallet_store.all():
  428. wallet.balance /= factor
  429. wallet.holdings /= factor
  430. wallet.cost_basis /= factor
  431. await wallet_store.save(wallet_id, wallet)
  432. return f"Economy deflated by a factor of 10^{power}"
  433. @as_command
  434. async def brrr(
  435. origin_admin: OriginAdmin,
  436. coins: Arg(0, convert=float, missing_msg="Must provide coins to mint", fail_msg="Coins to mint by must be a number"),
  437. state_store: Data(RollcoinState),
  438. ):
  439. """
  440. Admins only: Print more RollCoins into the treasury, which can be useful to help the economy! You can also delete them with a negative number
  441. """
  442. if not origin_admin:
  443. RollbotFailure.PERMISSIONS.raise_exc("Only admins can mint coins")
  444. if coins <= 0:
  445. RollbotFailure.INVALID_ARGUMENTS.raise_exc("Can only mint a positive number of coins")
  446. state = await state_store.load(GLOBAL_STATE_KEY)
  447. state.treasury += coins
  448. await state_store.save(GLOBAL_STATE_KEY, state)
  449. return f"{coins} new RollCoins have been added to the treasury!"
  450. @as_command
  451. async def tax(
  452. origin_admin: OriginAdmin,
  453. wallet_lookup: WalletLookup,
  454. target_name: Arg(0, missing_msg="Must provide a target to tax"),
  455. coins: Arg(1, convert=float, missing_msg="Must provide coins to tax", fail_msg="Coins to tax by must be a number"),
  456. wallet_store: Data(RollcoinWallet),
  457. state_store: Data(RollcoinState),
  458. ):
  459. """
  460. Admins only: Tax a user to put some of their RollCoins into the treasury, or use a negative number to provide a rebate
  461. """
  462. if not origin_admin:
  463. RollbotFailure.PERMISSIONS.raise_exc("Only admins can tax someone")
  464. if (target_id := wallet_lookup.get(target_name.lower(), None)) is None or (wallet := await wallet_store.load(target_id)) is None:
  465. RollbotFailure.INVALID_ARGUMENTS.raise_exc(f"Could not find a wallet for {target_name}")
  466. state = await state_store.load(GLOBAL_STATE_KEY)
  467. state.treasury += coins
  468. wallet.balance -= coins
  469. await state_store.save(GLOBAL_STATE_KEY, state)
  470. await wallet_store.save(target_id, wallet)
  471. return f"{coins} RollCoins were taken from {target_name} and put in the treasury!"
  472. @as_command
  473. async def market(
  474. origin_admin: OriginAdmin,
  475. target_state: Arg(0, convert=int, missing_msg="Must provide target state for market", fail_msg="Target market state must be an integer"),
  476. state_store: Data(RollcoinState),
  477. messages: MarketMessages,
  478. ):
  479. """
  480. Admins only: Force the market into a certain state
  481. """
  482. if not origin_admin:
  483. RollbotFailure.PERMISSIONS.raise_exc("Only admins can manipulate the market")
  484. if target_state < 0 or target_state >= len(messages):
  485. RollbotFailure.INVALID_ARGUMENTS.raise_exc("Market state invalid")
  486. state = await state_store.load(GLOBAL_STATE_KEY)
  487. state.market_state = target_state
  488. await state_store.save(GLOBAL_STATE_KEY, state)
  489. return f"Market status: {messages[state.market_state]}"