| 1 | #!/usr/bin/env python |
|---|
| 2 | |
|---|
| 3 | import re |
|---|
| 4 | import sys |
|---|
| 5 | import logging |
|---|
| 6 | |
|---|
| 7 | class Character(object): |
|---|
| 8 | |
|---|
| 9 | SECTION_RE = re.compile(r"^(?P<section>\w[\w\s]*):(?P<rest>.*)$") |
|---|
| 10 | RANK_RE = re.compile(r"^\s*(?P<rank>\d+)\s+\((?P<schools>.*)\)\s*$") |
|---|
| 11 | RING_RE = re.compile(r"^\s*(?P<ring>\S+)\s+(?P<ring_value>\d+)\s*(\(" \ |
|---|
| 12 | r"\s*(?P<stat1>\S+)\s+(?P<stat1_value>\d+)\s*;" \ |
|---|
| 13 | r"\s*(?P<stat2>\S+)\s+(?P<stat2_value>\d+)\s*" \ |
|---|
| 14 | r"\))?\s*$") |
|---|
| 15 | SKILL_RE = re.compile(r"^\s*(?P<skill>.+?)\s*(\((?P<emphases>.*)\))?\s*(?P<skill_value>\d+)\s*$") |
|---|
| 16 | ADVDIS_RE = re.compile(r"^\s*(?P<advdis>.+?)\s*\(\s*(?P<pt>-?\d+)\s*pt\s*;\s*(?P<xp>-?\d+)\s*xp\s*\)\s*$") |
|---|
| 17 | KATA_RE = re.compile(r"^\s*(?P<kata>.+?)\s*\(\s*(?P<xp>-?\d+)\s*xp\s*\)\s*$") |
|---|
| 18 | |
|---|
| 19 | def __init__(self): |
|---|
| 20 | self.name = None |
|---|
| 21 | self.clan = None |
|---|
| 22 | |
|---|
| 23 | self.schools = {} # name -> rank |
|---|
| 24 | self.rings = {} # name -> rank |
|---|
| 25 | self.stats = {} # name -> rank |
|---|
| 26 | self.skills = {} # name -> (rank, [emphases]) |
|---|
| 27 | self.advantages = {} # name -> (points, xp) |
|---|
| 28 | self.kata = {} # name -> xp |
|---|
| 29 | |
|---|
| 30 | def parse_source(self,sText): |
|---|
| 31 | sSection = None |
|---|
| 32 | aSectionsLeft = ["name","clan","rank","rings","skills","adv and dis","kata"] |
|---|
| 33 | aOptionalSectionsLeft = ["notes","spells"] |
|---|
| 34 | self.parse_error = None # (line #, line) |
|---|
| 35 | |
|---|
| 36 | for iLine, sLine in enumerate(sText.split("\n")): |
|---|
| 37 | self.parse_error = (iLine, sLine) |
|---|
| 38 | |
|---|
| 39 | oM = self.SECTION_RE.match(sLine) |
|---|
| 40 | if oM: |
|---|
| 41 | sSection = oM.group("section").lower().strip() |
|---|
| 42 | if sSection in aSectionsLeft: |
|---|
| 43 | aSectionsLeft.remove(sSection) |
|---|
| 44 | elif sSection in aOptionalSectionsLeft: |
|---|
| 45 | aOptionalSectionsLeft.remove(sSection) |
|---|
| 46 | else: |
|---|
| 47 | raise RuntimeError("Unknown or duplicate section %s." % (sSection,)) |
|---|
| 48 | |
|---|
| 49 | if sSection == "name": |
|---|
| 50 | self.name = oM.group("rest").strip() |
|---|
| 51 | elif sSection == "clan": |
|---|
| 52 | self.clan = oM.group("rest").strip() |
|---|
| 53 | elif sSection == "rank": |
|---|
| 54 | oR = self.RANK_RE.match(oM.group("rest")) |
|---|
| 55 | for sItem in oR.group("schools").split(";"): |
|---|
| 56 | sItem = sItem.strip() |
|---|
| 57 | aParts = sItem.split() |
|---|
| 58 | sName = " ".join(aParts[:-1]) |
|---|
| 59 | iRank = int(aParts[-1]) |
|---|
| 60 | assert sName not in self.schools |
|---|
| 61 | self.schools[sName] = iRank |
|---|
| 62 | assert int(oR.group("rank")) == sum(self.schools.values()) |
|---|
| 63 | |
|---|
| 64 | elif sSection == "rings" and sLine.strip(): |
|---|
| 65 | oR = self.RING_RE.match(sLine) |
|---|
| 66 | sRing, iRing = oR.group("ring").lower(), int(oR.group("ring_value")) |
|---|
| 67 | |
|---|
| 68 | assert sRing not in self.rings |
|---|
| 69 | self.rings[sRing] = iRing |
|---|
| 70 | |
|---|
| 71 | if sRing != "void": |
|---|
| 72 | sStat1, iStat1 = oR.group("stat1").lower(), int(oR.group("stat1_value")) |
|---|
| 73 | sStat2, iStat2 = oR.group("stat2").lower(), int(oR.group("stat2_value")) |
|---|
| 74 | |
|---|
| 75 | assert sStat1 not in self.stats |
|---|
| 76 | self.stats[sStat1] = iStat1 |
|---|
| 77 | |
|---|
| 78 | assert sStat2 not in self.stats |
|---|
| 79 | self.stats[sStat2] = iStat2 |
|---|
| 80 | |
|---|
| 81 | elif sSection == "skills" and sLine.strip(): |
|---|
| 82 | oR = self.SKILL_RE.match(sLine) |
|---|
| 83 | |
|---|
| 84 | sSkill, iSkill = oR.group("skill").lower(), int(oR.group("skill_value")) |
|---|
| 85 | sEmphases = oR.group("emphases") |
|---|
| 86 | if sEmphases is None: |
|---|
| 87 | aEmphases = [] |
|---|
| 88 | else: |
|---|
| 89 | aEmphases = [x.strip().lower() for x in sEmphases.split(",")] |
|---|
| 90 | |
|---|
| 91 | assert sSkill not in self.skills |
|---|
| 92 | self.skills[sSkill] = (iSkill, aEmphases) |
|---|
| 93 | |
|---|
| 94 | elif sSection == "adv and dis" and sLine.strip(): |
|---|
| 95 | oR = self.ADVDIS_RE.match(sLine) |
|---|
| 96 | sAdvDis, iPt, iXp = oR.group("advdis").lower().strip(), int(oR.group("pt")), int(oR.group("xp")) |
|---|
| 97 | |
|---|
| 98 | assert sAdvDis not in self.advantages |
|---|
| 99 | self.advantages[sAdvDis] = (iPt, iXp) |
|---|
| 100 | |
|---|
| 101 | elif sSection == "kata" and sLine.strip(): |
|---|
| 102 | oR = self.KATA_RE.match(sLine) |
|---|
| 103 | sKata, iXp = oR.group("kata").lower().strip(), int(oR.group("xp")) |
|---|
| 104 | |
|---|
| 105 | assert sKata not in self.kata |
|---|
| 106 | self.kata[sKata] = iXp |
|---|
| 107 | |
|---|
| 108 | elif sSection == "spells" and sLine.strip(): |
|---|
| 109 | pass |
|---|
| 110 | |
|---|
| 111 | elif sSection == "notes" and sLine.strip(): |
|---|
| 112 | pass |
|---|
| 113 | |
|---|
| 114 | else: |
|---|
| 115 | assert not sLine.strip() |
|---|
| 116 | |
|---|
| 117 | assert not aSectionsLeft |
|---|
| 118 | del self.parse_error |
|---|
| 119 | |
|---|
| 120 | def calculate_insight(self): |
|---|
| 121 | extra_insight = { # name -> [(rank, insight bonus)] |
|---|
| 122 | 'artisan': [(5,2),(10,2)], |
|---|
| 123 | 'games': [(5,2),(10,2)], |
|---|
| 124 | 'instruction': [(5,2),(10,2)], |
|---|
| 125 | 'lore': [(5,2),(10,2)], |
|---|
| 126 | 'meditation': [(7,2)], |
|---|
| 127 | 'storytelling': [(5,2),(10,2)], |
|---|
| 128 | 'theology': [(5,2),(7,2),(10,10)], |
|---|
| 129 | 'craft': [(5,2),(10,2)], |
|---|
| 130 | } |
|---|
| 131 | |
|---|
| 132 | def extra_skill_insight(skill,rank): |
|---|
| 133 | skill = skill.split(':')[0].strip() |
|---|
| 134 | insight = 0 |
|---|
| 135 | for bonus_rank, bonus in extra_insight.get(skill,[]): |
|---|
| 136 | if rank >= bonus_rank: |
|---|
| 137 | insight += bonus |
|---|
| 138 | return insight |
|---|
| 139 | |
|---|
| 140 | # Rings |
|---|
| 141 | insight = 10 * sum(self.rings.values()) |
|---|
| 142 | |
|---|
| 143 | # Skills |
|---|
| 144 | insight += sum([rank for rank, emphases in self.skills.values()]) |
|---|
| 145 | insight += 2 * len([rank for rank, emphases in self.skills.values() if rank >= 5]) |
|---|
| 146 | insight += sum([extra_skill_insight(skill,rank) for skill, (rank, emphases) in self.skills.items()]) |
|---|
| 147 | |
|---|
| 148 | return insight |
|---|
| 149 | |
|---|
| 150 | insight = property(fget=calculate_insight) |
|---|
| 151 | |
|---|
| 152 | def calculate_xp(self): |
|---|
| 153 | def stat_xp(skill): |
|---|
| 154 | return 4 * sum([x for x in range(2+1,skill+1)]) |
|---|
| 155 | |
|---|
| 156 | def skill_xp(skill): |
|---|
| 157 | return sum([x for x in range(1,skill+1)]) |
|---|
| 158 | |
|---|
| 159 | def emphases_xp(num): |
|---|
| 160 | return 2 * sum([x for x in range(1,num+1)]) |
|---|
| 161 | |
|---|
| 162 | xp_total = 0 |
|---|
| 163 | xp_breakdown = [] |
|---|
| 164 | |
|---|
| 165 | # Stats |
|---|
| 166 | xp = sum([stat_xp(rank) for rank in self.stats.values()]) + stat_xp(self.rings['void']) |
|---|
| 167 | xp_total += xp |
|---|
| 168 | xp_breakdown.append(("Stats",xp)) |
|---|
| 169 | |
|---|
| 170 | # Skills |
|---|
| 171 | xp = sum([skill_xp(skill) + emphases_xp(len(emphases)) |
|---|
| 172 | for skill, emphases in self.skills.values()]) |
|---|
| 173 | xp_total += xp |
|---|
| 174 | xp_breakdown.append(("Skills",xp)) |
|---|
| 175 | |
|---|
| 176 | # Adv and Dis |
|---|
| 177 | xp = sum([xp for pt, xp in self.advantages.values()]) |
|---|
| 178 | xp_total += xp |
|---|
| 179 | xp_breakdown.append(("Adv/Dis",xp)) |
|---|
| 180 | |
|---|
| 181 | # Kata |
|---|
| 182 | xp = sum([xp for xp in self.kata.values()]) |
|---|
| 183 | xp_total += xp |
|---|
| 184 | xp_breakdown.append(("Kata",xp)) |
|---|
| 185 | |
|---|
| 186 | return xp_total, xp_breakdown |
|---|
| 187 | |
|---|
| 188 | xp = property(fget=lambda self: self.calculate_xp()[0]) |
|---|
| 189 | xp_breakdown = property(fget=lambda self: self.calculate_xp()[1]) |
|---|
| 190 | |
|---|
| 191 | def calculate_rank(self): |
|---|
| 192 | insight = self.insight |
|---|
| 193 | rank = min((max(0, insight - 125) // 25) + 1, 8) |
|---|
| 194 | return rank |
|---|
| 195 | |
|---|
| 196 | rank = property(fget=calculate_rank) |
|---|
| 197 | |
|---|
| 198 | def process_file(sFile): |
|---|
| 199 | oFile = file(sFile,"rU") |
|---|
| 200 | try: |
|---|
| 201 | oC = Character() |
|---|
| 202 | oC.parse_source(oFile.read()) |
|---|
| 203 | except StandardError, e: |
|---|
| 204 | iLine, sLine = oC.parse_error |
|---|
| 205 | logging.error("Exception raised while parsing character file '%s' at line %d." % (sFile, iLine)) |
|---|
| 206 | logging.error("-> %s" % (sLine,)) |
|---|
| 207 | raise |
|---|
| 208 | finally: |
|---|
| 209 | oFile.close() |
|---|
| 210 | |
|---|
| 211 | print "Name:", oC.name |
|---|
| 212 | print " XP - Total:", oC.xp |
|---|
| 213 | print " -", ", ".join(["%s: %d" % (section, xp) for section, xp in oC.xp_breakdown]) |
|---|
| 214 | print " Insight:", oC.insight |
|---|
| 215 | print " Rank:", oC.rank |
|---|
| 216 | |
|---|
| 217 | def main(aArgs): |
|---|
| 218 | for sFile in aArgs: |
|---|
| 219 | process_file(sFile) |
|---|
| 220 | |
|---|
| 221 | if __name__ == "__main__": |
|---|
| 222 | sys.exit(main(sys.argv[1:])) |
|---|