From 13b91d0a39aa6a0bb5344eda0a0bfd33744d4139 Mon Sep 17 00:00:00 2001 From: Stefano Pigozzi Date: Fri, 25 Jan 2019 15:28:47 +0100 Subject: [PATCH] Many /matchmaking improvements --- db.py | 24 +++++++++-- strings.py | 50 +++++++++++++++++++++++ telegrambot.py | 107 +++++++++++++++---------------------------------- 3 files changed, 103 insertions(+), 78 deletions(-) create mode 100644 strings.py diff --git a/db.py b/db.py index af32d39c..9e74aa2b 100644 --- a/db.py +++ b/db.py @@ -1234,14 +1234,16 @@ class Match(Base): def active_players_count(self): count = 0 for player in self.players: - if player.status == MatchmakingStatus.READY or player.status == MatchmakingStatus.WAIT_FOR_ME: + if player.status == MatchmakingStatus.READY \ + or player.status == MatchmakingStatus.WAIT_FOR_ME \ + or player.status == MatchmakingStatus.SOMEONE_ELSE: count += 1 return count def generate_text(self, session): player_list = session.query(MatchPartecipation).filter_by(match=self).all() title = f"{self.match_title}" - description = self.match_desc if self.match_desc else "" + description = f"{self.match_desc}\n" if self.match_desc else "" if self.min_players: minimum = f" (minimo {self.min_players})" else: @@ -1264,14 +1266,14 @@ class Match(Base): continue plist += f"{icon} {player.user.royal.username}\n" if ignore_count: - ignored = f"❌ {ignore_count} persone non ne hanno voglia.\n" + ignored = f"❌ {ignore_count} persone non sono interessate.\n" else: ignored = "" if self.max_players: players = f"[{self.active_players_count()}/{self.max_players}]" else: players = f"[{self.active_players_count()}]" - close = f"[matchmaking concluso]\n" if self.closed else "" + close = f"[matchmaking terminato]\n" if self.closed else "" message = f"{title} {players}\n" \ f"{description}\n" \ f"{plist}\n" \ @@ -1282,6 +1284,20 @@ class Match(Base): def __repr__(self): return f"" + def format_dict(self) -> typing.Dict[str, str]: + return { + "id": self.id, + "timestamp": self.timestamp.isoformat(), + "creator_id": self.creator_id, + "creator_name": self.creator.mention(), + "match_title": self.match_title, + "match_desc": self.match_desc, + "min_players": self.min_players, + "max_players": self.max_players, + "active_players": self.active_players_count(), + "players": len(self.active_players_count()) + } + class MatchmakingStatus(enum.IntEnum): WAIT_FOR_ME = 1 diff --git a/strings.py b/strings.py new file mode 100644 index 00000000..c6fcbf9e --- /dev/null +++ b/strings.py @@ -0,0 +1,50 @@ +from db import MatchmakingStatus + + +class SafeDict(dict): + def __missing__(self, key): + return '' + key + '' + + +def safely_format_string(string, **kwargs): + return string.format_map(SafeDict(**kwargs)) + + +class ROYALNET: + class ERRORS: + TELEGRAM_NOT_LINKED = "⚠ Il tuo account Telegram non Γ¨ registrato a Royalnet! Registrati con `/register@royalgamesbot `." + + +# Matchmaking service strings +class MATCHMAKING: + TICKER_TEXT = { + "match_ready": "πŸ”΅ Hai detto che sei pronto per giocare!", + "match_wait_for_me": "πŸ•’ Hai chiesto agli altri di aspettarti.", + "match_maybe": "❔ Hai detto che forse ci sarai.", + "match_someone_else": "πŸ’¬ Hai detto che vuoi aspettare che venga qualcun altro.", + "match_ignore": "❌ Non hai intenzione di partecipare.", + "match_close": "🚩 Hai notificato tutti che la partita sta iniziando.", + "match_cancel": "πŸ—‘ Hai annullato la partita." + } + + GAME_START = { + MatchmakingStatus.READY: "πŸ”΅ Che {match_title} abbia inizio!", + MatchmakingStatus.WAIT_FOR_ME: "πŸ•’ Sbrigati! {match_title} sta per iniziare!", + MatchmakingStatus.SOMEONE_ELSE: "❔ {match_title} sta iniziando. Se vuoi partecipare, fai in fretta!", + MatchmakingStatus.MAYBE: "πŸ’¬ {match_title} sta per iniziare, e ci sono {active_players} giocatori." + } + + BUTTONS = { + "match_ready": "πŸ”΅ Sono pronto per iniziare!", + "match_wait_for_me": "πŸ•’ Ci sarΓ², aspettatemi!", + "match_maybe": "❔ Forse vengo, se non ci sono fate senza di me.", + "match_someone_else": "πŸ’¬ Solo se viene anche qualcun altro...", + "match_ignore": "❌ Non ci sarΓ².", + "match_close": "🚩 ADMIN: Avvia la partita", + "match_cancel": "πŸ—‘ ADMIN: Annulla la partita" + } + + class ERRORS: + INVALID_SYNTAX = "⚠ Sintassi del comando errata.\n Sintassi: `/mm [minplayers-][maxplayers] per \\n [descrizione]`" + NOT_ADMIN = "⚠ Non sei il creatore di questo match!" + MATCH_CLOSED = "⚠ Il matchmaking per questa partita Γ¨ terminato!" diff --git a/telegrambot.py b/telegrambot.py index 5b49cd58..f9c0445c 100644 --- a/telegrambot.py +++ b/telegrambot.py @@ -9,6 +9,7 @@ import stagismo from telegram import Bot, Update, InlineKeyboardMarkup, InlineKeyboardButton # noinspection PyPackageRequirements from telegram.ext import Updater, CommandHandler, CallbackQueryHandler +from telegram.error import TimedOut import dice import sys import os @@ -19,6 +20,8 @@ import configparser import markovify import raven import coloredlogs +import strings +s = strings.safely_format_string # Markov model try: @@ -50,6 +53,8 @@ def catch_and_report(func: "function"): # noinspection PyBroadException try: return func(bot, update) + except TimedOut: + logger.warning(f"Telegram timed out in {update}") except Exception: logger.error(f"Critical error: {sys.exc_info()}") # noinspection PyBroadException @@ -293,16 +298,13 @@ def cmd_mm(bot: Bot, update: Update): try: user = session.query(db.Telegram).filter_by(telegram_id=update.message.from_user.id).one_or_none() if user is None: - bot.send_message(update.message.chat.id, - "⚠ Il tuo account Telegram non Γ¨ registrato a Royalnet!" - " Registrati con `/register@royalgamesbot `.", parse_mode="Markdown") + bot.send_message(update.message.chat.id, strings.ROYALNET.ERRORS.TELEGRAM_NOT_LINKED, parse_mode="Markdown") return match = re.match(r"/(?:mm|matchmaking)(?:@royalgamesbot)?(?: (?:([0-9]+)-)?([0-9]+))? (?:per )?([A-Za-z0-9!\-_\. ]+)(?:.*\n(.+))?", update.message.text) if match is None: bot.send_message(update.message.chat.id, - "⚠ Sintassi del comando errata.\n" - "Sintassi: `/matchmaking@royalgamesbot [minplayers]-[maxplayers] per \\n [descrizione]`") + "") return min_players, max_players, match_name, match_desc = match.group(1, 2, 3, 4) db_match = db.Match(timestamp=datetime.datetime.now(), @@ -313,20 +315,9 @@ def cmd_mm(bot: Bot, update: Update): creator=user) session.add(db_match) session.flush() - inline_keyboard = InlineKeyboardMarkup([[InlineKeyboardButton("πŸ”΅ Possiamo iniziare!", - callback_data="match_ready")], - [InlineKeyboardButton("πŸ•’ Ci sarΓ², aspettatemi!", - callback_data="match_wait_for_me")], - [InlineKeyboardButton("❔ Forse vengo, se non ci sono fate senza di me.", - callback_data="match_maybe")], - [InlineKeyboardButton("πŸ’¬ Solo se viene anche qualcun altro...", - callback_data="match_someone_else")], - [InlineKeyboardButton("❌ Non ci sarΓ².", - callback_data="match_ignore")], - [InlineKeyboardButton("πŸ—‘ [annulla la partita]", - callback_data="match_delete")], - [InlineKeyboardButton("🚩 [avvia la partita]", - callback_data="match_close")]]) + inline_keyboard = InlineKeyboardMarkup([([InlineKeyboardButton(strings.MATCHMAKING.BUTTONS[key], + callback_data=key)]) + for key in strings.MATCHMAKING.BUTTONS]) message = bot.send_message(config["Telegram"]["announcement_group"], db_match.generate_text(session=session), parse_mode="HTML", reply_markup=inline_keyboard) @@ -393,55 +384,32 @@ def on_callback_query(bot: Bot, update: Update): try: user = session.query(db.Telegram).filter_by(telegram_id=update.callback_query.from_user.id).one_or_none() if user is None: - bot.answer_callback_query(update.callback_query.id, show_alert=True, - text="⚠ Il tuo account Telegram non Γ¨ registrato a Royalnet!" - " Registrati con `/register@royalgamesbot `.", + bot.answer_callback_query(update.callback_query.id, + show_alert=True, + text=strings.ROYALNET.ERRORS.TELEGRAM_NOT_LINKED, parse_mode="Markdown") return match = session.query(db.Match).filter_by(message_id=update.callback_query.message.message_id).one() - if update.callback_query.data == "match_ready": - status = db.MatchmakingStatus.READY - text = "πŸ”΅ Hai detto che sei pronto per giocare!" - elif update.callback_query.data == "match_wait_for_me": - status = db.MatchmakingStatus.WAIT_FOR_ME - text = "πŸ•’ Hai chiesto agli altri di aspettarti." - elif update.callback_query.data == "match_ignore": - status = db.MatchmakingStatus.IGNORED - text = "❌ Non ti interessa questa partita." - elif update.callback_query.data == "match_maybe": - status = db.MatchmakingStatus.MAYBE - text = "❔ Hai detto che forse ci sarai." - elif update.callback_query.data == "match_someone_else": - status = db.MatchmakingStatus.SOMEONE_ELSE - text = "πŸ’¬ Hai detto che vuoi aspettare che venga qualcun altro." - elif update.callback_query.data == "match_close" or update.callback_query.data == "match_delete": + if update.callback_query.data == "match_close": status = None - if match.creator == user: - match.closed = True - text = "🚩 Matchmaking chiuso!" - if update.callback_query.data == "match_close": - for player in match.players: - if player.status == db.MatchmakingStatus.READY or player.status == db.MatchmakingStatus.WAIT_FOR_ME: - try: - bot.send_message(player.user.telegram_id, - f"🚩 Sei pronto? {match.match_title} sta iniziando!", - parse_mode="HTML") - except Exception as e: - logger.warning(f"Failed to notify {player.user.username}: {e}") - else: - bot.answer_callback_query(update.callback_query.id, show_alert=True, - text="⚠ Non sei il creatore di questo match!") + if match.creator != user: + bot.answer_callback_query(update.callback_query.id, + show_alert=True, + text=strings.MATCHMAKING.ERRORS.NOT_ADMIN) return + match.closed = True + for player in match.players: + if player.status >= 1: + bot.send_message(player.user.telegram_id, + s(strings.MATCHMAKING.GAME_START[player.status], + **match.format_dict())) else: raise NotImplementedError() if status: if match.closed: - bot.answer_callback_query(update.callback_query.id, show_alert=True, - text="⚠ Il matchmaking Γ¨ terminato!") - return - if match.max_players and match.active_players_count() >= match.max_players: - bot.answer_callback_query(update.callback_query.id, show_alert=True, - text="⚠ La partita Γ¨ piena.") + bot.answer_callback_query(update.callback_query.id, + show_alert=True, + text=strings.MATCHMAKING.ERRORS.MATCH_CLOSED) return player = session.query(db.MatchPartecipation).filter_by(match=match, user=user).one_or_none() if player is None: @@ -450,22 +418,13 @@ def on_callback_query(bot: Bot, update: Update): else: player.status = status.value session.commit() - bot.answer_callback_query(update.callback_query.id, text=text, cache_time=1) + bot.answer_callback_query(update.callback_query.id, + text=s(strings.MATCHMAKING.TICKER_TEXT[update.callback_query.data]), + cache_time=1) if not match.closed: - inline_keyboard = InlineKeyboardMarkup([[InlineKeyboardButton("πŸ”΅ Possiamo iniziare!", - callback_data="match_ready")], - [InlineKeyboardButton("πŸ•’ Ci sarΓ², aspettatemi!", - callback_data="match_wait_for_me")], - [InlineKeyboardButton("❔ Forse vengo, se non ci sono fate senza di me.", - callback_data="match_maybe")], - [InlineKeyboardButton("πŸ’¬ Solo se viene anche qualcun altro...", - callback_data="match_someone_else")], - [InlineKeyboardButton("❌ Non ci sarΓ².", - callback_data="match_ignore")], - [InlineKeyboardButton("πŸ—‘ [annulla la partita]", - callback_data="match_delete")], - [InlineKeyboardButton("🚩 [avvia la partita]", - callback_data="match_close")]]) + inline_keyboard = InlineKeyboardMarkup([([InlineKeyboardButton(strings.MATCHMAKING.BUTTONS[key], + callback_data=key)]) + for key in strings.MATCHMAKING.BUTTONS]) else: inline_keyboard = None bot.edit_message_text(message_id=update.callback_query.message.message_id,