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. โšก
  57. """,
  58. """\
  59. ๐Ÿ–• ๐Ÿ˜Ÿ
  60. ๐Ÿ›๐Ÿ’ค๐Ÿ‘”๐Ÿ›
  61. โ›ฝ ๐Ÿ‘ข
  62. โšก๐Ÿ‘Š
  63. """,
  64. """\
  65. ๐Ÿ–• ๐Ÿ˜จ
  66. ๐Ÿ›๐Ÿ’ค๐Ÿ‘”๐Ÿ›
  67. โ›ฝ ๐Ÿ‘ข
  68. โšก8=๐Ÿ‘Š
  69. """,
  70. """\
  71. ๐Ÿ–• ๐Ÿ˜จ
  72. ๐Ÿ›๐Ÿ’ค๐Ÿ‘”๐Ÿ›
  73. โ›ฝ ๐Ÿ‘ข
  74. โšก8=๐Ÿ‘Š
  75. ๐ŸŽธ
  76. """,
  77. """\
  78. ๐Ÿ–• ๐Ÿ˜ฐ
  79. ๐Ÿ›๐Ÿ’ค๐Ÿ‘”๐Ÿ›
  80. โ›ฝ ๐Ÿ‘ข
  81. โšก8=๐Ÿ‘Š
  82. ๐ŸŽธ
  83. ๐Ÿ‘ข
  84. """,
  85. """\
  86. ๐Ÿ–• ๐Ÿ˜ฐ
  87. ๐Ÿ›๐Ÿ’ค๐Ÿ‘”๐Ÿ›
  88. โ›ฝ ๐Ÿ‘ข
  89. โšก8=๐Ÿ‘Š
  90. ๐ŸŽธ ๐ŸŒฝ
  91. ๐Ÿ‘ข
  92. """,
  93. """\
  94. ๐Ÿ–• ๐Ÿ˜ฐ
  95. ๐Ÿ›๐Ÿ’ค๐Ÿ‘”๐Ÿ›
  96. โ›ฝ ๐Ÿ‘ข
  97. โšก8=๐Ÿ‘Š
  98. ๐ŸŽธ ๐ŸŒฝ
  99. ๐Ÿ‘ข ๐Ÿ‘ข
  100. """,
  101. """\
  102. ๐Ÿ–• ๐Ÿ˜ซ
  103. ๐Ÿ›๐Ÿ’ค๐Ÿ‘”๐Ÿ›
  104. โ›ฝ ๐Ÿ‘ข
  105. โšก8=๐Ÿ‘Š=D
  106. ๐ŸŽธ ๐ŸŒฝ
  107. ๐Ÿ‘ข ๐Ÿ‘ข
  108. """,
  109. """\
  110. ๐Ÿ–• ๐Ÿ˜ต
  111. ๐Ÿ›๐Ÿ’ค๐Ÿ‘”๐Ÿ›
  112. โ›ฝ ๐Ÿ‘ข
  113. โšก8=๐Ÿ‘Š=D๐Ÿ’ฆ
  114. ๐ŸŽธ ๐ŸŒฝ
  115. ๐Ÿ‘ข ๐Ÿ‘ข
  116. """,
  117. ]
  118. @initialize_data
  119. class HangGuy:
  120. puzzle: str | None = None
  121. game_state: str | None = None
  122. bad_guesses: str = ""
  123. guy_state: int = 0
  124. guy_lifetime: int = 0
  125. def new_game(self, phrase: str):
  126. self.puzzle = b64decode(phrase).decode("utf-8").strip().upper()
  127. self.game_state = re.sub("[A-Z]", "_", self.puzzle)
  128. self.bad_guesses = ""
  129. def is_active(self):
  130. return self.puzzle is not None
  131. def is_finished(self):
  132. return self.puzzle == self.game_state
  133. def end_game(self):
  134. self.puzzle = None
  135. def reset_guy(self):
  136. self.guy_state = 0
  137. self.guy_lifetime = 0
  138. def is_dead(self):
  139. return self.guy_state >= (len(GUY_STAGES) - 1)
  140. def survival_msg(self):
  141. return f"The current guy has survived {self.guy_lifetime} game{'' if self.guy_lifetime == 1 else 's'}"
  142. def render(self):
  143. if 0 <= self.guy_state < len(GUY_STAGES):
  144. guy = GUY_STAGES[self.guy_state]
  145. elif self.guy_state >= len(GUY_STAGES):
  146. guy = GUY_STAGES[-1]
  147. else:
  148. guy = f"INVALID GUY STATE {self.guy_state}"
  149. if not self.is_active():
  150. return guy
  151. return f"{guy}\n{' '.join(self.game_state)}\nBad Guesses: {', '.join(self.bad_guesses)}"
  152. @initialize_data
  153. class DeadGuy:
  154. state: int
  155. lifetime: int
  156. timestamp: float
  157. @staticmethod
  158. def from_guy(hg: HangGuy):
  159. return DeadGuy(
  160. state=hg.guy_state,
  161. lifetime=hg.guy_lifetime,
  162. timestamp=datetime.now().timestamp(),
  163. )
  164. @as_command("hg")
  165. @as_command
  166. async def hangguy(
  167. cmd: Command,
  168. name: SenderName,
  169. origin: Origin,
  170. store: Data(HangGuy),
  171. dead_store: Data(DeadGuy),
  172. alert_channel: Config("hangguy.alert_channel"),
  173. ):
  174. if len(cmd.args) == 0:
  175. RollbotFailure.INVALID_ARGUMENTS.raise_exc(
  176. detail="Must provide subcommand or guess"
  177. )
  178. game = await store.load_or(KEY)
  179. is_active = game.is_active()
  180. subc = cmd.get_subcommand(inherit_bang=False) # get subcommand, requiring bang
  181. if subc is None or cmd.bang != subc.bang:
  182. if game.is_active():
  183. guess = cmd.args.strip().upper()
  184. prefix = ""
  185. if len(guess) == 1:
  186. if not guess.isalpha():
  187. prefix = "You should try sticking to letters!"
  188. elif guess in game.bad_guesses or guess in game.game_state:
  189. prefix = f"You've already guessed '{guess}'!"
  190. else:
  191. find = [i for i, x in enumerate(game.puzzle) if x == guess]
  192. if len(find) == 0:
  193. game.bad_guesses += guess
  194. game.guy_state += 1
  195. prefix = "Bad guess!"
  196. else:
  197. game.game_state = "".join(
  198. guess if i in find else s
  199. for i, s in enumerate(game.game_state)
  200. )
  201. prefix = f"Great! '{guess}' appears {len(find)} time{'' if len(find) == 1 else 's'}!"
  202. elif game.puzzle.split() == guess.split():
  203. prefix = "You've guessed the full phrase!"
  204. game.game_state = game.puzzle
  205. else:
  206. prefix = f"{guess} is not the phrase!"
  207. game.guy_state += 2
  208. txt = f"{prefix}\n{game.render()}"
  209. if game.is_finished():
  210. game.end_game()
  211. game.guy_lifetime += 1
  212. txt += (
  213. f"\nThe game is over, and the guy lives on!\n{game.survival_msg()}"
  214. )
  215. if game.is_dead():
  216. game.end_game()
  217. dead = DeadGuy.from_guy(game)
  218. game.reset_guy()
  219. await dead_store.save(
  220. f"{uuid4().hex}-{datetime.now().isoformat()}", dead
  221. )
  222. txt += f"\noh god oh fuck, the guy is dead!\nYou failed to guess {game.puzzle}\nThe guy has been buried."
  223. await store.save(KEY, game)
  224. return txt
  225. RollbotFailure.MISSING_SUBCOMMAND.raise_exc(
  226. detail="You are not currently in a game, and so you must use one of the subcommands: !view, !retire, !start, !alert"
  227. )
  228. if subc.name == "alert":
  229. return Response(
  230. origin_id=origin,
  231. channel_id=alert_channel,
  232. text=f"{name} wants you to check out the hangguy chat!",
  233. )
  234. if subc.name == "cancel" and is_active:
  235. game.end_game()
  236. await store.save(KEY, game)
  237. return "The game has been cancelled. The guy has not been reset."
  238. if subc.name == "retire" and not is_active:
  239. txt = f"{game.survival_msg()} and is retiring\n{game.render()}"
  240. dead_guy = DeadGuy.from_guy(game)
  241. game.reset_guy()
  242. await dead_store.save(f"{uuid4().hex}-{datetime.now().isoformat()}", dead_guy)
  243. await store.save(KEY, game)
  244. return txt
  245. if subc.name == "start" and not is_active:
  246. game.new_game(subc.args)
  247. await store.save(KEY, game)
  248. return game.render()
  249. if subc.name == "view" and not is_active:
  250. return f"{game.survival_msg()}\n" + game.render()
  251. if is_active:
  252. RollbotFailure.INVALID_SUBCOMMAND.raise_exc(
  253. detail="The only in-game subcommands are !cancel to end the game and !alert to alert the main chat."
  254. )
  255. RollbotFailure.INVALID_SUBCOMMAND.raise_exc(
  256. detail="You must use one of the subcommands: !view, !retire, !start, !alert"
  257. )