From 31e8c5278ce53f2604c550a78316d89f444b0d20 Mon Sep 17 00:00:00 2001 From: Jerry Date: Wed, 20 Nov 2019 20:32:27 +0800 Subject: [PATCH] add persistence for games and /reveal --- cards.py | 82 +++++++++++++++++++++++++++++++++++++++++++------- tgmsbot.py | 88 ++++++++++++++++++++++++++++++++++++++++++++++-------- 2 files changed, 147 insertions(+), 23 deletions(-) diff --git a/cards.py b/cards.py index d2111f4..db97f4b 100644 --- a/cards.py +++ b/cards.py @@ -3,13 +3,16 @@ from telegram import InlineKeyboardMarkup, InlineKeyboardButton from telegram.ext import run_async -from data import get_player from random import randrange from time import time -import logging +import logging logger = logging.getLogger('tgmsbot.cards') +# from the main module +get_player = lambda *args, **kwargs: None +game_manager = None + MAX_LEVEL: int = 100 MID_LEVEL: int = 80 LVL_UP_CARDS: int = 20 @@ -48,7 +51,7 @@ def _msg_users(update): @run_async def getperm(update, context): - logging.info(f'getperm from {getattr(update.effective_user, "id", None)}') + logger.info(f'getperm from {getattr(update.effective_user, "id", None)}') (from_user, reply_to_user) = _msg_users(update) if not from_user: return @@ -63,7 +66,7 @@ def getperm(update, context): @run_async def setperm(update, context): - logging.info(f'setperm from {getattr(update.effective_user, "id", None)}') + logger.info(f'setperm from {getattr(update.effective_user, "id", None)}') (from_user, reply_to_user) = _msg_users(update) if not from_user: return @@ -92,7 +95,7 @@ def lvlup(update, context): ''' use LVL_UP_CARDS cards to level up 1 lvl ''' - logging.info(f'lvlup from {getattr(update.effective_user, "id", None)}') + logger.info(f'lvlup from {getattr(update.effective_user, "id", None)}') LVLUP_TIMEOUT = 10 last_time = context.user_data.setdefault('lvlup_time', 0.0) ctime = time() @@ -149,7 +152,7 @@ def lvlup(update, context): @run_async def transfer_cards(update, context): - logging.info(f'transfer_cards from {getattr(update.effective_user, "id", None)}') + logger.info(f'transfer_cards from {getattr(update.effective_user, "id", None)}') (from_user, reply_to_user) = _msg_users(update) if not from_user: return @@ -190,7 +193,7 @@ def transfer_cards(update, context): @run_async def rob_cards(update, context): - logging.info(f'rob_cards from {getattr(update.effective_user, "id", None)}') + logger.info(f'rob_cards from {getattr(update.effective_user, "id", None)}') ROB_TIMEOUT = 10 last_time = context.user_data.setdefault('rob_time', 0.0) ctime = time() @@ -256,7 +259,7 @@ def rob_cards(update, context): @run_async def cards_lottery(update, context): - logging.info(f'cards_lottery from {getattr(update.effective_user, "id", None)}') + logger.info(f'cards_lottery from {getattr(update.effective_user, "id", None)}') LOTTERY_TIMEOUT = 10 last_time = context.user_data.setdefault('lottery_time', 0.0) ctime = time() @@ -281,7 +284,7 @@ def cards_lottery(update, context): @run_async def dist_cards(update, context): - logging.info(f'dist_cards from {getattr(update.effective_user, "id", None)}') + logger.info(f'dist_cards from {getattr(update.effective_user, "id", None)}') (from_user, _) = _msg_users(update) if not from_user: return @@ -308,7 +311,7 @@ def dist_cards(update, context): @run_async def dist_cards_btn_click(update, context): - logging.info(f'dist_cards_btn_click from {getattr(update.effective_user, "id", None)}') + logger.info(f'dist_cards_btn_click from {getattr(update.effective_user, "id", None)}') data = update.callback_query.data user = update.callback_query.from_user omsg = update.callback_query.message @@ -355,3 +358,62 @@ def dist_cards_btn_click(update, context): rp[0] = -1 omsg.edit_text(omsg.text_markdown + "褪裙了", parse_mode="Markdown", reply_markup=None) context.job_queue.run_once(free_mem, 5) + +@run_async +def reveal(update, context): + logger.info(f'reveal from {getattr(update.effective_user, "id", None)}') + (from_user, _) = _msg_users(update) + if not from_user: + return + if (msg := update.effective_message) and (rmsg := msg.reply_to_message): + try: + assert (rmarkup := rmsg.reply_markup) and (kbd := rmarkup.inline_keyboard) \ + and type((btn := kbd[0][0])) is InlineKeyboardButton and (data := btn.callback_data) + data = data.split(' ') + data = [int(i) for i in data] + (bhash, _, _) = data + except: + msg.reply_text('不是一条有效的消息') + return + game = game_manager.get_game_from_hash(bhash) + if not game: + msg.reply_text('这局似乎走丢了呢') + return + if (mmap := game.board.mmap) is None: + msg.reply_text('这局似乎还没开始呢') + return + def map_to_msg(): + ZERO_CELL = '\u23f9' + MINE_CELL = '\u2622' + NUM_CELL_SUFFIX = '\ufe0f\u20e3' + BAD_CELL = "\U0001f21a\ufe0f" + msg_text = "" + for row in mmap: + for cell in row: + if cell == 0: + msg_text += ZERO_CELL + elif cell == 9: + msg_text += MINE_CELL + elif cell in range(1,9): + msg_text += str(cell) + NUM_CELL_SUFFIX + else: + msg_text += BAD_CELL + msg_text += '\n' + return msg_text + fplayer = get_player(int(from_user.id)) + cards = abs(fplayer.immunity_cards) / 3 + def __floating(value): + return randrange(5000,15000)/10000 * value + cards = __floating(cards) + cards = int(cards) if cards > 1 else 1 + extra_text = "" + fplayer.immunity_cards -= cards + if fplayer.permission >= MID_LEVEL and fplayer.permission < MAX_LEVEL: + lvl = int(randrange(100,3000)/10000 * fplayer.permission) + lvl = lvl if lvl > 0 else 1 + fplayer.permission -= lvl + extra_text = f", {lvl}级" + fplayer.save() + msg.reply_text(f'本局地图如下:\n\n{map_to_msg()}\n您用去了{cards}张卡{extra_text}') + else: + msg.reply_text('请回复想要查看的雷区') diff --git a/tgmsbot.py b/tgmsbot.py index 96a6d45..feb89a5 100644 --- a/tgmsbot.py +++ b/tgmsbot.py @@ -12,6 +12,8 @@ from random import randint, choice, randrange from math import log from threading import Lock import time +from pathlib import Path +import pickle import logging logging.basicConfig(level=logging.INFO,format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') @@ -22,8 +24,11 @@ updater = Updater(token, workers=8, use_context=True) job_queue = updater.job_queue job_queue.start() +PICKLE_FILE = 'tgmsbot.pickle' + KBD_MIN_INTERVAL = 0.5 KBD_DELAY_SECS = 0.5 +GARBAGE_COLLECTION_INTERVAL = 86400 HEIGHT = 8 WIDTH = 8 @@ -70,7 +75,7 @@ def display_username(user, atuser=True, shorten=False, markdown=True): name += " ({})".format(user.username) return name -class Game(): +class Saved_Game(): def __init__(self, board, group, creator, lives=1): self.board = board self.group = group @@ -79,7 +84,6 @@ class Game(): self.last_player = None self.start_time = time.time() self.stopped = False - self.lock = Lock() # timestamp of the last update keyboard action, # it is used to calculate time gap between # two actions and identify unique actions. @@ -112,17 +116,44 @@ class Game(): msg = "{}{} - {}项操作\n".format(msg, display_username(user), count) return msg +class Game(): + def __init__(self, *args, **kwargs): + if 'unpickle' in args: + assert len(args) == 2 and args[0] == 'unpickle' + self.__sg = args[1] + else: + self.__sg = Saved_Game(*args, **kwargs) + self.lock = Lock() + def pickle(self): + return self.__sg + def __getattr__(self, name): + return getattr(self.__sg, name, None) + class GameManager: - __games = dict() + def __init__(self): + self.__games = dict() + self.__pf = Path(PICKLE_FILE) + if self.__pf.exists(): + try: + with open(self.__pf, 'rb') as fhandle: + saved_games = pickle.load(fhandle, fix_imports=True, errors="strict") + self.__games = {bhash: Game('unpickle', saved_games[bhash]) for bhash in saved_games} + except Exception as err: + logger.error(f'Unable to load pickle file, {type(err).__name__}: {err}') + assert type(self.__games) is dict + for board_hash in self.__games: + self.__games[board_hash].lock = Lock() def append(self, board, board_hash, group_id, creator_id): lives = int(board.mines/3) if lives <= 0: lives = 1 self.__games[board_hash] = Game(board, group_id, creator_id, lives=lives) + self.save_async() def remove(self, board_hash): board = self.get_game_from_hash(board_hash) if board: - del self.__games[board_hash] + self.__games.pop(board_hash) + self.save_async() return True else: return False @@ -130,6 +161,30 @@ class GameManager: return self.__games.get(board_hash, None) def count(self): return len(self.__games) + @run_async + def save_async(self): + self.save() + def save(self): + try: + games_without_locks = {bhash: self.__games[bhash].pickle() for bhash in self.__games} + with open(self.__pf, 'wb') as fhandle: + pickle.dump(games_without_locks, fhandle, fix_imports=True) + except Exception as err: + logger.error(f'Unable to save pickle file, {type(err).__name__}: {err}') + def do_garbage_collection(self, context): + g_checked: int = 0 + g_freed: int = 0 + games = self.__games + for board_hash in games: + g_checked += 1 + gm = games[board_hash] + start_time = getattr(gm, 'start_time', 0.0) + if time.time() - start_time > 86400*10: + g_freed += 1 + games.pop(board_hash) + self.save_async() + logger.info((f'Scheduled garbage collection checked {g_checked} games, ' + f'freed {g_freed} games.')) game_manager = GameManager() @@ -421,15 +476,18 @@ def handle_button_click(update, context): raise -from cards import getperm, setperm, lvlup, transfer_cards, rob_cards, cards_lottery, dist_cards, dist_cards_btn_click -updater.dispatcher.add_handler(CommandHandler('getlvl', getperm)) -updater.dispatcher.add_handler(CommandHandler('setlvl', setperm)) -updater.dispatcher.add_handler(CommandHandler('lvlup', lvlup)) -updater.dispatcher.add_handler(CommandHandler('transfer', transfer_cards)) -updater.dispatcher.add_handler(CommandHandler('rob', rob_cards)) -updater.dispatcher.add_handler(CommandHandler('lottery', cards_lottery)) -updater.dispatcher.add_handler(CommandHandler('dist', dist_cards)) -updater.dispatcher.add_handler(CallbackQueryHandler(dist_cards_btn_click, pattern=r'dist')) +import cards +setattr(cards, 'get_player', get_player) +setattr(cards, 'game_manager', game_manager) +updater.dispatcher.add_handler(CommandHandler('getlvl', cards.getperm)) +updater.dispatcher.add_handler(CommandHandler('setlvl', cards.setperm)) +updater.dispatcher.add_handler(CommandHandler('lvlup', cards.lvlup)) +updater.dispatcher.add_handler(CommandHandler('transfer', cards.transfer_cards)) +updater.dispatcher.add_handler(CommandHandler('rob', cards.rob_cards)) +updater.dispatcher.add_handler(CommandHandler('lottery', cards.cards_lottery)) +updater.dispatcher.add_handler(CommandHandler('dist', cards.dist_cards)) +updater.dispatcher.add_handler(CommandHandler('reveal', cards.reveal)) +updater.dispatcher.add_handler(CallbackQueryHandler(cards.dist_cards_btn_click, pattern=r'dist')) updater.dispatcher.add_handler(CommandHandler('start', send_help)) @@ -438,8 +496,12 @@ updater.dispatcher.add_handler(CommandHandler('status', send_status)) updater.dispatcher.add_handler(CommandHandler('stats', player_statistics)) updater.dispatcher.add_handler(CommandHandler('source', send_source)) updater.dispatcher.add_handler(CallbackQueryHandler(handle_button_click)) +updater.job_queue.run_repeating(game_manager.do_garbage_collection, GARBAGE_COLLECTION_INTERVAL, first=30) try: updater.start_polling() updater.idle() finally: + game_manager.save() + logger.info('Game_manager saved.') db.close() + logger.info('DB closed.')