Skip to content

Commit

Permalink
Added ability_check. Added tests. Updated readme
Browse files Browse the repository at this point in the history
  • Loading branch information
satanas committed Nov 13, 2019
1 parent 04677dd commit eb689d4
Show file tree
Hide file tree
Showing 8 changed files with 190 additions and 23 deletions.
11 changes: 6 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,13 @@ Character commands | Action
/link_char \<char\_id\>, (username) | links character to target username or self username
/status \<username\|character\> | shows the list of weapons of a character
/weapons \<username\|character\> | shows the list of weapons of a character
/attack_roll \<character\>, \<weapon\>, \<melee\|range\>, (distance), (adv\|disadv) | performs an attack roll on a character
/attack_roll \<weapon\>, \<melee\|range\>, (distance), (adv\|disadv) | performs an attack roll on a character
/initiative_roll \<character\> | performs an initiative roll for a character
/short_rest_roll \<username\|character\> | performs an short rest roll for a character
/talk \<message\> | prints a message using in-game conversation format
/say \<message\> | prints a normal message using in-game conversation format
/whisper \<message\> | prints a whisper message using in-game conversation format
/yell \<message\> | prints a yell message using in-game conversation format
/ability_check \<ability\>, (skill) | performs an ability check or a skill check if skill is specified
/say \<character\>, \<message\> | prints a message using in-game conversation format
/whisper \<character\>, \<message\> | prints a whisper message using in-game conversation format
/yell \<character\>, \<message\> | prints a yell message using in-game conversation format


## What do I need?
Expand Down Expand Up @@ -106,3 +106,4 @@ AWS credentials saved on your machine at ~/.aws/credentials.
## References
* https://github.com/python-telegram-bot/python-telegram-bot/blob/master/examples/README.md
* https://github.com/treetrnk/rollem-telegram-bot/blob/master/bot.py
* https://rpg.stackexchange.com/questions/83930/how-do-i-calculate-my-skill-modifier
12 changes: 7 additions & 5 deletions commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,10 @@
"/attack_roll": (character_handler, ["<weapon>", "<melee|range>", "(distance)", "(adv|disadv)"], "performs an attack roll on a character"),
"/initiative_roll": (character_handler, ["<character>"], "performs an initiative roll for a character"),
"/short_rest_roll": (character_handler, ["<username|character>"], "performs an short rest roll for a character"),
"/say": (character_handler, ["<message>"], "prints a message using in-game conversation format"),
"/whisper": (character_handler, ["<message>"], "prints a whisper message using in-game conversation format"),
"/yell": (character_handler, ["<message>"], "prints a yell message using in-game conversation format"),
"/ability_check": (character_handler, ["<ability>", "(skill)"], "performs an ability check or a skill check if skill is specified"),
"/say": (character_handler, ["<character>", "<message>"], "prints a message using in-game conversation format"),
"/whisper": (character_handler, ["<character>", "<message>"], "prints a whisper message using in-game conversation format"),
"/yell": (character_handler, ["<character>", "<message>"], "prints a yell message using in-game conversation format"),
}

ALL_COMMANDS = {}
Expand All @@ -53,8 +54,9 @@
ALL_COMMANDS.update(CHARACTER_COMMANDS)

def command_handler(command):
if command == "/help":
return default_handler
if command == "/help" or command == '/start':
raise CommandNotFound
# return default_handler
#elif command == "/start":
elif command in ALL_COMMANDS:
return ALL_COMMANDS[command][0]
Expand Down
5 changes: 5 additions & 0 deletions exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,9 @@ class CampaignNotFound(Exception):
pass

class CharacterNotFound(Exception):
"""Raised when a character is not defined or doesn't exist in the database"""
pass

class InvalidCommand(Exception):
"""Raised when a command doesn't have a valid structure"""
pass
53 changes: 47 additions & 6 deletions handlers/character.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@

from handlers.roll import roll
from database import Database
from models.character import Character
from exceptions import CharacterNotFound, CampaignNotFound
from utils import normalized_username
from models.character import Character, ABILITIES, SKILLS
from exceptions import CharacterNotFound, CampaignNotFound, InvalidCommand

CLOSE_COMBAT_DISTANCE = 5 # feet

Expand Down Expand Up @@ -46,6 +46,8 @@ def handler(bot, update, command, txt_args):
elif command == '/say' or command == '/yell' or command == '/whisper':
response = talk(command, txt_args)
#bot.delete_message(chat_id=update.message.chat_id, message_id=update.message.message_id)
elif command == '/ability_check':
response = ability_check(txt_args, db, chat_id, username)

bot.send_message(chat_id=update.message.chat_id, text=response, parse_mode="Markdown")

