584 lines
22 KiB
Python
584 lines
22 KiB
Python
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 <character name>`, 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 <name>`.')
|
|
@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 <name>`.')
|
|
@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 <container> <key> [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 <container> <key> [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 <container> <description>.')
|
|
@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 <dice>)'] = '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 <dice>)'] = '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 <dice>)'] = '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 <dice>)'] = '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()
|
|
|