add persistence for games and /reveal

This commit is contained in:
JerryXiao 2019-11-20 20:32:27 +08:00
parent 207bc43487
commit 31e8c5278c
Signed by: Jerry
GPG Key ID: 9D9CE43650FF2BAA
2 changed files with 147 additions and 23 deletions

View File

@ -3,13 +3,16 @@
from telegram import InlineKeyboardMarkup, InlineKeyboardButton from telegram import InlineKeyboardMarkup, InlineKeyboardButton
from telegram.ext import run_async from telegram.ext import run_async
from data import get_player
from random import randrange from random import randrange
from time import time from time import time
import logging
import logging
logger = logging.getLogger('tgmsbot.cards') logger = logging.getLogger('tgmsbot.cards')
# from the main module
get_player = lambda *args, **kwargs: None
game_manager = None
MAX_LEVEL: int = 100 MAX_LEVEL: int = 100
MID_LEVEL: int = 80 MID_LEVEL: int = 80
LVL_UP_CARDS: int = 20 LVL_UP_CARDS: int = 20
@ -48,7 +51,7 @@ def _msg_users(update):
@run_async @run_async
def getperm(update, context): 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) (from_user, reply_to_user) = _msg_users(update)
if not from_user: if not from_user:
return return
@ -63,7 +66,7 @@ def getperm(update, context):
@run_async @run_async
def setperm(update, context): 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) (from_user, reply_to_user) = _msg_users(update)
if not from_user: if not from_user:
return return
@ -92,7 +95,7 @@ def lvlup(update, context):
''' '''
use LVL_UP_CARDS cards to level up 1 lvl 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 LVLUP_TIMEOUT = 10
last_time = context.user_data.setdefault('lvlup_time', 0.0) last_time = context.user_data.setdefault('lvlup_time', 0.0)
ctime = time() ctime = time()
@ -149,7 +152,7 @@ def lvlup(update, context):
@run_async @run_async
def transfer_cards(update, context): 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) (from_user, reply_to_user) = _msg_users(update)
if not from_user: if not from_user:
return return
@ -190,7 +193,7 @@ def transfer_cards(update, context):
@run_async @run_async
def rob_cards(update, context): 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 ROB_TIMEOUT = 10
last_time = context.user_data.setdefault('rob_time', 0.0) last_time = context.user_data.setdefault('rob_time', 0.0)
ctime = time() ctime = time()
@ -256,7 +259,7 @@ def rob_cards(update, context):
@run_async @run_async
def cards_lottery(update, context): 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 LOTTERY_TIMEOUT = 10
last_time = context.user_data.setdefault('lottery_time', 0.0) last_time = context.user_data.setdefault('lottery_time', 0.0)
ctime = time() ctime = time()
@ -281,7 +284,7 @@ def cards_lottery(update, context):
@run_async @run_async
def dist_cards(update, context): 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) (from_user, _) = _msg_users(update)
if not from_user: if not from_user:
return return
@ -308,7 +311,7 @@ def dist_cards(update, context):
@run_async @run_async
def dist_cards_btn_click(update, context): 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 data = update.callback_query.data
user = update.callback_query.from_user user = update.callback_query.from_user
omsg = update.callback_query.message omsg = update.callback_query.message
@ -355,3 +358,62 @@ def dist_cards_btn_click(update, context):
rp[0] = -1 rp[0] = -1
omsg.edit_text(omsg.text_markdown + "褪裙了", parse_mode="Markdown", reply_markup=None) omsg.edit_text(omsg.text_markdown + "褪裙了", parse_mode="Markdown", reply_markup=None)
context.job_queue.run_once(free_mem, 5) 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('请回复想要查看的雷区')

View File

