|
@@ -1,6 +1,226 @@
|
|
|
-from command_system import as_plugin, RollbotResponse
|
|
|
+import datettime
|
|
|
+import re
|
|
|
+
|
|
|
+from sqlalchemy import Column, Integer, String, DateTime
|
|
|
+
|
|
|
+from command_system import as_plugin, RollbotResponse, RollbotFailure, ModelBase
|
|
|
+
|
|
|
+
|
|
|
+class GameState(ModelBase):
|
|
|
+ __tablename__ = "hangguy_game_state"
|
|
|
+ group_id = Column(String, primary_key=True)
|
|
|
+ target = Column(String)
|
|
|
+ state = Column(String)
|
|
|
+ bad_guesses = Column(String)
|
|
|
+
|
|
|
+
|
|
|
+class GuyState(ModelBase):
|
|
|
+ __tablename__ = "hangguy_guy_state"
|
|
|
+ group_id = Column(String, primary_key=True)
|
|
|
+ state = Column(Integer)
|
|
|
+ games = Column(Integer)
|
|
|
+
|
|
|
+
|
|
|
+class DeadGuy(ModelBase):
|
|
|
+ """
|
|
|
+ Tracks "dead" and "retired" guys for record-keeping purposes.
|
|
|
+ If state >= 17, the guy was dead, otherwise it was retired.
|
|
|
+ """
|
|
|
+ __tablename__ = "hangguy_dead_guy"
|
|
|
+ guy_id = Column(Integer, primary_key=True, autoincrement=True)
|
|
|
+ group_id = Column(String)
|
|
|
+ state = Column(Integer)
|
|
|
+ games = Column(Integer)
|
|
|
+ timestamp = Column(DateTime)
|
|
|
+
|
|
|
+ @staticmethod
|
|
|
+ def from_guy(guy):
|
|
|
+ return DeadGuy(
|
|
|
+ group_id=guy.group_id,
|
|
|
+ state=guy.state,
|
|
|
+ games=guy.games,
|
|
|
+ timestamp=datetime.datetime.now():
|
|
|
+ )
|
|
|
+
|
|
|
+
|
|
|
+def get_game(db, group_id):
|
|
|
+ game_state = db.query(GameState).get(group_id)
|
|
|
+ if game_state is None:
|
|
|
+ game_state = GameState(
|
|
|
+ group_id=group_id,
|
|
|
+ target=None,
|
|
|
+ state=None,
|
|
|
+ bad_guesses=""
|
|
|
+ )
|
|
|
+ db.add(game_state)
|
|
|
+ return game_state
|
|
|
+
|
|
|
+
|
|
|
+def get_guy(db, group_id):
|
|
|
+ guy_state = db.query(GuyState).get(group_id)
|
|
|
+ if guy_state is None:
|
|
|
+ guy_state = GuyState(
|
|
|
+ group_id=group_id,
|
|
|
+ state=0,
|
|
|
+ games=0
|
|
|
+ )
|
|
|
+ db.add(guy_state)
|
|
|
+ return guy_state
|
|
|
+
|
|
|
+
|
|
|
+def init_game(game_state):
|
|
|
+ new_phrase = "TEST PHRASE" # TODO this needs to actually pull from something (and sanitize it) eventually
|
|
|
+
|
|
|
+ game_state.state = re.sub("[A-Z]", "_", new_phrase)
|
|
|
+ game_state.target = new_phrase
|
|
|
+ game_state.bad_guesses = ""
|
|
|
+
|
|
|
+
|
|
|
+def end_game(game_state):
|
|
|
+ game_state.target = None
|
|
|
+
|
|
|
+
|
|
|
+def game_is_active(game_state):
|
|
|
+ return game_state.target is not None
|
|
|
+
|
|
|
+
|
|
|
+def game_is_finished(game_state):
|
|
|
+ return game_state.target == game_state.state
|
|
|
+
|
|
|
+
|
|
|
+def guess_letter(game_state, letter):
|
|
|
+ """
|
|
|
+ Guess a letter in a given game state. Assumes letter is already
|
|
|
+ a single, uppercased, alpha character.
|
|
|
+
|
|
|
+ Returns number of appearances of the letter, or -1 if it was
|
|
|
+ already guessed.
|
|
|
+ """
|
|
|
+
|
|
|
+ if letter in game_state.bad_guesses or letter in game_state.state:
|
|
|
+ return -1
|
|
|
+
|
|
|
+ find = [i for i, x in game_state.target if x == letter]
|
|
|
+ if len(find) == 0:
|
|
|
+ game_state.bad_guesses = game_state.bad_guesses + letter
|
|
|
+ return 0
|
|
|
+
|
|
|
+ game_state.state = "".join(letter if i in find else s for i, s in enumerate(game_state.state))
|
|
|
+ return len(find)
|
|
|
+
|
|
|
+
|
|
|
+def end_guy(db, guy_state):
|
|
|
+ db.add(DeadGuy.from_guy(guy_state))
|
|
|
+ guy_state.state = 0
|
|
|
+ guy_state.games = 0
|
|
|
+
|
|
|
+
|
|
|
+def guy_is_dead(guy_state):
|
|
|
+ return guy_state.state >= 17
|
|
|
+
|
|
|
+
|
|
|
+def survival_msg(guy_state):
|
|
|
+ return f"survived {guy_state.games} game{'' if guy_state.games == 1 else 's'}"
|
|
|
+
|
|
|
+
|
|
|
+def render_guy(guy_state):
|
|
|
+ return str(guy_state.state) # TODO actually render guy
|
|
|
+
|
|
|
+
|
|
|
+def render_game(game_state, guy_state):
|
|
|
+ return render_guy(guy_state) + "\n" + \
|
|
|
+ " ".join(game_state.state) + "\n" + \
|
|
|
+ "Bad Guesses: " + ", ".join(game_state.bad_guesses)
|
|
|
+
|
|
|
+
|
|
|
+def handle_in_game(db, msg, game_state, guy_state):
|
|
|
+ if msg.raw_args.startswith("!"):
|
|
|
+ subc = msg.raw_args[1:].strip().lower()
|
|
|
+ if subc == "cancel":
|
|
|
+ end_game(game_state)
|
|
|
+ return RollbotResponse(msg, txt="The game has been cancelled. The guy has not been reset.")
|
|
|
+ else:
|
|
|
+ return RollbotResponse(
|
|
|
+ msg,
|
|
|
+ failure=RollbotFailure.INVALID_SUBCOMMAND,
|
|
|
+ debugging=dict(
|
|
|
+ explain="The only in-game subcommand is !cancel to end the game"
|
|
|
+ )
|
|
|
+ )
|
|
|
+
|
|
|
+ guess = msg.raw_args.upper()
|
|
|
+ prefix = ""
|
|
|
+ if len(guess) == 1:
|
|
|
+ res = guess_letter(game_state, guess)
|
|
|
+ if res == -1:
|
|
|
+ prefix += f"You've already guessed '{guess}'!"
|
|
|
+ elif res == 0:
|
|
|
+ prefix += "Bad guess!"
|
|
|
+ else:
|
|
|
+ prefix += f"Great! '{guess}' appears {res} time{'' if res == 1 else 's'}!"
|
|
|
+ else:
|
|
|
+ prefix = "Full phrase guessing not yet supported!"
|
|
|
+ # TODO guess and handle the result
|
|
|
+
|
|
|
+ txt = prefix + "\n" + render_game(game_state, guy_state)
|
|
|
+
|
|
|
+ if game_is_finished(game_state):
|
|
|
+ txt += "\nThe game is over, and the guy lives on!"
|
|
|
+ end_game(game_state)
|
|
|
+ guy_state.games = guy_state.games + 1
|
|
|
+ txt += "\nThis guy has now {survival_msg(guy_state)}"
|
|
|
+
|
|
|
+ if guy_is_dead(guy_state):
|
|
|
+ txt += "\noh god oh fuck, the guy is dead!"
|
|
|
+ end_game(game_state)
|
|
|
+ end_guy(db, guy_state)
|
|
|
+ txt += "\nThe guy has been buried."
|
|
|
+
|
|
|
+ return RollbotResponse(msg, txt=txt)
|
|
|
+
|
|
|
+
|
|
|
+def subc_retire(db, guy_state):
|
|
|
+ txt = f"Retiring the previous guy, who {survival_msg(guy_state)}\n"
|
|
|
+ txt += render_guy(guy_state)
|
|
|
+ end_guy(db, guy_state)
|
|
|
+ return txt
|
|
|
|
|
|
|
|
|
@as_plugin("hangguy")
|
|
|
def hangguy(db, msg):
|
|
|
+ game_state = get_game(db, msg.group_id)
|
|
|
+ guy_state = get_guy(db, msg.group_id)
|
|
|
+
|
|
|
+ if game_is_active(game_state):
|
|
|
+ return handle_in_game(db, msg, game_state, guy_state)
|
|
|
+
|
|
|
+ if not msg.raw_args.startswith("!"):
|
|
|
+ return RollbotResponse(
|
|
|
+ msg,
|
|
|
+ failure=RollbotFailure.MISSING_SUBCOMMAND,
|
|
|
+ debugging=dict(
|
|
|
+ explain="You are not currently in a game, and so you must use one of the subcommands: !view, !retire, !start"
|
|
|
+ )
|
|
|
+ )
|
|
|
+
|
|
|
+ subc = msg.raw_args[1:].strip().lower()
|
|
|
+
|
|
|
+ if msg.raw_args == "view":
|
|
|
+ txt = f"The current guy has {survival_msg(guy_state)}\n"
|
|
|
+ txt += render_guy(guy_state)
|
|
|
+ return RollbotResponse(msg, txt=txt)
|
|
|
+ elif msg.raw_args == "retire":
|
|
|
+ return RollbotResponse(msg, txt=subc_retire(db, guy_state))
|
|
|
+ elif msg.raw_args == "start"
|
|
|
+ init_game(game_state)
|
|
|
+ return RollbotResponse(msg, txt=render_game(game_state, guy_state))
|
|
|
+ else:
|
|
|
+ return RollbotResponse(
|
|
|
+ msg,
|
|
|
+ failure=RollbotFailure.INVALID_SUBCOMMAND,
|
|
|
+ debugging=dict(
|
|
|
+ explain="You must use one of the subcommands: !view, !retire, !start"
|
|
|
+ )
|
|
|
+ )
|
|
|
+
|
|
|
return RollbotResponse(msg, txt="My first plugin!")
|