import telepot import functools from collections import OrderedDict import db import diceroller all_commands = {} def newgame_already_started_usage(): return """This game was already started in this group. Now invite some players, make them join with `/player `, check your characters with `/show`, adjust your character sheet with `/update`, and roll dices with `/roll`. For a more complete list of commands, see https://github.com/simonebaracchi/rpgbot.""" def add_command(name): global all_commands def decorator(func): @functools.wraps(func) def wrapper(*args, **kwargs): return func(*args, **kwargs) all_commands[name] = func return wrapper return decorator def need_args(number, errormessage): def decorator(func): @functools.wraps(func) def wrapper(handler): if len(handler.args) < 1 + number: handler.send(errormessage) return False return func(handler) return wrapper return decorator def read_args(string, argname): def decorator(func): @functools.wraps(func) def wrapper(handler, **kwargs): handler.send(string, allowedit=True) handler.read_answer(func, argname, kwargs) return wrapper return decorator def choose_container(string, argname, allownew, adding): def decorator(func): @functools.wraps(func) def wrapper(handler, **kwargs): items = db.get_items(handler.dbc, handler.group.gameid, handler.sender_id) room = db.get_items(handler.dbc, handler.group.gameid, handler.chat_id) options = OrderedDict() if adding: # show special containers even if empty options['Room items'] = db.room_container options['Saved rolls'] = db.rolls_container else: # show special containers if not empty if db.room_container in room: options['Room items'] = db.room_container if db.rolls_container in items: options['Saved rolls'] = db.rolls_container for container in items.keys(): if container == db.rolls_container: continue else: options[container] = container if allownew: options['New container...'] = '__new__container__' handler.send(string, options=options, allowedit=True) kwargs['containercallback'] = func kwargs['argname'] = argname handler.read_answer(new_container_callback, 'newcontainer', kwargs) elif len(options) == 0: # no options to show handler.send('You don\'t seem to have anything with you.') return False else: handler.send(string, options=options, allowedit=True) handler.read_answer(func, argname, kwargs) return wrapper return decorator def choose_another_player(string, argname): def decorator(func): @functools.wraps(func) def wrapper(handler, **kwargs): players = db.get_all_players_from_game(handler.dbc, handler.group.gameid) options = OrderedDict() for p in players: options[p.playername] = p.playerid handler.send(string, options=options, allowedit=True) handler.read_answer(func, argname, kwargs) return wrapper return decorator def new_container_callback(handler, newcontainer, argname, containercallback, **kwargs): if newcontainer == '__new__container__': handler.send('How do you want to name the container?', allowedit=True) handler.read_answer(containercallback, argname, kwargs) else: kwargs[argname] = newcontainer return containercallback(handler, **kwargs) def choose_item(string, argname): def decorator(func): @functools.wraps(func) def wrapper(handler, container, **kwargs): if container == db.room_container: items = db.get_items(handler.dbc, handler.group.gameid, handler.chat_id) else: items = db.get_items(handler.dbc, handler.group.gameid, handler.sender_id) options = OrderedDict() for key, value in items[container].items(): options['{} ({})'.format(key, value)] = key handler.send(string, options=options, allowedit=True) kwargs['container'] = container handler.read_answer(func, argname, kwargs) return wrapper return decorator def choose_template(string, argname): def decorator(func): @functools.wraps(func) def wrapper(handler, **kwargs): options = OrderedDict() for key, value in db.game_templates.items(): options[value] = key handler.send(string, options=options, allowedit=True) handler.read_answer(func, argname, kwargs) return wrapper return decorator def need_group(func): @functools.wraps(func) def wrapper(handler): if handler.is_group is not True: handler.send('You must run this command in a group.') return False return func(handler) return wrapper def check_too_many_games(func): @functools.wraps(func) def wrapper(handler): if db.number_of_games(handler.dbc, handler.sender_id) > 1: handler.send('Sorry, only one game at a time is currently supported.') return False return func(handler) return wrapper def need_gameid(allownotexisting=False, allowexisting=False, errormessage=None): def decorator(func): @functools.wraps(func) def wrapper(handler): group = handler.group player = None if group is None: group, player = db.get_group_from_playerid(handler.dbc, handler.sender_id) handler.group = group handler.player = player if (allowexisting is False and group is not None) or (allownotexisting is False and group is None): handler.send(errormessage) return False return func(handler) return wrapper return decorator def get_default_group(func): @functools.wraps(func) def wrapper(handler): if handler.is_group is True and handler.group is None: # Not in a game handler.group = db.get_group_from_groupid(handler.dbc, handler.chat_id) return func(handler) return wrapper def need_role(role, errormessage): def decorator(func): @functools.wraps(func) def wrapper(handler): user_role = db.get_player_role(handler.dbc, handler.sender_id, handler.group.gameid) if user_role != role: handler.send(errormessage) return False return func(handler) return wrapper return decorator @add_command('newgame') @need_group @need_gameid(allownotexisting=True, errormessage=newgame_already_started_usage()) #@need_args(1, 'Please specify the game name like this: `/newgame `.') @check_too_many_games @read_args('How are we going to call the game?', 'name') @choose_template('Please choose a game template. This will only affect the default character sheets and dices.', 'template') def newgame(handler, name, template): template, skip_sheet = db.convert_template(template) gameid = db.new_game(handler.dbc, handler.sender_id, handler.username, name, handler.chat_id, handler.groupname, template) if gameid is None: handler.send(newgame_already_started_usage()) return False if not skip_sheet: db.add_default_items(handler.dbc, handler.sender_id, gameid, template) handler.send('New game created: {}.'.format(name)) @add_command('delgame') @need_group @need_gameid(allowexisting=True, errormessage='No game found.') @need_role(db.ROLE_MASTER, 'You need to be a game master to close a game.') def delgame(handler): db.del_game(handler.dbc, handler.group.gameid) handler.send('GG, humans.') handler.group = None @add_command('showgame') @need_gameid(allowexisting=True, errormessage='No game found.') def showgame(handler): gamename, template, groups, players = db.get_game_info(handler.dbc, handler.group.gameid) players_string = [x + (' (gm)' if (y == db.ROLE_MASTER) else '') for x,y in players.items()] ret = '{} ({})\nGroups: {}\nPlayers: {}'.format(gamename, db.game_templates[template], ', '.join(groups), ', '.join(players_string)) items = db.get_items(handler.dbc, handler.group.gameid, handler.chat_id) if db.room_container in items: room_items = [' - {}: {}\n'.format(key, items[db.room_container][key]) for key in sorted(items[db.room_container])] if len(room_items) > 0: ret += '\nRoom aspects:\n{}'.format('\n'.join(room_items)) handler.send(ret) @add_command('player') @need_group @get_default_group @need_gameid(allowexisting=True, allownotexisting=True, errormessage='No game found.') #@need_args(1, 'Please specify the player name like this: `/player `.') @check_too_many_games @read_args('What is your name, adventurer?', 'name') def player(handler, name): new_player_added = db.add_player(handler.dbc, handler.sender_id, name, handler.group.gameid, db.ROLE_PLAYER) if new_player_added: template = db.get_template_from_gameid(handler.dbc, handler.group.gameid) db.add_default_items(handler.dbc, handler.sender_id, handler.group.gameid, template) handler.send('Welcome, {}.'.format(name)) else: handler.send('You will now be known as {}.'.format(name)) @add_command('add') @need_gameid(allowexisting=True, errormessage='No game found.') #@need_args(2, 'Use the format: /add [change].') @choose_container('In which container?', 'container', allownew=True, adding=True) @read_args('What item would you like to add?', 'key') @read_args('What would you like to set it to?', 'change') def add(handler, container, key, change): command = handler.command return add_or_update_item(handler, container, key, change, command) @add_command('update') @need_gameid(allowexisting=True, errormessage='No game found.') #@need_args(2, 'Use the format: /add [change].') @choose_container('In which container?', 'container', allownew=False, adding=False) @choose_item('Which item?', 'key') @read_args('What would you like to set it to?', 'change') def update(handler, container, key, change): command = handler.command return add_or_update_item(handler, container, key, change, command) def add_or_update_item(handler, container, key, change, command): dbc = handler.dbc gameid = handler.group.gameid sender_id = handler.sender_id chat_id = handler.chat_id if command == '/add' and db.number_of_items(dbc, gameid, sender_id) > 50: handler.send('You exceeded the maximum number of items. Please delete some first.') return #container = input_args[1] #key = input_args[2] #if len(input_args) <= 3: # change = '+1' #else: # change = ' '.join(input_args[3:]) owner = sender_id if container == db.room_container: owner = chat_id if command == 'update': replace_only = True else: replace_only = False oldvalue, newvalue = db.update_item(dbc, gameid, owner, container, key, change, replace_only) if newvalue is None: handler.send('Item {}/{} not found.'.format(container, key)) elif isinstance(oldvalue, int) and isinstance(newvalue, int): handler.send('Updated {}/{} from {} to {} (changed {}).'.format(container, key, oldvalue, newvalue, newvalue-oldvalue)) else: handler.send('Updated {}/{} to "{}".'.format(container, key, newvalue)) @add_command('addlist') @need_gameid(allowexisting=True, errormessage='No game found.') #@need_args(2, 'Use the format: /addlist .') @choose_container('In which container?', 'container', allownew=True, adding=True) @read_args('What would you like to add?', 'description') def addlist(handler, container, description): dbc = handler.dbc gameid = handler.group.gameid sender_id = handler.sender_id chat_id = handler.chat_id if db.number_of_items(dbc, gameid, sender_id) > 50: handler.send('You exceeded the maximum number of items. Please delete some first.') return #container = input_args[1] #description = ' '.join(input_args[2:]) owner = sender_id if container == db.room_container: owner = chat_id db.add_to_list(dbc, gameid, owner, container, description) handler.send('Added "{}" to container {}.'.format(description, container)) @add_command('del') @need_gameid(allowexisting=True, errormessage='No game found.') @choose_container('In which container?', 'container', allownew=False, adding=False) @choose_item('Which item?', 'key') def delitem(handler, container, key): dbc = handler.dbc gameid = handler.group.gameid sender_id = handler.sender_id chat_id = handler.chat_id #container = input_args[1] #key = input_args[2] owner = sender_id if container == db.room_container: owner = chat_id oldvalue = db.delete_item(dbc, gameid, owner, container, key) if oldvalue == None: handler.send('Item {}/{} not found.'.format(container, key)) else: handler.send('Deleted {}/{} (was {}).'.format(container, key, oldvalue)) def show_player(handler, playerid=None): dbc = handler.dbc gameid = handler.group.gameid items = db.get_items(dbc, gameid, playerid) playername = db.get_player_name(dbc, gameid, playerid) if playername is None: handler.send('You are not in a game.') return ret = '' ret += 'Character sheet for {}:\n'.format(playername) if items is None: handler.send('No items found.') return for container in db.preferred_container_order: if container not in items: continue if container in db.preferred_key_order: keys = db.preferred_key_order[container] else: keys = [] ret += container + ':\n' # print keys in preferred order for key in keys: if key not in items[container]: continue ret += ' - {} ({})\n'.format(key, items[container][key]) del items[container][key] # print remaining keys for key in sorted(items[container]): ret += ' - {} ({})\n'.format(key, items[container][key]) del items[container] # print everything in remaining containers for container in items: ret += container + ':\n' for key in sorted(items[container]): ret += ' - {} ({})\n'.format(key, items[container][key]) handler.send(ret) @add_command('show') @need_gameid(allowexisting=True, errormessage='No game found.') def show(handler): return show_player(handler, handler.sender_id) @add_command('showother') @need_gameid(allowexisting=True, errormessage='No game found.') @choose_another_player('Which player?', 'playerid') def showother(handler, playerid): return show_player(handler, playerid) @add_command('roll') @add_command('r') @add_command('gmroll') @need_gameid(allowexisting=True, allownotexisting=True) def roll(handler): dbc = handler.dbc username = handler.username gameid = None groupid = None if handler.group is not None: gameid = handler.group.gameid groupid = handler.group.groupid sender_id = handler.sender_id args = handler.args if handler.args is not None else [] command = handler.command if len(args) < 2: template = db.get_template_from_groupid(dbc, groupid) if template == 'fae': dice = '4dF' else: # more templates here dice = '1d20' else: dice = args[1].strip() value = 0 outcome = '' description = '' invalid_format = False try: description, value, outcome = diceroller.roll(dice) except (diceroller.InvalidFormat): invalid_format = True except (diceroller.TooManyDices): handler.send('Sorry, try with less dices.') return if invalid_format: # Check saved rolls saved_roll = db.get_item_value(dbc, gameid, sender_id, db.rolls_container, dice) if saved_roll is None: invalid_format = True else: invalid_format = False dice = saved_roll try: description, value, outcome = diceroller.roll(dice) except (diceroller.InvalidFormat): invalid_format = True except (diceroller.TooManyDices): handler.send('Sorry, try with less dices.') return if invalid_format: handler.send('Invalid dice format.') return if command == 'roll' or command == 'r': handler.send('Rolled {} = {}.'.format(outcome, value)) elif command == 'gmroll': if gameid is None: handler.send('You are not in a game.') return playername = db.get_player_name(dbc, gameid, sender_id) if playername is None: handler.send('You are not in a game.') return masters = db.get_masters_for_game(dbc, gameid) handler.send('{} secretly rolls {}...'.format(playername, description)) try: handler.send('You rolled {} = {}.'.format(outcome, value), target=sender_id) except telepot.exception.TelegramError: handler.send('{}, I couldn\'t send you the roll results. Please send me a private message to allow me sending future rolls.'.format(username)) for master in masters: if sender_id == master: continue try: handler.send('{} ({}) rolled {} = {}.'.format(playername, username, outcome, value), target=master) except telepot.exception.TelegramError: handler.send('{}, I couldn\'t send you the roll results. Please send me a private message to allow me sending future rolls.'.format(username)) @add_command('start') @need_gameid(allownotexisting=True, allowexisting=True) def start(handler): if handler.is_group is False and handler.group is None: # I am in a private chat, suggest to add me to a group message = """Howdy, human. I am a character sheet bot for Fate RPG. To use my services, add me to a group, invite other players, and call me again to start a new game. Use the inline keyboard to navigate my character sheet functions, or use the shortcut `/roll` to roll dices. Visit the official site for more details. Hope you have fun!""" options = OrderedDict() options['Go to official site ->'] = {'url': 'https://github.com/simonebaracchi/rpgbot'} handler.send(message, options=options, allowedit=True) elif handler.is_group is True and handler.group is None: # I am in a group, gameid = db.get_game_from_group(handler.dbc, handler.chat_id) if gameid is not None: # caller is not in game, but a game is ongoing options = OrderedDict() options['Join game'] = 'player' options['Roll dices (shortcut: /roll )'] = 'roll' handler.send('How can I help you?', options=options, allowedit=True) else: # suggest to start a new game message = """Howdy, earthlings. I am a character sheet bot for Fate RPG. How can I help you?""" options = OrderedDict() options['Start new game'] = 'newgame' options['Roll dices (shortcut: /roll )'] = 'roll' options['Go to official site ->'] = {'url': 'https://github.com/simonebaracchi/rpgbot'} handler.send(message, options=options, allowedit=True) elif handler.is_group is False and handler.group is not None: # I am in a private chat with a player options = OrderedDict() subopts = OrderedDict() subopts['Show game status'] = 'showgame' subopts['Show player status'] = 'show' options['dummy'] = subopts options['Show other player status'] = 'showother' subopts = OrderedDict() subopts['Add item'] = 'add' subopts['Update item'] = 'update' subopts['Add list item'] = 'addlist' subopts['Delete item'] = 'del' options['dummy2'] = subopts options['Roll dices (shortcut: /roll )'] = 'roll' options['Roll dices secretly (shortcut: /gmroll)'] = 'gmroll' subopts = OrderedDict() subopts['Go to official site ->'] = {'url': 'https://github.com/simonebaracchi/rpgbot'} subopts['Cancel'] = 'cancel' options['dummy3'] = subopts handler.send('How can I help you?', options=options, allowedit=True) else: # Game is started in the group! options = OrderedDict() subopts = OrderedDict() subopts['Show game status'] = 'showgame' subopts['Show player status'] = 'show' options['dummy'] = subopts subopts = OrderedDict() subopts['Add item'] = 'add' subopts['Update item'] = 'update' subopts['Add list item'] = 'addlist' subopts['Delete item'] = 'del' options['dummy2'] = subopts options['Roll dices (shortcut: /roll )'] = 'roll' options['Roll dices secretly (shortcut: /gmroll)'] = 'gmroll' subopts = OrderedDict() subopts['More ...'] = 'more' subopts['Cancel'] = 'cancel' options['dummy3'] = subopts handler.send('How can I help you?', options=options, allowedit=True) @add_command('more') @need_group @need_gameid(allowexisting=True) def more(handler): # More options when game is started... options = OrderedDict() options['Show other player status'] = 'showother' options['Change player name'] = 'player' #options['Leave game'] = 'leave' options['Delete game'] = 'delgame' options['Go to official site ->'] = {'url': 'https://github.com/simonebaracchi/rpgbot'} subopts = OrderedDict() subopts['<- Back'] = 'start' subopts['Cancel'] = 'cancel' options['dummy'] = subopts handler.send('How can I help you?', options=options, allowedit=True) @add_command('cancel') def cancel(handler): handler.delete()