Expand Down Expand Up @@ -114,7 +116,7 @@ def attack_roll(txt_args, db, chat_id, username):
return f"You can attack a target beyond the range of your weapon ({weapon_name}, {weapon.long_range}ft)"

prof = " + PRO(0)"
if character.has_weapon_proficiency(weapon_name):
if character.has_proficiency(weapon_name):
mods += character.proficiency
prof = f" + PRO({character.proficiency})"

Expand Down Expand Up @@ -167,7 +169,7 @@ def initiative_roll(txt_args, db, chat_id, username):
dice_notation = f'1d20+{character.dex_mod}'
results = roll(dice_notation)
dice_rolls = results[list(results.keys())[0]][0]
return f'@{username} initiave roll for {character.name} ({dice_notation}): {dice_rolls}'
return f'@{username} initiative roll for {character.name} ({dice_notation}): {dice_rolls}'

def short_rest_roll(txt_args, db, chat_id, username):
character = get_linked_character(db, chat_id, username)
Expand All @@ -182,6 +184,7 @@ def short_rest_roll(txt_args, db, chat_id, username):

def get_weapons(other_username, db, chat_id, username):
search_param = other_username if other_username != '' else username
search_param = utils.normalized_username(search_param)
character = get_linked_character(db, chat_id, search_param)

if len(character.weapons) > 0:
Expand Down Expand Up @@ -213,8 +216,46 @@ def talk(command, txt_args):

return f"```\r\n{character_name} says:\r\n{message}\r\n```"

def ability_check(chat_id, username, ability):
pass
def ability_check(txt_args, db, chat_id, username):
args = txt_args.split(' ')
if len(args) == 0:
return ('Invalid syntax. Usage:'
'\r\n/ability_check <ability> (skill)')

skill = None
ability = args[0].lower()
base_notation = '1d20'
if len(args) == 2:
skill = args[1].lower()

if ability not in ABILITIES:
return ('Invalid ability. Supported options: ' + ', '.join(ABILITIES))
if skill is not None and skill not in SKILLS[ability]:
return ('Invalid skill. Supported options: ' + ', '.join(SKILLS[ability]))

character = get_linked_character(db, chat_id, username)

txt_skill_mod = ''
txt_ability_mod = f' + {ability.upper()}({character.mods[ability]})'
ability_desc = ability.upper()
mods = character.mods[ability]
if skill is not None:
ability_desc = f'{ability_desc} ({skill.capitalize()})'
mods += character.mods[skill]
txt_skill_mod = f' + {skill.capitalize()}({character.mods[skill]})'

txt_formula = f"{base_notation}{txt_ability_mod}{txt_skill_mod}"
if mods > 0:
dice_notation = f"{base_notation}+{mods}"
else:
dice_notation = base_notation

results = roll(dice_notation)
dice_rolls = results[list(results.keys())[0]]

return (f"@{username} ability check for {character.name} with {ability_desc}:"
f"\r\nFormula: {txt_formula}"
f"\r\n*{dice_notation}*: {dice_rolls}")

def get_linked_character(db, chat_id, username):
campaign_id, campaign = db.get_campaign(chat_id)
Expand Down
6 changes: 3 additions & 3 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@
if logger.handlers:
for handler in logger.handlers:
logger.removeHandler(handler)
logging.basicConfig(format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
level=logging.INFO)
#logging.basicConfig(format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', level=logging.INFO)
logging.basicConfig(format='%(message)s', level=logging.INFO)