@ -12,6 +12,8 @@ from random import randint, choice, randrange
from math import log from math import log
from threading import Lock from threading import Lock
import time import time
from pathlib import Path
import pickle
import logging import logging
logging.basicConfig(level=logging.INFO,format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') 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 = updater.job_queue
job_queue.start() job_queue.start()
PICKLE_FILE = 'tgmsbot.pickle'
KBD_MIN_INTERVAL = 0.5 KBD_MIN_INTERVAL = 0.5
KBD_DELAY_SECS = 0.5 KBD_DELAY_SECS = 0.5
GARBAGE_COLLECTION_INTERVAL = 86400
HEIGHT = 8 HEIGHT = 8
WIDTH = 8 WIDTH = 8
@ -70,7 +75,7 @@ def display_username(user, atuser=True, shorten=False, markdown=True):
name += " ({})".format(user.username) name += " ({})".format(user.username)
return name return name
class Game(): class Saved_Game():
def __init__(self, board, group, creator, lives=1): def __init__(self, board, group, creator, lives=1):
self.board = board self.board = board
self.group = group self.group = group
@ -79,7 +84,6 @@ class Game():
self.last_player = None self.last_player = None
self.start_time = time.time() self.start_time = time.time()
self.stopped = False self.stopped = False
self.lock = Lock()
# timestamp of the last update keyboard action, # timestamp of the last update keyboard action,
# it is used to calculate time gap between # it is used to calculate time gap between
# two actions and identify unique actions. # two actions and identify unique actions.
@ -112,17 +116,44 @@ class Game():
msg = "{}{} - {}项操作\n".format(msg, display_username(user), count) msg = "{}{} - {}项操作\n".format(msg, display_username(user), count)
return msg 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: 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): def append(self, board, board_hash, group_id, creator_id):
lives = int(board.mines/3) lives = int(board.mines/3)
if lives <= 0: if lives <= 0:
lives = 1 lives = 1
self.__games[board_hash] = Game(board, group_id, creator_id, lives=lives) self.__games[board_hash] = Game(board, group_id, creator_id, lives=lives)
self.save_async()
def remove(self, board_hash): def remove(self, board_hash):
board = self.get_game_from_hash(board_hash) board = self.get_game_from_hash(board_hash)
if board: if board:
del self.__games[board_hash] self.__games.pop(board_hash)
self.save_async()
return True return True
else: else:
return False return False
@ -130,6 +161,30 @@ class GameManager:
return self.__games.get(board_hash, None) return self.__games.get(board_hash, None)
def count(self): def count(self):
return len(self.__games) 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() game_manager = GameManager()
@ -421,15 +476,18 @@ def handle_button_click(update, context):
raise raise
from cards import getperm, setperm, lvlup, transfer_cards, rob_cards, cards_lottery, dist_cards, dist_cards_btn_click import cards
updater.dispatcher.add_handler(CommandHandler('getlvl', getperm)) setattr(cards, 'get_player', get_player)
updater.dispatcher.add_handler(CommandHandler('setlvl', setperm)) setattr(cards, 'game_manager', game_manager)
updater.dispatcher.add_handler(CommandHandler('lvlup', lvlup)) updater.dispatcher.add_handler(CommandHandler('getlvl', cards.getperm))
updater.dispatcher.add_handler(CommandHandler('transfer', transfer_cards)) updater.dispatcher.add_handler(CommandHandler('setlvl', cards.setperm))
updater.dispatcher.add_handler(CommandHandler('rob', rob_cards)) updater.dispatcher.add_handler(CommandHandler('lvlup', cards.lvlup))
updater.dispatcher.add_handler(CommandHandler('lottery', cards_lottery)) updater.dispatcher.add_handler(CommandHandler('transfer', cards.transfer_cards))
updater.dispatcher.add_handler(CommandHandler('dist', dist_cards)) updater.dispatcher.add_handler(CommandHandler('rob', cards.rob_cards))
updater.dispatcher.add_handler(CallbackQueryHandler(dist_cards_btn_click, pattern=r'dist')) 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)) 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('stats', player_statistics))
updater.dispatcher.add_handler(CommandHandler('source', send_source)) updater.dispatcher.add_handler(CommandHandler('source', send_source))
updater.dispatcher.add_handler(CallbackQueryHandler(handle_button_click)) updater.dispatcher.add_handler(CallbackQueryHandler(handle_button_click))
updater.job_queue.run_repeating(game_manager.do_garbage_collection, GARBAGE_COLLECTION_INTERVAL, first=30)
try: try:
updater.start_polling() updater.start_polling()
updater.idle() updater.idle()
finally: finally:
game_manager.save()
logger.info('Game_manager saved.')
db.close() db.close()
logger.info('DB closed.')