#!/usr/local/bin/python3
# Verify and calculate Daredevils pregen sheets from character files
# Jerry Stratton astoundingscripts.com

import argparse, math, sys

parser = argparse.ArgumentParser(description='Verify and calculate pregen Daredevils characters.')
parser.add_argument('character', nargs='*', help='file to import')
parser.add_argument('--skills', action='store_true', help='show all Skills by Governing Talent')
args = parser.parse_args()
if len(args.character) == 0 and not args.skills:
	parser.print_help()
	sys.exit()

attributeNames = ['wit', 'will', 'strength', 'deftness', 'speed', 'health']
talentNames = ['charismatic', 'combative', 'communicative', 'esthetic', 'mechanical', 'natural', 'scientific']

#skills requiring a specialization
specializedSkillNames = {
	'archaic': ['strength', 'deftness', 'combative'],
	'culture': ['wit', 'esthetic', 'communicative'],
	'driver': ['deftness', 'wit', 'mechanical'],
	'history': ['wit', 'natural', 'scientific'],
	'language': ['wit', 'communicative', 'esthetic'],
	'medical specialty': ['wit', 'scientific', 'scientific'],
	'musical instrument': ['esthetic', 'deftness', 'communicative'],
	'sub-culture': ['wit', 'communicative', 'charismatic'],
}
skillNames = {
	#practical skills
	'acrobat': ['deftness', 'speed', 'natural'],
	'climbing': ['strength', 'deftness', 'natural'],
	'jumping': ['deftness', 'strength', 'natural'],
	'running': ['speed', 'strength', 'natural'],
	'swimming': ['speed', 'strength', 'natural'],

	'cyclist': ['deftness', 'wit', 'mechanical'],
	'driver': ['deftness', 'wit', 'mechanical'],
	'driver, heavy equipment': ['deftness', 'wit', 'mechanical'],
	'gambling': ['deftness', 'wit', 'charismatic'],
	'horsemanship': ['will', 'deftness', 'natural'],
	'hunting': ['wit', 'deftness', 'natural'],
	'mimicry': ['wit', 'esthetic', 'communicative'],
	'pilot': ['deftness', 'wit', 'mechanical'],
	'pilot, aerostat': ['deftness', 'wit', 'mechanical'],
	'seamanship': ['deftness', 'wit', 'natural'],
	'stealth': ['deftness', 'wit', 'natural'],
	'survival': ['health', 'wit', 'natural'],
	'throwing': ['deftness', 'wit', 'combative'],
	'tracking': ['wit', 'natural', 'natural'],

	#task skills
	'blacksmith': ['deftness', 'wit', 'mechanical'],
	'carpenter': ['deftness', 'wit', 'mechanical'],
	'electrician': ['deftness', 'wit', 'mechanical'],
	'gunsmith': ['deftness', 'wit', 'mechanical'],
	'machinist': ['deftness', 'wit', 'mechanical'],

	'cryptography': ['wit', 'scientific', 'esthetic'],
	'demolitions': ['deftness', 'wit', 'mechanical'],
	'disguise': ['wit', 'deftness', 'charismatic'],
	'interrogation': ['wit', 'will', 'charismatic'],
	'lockpicking': ['deftness', 'wit', 'mechanical'],
	'mechanic': ['deftness', 'wit', 'mechanical'],
	'pickpocket': ['deftness', 'wit', 'charismatic'],
	'research': ['wit', 'scientific', 'communicative'],
	'restoration': ['deftness', 'wit', 'esthetic'],
	'safecracking': ['deftness', 'wit', 'mechanical'],
	'traps': ['deftness', 'wit', 'mechanical'],

	#knowledge skills
	'anthropology': ['wit', 'scientific', 'communicative'],
	'archaeology': ['wit', 'esthetic', 'mechanical'],
	'botany': ['wit', 'scientific', 'natural'],
	'chemistry': ['wit', 'scientific', 'mechanical'],
	'civil engineering': ['wit', 'mechanical', 'scientific'],
	'electrical engineering': ['wit', 'scientific', 'mechanical'],
	'finance': ['wit', 'scientific', 'charismatic'],
	'forensic science': ['wit', 'deftness', 'scientific'],
	'geology': ['wit', 'scientific', 'natural'],
	'law': ['wit', 'communicative', 'charismatic'],
	'linguistics': ['wit', 'communicative', 'scientific'],
	'advanced medical': ['wit', 'deftness', 'scientific'],
	'first aid': ['wit', 'deftness', 'scientific'],
	'pathology': ['wit', 'deftness', 'scientific'],
	'therapy': ['wit', 'deftness', 'scientific'],
	'military science': ['wit', 'scientific', 'combative'],
	'navigation': ['wit', 'natural', 'scientific'],
	'occult studies': ['wit', 'scientific', 'natural'],
	'paleontology': ['wit', 'scientific', 'natural'],
	'physics': ['wit', 'scientific', 'mechanical'],
	'rhetoric': ['will', 'charismatic', 'communicative'],
	'trivia': ['wit', 'deftness', 'combative'],
	'zoology': ['wit', 'scientific', 'natural'],

	#firearm skills
	'pistol': ['deftness', 'wit', 'combative'],
	'rifle': ['deftness', 'wit', 'combative'],

	#armed combat skills
	'bayonet training': ['strength', 'deftness', 'combative'],
	'fencing': ['speed', 'deftness', 'combative'],
	'knife': ['deftness', 'speed', 'combative'],
	'nightstick': ['strength', 'deftness', 'combative'],

	#unarmed combat skills
	'brawling': ['strength', 'deftness', 'combative'],
	'martial arts': ['deftness', 'speed', 'combative'],

	#custom skills
	'computer programming': ['wit', 'mechanical', 'scientific'], #practical
	'cooking': ['esthetic', 'wit', 'natural'], #practical
	'cybernetics': ['wit', 'scientific', 'mechanical'], #knowledge
	'music': ['deftness', 'communicative', 'mechanical'], #practical
	'philosophy': ['wit', 'esthetic', 'communicative'], #knowledge
	'photography': ['esthetic', 'deftness', 'wit'], #practical
	'steelworker': ['deftness', 'wit', 'mechanical'], #practical/construction
}

