123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291 |
- 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, Origin, SenderName
- KEY = "HANG_GUY_SINGLETON"
- GUY_STAGES = [
- "",
- """\
- 😎
- """,
- """\
- 😎
- 👔
- """,
- """\
- 😎
- 👔
- ⛽
- """,
- """\
- 😎
- 👔
- ⛽
- ⚡
- """,
- """\
- 👍 😎
- 👔
- ⛽
- ⚡
- """,
- """\
- 👍 😎
- 🐛 👔
- ⛽
- ⚡
- """,
- """\
- 🖕 😎
- 🐛💤👔
- ⛽
- ⚡
- """,
- """\
- 🖕 😎
- 🐛💤👔🐛
- ⛽
- ⚡
- """,
- """\
- 🖕 😟
- 🐛💤👔🐛
- ⛽ 👢
- ⚡
- """,
- """\
- 🖕 😟
- 🐛💤👔🐛
- ⛽ 👢
- ⚡👊
- """,
- """\
- 🖕 😨
- 🐛💤👔🐛
- ⛽ 👢
- ⚡8=👊
- """,
- """\
- 🖕 😨
- 🐛💤👔🐛
- ⛽ 👢
- ⚡8=👊
- 🎸
- """,
- """\
- 🖕 😰
- 🐛💤👔🐛
- ⛽ 👢
- ⚡8=👊
- 🎸
- 👢
- """,
- """\
- 🖕 😰
- 🐛💤👔🐛
- ⛽ 👢
- ⚡8=👊
- 🎸 🌽
- 👢
- """,
- """\
- 🖕 😰
- 🐛💤👔🐛
- ⛽ 👢
- ⚡8=👊
- 🎸 🌽
- 👢 👢
- """,
- """\
- 🖕 😫
- 🐛💤👔🐛
- ⛽ 👢
- ⚡8=👊=D
- 🎸 🌽
- 👢 👢
- """,
- """\
- 🖕 😵
- 🐛💤👔🐛
- ⛽ 👢
- ⚡8=👊=D💦
- 🎸 🌽
- 👢 👢
- """,
- ]
- @initialize_data
- class HangGuy:
- puzzle: str | None = None
- game_state: str | None = 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("hg")
- @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
- )
- 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"
- )
|