wordbot: Add round extension and some other stuff
If nobody scored in this round of wordbot, then silently extend the round (if configured to do so; default yes). Also add some type annotations and an index to ensure_db
This commit is contained in:
@@ -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}")
|
||||
|
||||
Reference in New Issue
Block a user