skillPrerequisites = {
	'advanced medical': ('first aid', 5),
	'pathology': ('advanced medical', 5),
	'therapy': ('advanced medical', 5),
	'medical specialization': ('advanced medical', 5),
}

skillNotes = {
	'linguistics': 'If you know one language in a language family, you can use half of your score in that language to communicate in other languages in that family.',
}

wdaCombatSkills = ['bayonet training', 'fencing', 'knife', 'nightstick', 'whip', 'brawling', 'martial arts']

effectDice = ['None', '1d3', '1d6', '1d10', '2d6', '2d10', '2d10+1', '2d10+2', '2d10+3']

class Daredevil:
	def __init__(self, name, aspects):
		self.name = name
		self.skills = {}
		self.traits = {
			'attributes':{},
			'talents':{}
		}
		self.background = None
		self.quotes = None

		#required aspects
		for key in ('age', 'nationality', 'player'):
			if not key in aspects:
				tools.die('Missing', key, 'in', name)
		self.age = int(aspects['age'])
		self.nationality = aspects['nationality']
		self.player = aspects['player']

		self.aspects = aspects

	def __str__(self):
		return self.name + ' (' + self.player + ')'

	def group(self, attribute):
		value = self.traits['attributes'][attribute]
		return int((value+15)/10)

	#get a background item
	def getBackground(self, title):
		for background in self.background:
			if background == title.lower():
				return self.background[background]
		return None

	#get prerequisites for a skill
	def validatePrerequisites(self, skill):
		if skill in self.skills:
			skillName = skill
		else:
			skillName = tools.getOverallSkill(skill)

		if skillName in skillPrerequisites:
			prerequisite, requirement = skillPrerequisites[skillName]
			if prerequisite not in self.skills or self.skills[prerequisite] < requirement:
				tools.die(skill.title(), 'requires', prerequisite.title(), requirement)

	def weaponDefenseAbility(self, skill):
		if skill in wdaCombatSkills or skill.endswith(', archaic') and 'bow' not in skill:
			return tools.wda(self.skills[skill])

	def showAspects(self):
		tools.title(self.name, 0)
		for aspect in self.aspects:
			tools.datum(aspect, self.aspects[aspect])

	def showAttributes(self):
		tools.title('attributes')
		tools.datum('', 'Score', 'AST', 'CST', 'Group')
		for attribute in attributeNames:
			value = self.traits['attributes'][attribute]
			#does the character have heightened attribute use?
			heightenedAttribute = self.getBackground('heightened attribute use')
			if heightenedAttribute and heightenedAttribute.lower().startswith(attribute):
				ast = tools.round(value/1.5)
				cst = tools.cst(value)
			else:
				ast = int(value/2)
				cst = tools.round(value/3)
			tools.datum(attribute, value, ast, cst, self.group(attribute))

	def showCalculations(self):
		tools.title('calculations')
		attributes = self.traits['attributes']

		combatDodge = tools.round((self.group('deftness') + self.group('speed'))/2)
		damageResistance = attributes['health'] + attributes['strength']/2 + attributes['will']/2
		damageResistance = tools.round(damageResistance)
		encumbrance = 5 + tools.round(attributes['strength']/2)
		healing = self.group('health')
		damage = effectDice[self.group('strength')]
		perception = tools.cst(attributes['wit'])
		shockFactor = 10+self.group('health')
		woundedLevel = math.ceil(damageResistance/2)
		drt = str(damageResistance) + '/' + str(woundedLevel)

		tools.datum('Combat Dodge Ability', combatDodge)
		tools.datum('Damage Resistance Total', drt)
		tools.datum('Shock Factor', shockFactor)
		tools.datum('Off Hand Dexterity', self.offhand)
		tools.datum('Hand damage', damage)
		tools.datum('Healing Rate', healing)
		tools.datum('Perception', perception)
		tools.datum('Encumbrance Capacity', encumbrance)
		tools.datum('Luck', '4')

	def showQuotes(self):
		if self.quotes:
			tools.title('quotes')
			for quote in self.quotes:
				print(quote)

	def showSkills(self):
		tools.title('skills')
		skills = self.skills.keys()
		skills = sorted(skills)
		tools.datum('', 'BCS')

		talentGroups = tools.skillsByGovernor(skills, talentNames)

		for talent in talentNames:
			if talent in talentGroups:
				if self.traits['talents'][talent] > 0:
					tools.title(talent + '*', 2)
				else:
					tools.title(talent, 2)
				for skill in talentGroups[talent]:
					skillTitle = skill + ' (' + str(self.skills[skill]) + ')'
					bcs = tools.bcs(self.skills[skill])
					wda = self.weaponDefenseAbility(skill)
					if wda:
						bcs = str(bcs) + '/' + str(wda)
					tools.datum(skillTitle, bcs)

	def showBackground(self):
		if self.background:
			tools.title('background')
			for item in self.background:
				print(item.title() + ':', self.background[item])
			for skill in self.skills.keys():
				if skill in skillNotes:
					print(skill.title() + ':', skillNotes[skill])

	def showTalents(self):
		tools.title('talents')
		tools.datum('', 'Score', 'BCS')
		for talent in talentNames:
			value = self.traits['talents'][talent]
			bcs = tools.bcs(value)
			tools.datum(talent, value, bcs)

	def addSkill(self, skill, multiplier=1):
		parts = tools.getSkillParts(skill)

		initialValue = 0
		for part in parts:
			if part in attributeNames:
				initialValue += self.traits['attributes'][part]
			elif part in talentNames:
				initialValue += self.traits['talents'][part]
			else:
				tools.die(part, 'is neither an attribute nor a talent')

		self.skills[skill] = initialValue*multiplier

	def initialAttributes(self, attributes):
		total = self.setItemList('attributes', attributes, attributeNames)
		if total != 75:
			tools.die('Sum of attributes is not 75:', total)

	#talent profile
	def initialTalents(self, talents):
		total = self.setItemList('talents', talents, talentNames)
		if total < -2 or total > 3:
			tools.die('Sum of talents is outside of expected range:', total)

	def initialDevelopment(self, development):
		self.undevelopedAttributes = self.traits['attributes'].copy()
		self.talentProfile = self.traits['talents'].copy()
		attributeIncreases = {}
		talentIncreases = {}
		skillChoices = {}
		total = 0
		talentTotal = 0

		#collect things to increase
		for trait in development.keys():
			increase = int(development[trait])
			if trait in attributeNames:
				attributeIncreases[trait] = increase
			elif trait in talentNames:
				talentIncreases[trait] = increase
			else:
				skillChoices[trait] = increase

		#talent increases come first
		for talent in talentIncreases.keys():
			increase = talentIncreases[talent]
			talentTotal += increase
			self.traits['talents'][talent] += increase
			if self.traits['talents'][talent] > 20:
				tools.die('Talents cannot be raised beyond 20:', talent, self.traits['talents'][talent])

		if talentTotal != 27:
			tools.die('Talent increases should be 27, but are', talentTotal)

		#skill choices are the first in the development process
		self.setNationality()
		for skill in skillChoices.keys():
			increase = skillChoices[skill]
			total += increase
			if skill not in self.skills:
				total += 1
				self.addSkill(skill)
			if increase > 0:
				self.skills[skill] += increase*7
				if self.skills[skill] > 100:
					tools.die('Development has raised', skill, 'above 100', self.skills[skill])

		#validate skills
		for skill in self.skills:
			self.validatePrerequisites(skill)

		#attribute increases come after skill increases
		for attribute in attributeIncreases.keys():
			increase = attributeIncreases[attribute]
			total += increase
			self.traits['attributes'][attribute] += increase*2
			if self.traits['attributes'][attribute] > 40:
				tools.die('Development has raised', attribute, 'above 40:', self.traits['attributes'][attribute])

		if total != self.age:
			tools.die('Development must match age of', self.age, 'but is', total)

		#off-hand calculation appears to come after development increases but before old age
		offhand = self.traits['attributes']['wit'] + self.traits['attributes']['will'] + self.traits['attributes']['deftness']
		self.offhand = tools.round(offhand/6)

		#handle old age
		if self.age >= 44:
			mentalBonus = 0
			mentalPenalty = 0
			physicalPenalty = 0
			for age in [44, 48, 52, 56]:
				if self.age >= age:
					mentalBonus += 2
					physicalPenalty += 1
			for age in [60, 64, 68]:
				if self.age >= age:
					physicalPenalty += 2

			for age in [72, 76]:
				if self.age >= age:
					physicalPenalty += 4
					mentalPenalty += 1
			if self.age >= 80:
				physicalPenalty += int((self.age-78)/2)*6
				mentalPenalty += int((self.age-78)/2)*2

			for attribute in ['wit', 'will']:
				self.traits['attributes'][attribute] += mentalBonus
				#age can't bring a stat over 40
				if self.traits['attributes'][attribute] > 40:
					self.traits['attributes'][attribute] = 40
				self.traits['attributes'][attribute] -= mentalPenalty
			for attribute in ['strength', 'deftness', 'speed', 'health']:
				self.traits['attributes'][attribute] -= physicalPenalty
				if self.traits['attributes'][attribute] <= 0:
					tools.die(self.name, 'has died from old age;', attribute, 'has dropped to', self.traits['attributes'][attribute])

			#off hand dexterity is affected by aging: 1.8.4
			self.offhand -= physicalPenalty

	def setNationality(self):
		nationality = self.nationality.lower()
		if nationality in ['american', 'british']:
			self.addSkill('english language', 2)
		elif nationality in ['puerto rican', 'mexican', 'south american']:
			self.addSkill('spanish language', 2)
		else:
			self.addSkill(nationality + ' language', 2)
		self.addSkill(nationality + ' culture')
		self.addSkill(nationality + ' history')

	def setItemList(self, itemName, items, itemKeys):
		total = 0
		for key in itemKeys:
			if not key in items:
				tools.die('Missing', key, 'in', itemName)
			value = items[key]
			if value.isdigit() or value[0] == '-' and value[1:].isdigit():
				value = int(value)
				total += value
				self.traits[itemName][key] = value
			else:
				tools.die(key.capitalize(), 'is not an integer:', value)

		return total

