diff --git a/plugins/wordbot.py b/plugins/wordbot.py index 71ebc34..01cf8b0 100644 --- a/plugins/wordbot.py +++ b/plugins/wordbot.py @@ -20,10 +20,10 @@ def denotify_nick(nick: str) -> str: class Db: - def __init__(self, path: Path): + def __init__(self, path: Path) -> None: self.path = path - def ensure_db(self): + def ensure_db(self) -> None: self.path.parent.mkdir(parents=True, exist_ok=True) with sqlite3.connect(self.path) as conn: conn.executescript( @@ -51,6 +51,7 @@ class Db: FOREIGN KEY (word) REFERENCES word(id), UNIQUE(game, word) ); + CREATE INDEX IF NOT EXISTS idx_score_game_word ON score(game, word); """ ) @@ -82,7 +83,7 @@ class Db: def start_round( self, channel: str, duration: int, words: Set[str], allow_early_end=False - ): + ) -> None: self.ensure_db() if self.is_game_active(channel) and not allow_early_end: # Don't start a new game if you don't have to @@ -102,7 +103,13 @@ class Db: "INSERT INTO word (game, word) VALUES (?, ?)", game_words_iter ) - def add_score(self, channel: str, user: str, word: str, line: str): + def extend_round(self, game_id: int, duration: int) -> None: + self.ensure_db() + end = time.time() + duration + with sqlite3.connect(self.path) as conn: + conn.execute("UPDATE game SET end = end + ? WHERE id = ?", (end, game_id)) + + def add_score(self, channel: str, user: str, word: str, line: str) -> None: self.ensure_db() game_id = self.current_game(channel) if not game_id: @@ -124,7 +131,7 @@ class Db: {"game_id": game_id, "word": word, "user": user, "line": line}, ) - def scores(self, channel: str): + def scores(self, channel: str) -> dict[str, int]: # This differs from .leaderboard() by using a specific game ID, rather # than all games for the channel. game_id = self.current_game(channel) @@ -143,7 +150,7 @@ class Db: rows = cur.fetchall() return {row[0]: row[1] for row in rows} - def leaderboard(self, channel: str): + def leaderboard(self, channel: str) -> dict[str, int]: # This differs from .scores() by using the game.channel = ?, rather than # a specific game id. with sqlite3.connect(self.path) as conn: @@ -179,7 +186,7 @@ class Db: class Wordbot(Plugin): - def __init__(self, *args, **kwargs): + def __init__(self, *args, **kwargs) -> None: super(Wordbot, self).__init__(*args, **kwargs) self.db_path = Path( self.plugin_config.get("db_path", "data/wordbot/wordbot.db") @@ -188,40 +195,41 @@ class Wordbot(Plugin): self.db = Db(self.db_path) self.duration = int(self.plugin_config.get("hours_per_round", 5)) * 3600 self.words_per_round = int(self.plugin_config.get("words_per_round", 300)) - self.__watch_games_task = None - self.__db_lock = asyncio.Lock() + self.extend_game = self.plugin_config.get("extend_game", True) + self._watch_games_task = None + self._db_lock = asyncio.Lock() def get_words(self) -> Set[str]: with open(self.words_path) as fp: return {word.strip().lower() for word in fp} - async def on_load(self): + async def on_load(self) -> None: # Make sure games are running on all channels # This happens before on_connect for channel in self.channels: if not self.db.is_game_active(channel): self.start_round(channel) - async def on_connect(self, conn: IrcProtocol): + async def on_connect(self, conn: IrcProtocol) -> None: # Start watcher up to end games - self.__watch_games_task = asyncio.create_task(self.__watch_games(conn)) + self._watch_games_task = asyncio.create_task(self._watch_games(conn)) - async def __watch_games(self, conn: IrcProtocol): + async def _watch_games(self, conn: IrcProtocol) -> None: while True: await asyncio.sleep(1.0) for channel in self.bot.joined_channels: if not self.db.is_game_active(channel): - async with self.__db_lock: + async with self._db_lock: # End round self.end_round(conn, channel) # Create new round self.start_round(channel) - async def on_unload(self, conn: IrcProtocol): - if self.__watch_games_task: - self.__watch_games_task.cancel() + async def on_unload(self, conn: IrcProtocol) -> None: + if self._watch_games_task: + self._watch_games_task.cancel() - async def on_message(self, conn: IrcProtocol, channel: str, who: Prefix, line: str): + async def on_message(self, conn: IrcProtocol, channel: str, who: Prefix, line: str) -> None: if who.nick == self.server_config.nick: return line = line.strip() @@ -230,7 +238,7 @@ class Wordbot(Plugin): elif line[0] == "!": await self.handle_command(conn, channel, who, line) else: - async with self.__db_lock: + async with self._db_lock: if not self.db.is_game_active(channel): # Don't try to score words for inactive games return @@ -269,11 +277,11 @@ class Wordbot(Plugin): async def handle_command( self, conn: IrcProtocol, channel: str, who: Prefix, line: str - ): + ) -> None: parts = line.strip().split() match parts: # case ["!wordbot", "end_now"]: - # async with self.__db_lock: + # async with self._db_lock: # self.end_round(conn, channel) # self.start_round(channel, allow_early_end=True) case ["!wordbot", "leaderboard", *args]: @@ -306,7 +314,7 @@ class Wordbot(Plugin): case _: pass - def start_round(self, channel: str, allow_early_end: bool = False): + def start_round(self, channel: str, allow_early_end: bool = False) -> None: log.debug("Starting new wordbot round for %s", channel) # Choose words for new round with open(self.words_path) as fp: @@ -315,11 +323,18 @@ class Wordbot(Plugin): words = words[: self.words_per_round] self.db.start_round(channel, self.duration, words, allow_early_end) - def end_round(self, conn: IrcProtocol, channel: str): + def end_round(self, conn: IrcProtocol, channel: str) -> None: log.debug("Ending wordbot round for %s", channel) - # Sort the scores scores = sorted(self.db.scores(channel).items(), key=lambda value: -value[1]) - # Add their ordering + game_id = self.db.current_game(channel) + if not scores and self.extend_game: + log.debug( + "No scores were made in this round - extending the game by %s hours", + self.hours_per_round + ) + self.db.extend_round(game_id, self.duration) + return + # Add score ordering rankings = { score: rank for rank, score in enumerate( @@ -328,7 +343,6 @@ class Wordbot(Plugin): ) ) } - game_id = self.db.current_game(channel) self.send_to(conn, channel, f"Game #{game_id} over. Here were the scores:") for user, score in scores: self.send_to(conn, channel, f"{rankings[score] + 1}. {user}. {score}")