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.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('请回复想要查看的雷区')

View File

@ -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.')