class Tools:
	def die(self, *args):
		self.warn(*args)
		sys.exit()

	def warn(self, *args):
		print(*args, file=sys.stderr)

	#Daredevils uses classical rounding, not even/odd rounding
	def round(self, value):
		return round(value+.001)

	def bcs(self, value):
		return int(value/5)

	def cst(self, value):
		return tools.round(value/2.5)

	def wda(self, value):
		bcs = self.bcs(value)
		return self.round(bcs/4)

	def title(self, text, level=1):
		level += 1
		prefix = '#' * level
		if level > 1:
			prefix = "\n" + prefix
		print(prefix, text.title())

	def datum(self, title, *cells):
		text = ''
		for item in cells:
			text += "\t" + str(item)
		if title != '':
			title = title.title() + ":"
		print(title, text, sep='')

	#return the skill as it appears in the skill list
	def getOverallSkill(self, skill):
		if skill in skillNames:
			return skill
		elif skill.endswith(tuple(specializedSkillNames.keys())):
			skillType = skill.split(' ')[-1]
			if skillType not in specializedSkillNames.keys():
				skillType = ' '.join(skill.split(' ')[-2:])
			return skillType
		else:
			tools.die('There is no skill', skill)

	#return the parts that make up the initial score for a skill
	def getSkillParts(self, skill):
		skillName = self.getOverallSkill(skill)
		if skillName in skillNames:
			return skillNames[skillName]
		else:
			return specializedSkillNames[skillName]

	#organize list of skills by governing talent or attribute
	def skillsByGovernor(self, skills, categories):
		groups = {}
		for skill in sorted(skills):
			parts = self.getSkillParts(skill)
			for part in parts:
				if part in categories:
					category = part
					break
			if category not in groups:
				groups[category] = [skill]
			else:
				groups[category].append(skill)
		return groups

