|
@@ -0,0 +1,301 @@
|
|
|
+from typing import Optional
|
|
|
+from datetime import datetime
|
|
|
+from base64 import b64decode
|
|
|
+from uuid import uuid4
|
|
|
+import re
|
|
|
+
|
|
|
+from rollbot import as_command, initialize_data, RollbotFailure, Response, Command
|
|
|
+from rollbot.injection import Data, Config, Subcommand, Lazy, Args, Origin, SenderName
|
|
|
+
|
|
|
+
|
|
|
+KEY = "HANG_GUY_SINGLETON"
|
|
|
+GUY_STAGES = [
|
|
|
+""
|
|
|
+,
|
|
|
+"""\
|
|
|
+๐
|
|
|
+"""
|
|
|
+,
|
|
|
+"""\
|
|
|
+๐
|
|
|
+๐
|
|
|
+"""
|
|
|
+,
|
|
|
+"""\
|
|
|
+๐
|
|
|
+๐
|
|
|
+โฝ
|
|
|
+"""
|
|
|
+,
|
|
|
+"""\
|
|
|
+๐
|
|
|
+๐
|
|
|
+โฝ
|
|
|
+โก
|
|
|
+"""
|
|
|
+,
|
|
|
+"""\
|
|
|
+๐ ๐
|
|
|
+ ๐
|
|
|
+ โฝ
|
|
|
+ โก
|
|
|
+"""
|
|
|
+,
|
|
|
+"""\
|
|
|
+๐ ๐
|
|
|
+๐ ๐
|
|
|
+ โฝ
|
|
|
+ โก
|
|
|
+"""
|
|
|
+,
|
|
|
+"""\
|
|
|
+๐ ๐
|
|
|
+๐๐ค๐
|
|
|
+ โฝ
|
|
|
+ โก
|
|
|
+"""
|
|
|
+,
|
|
|
+"""\
|
|
|
+๐ ๐
|
|
|
+๐๐ค๐๐
|
|
|
+ โฝ
|
|
|
+ โก
|
|
|
+"""
|
|
|
+,
|
|
|
+"""\
|
|
|
+๐ ๐
|
|
|
+๐๐ค๐๐
|
|
|
+ โฝ ๐ข
|
|
|
+ โก
|
|
|
+"""
|
|
|
+,
|
|
|
+"""\
|
|
|
+๐ ๐
|
|
|
+๐๐ค๐๐
|
|
|
+ โฝ ๐ข
|
|
|
+ โก๐
|
|
|
+"""
|
|
|
+,
|
|
|
+"""\
|
|
|
+๐ ๐จ
|
|
|
+๐๐ค๐๐
|
|
|
+ โฝ ๐ข
|
|
|
+ โก8=๐
|
|
|
+"""
|
|
|
+,
|
|
|
+"""\
|
|
|
+๐ ๐จ
|
|
|
+๐๐ค๐๐
|
|
|
+ โฝ ๐ข
|
|
|
+ โก8=๐
|
|
|
+ ๐ธ
|
|
|
+"""
|
|
|
+,
|
|
|
+"""\
|
|
|
+๐ ๐ฐ
|
|
|
+๐๐ค๐๐
|
|
|
+ โฝ ๐ข
|
|
|
+ โก8=๐
|
|
|
+ ๐ธ
|
|
|
+ ๐ข
|
|
|
+"""
|
|
|
+,
|
|
|
+"""\
|
|
|
+๐ ๐ฐ
|
|
|
+๐๐ค๐๐
|
|
|
+ โฝ ๐ข
|
|
|
+ โก8=๐
|
|
|
+ ๐ธ ๐ฝ
|
|
|
+ ๐ข
|
|
|
+"""
|
|
|
+,
|
|
|
+"""\
|
|
|
+๐ ๐ฐ
|
|
|
+๐๐ค๐๐
|
|
|
+ โฝ ๐ข
|
|
|
+ โก8=๐
|
|
|
+ ๐ธ ๐ฝ
|
|
|
+ ๐ข ๐ข
|
|
|
+"""
|
|
|
+,
|
|
|
+"""\
|
|
|
+๐ ๐ซ
|
|
|
+๐๐ค๐๐
|
|
|
+ โฝ ๐ข
|
|
|
+ โก8=๐=D
|
|
|
+ ๐ธ ๐ฝ
|
|
|
+ ๐ข ๐ข
|
|
|
+"""
|
|
|
+,
|
|
|
+"""\
|
|
|
+๐ ๐ต
|
|
|
+๐๐ค๐๐
|
|
|
+ โฝ ๐ข
|
|
|
+ โก8=๐=D๐ฆ
|
|
|
+ ๐ธ ๐ฝ
|
|
|
+ ๐ข ๐ข
|
|
|
+"""
|
|
|
+,
|
|
|
+]
|
|
|
+
|
|
|
+
|
|
|
+@initialize_data
|
|
|
+class HangGuy:
|
|
|
+ puzzle: Optional[str] = None
|
|
|
+ game_state: Optional[str] = None
|
|
|
+ bad_guesses: str = ""
|
|
|
+ guy_state: int = 0
|
|
|
+ guy_lifetime: int = 0
|
|
|
+
|
|
|
+ def new_game(self, phrase: str):
|
|
|
+ self.puzzle = b64decode(phrase).decode("utf-8").strip().upper()
|
|
|
+ self.game_state = re.sub("[A-Z]", "_", self.puzzle)
|
|
|
+ self.bad_guesses = ""
|
|
|
+
|
|
|
+ def is_active(self):
|
|
|
+ return self.puzzle is not None
|
|
|
+
|
|
|
+ def is_finished(self):
|
|
|
+ return self.puzzle == self.game_state
|
|
|
+
|
|
|
+ def end_game(self):
|
|
|
+ self.puzzle = None
|
|
|
+
|
|
|
+ def reset_guy(self):
|
|
|
+ self.guy_state = 0
|
|
|
+ self.guy_lifetime = 0
|
|
|
+
|
|
|
+ def is_dead(self):
|
|
|
+ return self.guy_state >= (len(GUY_STAGES) - 1)
|
|
|
+
|
|
|
+ def survival_msg(self):
|
|
|
+ return f"The current guy has survived {self.guy_lifetime} game{'' if self.guy_lifetime == 1 else 's'}"
|
|
|
+
|
|
|
+ def render(self):
|
|
|
+ if 0 <= self.guy_state < len(GUY_STAGES):
|
|
|
+ guy = GUY_STAGES[self.guy_state]
|
|
|
+ elif self.guy_state >= len(GUY_STAGES):
|
|
|
+ guy = GUY_STAGES[-1]
|
|
|
+ else:
|
|
|
+ guy = f"INVALID GUY STATE {self.guy_state}"
|
|
|
+
|
|
|
+ if not self.is_active():
|
|
|
+ return guy
|
|
|
+
|
|
|
+ return f"{guy}\n{' '.join(self.game_state)}\nBad Guesses: {', '.join(self.bad_guesses)}"
|
|
|
+
|
|
|
+
|
|
|
+@initialize_data
|
|
|
+class DeadGuy:
|
|
|
+ state: int
|
|
|
+ lifetime: int
|
|
|
+ timestamp: float
|
|
|
+
|
|
|
+ @staticmethod
|
|
|
+ def from_guy(hg: HangGuy):
|
|
|
+ return DeadGuy(
|
|
|
+ state=hg.guy_state,
|
|
|
+ lifetime=hg.guy_lifetime,
|
|
|
+ timestamp=datetime.now().timestamp(),
|
|
|
+ )
|
|
|
+
|
|
|
+
|
|
|
+@as_command
|
|
|
+async def hangguy(
|
|
|
+ cmd: Command,
|
|
|
+ name: SenderName,
|
|
|
+ origin: Origin,
|
|
|
+ store: Data(HangGuy),
|
|
|
+ dead_store: Data(DeadGuy),
|
|
|
+ alert_channel: Config("hangguy.alert_channel"),
|
|
|
+):
|
|
|
+ if len(cmd.args) == 0:
|
|
|
+ RollbotFailure.INVALID_ARGUMENTS.raise_exc(detail="Must provide subcommand or guess")
|
|
|
+
|
|
|
+ game = await store.load_or(KEY)
|
|
|
+ is_active = game.is_active()
|
|
|
+
|
|
|
+ subc = cmd.get_subcommand(inherit_bang=False) # get subcommand, requiring bang
|
|
|
+
|
|
|
+ if subc is None or cmd.bang != subc.bang:
|
|
|
+ if game.is_active():
|
|
|
+ guess = cmd.args.strip().upper()
|
|
|
+ prefix = ""
|
|
|
+ if len(guess) == 1:
|
|
|
+ if not guess.isalpha():
|
|
|
+ prefix = "You should try sticking to letters!"
|
|
|
+ elif guess in game.bad_guesses or guess in game.game_state:
|
|
|
+ prefix = f"You've already guessed '{guess}'!"
|
|
|
+ else:
|
|
|
+ find = [i for i, x in enumerate(game.puzzle) if x == guess]
|
|
|
+ if len(find) == 0:
|
|
|
+ game.bad_guesses += guess
|
|
|
+ game.guy_state += 1
|
|
|
+ prefix = "Bad guess!"
|
|
|
+ else:
|
|
|
+ game.game_state = "".join(guess if i in find else s for i, s in enumerate(game.game_state))
|
|
|
+ prefix = f"Great! '{guess}' appears {len(find)} time{'' if len(find) == 1 else 's'}!"
|
|
|
+ elif game.puzzle.split() == guess.split():
|
|
|
+ prefix = "You've guessed the full phrase!"
|
|
|
+ game.game_state = game.puzzle
|
|
|
+ else:
|
|
|
+ prefix = f"{guess} is not the phrase!"
|
|
|
+ game.guy_state += 2
|
|
|
+
|
|
|
+ txt = f"{prefix}\n{game.render()}"
|
|
|
+
|
|
|
+ if game.is_finished():
|
|
|
+ game.end_game()
|
|
|
+ game.guy_lifetime += 1
|
|
|
+ txt += f"\nThe game is over, and the guy lives on!\n{game.survival_msg()}"
|
|
|
+
|
|
|
+ if game.is_dead():
|
|
|
+ game.end_game()
|
|
|
+ dead = DeadGuy.from_guy(game)
|
|
|
+ game.reset_guy()
|
|
|
+ await dead_store.save(f"{uuid4().hex}-{datetime.now().isoformat()}", dead_guy)
|
|
|
+ txt += f"\noh god oh fuck, the guy is dead!\nYou failed to guess {game.puzzle}\nThe guy has been buried."
|
|
|
+
|
|
|
+ await store.save(KEY, game)
|
|
|
+ return txt
|
|
|
+ RollbotFailure.MISSING_SUBCOMMAND.raise_exc(
|
|
|
+ detail="You are not currently in a game, and so you must use one of the subcommands: !view, !retire, !start, !alert"
|
|
|
+ )
|
|
|
+
|
|
|
+ if subc.name == "alert":
|
|
|
+ return Response(
|
|
|
+ origin_id=origin,
|
|
|
+ channel_id=alert_channel,
|
|
|
+ text=f"{name} wants you to check out the hangguy chat!",
|
|
|
+ )
|
|
|
+
|
|
|
+ if subc.name == "cancel" and is_active:
|
|
|
+ game.end_game()
|
|
|
+ await store.save(KEY, game)
|
|
|
+ return "The game has been cancelled. The guy has not been reset."
|
|
|
+
|
|
|
+ if subc.name == "retire" and not is_active:
|
|
|
+ txt = f"{game.survival_msg()} and is retiring\n{game.render()}"
|
|
|
+ dead_guy = DeadGuy.from_guy(game)
|
|
|
+ game.reset_guy()
|
|
|
+ await dead_store.save(f"{uuid4().hex}-{datetime.now().isoformat()}", dead_guy)
|
|
|
+ await store.save(KEY, game)
|
|
|
+ return txt
|
|
|
+
|
|
|
+ if subc.name == "start" and not is_active:
|
|
|
+ game.new_game(subc.args)
|
|
|
+ await store.save(KEY, game)
|
|
|
+ return game.render()
|
|
|
+
|
|
|
+ if subc.name == "view" and not is_active:
|
|
|
+ return f"{game.survival_msg()}\n" + game.render()
|
|
|
+
|
|
|
+
|
|
|
+ if is_active:
|
|
|
+ RollbotFailure.INVALID_SUBCOMMAND.raise_exc(
|
|
|
+ detail="The only in-game subcommands are !cancel to end the game and !alert to alert the main chat."
|
|
|
+ )
|
|
|
+
|
|
|
+ RollbotFailure.INVALID_SUBCOMMAND.raise_exc(
|
|
|
+ detail="You must use one of the subcommands: !view, !retire, !start, !alert"
|
|
|
+ )
|