tgmsbot/tgmsbot.py

446 lines
17 KiB
Python
Raw Normal View History

2018-12-29 20:37:07 +08:00
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from mscore import Board, check_params
from copy import deepcopy
from telegram import InlineKeyboardMarkup, InlineKeyboardButton
2019-01-15 00:57:48 +08:00
from telegram.ext import Updater, CommandHandler, CallbackQueryHandler, run_async
2018-12-30 15:24:45 +08:00
from telegram.error import TimedOut as TimedOutError
2018-12-29 20:37:07 +08:00
from numpy import array_equal
# If no peewee orm is installed, try `from data_ram import get_player, db`
from data import get_player, db
from random import randint, choice, randrange
from math import log
2019-01-19 14:58:04 +08:00
from threading import Lock
2018-12-30 15:24:45 +08:00
import time
2018-12-29 20:37:07 +08:00
import logging
logging.basicConfig(level=logging.INFO,format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
2019-11-18 22:10:52 +08:00
logger = logging.getLogger('tgmsbot')
2018-12-29 20:37:07 +08:00
token = "token_here"
updater = Updater(token, workers=8, use_context=True)
job_queue = updater.job_queue
job_queue.start()
2019-01-14 21:31:42 +08:00
KBD_MIN_INTERVAL = 0.5
KBD_DELAY_SECS = 0.5
2018-12-29 20:37:07 +08:00
HEIGHT = 8
WIDTH = 8
MINES = 9
UNOPENED_CELL = "\u2588"
FLAGGED_CELL = "\U0001f6a9"
STEPPED_CELL = "\u2622"
2019-10-26 14:36:12 +08:00
NUM_CELL_0 = "\u2800"
NUM_CELL_ORD = ord("\uff11") - 1
2018-12-29 20:37:07 +08:00
2018-12-30 15:24:45 +08:00
WIN_TEXT_TEMPLATE = "哇所有奇怪的地方都被你打开啦…好羞羞\n" \
"地图Op {s_op} / Is {s_is} / 3bv {s_3bv}\n操作总数 {ops_count}\n" \
"统计:\n{ops_list}\n{last_player} 你要对人家负责哟/// ///\n\n" \
"用时{time}秒,超时{timeouts}\n\n" \
2019-01-15 00:57:48 +08:00
"{last_player} {reward}\n\n" \
2018-12-30 21:00:54 +08:00
"/mine 开始新游戏"
2019-01-15 00:57:48 +08:00
STEP_TEXT_TEMPLATE = "{last_player} 踩到了地雷!\n" \
"时间{time}秒,超时{timeouts}\n\n" \
"{last_player} {reward}\n\n" \
"雷区生命值:({remain}/{ttl})"
2018-12-30 15:24:45 +08:00
LOSE_TEXT_TEMPLATE = "一道火光之后,你就在天上飞了呢…好奇怪喵\n" \
"地图Op {s_op} / Is {s_is} / 3bv {s_3bv}\n操作总数 {ops_count}\n" \
"统计:\n{ops_list}\n{last_player} 是我们中出的叛徒!\n\n" \
"用时{time}秒,超时{timeouts}\n\n" \
2019-01-15 00:57:48 +08:00
"{last_player} {reward}\n\n" \
2018-12-30 21:00:54 +08:00
"/mine 开始新游戏"
2018-12-30 15:24:45 +08:00
2018-12-29 20:37:07 +08:00
def display_username(user, atuser=True, shorten=False, markdown=True):
"""
atuser and shorten has no effect if markdown is True.
"""
name = user.full_name
if markdown:
mdtext = user.mention_markdown(name=user.full_name)
return mdtext
if shorten:
return name
if user.username:
if atuser:
name += " (@{})".format(user.username)
else:
name += " ({})".format(user.username)
return name
class Game():
2019-01-15 00:57:48 +08:00
def __init__(self, board, group, creator, lives=1):
2018-12-29 20:37:07 +08:00
self.board = board
self.group = group
2018-12-30 15:24:45 +08:00
self.creator = creator
self.__actions = dict()
2019-01-15 00:57:48 +08:00
self.last_player = None
2018-12-30 15:24:45 +08:00
self.start_time = time.time()
self.stopped = False
2019-01-19 14:58:04 +08:00
self.lock = Lock()
# timestamp of the last update keyboard action,
# it is used to calculate time gap between
# two actions and identify unique actions.
self.last_action = 0
# number of timeout error catched
self.timeouts = 0
2019-01-15 00:57:48 +08:00
self.lives = lives
self.ttl_lives = lives
2018-12-29 20:37:07 +08:00
def save_action(self, user, spot):
'''spot is supposed to be a tuple'''
2019-01-15 00:57:48 +08:00
self.last_player = user
if self.__actions.get(user, None):
self.__actions[user].append(spot)
2018-12-29 20:37:07 +08:00
else:
self.__actions[user] = [spot,]
2018-12-30 15:24:45 +08:00
def actions_sum(self):
mysum = 0
for user in self.__actions:
2019-01-15 00:57:48 +08:00
game_count(user)
count = len(self.__actions.get(user, list()))
2018-12-30 15:24:45 +08:00
mysum += count
return mysum
def get_last_player(self):
2019-01-15 00:57:48 +08:00
return display_username(self.last_player)
2018-12-29 20:37:07 +08:00
def get_actions(self):
'''Convert actions into text'''
msg = ""
for user in self.__actions:
count = len(self.__actions.get(user, list()))
2018-12-30 15:24:45 +08:00
msg = "{}{} - {}项操作\n".format(msg, display_username(user), count)
2018-12-29 20:37:07 +08:00
return msg
class GameManager:
2018-12-30 15:24:45 +08:00
__games = dict()
2018-12-29 20:37:07 +08:00
def append(self, board, board_hash, group_id, creator_id):
2019-01-15 00:57:48 +08:00
lives = int(board.mines/3)
if lives <= 0:
lives = 1
self.__games[board_hash] = Game(board, group_id, creator_id, lives=lives)
2018-12-29 20:37:07 +08:00
def remove(self, board_hash):
board = self.get_game_from_hash(board_hash)
if board:
2018-12-30 15:24:45 +08:00
del self.__games[board_hash]
2018-12-29 20:37:07 +08:00
return True
else:
return False
def get_game_from_hash(self, board_hash):
2018-12-30 15:24:45 +08:00
return self.__games.get(board_hash, None)
2018-12-29 20:37:07 +08:00
def count(self):
return len(self.__games)
game_manager = GameManager()
2019-01-15 00:57:48 +08:00
@run_async
def send_keyboard(update, context):
(bot, args) = (context.bot, context.args)
2018-12-29 20:37:07 +08:00
msg = update.message
logger.info("Mine from {0}".format(update.message.from_user.id))
2019-01-15 00:57:48 +08:00
if check_restriction(update.message.from_user):
update.message.reply_text("爆炸这么多次还想扫雷?")
return
2018-12-29 20:37:07 +08:00
# create a game board
if args is None:
args = list()
2018-12-29 20:37:07 +08:00
if len(args) == 3:
height = HEIGHT
width = WIDTH
mines = MINES
try:
height = int(args[0])
width = int(args[1])
mines = int(args[2])
except:
pass
2018-12-30 17:07:00 +08:00
# telegram doesn't like keyboard width to exceed 8
if width > 8:
width = 8
msg.reply_text('宽度太大已经帮您设置成8了')
# telegram doesn't like keyboard keys to exceed 100
if height * width > 100:
msg.reply_text('格数不能超过100')
return
2018-12-29 20:37:07 +08:00
ck = check_params(height, width, mines)
if ck[0]:
board = Board(height, width, mines)
else:
msg.reply_text(ck[1])
return
2018-12-29 21:25:05 +08:00
elif len(args) == 0:
2018-12-29 20:37:07 +08:00
board = Board(HEIGHT, WIDTH, MINES)
else:
msg.reply_text('你输入的是什么鬼!')
return
bhash = hash(board)
game_manager.append(board, bhash, msg.chat, msg.from_user)
# create a new keyboard
keyboard = list()
for row in range(board.height):
current_row = list()
for col in range(board.width):
cell = InlineKeyboardButton(text=UNOPENED_CELL, callback_data="{} {} {}".format(bhash, row, col))
current_row.append(cell)
keyboard.append(current_row)
# send the keyboard
bot.send_message(chat_id=msg.chat.id, text="路过的大爷~来扫个雷嘛~", reply_to_message_id=msg.message_id,
parse_mode="Markdown", reply_markup=InlineKeyboardMarkup(keyboard))
def send_help(update, context):
logger.debug("Start from {0}".format(update.message.from_user.id))
2018-12-30 17:07:00 +08:00
msg = update.message
2018-12-30 21:00:54 +08:00
msg.reply_text("这是一个扫雷bot\n\n/mine 开始新游戏")
2018-12-29 20:37:07 +08:00
def send_source(update, context):
2018-12-29 20:37:07 +08:00
logger.debug("Source from {0}".format(update.message.from_user.id))
update.message.reply_text('Source code: https://git.jerryxiao.com/Jerry/tgmsbot')
def send_status(update, context):
2018-12-29 20:37:07 +08:00
logger.info("Status from {0}".format(update.message.from_user.id))
count = game_manager.count()
update.message.reply_text('当前进行的游戏: {}'.format(count))
2019-01-15 00:57:48 +08:00
def gen_reward(user, negative=True):
''' Reward the player :) '''
def __chance(percentage):
if randrange(0,10000)/10000 < percentage:
return True
else:
return False
def __floating(value):
return randrange(8000,12000)/10000 * value
def __lose_cards(cardnum):
if cardnum <= 6:
return 1
else:
return int(__floating(log(cardnum, 2)))
def __get_cards(cardnum):
if cardnum >= 2:
cards = __floating(1 / log(cardnum, 100))
if cards > 1.0:
return int(cards)
else:
return int(__chance(cards))
else:
return int(__floating(8.0))
2019-01-15 00:57:48 +08:00
# Negative rewards
def restrict_mining(player):
lost_cards = __lose_cards(player.immunity_cards)
player.immunity_cards -= lost_cards
if player.immunity_cards >= 0:
2019-01-15 15:25:55 +08:00
ret = "用去{}张免疫卡,还剩{}".format(lost_cards, player.immunity_cards)
2019-01-15 00:57:48 +08:00
else:
now = int(time.time())
seconds = randint(30, 120)
player.restricted_until = now + seconds
2019-01-15 15:25:55 +08:00
ret = "没有免疫卡了,被限制扫雷{}".format(seconds)
2019-01-15 00:57:48 +08:00
return ret
# Positive rewards
def give_immunity_cards(player):
rewarded_cards = __get_cards(player.immunity_cards)
2019-01-15 15:25:55 +08:00
player.immunity_cards += rewarded_cards
if rewarded_cards == 0:
return "共有{}张免疫卡".format(player.immunity_cards)
else:
return "被奖励了{}张免疫卡,共有{}".format(rewarded_cards, player.immunity_cards)
2019-01-15 00:57:48 +08:00
player = get_player(user.id)
try:
if negative:
player.death += 1
return restrict_mining(player)
else:
player.wins += 1
return give_immunity_cards(player)
finally:
player.save()
2019-01-15 00:57:48 +08:00
def game_count(user):
player = get_player(user.id)
player.mines += 1
player.save()
def check_restriction(user):
player = get_player(user.id)
now = int(time.time())
if now >= player.restricted_until:
return False
else:
return player.restricted_until - now
@run_async
def player_statistics(update, context):
2019-01-15 00:57:48 +08:00
logger.info("Statistics from {0}".format(update.message.from_user.id))
user = update.message.from_user
player = get_player(user.id)
mines = player.mines
death = player.death
wins = player.wins
cards = player.immunity_cards
TEMPLATE = "一共玩了{mines}局,爆炸{death}次,赢了{wins}\n" \
"口袋里有{cards}张免疫卡"
update.message.reply_text(TEMPLATE.format(mines=mines, death=death,
wins=wins, cards=cards))
def update_keyboard_request(context, bhash, game, chat_id, message_id):
current_action_timestamp = time.time()
if current_action_timestamp - game.last_action <= KBD_MIN_INTERVAL:
logger.debug('Rate limit triggered.')
game.last_action = current_action_timestamp
job_queue.run_once(update_keyboard, KBD_DELAY_SECS,
context=(bhash, game, chat_id, message_id, current_action_timestamp))
else:
game.last_action = current_action_timestamp
update_keyboard(context, noqueue=(bhash, game, chat_id, message_id))
def update_keyboard(context, noqueue=None):
(bot, job) = (context.bot, context.job)
if noqueue:
(bhash, game, chat_id, message_id) = noqueue
else:
(bhash, game, chat_id, message_id, current_action_timestamp) = job.context
if current_action_timestamp != game.last_action:
logger.debug('New update action requested, abort this one.')
return
2018-12-30 15:24:45 +08:00
def gen_keyboard(board):
keyboard = list()
for row in range(board.height):
current_row = list()
for col in range(board.width):
if board.map[row][col] <= 9:
cell_text = UNOPENED_CELL
elif board.map[row][col] == 10:
2019-10-26 14:36:12 +08:00
cell_text = NUM_CELL_0
2018-12-30 15:24:45 +08:00
elif board.map[row][col] == 19:
cell_text = FLAGGED_CELL
elif board.map[row][col] == 20:
cell_text = STEPPED_CELL
else:
2019-10-26 14:36:12 +08:00
cell_text = chr(NUM_CELL_ORD + board.map[row][col] - 10)
2018-12-30 15:24:45 +08:00
cell = InlineKeyboardButton(text=cell_text, callback_data="{} {} {}".format(bhash, row, col))
current_row.append(cell)
keyboard.append(current_row)
return keyboard
keyboard = gen_keyboard(game.board)
try:
bot.edit_message_reply_markup(chat_id=chat_id, message_id=message_id,
reply_markup=InlineKeyboardMarkup(keyboard))
except TimedOutError:
logger.debug('time out in game {}.'.format(bhash))
game.timeouts += 1
2018-12-29 20:37:07 +08:00
2019-01-15 00:57:48 +08:00
@run_async
def handle_button_click(update, context):
bot = context.bot
2018-12-29 20:37:07 +08:00
msg = update.callback_query.message
user = update.callback_query.from_user
chat_id = update.callback_query.message.chat.id
data = update.callback_query.data
logger.debug('Button clicked by {}, data={}.'.format(user.id, data))
2019-01-15 00:57:48 +08:00
restriction = check_restriction(user)
if restriction:
bot.answer_callback_query(callback_query_id=update.callback_query.id,
text="还有{}秒才能扫雷".format(restriction), show_alert=True)
return
2018-12-29 20:37:07 +08:00
bot.answer_callback_query(callback_query_id=update.callback_query.id)
try:
data = data.split(' ')
data = [int(i) for i in data]
(bhash, row, col) = data
except:
2018-12-29 20:37:07 +08:00
logger.info('Unknown callback data: {} from user {}'.format(data, user.id))
return
game = game_manager.get_game_from_hash(bhash)
if game is None:
2018-12-30 15:24:45 +08:00
logger.debug("No game found for hash {}".format(bhash))
2018-12-29 20:37:07 +08:00
return
2019-03-01 17:06:38 +08:00
try:
if game.stopped:
return
game.lock.acquire()
2019-03-01 17:06:38 +08:00
board = game.board
if board.state == 0:
mmap = None
else:
mmap = deepcopy(board.map)
board.move((row, col))
if board.state != 1:
game.stopped = True
game.lock.release()
game.save_action(user, (row, col))
if not array_equal(board.map, mmap):
update_keyboard_request(context, bhash, game, chat_id, msg.message_id)
2019-03-01 17:06:38 +08:00
(s_op, s_is, s_3bv) = board.gen_statistics()
ops_count = game.actions_sum()
ops_list = game.get_actions()
last_player = game.get_last_player()
time_used = time.time() - game.start_time
timeouts = game.timeouts
remain = 0
ttl = 0
if board.state == 2:
reward = gen_reward(game.last_player, negative=False)
template = WIN_TEXT_TEMPLATE
elif board.state == 3:
reward = gen_reward(game.last_player, negative=True)
game.lives -= 1
if game.lives <= 0:
template = LOSE_TEXT_TEMPLATE
else:
game.stopped = False
board.state = 1
remain = game.lives
ttl = game.ttl_lives
template = STEP_TEXT_TEMPLATE
2019-01-15 00:57:48 +08:00
else:
2019-03-01 17:06:38 +08:00
# Should not reach here
reward = None
myreply = template.format(s_op=s_op, s_is=s_is, s_3bv=s_3bv, ops_count=ops_count,
ops_list=ops_list, last_player=last_player,
time=round(time_used, 4), timeouts=timeouts, reward=reward,
remain=remain, ttl=ttl)
try:
msg.reply_text(myreply, parse_mode="Markdown")
except TimedOutError:
logger.debug('timeout sending report for game {}'.format(bhash))
if game.stopped:
game_manager.remove(bhash)
elif mmap is None or (not array_equal(board.map, mmap)):
game.lock.release()
game.save_action(user, (row, col))
update_keyboard_request(context, bhash, game, chat_id, msg.message_id)
2018-12-29 20:37:07 +08:00
else:
2019-03-01 17:06:38 +08:00
game.lock.release()
except:
try:
2019-03-01 17:06:38 +08:00
game.lock.release()
except RuntimeError:
pass
raise
2018-12-30 15:24:45 +08:00
2018-12-29 20:37:07 +08:00
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'))
2018-12-29 20:37:07 +08:00
updater.dispatcher.add_handler(CommandHandler('start', send_help))
updater.dispatcher.add_handler(CommandHandler('mine', send_keyboard))
2018-12-29 20:37:07 +08:00
updater.dispatcher.add_handler(CommandHandler('status', send_status))
2019-01-15 00:57:48 +08:00
updater.dispatcher.add_handler(CommandHandler('stats', player_statistics))
2018-12-29 20:37:07 +08:00
updater.dispatcher.add_handler(CommandHandler('source', send_source))
updater.dispatcher.add_handler(CallbackQueryHandler(handle_button_click))
try:
updater.start_polling()
updater.idle()
finally:
db.close()