tools = Tools()

# gets the next headline ('# ' or '## ') and then reads the data from that section
# lines that are not a headline and that do not contain key: value are ignored
# lines that begin with // are also ignored
def readNextSection(data):
	while len(data) > 0 and not data[0].startswith(('# ', '## ')):
		data.pop(0)
	if len(data) == 0:
		return None, None, None

	#read section parts
	sectionHeadline = data.pop(0)
	headlineMarker, name = sectionHeadline.split(' ', 1)
	level = len(headlineMarker)
	name = name.strip()

	values = {}
	quotes = []
	while len(data)> 0 and not data[0].startswith(('# ', '## ')):
		valueLine = data.pop(0)
		valueLine = valueLine.strip()
		if valueLine == '':
			continue
		if valueLine.startswith('//'):
			continue

		if name.lower() == 'quotes':
			quotes.append(valueLine)
			continue

		if ': ' not in valueLine:
			continue

		valueName, value = valueLine.split(': ', 1)
		valueName = valueName.lower()
		values[valueName] = value

	if quotes:
		return name, quotes, level

	return name, values, level

#show Skills by Governing Talent
if args.skills:
	for categorization, categories in [('talent', talentNames), ('attribute', attributeNames)]:
		tools.title('Skills by governing ' + categorization, 0)
		allSkills = list(skillNames.keys()) + list(specializedSkillNames.keys())
		groups = tools.skillsByGovernor(allSkills, categories)
		for category in categories:
			tools.title(category)
			for skill in groups[category]:
				print('-', skill.title())
		print()
	sys.exit()

#go through each file and create characters
for characterFile in args.character:
	characterHandle = open(characterFile)
	data = characterHandle.readlines()
	characterHandle.close()

	name, aspects, level = readNextSection(data)
	if level != 1:
		tools.die('Character name not found in ', characterFile)

	character = Daredevil(name, aspects)
	development = None

	while (True):
		section, values, level = readNextSection(data)
		if level == 1:
			tools.die('Multiple characters per file not implemented:', section)
		if section == None:
			break
		if section.lower() == 'attributes':
			character.initialAttributes(values)
		elif section.lower() == 'talents':
			character.initialTalents(values)
		elif section.lower() == 'development':
			development = values
		elif section.lower() == 'background':
			character.background = values
		elif section.lower() == 'quotes':
			character.quotes = values
		else:
			tools.die('Unknown section found', section)

	if development:
		character.initialDevelopment(development)
	else:
		tools.die(character, 'requires development')

	character.showAspects()

	character.showAttributes()
	character.showTalents()
	character.showCalculations()
	character.showSkills()
	character.showBackground()
	character.showQuotes()
