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:
|
class Db:
|
||||||
def __init__(self, path: Path):
|
def __init__(self, path: Path) -> None:
|
||||||
self.path = path
|
self.path = path
|
||||||
|
|
||||||
def ensure_db(self):
|
def ensure_db(self) -> None:
|
||||||
self.path.parent.mkdir(parents=True, exist_ok=True)
|
self.path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
with sqlite3.connect(self.path) as conn:
|
with sqlite3.connect(self.path) as conn:
|
||||||
conn.executescript(
|
conn.executescript(
|
||||||
@@ -51,6 +51,7 @@ class Db:
|
|||||||
FOREIGN KEY (word) REFERENCES word(id),
|
FOREIGN KEY (word) REFERENCES word(id),
|
||||||
UNIQUE(game, word)
|
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(
|
def start_round(
|
||||||
self, channel: str, duration: int, words: Set[str], allow_early_end=False
|
self, channel: str, duration: int, words: Set[str], allow_early_end=False
|
||||||
):
|
) -> None:
|
||||||
self.ensure_db()
|
self.ensure_db()
|
||||||
if self.is_game_active(channel) and not allow_early_end:
|
if self.is_game_active(channel) and not allow_early_end:
|
||||||
# Don't start a new game if you don't have to
|
# 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
|
"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()
|
self.ensure_db()
|
||||||
game_id = self.current_game(channel)
|
game_id = self.current_game(channel)
|
||||||
if not game_id:
|
if not game_id:
|
||||||
@@ -124,7 +131,7 @@ class Db:
|
|||||||
{"game_id": game_id, "word": word, "user": user, "line": line},
|
{"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
|
# This differs from .leaderboard() by using a specific game ID, rather
|
||||||
# than all games for the channel.
|
# than all games for the channel.
|
||||||
game_id = self.current_game(channel)
|
game_id = self.current_game(channel)
|
||||||
@@ -143,7 +150,7 @@ class Db:
|
|||||||
rows = cur.fetchall()
|
rows = cur.fetchall()
|
||||||
return {row[0]: row[1] for row in rows}
|
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
|
# This differs from .scores() by using the game.channel = ?, rather than
|
||||||
# a specific game id.
|
# a specific game id.
|
||||||
with sqlite3.connect(self.path) as conn:
|
with sqlite3.connect(self.path) as conn:
|
||||||
@@ -179,7 +186,7 @@ class Db:
|
|||||||
|
|
||||||
|
|
||||||
class Wordbot(Plugin):
|
class Wordbot(Plugin):
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs) -> None:
|
||||||
super(Wordbot, self).__init__(*args, **kwargs)
|
super(Wordbot, self).__init__(*args, **kwargs)
|
||||||
self.db_path = Path(
|
self.db_path = Path(
|
||||||
self.plugin_config.get("db_path", "data/wordbot/wordbot.db")
|
self.plugin_config.get("db_path", "data/wordbot/wordbot.db")
|
||||||
@@ -188,40 +195,41 @@ class Wordbot(Plugin):
|
|||||||
self.db = Db(self.db_path)
|
self.db = Db(self.db_path)
|
||||||
self.duration = int(self.plugin_config.get("hours_per_round", 5)) * 3600
|
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.words_per_round = int(self.plugin_config.get("words_per_round", 300))
|
||||||
self.__watch_games_task = None
|
self.extend_game = self.plugin_config.get("extend_game", True)
|
||||||
self.__db_lock = asyncio.Lock()
|
self._watch_games_task = None
|
||||||
|
self._db_lock = asyncio.Lock()
|
||||||
|
|
||||||
def get_words(self) -> Set[str]:
|
def get_words(self) -> Set[str]:
|
||||||
with open(self.words_path) as fp:
|
with open(self.words_path) as fp:
|
||||||
return {word.strip().lower() for word in 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
|
# Make sure games are running on all channels
|
||||||
# This happens before on_connect
|
# This happens before on_connect
|
||||||
for channel in self.channels:
|
for channel in self.channels:
|
||||||
if not self.db.is_game_active(channel):
|
if not self.db.is_game_active(channel):
|
||||||
self.start_round(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
|
# 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:
|
while True:
|
||||||
await asyncio.sleep(1.0)
|
await asyncio.sleep(1.0)
|
||||||
for channel in self.bot.joined_channels:
|
for channel in self.bot.joined_channels:
|
||||||
if not self.db.is_game_active(channel):
|
if not self.db.is_game_active(channel):
|
||||||
async with self.__db_lock:
|
async with self._db_lock:
|
||||||
# End round
|
# End round
|
||||||
self.end_round(conn, channel)
|
self.end_round(conn, channel)
|
||||||
# Create new round
|
# Create new round
|
||||||
self.start_round(channel)
|
self.start_round(channel)
|
||||||
|
|
||||||
async def on_unload(self, conn: IrcProtocol):
|
async def on_unload(self, conn: IrcProtocol) -> None:
|
||||||
if self.__watch_games_task:
|
if self._watch_games_task:
|
||||||
self.__watch_games_task.cancel()
|
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:
|
if who.nick == self.server_config.nick:
|
||||||
return
|
return
|
||||||
line = line.strip()
|
line = line.strip()
|
||||||
@@ -230,7 +238,7 @@ class Wordbot(Plugin):
|
|||||||
elif line[0] == "!":
|
elif line[0] == "!":
|
||||||
await self.handle_command(conn, channel, who, line)
|
await self.handle_command(conn, channel, who, line)
|
||||||
else:
|
else:
|
||||||
async with self.__db_lock:
|
async with self._db_lock:
|
||||||
if not self.db.is_game_active(channel):
|
if not self.db.is_game_active(channel):
|
||||||
# Don't try to score words for inactive games
|
# Don't try to score words for inactive games
|
||||||
return
|
return
|
||||||
@@ -269,11 +277,11 @@ class Wordbot(Plugin):
|
|||||||
|
|
||||||
async def handle_command(
|
async def handle_command(
|
||||||
self, conn: IrcProtocol, channel: str, who: Prefix, line: str
|
self, conn: IrcProtocol, channel: str, who: Prefix, line: str
|
||||||
):
|
) -> None:
|
||||||
parts = line.strip().split()
|
parts = line.strip().split()
|
||||||
match parts:
|
match parts:
|
||||||
# case ["!wordbot", "end_now"]:
|
# case ["!wordbot", "end_now"]:
|
||||||
# async with self.__db_lock:
|
# async with self._db_lock:
|
||||||
# self.end_round(conn, channel)
|
# self.end_round(conn, channel)
|
||||||
# self.start_round(channel, allow_early_end=True)
|
# self.start_round(channel, allow_early_end=True)
|
||||||
case ["!wordbot", "leaderboard", *args]:
|
case ["!wordbot", "leaderboard", *args]:
|
||||||
@@ -306,7 +314,7 @@ class Wordbot(Plugin):
|
|||||||
case _:
|
case _:
|
||||||
pass
|
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)
|
log.debug("Starting new wordbot round for %s", channel)
|
||||||
# Choose words for new round
|
# Choose words for new round
|
||||||
with open(self.words_path) as fp:
|
with open(self.words_path) as fp:
|
||||||
@@ -315,11 +323,18 @@ class Wordbot(Plugin):
|
|||||||
words = words[: self.words_per_round]
|
words = words[: self.words_per_round]
|
||||||
self.db.start_round(channel, self.duration, words, allow_early_end)
|
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)
|
log.debug("Ending wordbot round for %s", channel)
|
||||||
# Sort the scores
|
|
||||||
scores = sorted(self.db.scores(channel).items(), key=lambda value: -value[1])
|
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 = {
|
rankings = {
|
||||||
score: rank
|
score: rank
|
||||||
for rank, score in enumerate(
|
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:")
|
self.send_to(conn, channel, f"Game #{game_id} over. Here were the scores:")
|
||||||
for user, score in scores:
|
for user, score in scores:
|
||||||
self.send_to(conn, channel, f"{rankings[score] + 1}. {user}. {score}")
|
self.send_to(conn, channel, f"{rankings[score] + 1}. {user}. {score}")
|
||||||
|
|||||||
Reference in New Issue
Block a user