hangguy.py 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291
  1. from datetime import datetime
  2. from base64 import b64decode
  3. from uuid import uuid4
  4. import re
  5. from rollbot import as_command, initialize_data, RollbotFailure, Response, Command
  6. from rollbot.injection import Data, Config, Origin, SenderName
  7. KEY = "HANG_GUY_SINGLETON"
  8. GUY_STAGES = [
  9. "",
  10. """\
  11. 😎
  12. """,
  13. """\
  14. 😎
  15. 👔
  16. """,
  17. """\
  18. 😎
  19. 👔
  20. """,
  21. """\
  22. 😎
  23. 👔
  24. """,
  25. """\
  26. 👍 😎
  27. 👔
  28. """,
  29. """\
  30. 👍 😎
  31. 🐛 👔
  32. """,
  33. """\
  34. 🖕 😎
  35. 🐛💤👔
  36. """,
  37. """\
  38. 🖕 😎
  39. 🐛💤👔🐛
  40. """,
  41. """\
  42. 🖕 😟
  43. 🐛💤👔🐛
  44. ⛽ 👢
  45. """,
  46. """\
  47. 🖕 😟
  48. 🐛💤👔🐛
  49. ⛽ 👢
  50. ⚡👊
  51. """,
  52. """\
  53. 🖕 😨
  54. 🐛💤👔🐛
  55. ⛽ 👢
  56. ⚡8=👊
  57. """,
  58. """\
  59. 🖕 😨
  60. 🐛💤👔🐛
  61. ⛽ 👢
  62. ⚡8=👊
  63. 🎸
  64. """,
  65. """\
  66. 🖕 😰
  67. 🐛💤👔🐛
  68. ⛽ 👢
  69. ⚡8=👊
  70. 🎸
  71. 👢
  72. """,
  73. """\
  74. 🖕 😰
  75. 🐛💤👔🐛
  76. ⛽ 👢
  77. ⚡8=👊
  78. 🎸 🌽
  79. 👢
  80. """,
  81. """\
  82. 🖕 😰
  83. 🐛💤👔🐛
  84. ⛽ 👢
  85. ⚡8=👊
  86. 🎸 🌽
  87. 👢 👢
  88. """,
  89. """\
  90. 🖕 😫
  91. 🐛💤👔🐛
  92. ⛽ 👢
  93. ⚡8=👊=D
  94. 🎸 🌽
  95. 👢 👢
  96. """,
  97. """\
  98. 🖕 😵
  99. 🐛💤👔🐛
  100. ⛽ 👢
  101. ⚡8=👊=D💦
  102. 🎸 🌽
  103. 👢 👢
  104. """,
  105. ]
  106. @initialize_data
  107. class HangGuy:
  108. puzzle: str | None = None
  109. game_state: str | None = None
  110. bad_guesses: str = ""
  111. guy_state: int = 0
  112. guy_lifetime: int = 0
  113. def new_game(self, phrase: str):
  114. self.puzzle = b64decode(phrase).decode("utf-8").strip().upper()
  115. self.game_state = re.sub("[A-Z]", "_", self.puzzle)
  116. self.bad_guesses = ""
  117. def is_active(self):
  118. return self.puzzle is not None
  119. def is_finished(self):
  120. return self.puzzle == self.game_state
  121. def end_game(self):
  122. self.puzzle = None
  123. def reset_guy(self):
  124. self.guy_state = 0
  125. self.guy_lifetime = 0
  126. def is_dead(self):
  127. return self.guy_state >= (len(GUY_STAGES) - 1)
  128. def survival_msg(self):
  129. return f"The current guy has survived {self.guy_lifetime} game{'' if self.guy_lifetime == 1 else 's'}"
  130. def render(self):
  131. if 0 <= self.guy_state < len(GUY_STAGES):
  132. guy = GUY_STAGES[self.guy_state]
  133. elif self.guy_state >= len(GUY_STAGES):
  134. guy = GUY_STAGES[-1]
  135. else:
  136. guy = f"INVALID GUY STATE {self.guy_state}"
  137. if not self.is_active():
  138. return guy
  139. return f"{guy}\n{' '.join(self.game_state)}\nBad Guesses: {', '.join(self.bad_guesses)}"
  140. @initialize_data
  141. class DeadGuy:
  142. state: int
  143. lifetime: int
  144. timestamp: float
  145. @staticmethod
  146. def from_guy(hg: HangGuy):
  147. return DeadGuy(
  148. state=hg.guy_state,
  149. lifetime=hg.guy_lifetime,
  150. timestamp=datetime.now().timestamp(),
  151. )
  152. @as_command("hg")
  153. @as_command
  154. async def hangguy(
  155. cmd: Command,
  156. name: SenderName,
  157. origin: Origin,
  158. store: Data(HangGuy),
  159. dead_store: Data(DeadGuy),
  160. alert_channel: Config("hangguy.alert_channel"),
  161. ):
  162. if len(cmd.args) == 0:
  163. RollbotFailure.INVALID_ARGUMENTS.raise_exc(
  164. detail="Must provide subcommand or guess"
  165. )
  166. game = await store.load_or(KEY)
  167. is_active = game.is_active()
  168. subc = cmd.get_subcommand(inherit_bang=False) # get subcommand, requiring bang
  169. if subc is None or cmd.bang != subc.bang:
  170. if game.is_active():
  171. guess = cmd.args.strip().upper()
  172. prefix = ""
  173. if len(guess) == 1:
  174. if not guess.isalpha():
  175. prefix = "You should try sticking to letters!"
  176. elif guess in game.bad_guesses or guess in game.game_state:
  177. prefix = f"You've already guessed '{guess}'!"
  178. else:
  179. find = [i for i, x in enumerate(game.puzzle) if x == guess]
  180. if len(find) == 0:
  181. game.bad_guesses += guess
  182. game.guy_state += 1
  183. prefix = "Bad guess!"
  184. else:
  185. game.game_state = "".join(
  186. guess if i in find else s
  187. for i, s in enumerate(game.game_state)
  188. )
  189. prefix = f"Great! '{guess}' appears {len(find)} time{'' if len(find) == 1 else 's'}!"
  190. elif game.puzzle.split() == guess.split():
  191. prefix = "You've guessed the full phrase!"
  192. game.game_state = game.puzzle
  193. else:
  194. prefix = f"{guess} is not the phrase!"
  195. game.guy_state += 2
  196. txt = f"{prefix}\n{game.render()}"
  197. if game.is_finished():
  198. game.end_game()
  199. game.guy_lifetime += 1
  200. txt += (
  201. f"\nThe game is over, and the guy lives on!\n{game.survival_msg()}"
  202. )
  203. if game.is_dead():
  204. game.end_game()
  205. dead = DeadGuy.from_guy(game)
  206. game.reset_guy()
  207. await dead_store.save(
  208. f"{uuid4().hex}-{datetime.now().isoformat()}", dead
  209. )
  210. txt += f"\noh god oh fuck, the guy is dead!\nYou failed to guess {game.puzzle}\nThe guy has been buried."
  211. await store.save(KEY, game)
  212. return txt
  213. RollbotFailure.MISSING_SUBCOMMAND.raise_exc(
  214. detail="You are not currently in a game, and so you must use one of the subcommands: !view, !retire, !start, !alert"
  215. )
  216. if subc.name == "alert":
  217. return Response(
  218. origin_id=origin,
  219. channel_id=alert_channel,
  220. text=f"{name} wants you to check out the hangguy chat!",
  221. )
  222. if subc.name == "cancel" and is_active:
  223. game.end_game()
  224. await store.save(KEY, game)
  225. return "The game has been cancelled. The guy has not been reset."
  226. if subc.name == "retire" and not is_active:
  227. txt = f"{game.survival_msg()} and is retiring\n{game.render()}"
  228. dead_guy = DeadGuy.from_guy(game)
  229. game.reset_guy()
  230. await dead_store.save(f"{uuid4().hex}-{datetime.now().isoformat()}", dead_guy)
  231. await store.save(KEY, game)
  232. return txt
  233. if subc.name == "start" and not is_active:
  234. game.new_game(subc.args)
  235. await store.save(KEY, game)
  236. return game.render()
  237. if subc.name == "view" and not is_active:
  238. return f"{game.survival_msg()}\n" + game.render()
  239. if is_active:
  240. RollbotFailure.INVALID_SUBCOMMAND.raise_exc(
  241. detail="The only in-game subcommands are !cancel to end the game and !alert to alert the main chat."
  242. )
  243. RollbotFailure.INVALID_SUBCOMMAND.raise_exc(
  244. detail="You must use one of the subcommands: !view, !retire, !start, !alert"
  245. )