#!/usr/bin/env python3 import json import re from fractions import Fraction skillsByAbility = {'str': ['Athletics'], 'dex': ['Acrobatics', 'Sleight of Hand', 'Stealth'], 'con': [], 'int': ['Arcana', 'History', 'Investigation', 'Nature', 'Religion'], 'wis': ['Animal Handling', 'Insight', 'Medicine', 'Perception', 'Survival'], 'cha': ['Deception', 'Intimidation', 'Performance', 'Persuasion']} armorByType = {'light': [('padded', 11), ('leather', 11), ('studded leather', 12)], 'medium': [('hide', 12), ('chain shirt', 13), ('scale mail', 14), ('breastplate', 14), ('half plate', 15)], 'heavy': [('ring mail', 14), ('chain mail', 16), ('splint', 17), ('plate', 18)], 'misc': [('shield', 2), ('ring of protection', 1)]} def procSkill(skillStr): skill = skillStr.strip() skillName = re.search('[^\+]*?(?= \+)', skill).group(0) skillBonus = int(re.search('\+(\d+)', skill).group(1)) ability = '' for a in skillsByAbility: if skillName in skillsByAbility[a]: ability = a if not ability: print('Could not find ability for skill {}'.format(skillName)) return skillName, skillBonus, ability def guessProficiency(desc): if desc['cr'] <= 1: return 2 def getBonus(ability): return (desc['stats'][ability] - 10) // 2 # Guess proficiency based on saves, skills, and attacks if desc['saves']: for save in desc['saves'].split(','): save = save.strip() ability = save.split(' ')[0].lower() saveBonus = int(save.split('+')[1]) return saveBonus - getBonus(ability) # This is the answer. skillGuesses = [] if desc['skills']: for skill in desc['skills'].split(','): skillName, skillBonus, ability = procSkill(skill) skillGuesses.append(skillBonus - getBonus(ability)) attackGuesses = [] for action in desc['actions'].values(): if re.match('.*Attack:', action): toHit = int(re.search('\+(\d+) to hit', action).group(1)) dmgBonusMatch = re.search('\d+d\d+ (\+|−) (\d+)\)', action) if dmgBonusMatch: dmgBonus = int(dmgBonusMatch.group(2)) if dmgBonusMatch.group(1) == '−': #print('We match here for the {}!'.format(desc['name'])) dmgBonus *= -1 else: dmgBonus = 0 if toHit - dmgBonus > 1: attackGuesses.append(toHit - dmgBonus) if not skillGuesses and not attackGuesses: print('We got here for the {}!'.format(desc['name'])) return 2 else: profGuesses = skillGuesses + attackGuesses if min(profGuesses) != 0 and any(guess % min(profGuesses) != 0 for guess in profGuesses): print('We had conflicting guesses for {}: {}'.format(desc['name'], profGuesses)) best = (0, 0) for guess in profGuesses: numHappy = sum(1 for other in profGuesses if other % guess == 0) if numHappy > best[1]: best = (guess, numHappy) return best[0] def cost2copper(cost): amnt = int(cost.split(' ')[0].replace(',', '')) den = cost.split(' ')[1] if den == 'pp': return amnt * 1000 elif den == 'gp': return amnt * 100 elif den == 'ep': return amnt * 50 elif den == 'sp': return amnt * 10 elif den == 'cp': return amnt def getArmor(): with open('../../5thSRD/docs/adventuring/equipment/armor.md') as f: data = f.read() tables = re.search('(?sm)(?<=## Armor Table).*?(?=##)', data).group(0) armors = [] header = '' for armor in re.findall('\| (.*) \| (.*) \| (.*) \| (.*) \| (.*) \| (.*) \|', tables): armor = [part.strip().lower() for part in armor] if armor[1] == 'cost': header = armor[0] else: armors.append({'name': armor[0], 'cost': cost2copper(armor[1]), 'ac': int(armor[2].split(' ')[0]), 'strength': int(armor[3].replace('-', '0').split(' ')[-1]), 'disadvantage': armor[4] == 'Disadvantage', 'weight': float(armor[5].split(' ')[0]), 'type': header.split(' ')[0]}) return armors weapons = [] def getWeapons(): global weapons if weapons: return weapons with open('../../5thSRD/docs/adventuring/equipment/weapons.md') as f: data = f.read() special = {} for s in ['Lance', 'Net']: special[s.lower()] = re.search('(?<=\*\*{}.\*\*).*'.format(s), data).group(0).strip() tables = re.search('(?sm)## Weapons Table.*', data).group(0) weapons = [] header = '' for weapon in re.findall('\| (.*) \| (.*) \| (.*) \| (.*) \| (.*) \|', tables): weapon = [part.strip().lower() for part in weapon] if weapon[1] == 'cost': header = weapon[0] else: name = weapon[0] if ',' in name: parts = name.split(', ') name = parts[1] + ' ' + parts[0] if weapon[2] == '-': weapon[2] = '0d0 -' damage = {'dmg_type': weapon[2].split(' ')[1]} if 'd' in weapon[2].split(' ')[0]: damage['dmg_die_count'] = int(weapon[2].split('d')[0]) damage['dmg_die_sides'] = int(weapon[2].split(' ')[0].split('d')[1]) else: damage['dmg_die_count'] = 1 damage['dmg_die_sides'] = 1 rang = [0, 0] reach = 5 proporties = [] if weapon[4] != '-': proporties = weapon[4].split(', ') for i, p in enumerate(list(proporties)): if 'versatile' in p: proporties[i] = 'versatile' elif 'range' in p: proporties[i] = p.split(' (')[0] rang = [int(r) for r in p.split(' ')[-1][:-1].split('/')] if 'ammunition' in p: reach = 0 elif 'reach' in p: reach += 5 if name in special: proporties.append(special[name]) weapons.append({'name': name, 'cost': cost2copper(weapon[1]), 'damage': damage, 'weight': float(Fraction(weapon[3].split(' ')[0].replace('-', '0'))), 'range': rang, 'reach': reach, 'properties': proporties, 'type': header, 'text': 'Provided from PHB'}) return weapons def formatWeapon(name, rangeShort, rangeLong, reach, dmgType, dmgCount, dmgSides, text): baseWeapon = {'cost': 0, 'weight': 0.0, 'properties': [], 'type': 'unknown'} for weapon in weapons: if weapon['name'] == name: baseWeapon = weapon return {'name': name, 'cost': baseWeapon['cost'], 'damage': {'dmg_type': dmgType, 'dmg_die_count': dmgCount, 'dmg_die_sides': dmgSides}, 'weight': baseWeapon['weight'], 'range': [rangeShort, rangeLong], 'reach': reach, 'properties': baseWeapon['properties'], 'type': baseWeapon['type'], 'text': text} spells = [] def getSpells(): global spells if spells: return spells names2names = {'name': 'name', 'level': 'level', 'school': 'school', 'classes': 'classes', 'casting_time': 'Casting Time', 'range': 'Range', 'components': 'Components', 'duration': 'Duration'} from pathlib import Path for s in Path('../../5thSRD/docs/spellcasting/spells/').iterdir(): with s.open() as f: data = f.read() spell = {} for name in names2names: spell[name] = re.search('(?sm)[\*]*{}[\*:]* (.*?)^[a-zA-Z#\*]'.format(names2names[name]), data).group(1).strip() spell['level'] = int(spell['level']) spell['text'] = re.search('(?sm)\*\*Duration:?\*\*:? .*?$(.*)', data).group(1).strip() spell['classes'] = spell['classes'].split() spells.append(spell) return spells