OK_RESPONSE = {
'statusCode': 200,
Expand Down Expand Up @@ -45,7 +45,7 @@ def webhook(event, context):
"""

bot = configure_telegram()
logger.info('Event: {}'.format(event))
logger.info(json.loads(event.get('body')))

if event.get('httpMethod') == 'POST' and event.get('body'):
update = telegram.Update.de_json(json.loads(event.get('body')), bot)
Expand Down
68 changes: 65 additions & 3 deletions models/character.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,49 @@
import math

import utils

from models.armor import Armor
from models.weapon import Weapon

ABILITIES = [
'str',
'dex',
'int',
'wis',
'cha'
]

SKILLS = {
'str': [
'athletics'
],
'dex': [
'acrobatics',
'sleight-of-hand',
'stealth'
],
'int': [
'arcana',
'history',
'investigation',
'nature',
'religion'
],
'wis': [
'animal-handling',
'insight',
'medicine',
'perception',
'survival'
],
'cha': [
'deception',
'intimidation',
'performance',
'persuasion'
]
}

class Character:
def __init__(self, json_data, race_data, by_id):
character = json_data if by_id else json_data['character']
Expand Down Expand Up @@ -33,13 +74,15 @@ def __init__(self, json_data, race_data, by_id):
self.initiative = self.dex_mod
self.weapons = [Weapon(x) for x in character['inventory'] if x['definition']['filterType'] == "Weapon"]
self.armor = [Armor(x) for x in character['inventory'] if x['definition']['filterType'] == "Armor"]
self.proficiencies = [x['friendlySubtypeName'] for x in character['modifiers']['class'] if x['type'] == 'proficiency']
self.proficiencies = [x['subType'] for x in character['modifiers']['class'] if x['type'] == 'proficiency']
self.size = character['race']['size']
self.proficiency = math.floor((self.level + 7) / 4)

self.mods = self.__calculate_modifiers()


def has_weapon_proficiency(self, weapon):
return True if weapon in self.proficiencies else False
def has_proficiency(self, arg):
return True if utils.to_snake_case(arg) in self.proficiencies else False

def get_weapon(self, weapon_name):
result = [w for w in self.weapons if w.name == weapon_name]
Expand All @@ -48,6 +91,25 @@ def get_weapon(self, weapon_name):
else:
return None

def __calculate_modifiers(self):
mods = {
'str': self.str_mod,
'dex': self.dex_mod,
'con': self.con_mod,
'int': self.int_mod,
'wis': self.wis_mod,
'cha': self.cha_mod
}

for ability in ABILITIES:
for skill in SKILLS[ability]:
mods[skill] = mods[ability]
if self.has_proficiency(skill):
mods[skill] += self.proficiency

return mods


def __str__(self):
return (f"Character name={self.name}, race={self.race}, str={self.str}({self.str_mod}), dex={self.dex}({self.dex_mod}), "
f"con={self.con}({self.con_mod}), int={self.int}({self.int_mod}), wis={self.wis}({self.wis_mod}), "
Expand Down
53 changes: 52 additions & 1 deletion tests/test_character.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from unittest.mock import patch, Mock, PropertyMock

from models.character import Character
from handlers.character import talk, import_character, link_character, get_status
from handlers.character import talk, import_character, link_character, get_status, ability_check

CHARACTER_JSON = {
'character': {
Expand Down Expand Up @@ -150,6 +150,57 @@ def test_status_with_params(self):
db.get_character_id.assert_called_with(campaign_id, 'foobar')
self.assertEqual('Amarok Skullsorrow | Human Sorcerer Level 1\r\nHP: 6/6 | XP: 25', rtn)

def test_ability_check_with_empty_params(self):
# conditions
chat_id = 123456
username = 'foo'
db = Mock()

# execution
rtn = ability_check('', db, chat_id, username)
self.assertEqual('Invalid ability. Supported options: str, dex, int, wis, cha', rtn)

def test_ability_check_with_ability(self):
# conditions
character_id = 987654321
campaign_id = 666
chat_id = 123456
username = 'foo'
db = Mock()
db.get_campaign = Mock(return_value=(campaign_id, None))
db.get_character_id = Mock(return_value=(character_id))
db.get_character = Mock(return_value=self.__get_character())

# execution
rtn = ability_check('str', db, chat_id, username)
self.assertEqual(True, rtn.find("@foo ability check for Amarok Skullsorrow with STR:\r\nFormula: 1d20 + STR(4)\r\n*1d20+4*") == 0)

def test_ability_check_with_ability_and_skill(self):
# conditions
character_id = 987654321
campaign_id = 666
chat_id = 123456
username = 'foo'
db = Mock()
db.get_campaign = Mock(return_value=(campaign_id, None))
db.get_character_id = Mock(return_value=(character_id))
db.get_character = Mock(return_value=self.__get_character())

# execution
rtn = ability_check('wis perception', db, chat_id, username)
self.assertEqual(True, rtn.find("@foo ability check for Amarok Skullsorrow with WIS (Perception):\r\nFormula: 1d20 + WIS(1) + Perception(1)\r\n*1d20+2*") == 0)

def test_ability_check_with_ability_and_invalid_skill(self):
# conditions
chat_id = 123456
username = 'foo'
db = Mock()

# execution
rtn = ability_check('str perception', db, chat_id, username)
self.assertEqual('Invalid skill. Supported options: athletics', rtn)


def __get_character(self):
with open('tests/fixtures/character.json', 'r') as jd:
json_data = json.loads(jd.read())
Expand Down
5 changes: 5 additions & 0 deletions utils.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,7 @@
def normalized_username(username):
return username.replace('@', '').strip()

def to_snake_case(text):
text = text.lower()
text = text.replace(' ', '-')
return text

0 comments on commit eb689d4

Please sign in to comment.