diff --git a/source/tools/templatesanalyzer/Readme.md b/source/tools/templatesanalyzer/Readme.md new file mode 100644 index 0000000000..9a28e4b9ed --- /dev/null +++ b/source/tools/templatesanalyzer/Readme.md @@ -0,0 +1,69 @@ +## Template Analyzer + +This python tool has been written by wraitii and updated to 0ad A25 by hyiltiz. +Its purpose is to help with unit and civ balancing by allowing quick comparison +between important template data. + +Run it using `python unitTables.py` or `pypy unitTables.py` (if you have pypy +installed). The output will be located in an HTML file called +`unit_summary_table.html` in this folder. + +The script generates 3 informative tables: +- A comparison table of generic templates; +- A comparison table of civilization units (it shows the differences with the + generic templates); +- A comparison of civilization rosters. + +You can customize the script by changing the units to include (loading all units +might make it slightly unreadable). To change this, change the +`LoadTemplatesIfParent` variable. You can also consider only some civilizations. +You may also filter some templates based on their name, if you want to remove +specific templates. By default it loads all citizen soldiers and all champions, +and ignores non-interesting units for the comparison/efficienicy table (2nd +table). + +The HTML page comes with a JavaScript extension that allows to filter and sort +in-place, to help with comparisons. You can disable this by disabling javascript +or by changing the `AddSortingOverlay` parameter in the script. This JS +extension, called TableFilter, is released under the MIT license. The version +used can be found at https://github.com/koalyptus/TableFilter/ + +All contents of this folder are under the MIT License. + + +## Contributing + +The script intentionally only relies on Python 3 Standard Library to avoid +installing 3rd party libraries as dependencies. However, you might want to +install a few packages to make hacking around easier. + +### Debugging +IPython can be used as a good REPL and to easily insert a debug break point. +Install it with: + + pip3 install ipython + +then to insert a break point, simply insert the following line to the script where +you want execution to pause: + + import IPython; IPython.embed() + +Then, run the script as normal. Once you hit the breakpoint, you can use IPython +as a normal REPL. A useful IPython magic is `whos`, which shows all local +variables. + +### Exploration +To understand the internal logic, generating a function call dependency graph +can be helpful by providing a quick visual overview. Use the following code to +create the function call dependency graph. It is dynamic, and allows quickly +getting familiarized with the analyzer. Note that you'll need `dot` engine provided +by the `graphviz` package. You can install `graphviz` using your system's package manager. + + pip3 install pyan3==1.1.1 + python3 -m pyan unitTables.py --uses --no-defines --colored --grouped --annotated --html > fundeps.html + +Alternatively, only create the `.dot` file using the following line, and render it with an online renderer like http://viz-js.com/ + + python3 -m pyan unitTables.py --uses --no-defines --colored --grouped --annotated --dot > fundeps.dot + +Enjoy! diff --git a/source/tools/templatesanalyzer/Readme.txt b/source/tools/templatesanalyzer/Readme.txt deleted file mode 100644 index 7027aa5dd3..0000000000 --- a/source/tools/templatesanalyzer/Readme.txt +++ /dev/null @@ -1,29 +0,0 @@ -Template Analyzer. - -This python tool has been written by wraitii. Its purpose is to help with unit and civ balancing by allowing quick comparison between important template data. - -Run it using "python unitTables.py" or "pypy unitTables.py" if you have pypy installed. - -The output will be located in an HTML file called "unit_summary_table.html" in this folder. - - -The script gives 3 informations: --A comparison table of generic templates. --A comparison table of civilization units (it shows the differences with the generic templates) --A comparison of civilization rosters. - -The script can be customized to change the units that are considered, since loading all units make sit slightly unreadable. -By default it loads all citizen soldiers and all champions. - -To change this, change the "LoadTemplatesIfParent" variable. -You can also consider only some civilizations. -You may also filter some templates based on their name, if you want to remove specific templates. - - -The HTML page comes with a JS extension that allows to filter and sort in-place, to help with comparisons. You can disable this by disabling javascript or by changing the "AddSortingOverlay" parameter in the script. - -This extension, called TableFilter, is released under the MIT license. The version I used was the one found at https://github.com/koalyptus/TableFilter/ - -All contents of this folder are under the MIT License. - -Enjoy! diff --git a/source/tools/templatesanalyzer/unitTables.py b/source/tools/templatesanalyzer/unitTables.py index 922ec95c93..d764d663cf 100644 --- a/source/tools/templatesanalyzer/unitTables.py +++ b/source/tools/templatesanalyzer/unitTables.py @@ -1,17 +1,18 @@ #!/usr/bin/env python3 - -# Copyright (C) 2015 Wildfire Games. -# +# -*- mode: python-mode; python-indent-offset: 4; -*- +# +# Copyright (C) 2022 Wildfire Games. +# # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: -# +# # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. -# +# # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -20,549 +21,1053 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. + + import xml.etree.ElementTree as ET import os import glob -AttackTypes = ["Hack","Pierce","Crush"] +# TODO: Add other damage types. +AttackTypes = ["Hack", "Pierce", "Crush"] Resources = ["food", "wood", "stone", "metal"] # Generic templates to load # The way this works is it tries all generic templates # But only loads those who have one of the following parents # EG adding "template_unit.xml" will load all units. -LoadTemplatesIfParent = ["template_unit_infantry.xml", "template_unit_cavalry.xml", "template_unit_champion.xml", "template_unit_hero.xml"] +LoadTemplatesIfParent = [ + "template_unit_infantry.xml", + "template_unit_cavalry.xml", + "template_unit_champion.xml", + "template_unit_hero.xml", +] # Those describe Civs to analyze. -# The script will load all entities that derive (to the nth degree) from one of the above templates. -Civs = ["athen", "brit", "cart", "gaul", "iber", "kush", "mace", "maur", "pers", "ptol", "rome", "sele", "spart"] +# The script will load all entities that derive (to the nth degree) from one of +# the above templates. +Civs = [ + "athen", + "brit", + "cart", + "gaul", + "iber", + "kush", + "mace", + "maur", + "pers", + "ptol", + "rome", + "sele", + "spart", + # "gaia", +] # Remote Civ templates with those strings in their name. FilterOut = ["marian", "thureophoros", "thorakites", "kardakes"] +# In the Civilization specific units table, do you want to only show the units +# that are different from the generic templates? +showChangedOnly = True + # Sorting parameters for the "roster variety" table ComparativeSortByCav = True ComparativeSortByChamp = True -SortTypes = ["Support", "Pike", "Spear", "Sword", "Archer", "Javelin", "Sling", "Elephant"] # Classes +ClassesUsedForSort = [ + "Support", + "Pike", + "Spear", + "Sword", + "Archer", + "Javelin", + "Sling", + "Elephant", +] -# Disable if you want the more compact basic data. Enable to allow filtering and sorting in-place. +# Disable if you want the more compact basic data. Enable to allow filtering and +# sorting in-place. AddSortingOverlay = True -# This is the path to the /templates/ folder to consider. Change this for mod support. -basePath = os.path.realpath(__file__).replace("unitTables.py","") + "../../../binaries/data/mods/public/simulation/templates/" +# This is the path to the /templates/ folder to consider. Change this for mod +# support. +basePath = ( + os.path.realpath(__file__).replace("unitTables.py", "") + + "../../../binaries/data/mods/public/simulation/templates/" +) # For performance purposes, cache opened templates files. globalTemplatesList = {} + +def showChild(root): + """root is of ElementTree.getroot() type""" + # Not used; use it for debugging + print("---------------- Children: ----------------") + [print(child.tag, child.attrib) for child in root] + + print("\n-------------- Neighbours: --------------") + [print(neighbor.attrib) for neighbor in root.iter("neighbor")] + + def htbout(file, balise, value): - file.write("<" + balise + ">" + value + "\n" ) + file.write("<" + balise + ">" + value + "\n") + + def htout(file, value): - file.write("

" + value + "

\n" ) + file.write("

" + value + "

\n") + def fastParse(templateName): - if templateName in globalTemplatesList: - return globalTemplatesList[templateName] - globalTemplatesList[templateName] = ET.parse(templateName) - return globalTemplatesList[templateName] + """Run ET.parse() with memoising in a global table.""" + if templateName in globalTemplatesList: + return globalTemplatesList[templateName] + globalTemplatesList[templateName] = ET.parse(templateName) + return globalTemplatesList[templateName] + # This function checks that a template has the given parent. def hasParentTemplate(UnitName, parentName): - Template = fastParse(UnitName) + Template = fastParse(UnitName) - found = False - Name = UnitName - while found != True and Template.getroot().get("parent") != None: - Name = Template.getroot().get("parent") + ".xml" - if Name == parentName: - return True - Template = ET.parse(Name) + found = False + Name = UnitName + + # parent_str = 'parent ' + while found != True and Template.getroot().get("parent") != None: + longName = Template.getroot().get("parent") + ".xml" + # 0ad started using unit class/category prefixed to the unit name + # separated by |, known as mixins since A25 (rP25223) + # + # We strip these categories for now. The | syntax + # gives a unit its "category" (like merc_cav, merc_inf, hoplite, + # builder, shrine, civ/athen). This can be used later for + # classification + + Name = longName.split("|")[-1] + + mixins = {x.replace('civ/', '') for x in longName.split("|")[0:-1]} + civ = set(Civs).intersection(mixins) + if len(civ) > 0: + # mixin category contains a civ name + # we honor mixin civ hierarchy + # This assumes a unit only belongs to a single civ parent + unit_civ = list(civ)[0] + '.xml' + # unit_civ is not used in this function for now + + if Name == parentName: + return True + + Template = ET.parse(Name) + + # parent_str += 'parent ' + + return False - return False def NumericStatProcess(unitValue, templateValue): - if not "op" in templateValue.attrib: - return float(templateValue.text) - if (templateValue.attrib["op"] == "add"): - unitValue += float(templateValue.text) - elif (templateValue.attrib["op"] == "sub"): - unitValue -= float(templateValue.text) - elif (templateValue.attrib["op"] == "mul"): - unitValue *= float(templateValue.text) - elif (templateValue.attrib["op"] == "div"): - unitValue /= float(templateValue.text) - return unitValue + val = float(templateValue.text) + if not "op" in templateValue.attrib: + return val + if templateValue.attrib["op"] == "add": + unitValue += val + elif templateValue.attrib["op"] == "sub": + unitValue -= val + elif templateValue.attrib["op"] == "mul": + unitValue *= val + elif templateValue.attrib["op"] == "mul_round": + unitValue = round(unitValue * val) + elif templateValue.attrib["op"] == "div": + unitValue /= val + return unitValue -# This function parses the entity values manually. -def CalcUnit(UnitName, existingUnit = None): - unit = { 'HP' : "0", "BuildTime" : "0", "Cost" : { 'food' : "0", "wood" : "0", "stone" : "0", "metal" : "0", "population" : "0"}, - 'Attack' : { "Melee" : { "Hack" : 0, "Pierce" : 0, "Crush" : 0 }, "Ranged" : { "Hack" : 0, "Pierce" : 0, "Crush" : 0 } }, - 'RepeatRate' : {"Melee" : "0", "Ranged" : "0"},'PrepRate' : {"Melee" : "0", "Ranged" : "0"}, "Armour" : { "Hack" : 0, "Pierce" : 0, "Crush" : 0}, - "Ranged" : False, "Classes" : [], "AttackBonuses" : {}, "Restricted" : [], "WalkSpeed" : 0, "Range" : 0, "Spread" : 0, - "Civ" : None } - - if (existingUnit != None): - unit = existingUnit +def CalcUnit(UnitName, existingUnit=None): + """Parse the entity values recursively through fastParse().""" + unit = { + "HP": "0", + "BuildTime": "0", + "Cost": { + "food": "0", + "wood": "0", + "stone": "0", + "metal": "0", + "population": "0", + }, + "Attack": { + "Melee": {"Hack": 0, "Pierce": 0, "Crush": 0}, + "Ranged": {"Hack": 0, "Pierce": 0, "Crush": 0}, + }, + "RepeatRate": {"Melee": "0", "Ranged": "0"}, + "PrepRate": {"Melee": "0", "Ranged": "0"}, + "Resistance": {"Hack": 0, "Pierce": 0, "Crush": 0}, + "Ranged": False, + "Classes": [], + "AttackBonuses": {}, + "Restricted": [], + "WalkSpeed": 0, + "Range": 0, + "Spread": 0, + "Civ": None, + } - Template = fastParse(UnitName) - - # Recursively get data from our parent which we'll override. - if (Template.getroot().get("parent") != None): - unit = CalcUnit(Template.getroot().get("parent") + ".xml", unit) - unit["Parent"] = Template.getroot().get("parent") + ".xml" + if existingUnit != None: + unit = existingUnit - if (Template.find("./Identity/Civ") != None): - unit['Civ'] = Template.find("./Identity/Civ").text + Template = fastParse(UnitName) - if (Template.find("./Health/Max") != None): - unit['HP'] = NumericStatProcess(unit['HP'], Template.find("./Health/Max")) + # Recursively get data from our parent which we'll override. + unit_civ = None + if Template.getroot().get("parent") != None: + # 0ad started using unit class/category prefixed to the unit name + # separated by |, known as mixins since A25 (rP25223) + # We strip these categories for now + # This can be used later for classification + longName = Template.getroot().get("parent") + Name = longName.split("|")[-1] + ".xml" - if (Template.find("./Cost/BuildTime") != None): - unit['BuildTime'] = NumericStatProcess(unit['BuildTime'], Template.find("./Cost/BuildTime")) - - if (Template.find("./Cost/Resources") != None): - for type in list(Template.find("./Cost/Resources")): - unit['Cost'][type.tag] = NumericStatProcess(unit['Cost'][type.tag], type) - - if (Template.find("./Cost/Population") != None): - unit['Cost']["population"] = NumericStatProcess(unit['Cost']["population"], Template.find("./Cost/Population")) - - if (Template.find("./Attack/Melee") != None): - if (Template.find("./Attack/Melee/RepeatTime") != None): - unit['RepeatRate']["Melee"] = NumericStatProcess(unit['RepeatRate']["Melee"], Template.find("./Attack/Melee/RepeatTime")) - if (Template.find("./Attack/Melee/PrepareTime") != None): - unit['PrepRate']["Melee"] = NumericStatProcess(unit['PrepRate']["Melee"], Template.find("./Attack/Melee/PrepareTime")) - for atttype in AttackTypes: - if (Template.find("./Attack/Melee/"+atttype) != None): - unit['Attack']['Melee'][atttype] = NumericStatProcess(unit['Attack']['Melee'][atttype], Template.find("./Attack/Melee/"+atttype)) - if (Template.find("./Attack/Melee/Bonuses") != None): - for Bonus in Template.find("./Attack/Melee/Bonuses"): - Against = [] - CivAg = [] - if (Bonus.find("Classes") != None and Bonus.find("Classes").text != None): - Against = Bonus.find("Classes").text.split(" ") - if (Bonus.find("Civ") != None and Bonus.find("Civ").text != None): - CivAg = Bonus.find("Civ").text.split(" ") - Val = float(Bonus.find("Multiplier").text) - unit["AttackBonuses"][Bonus.tag] = {"Classes" : Against, "Civs" : CivAg, "Multiplier" : Val} - if (Template.find("./Attack/Melee/RestrictedClasses") != None): - newClasses = Template.find("./Attack/Melee/RestrictedClasses").text.split(" ") - for elem in newClasses: - if (elem.find("-") != -1): - newClasses.pop(newClasses.index(elem)) - if elem in unit["Restricted"]: - unit["Restricted"].pop(newClasses.index(elem)) - unit["Restricted"] += newClasses + mixins = {x.replace('civ/', '') for x in longName.split("|")[0:-1]} + civ = set(Civs).intersection(mixins) + if len(civ) > 0: + # mixin category contains a civ name + # we honor mixin civ hierarchy + # This assumes a unit only belongs to a single civ parent + unit_civ = list(civ)[0] - if (Template.find("./Attack/Ranged") != None): - unit['Ranged'] = True - if (Template.find("./Attack/Ranged/MaxRange") != None): - unit['Range'] = NumericStatProcess(unit['Range'], Template.find("./Attack/Ranged/MaxRange")) - if (Template.find("./Attack/Ranged/Spread") != None): - unit['Spread'] = NumericStatProcess(unit['Spread'], Template.find("./Attack/Ranged/Spread")) - if (Template.find("./Attack/Ranged/RepeatTime") != None): - unit['RepeatRate']["Ranged"] = NumericStatProcess(unit['RepeatRate']["Ranged"], Template.find("./Attack/Ranged/RepeatTime")) - if (Template.find("./Attack/Ranged/PrepareTime") != None): - unit['PrepRate']["Ranged"] = NumericStatProcess(unit['PrepRate']["Ranged"], Template.find("./Attack/Ranged/PrepareTime")) - for atttype in AttackTypes: - if (Template.find("./Attack/Ranged/"+atttype) != None): - unit['Attack']['Ranged'][atttype] = NumericStatProcess(unit['Attack']['Ranged'][atttype], Template.find("./Attack/Ranged/"+atttype)) - if (Template.find("./Attack/Ranged/Bonuses") != None): - for Bonus in Template.find("./Attack/Ranged/Bonuses"): - Against = [] - CivAg = [] - if (Bonus.find("Classes") != None and Bonus.find("Classes").text != None): - Against = Bonus.find("Classes").text.split(" ") - if (Bonus.find("Civ") != None and Bonus.find("Civ").text != None): - CivAg = Bonus.find("Civ").text.split(" ") - Val = float(Bonus.find("Multiplier").text) - unit["AttackBonuses"][Bonus.tag] = {"Classes" : Against, "Civs" : CivAg, "Multiplier" : Val} - if (Template.find("./Attack/Melee/RestrictedClasses") != None): - newClasses = Template.find("./Attack/Melee/RestrictedClasses").text.split(" ") - for elem in newClasses: - if (elem.find("-") != -1): - newClasses.pop(newClasses.index(elem)) - if elem in unit["Restricted"]: - unit["Restricted"].pop(newClasses.index(elem)) - unit["Restricted"] += newClasses + unit = CalcUnit(Name, unit) + unit["Parent"] = Name - if (Template.find("./Armour") != None): - for atttype in AttackTypes: - if (Template.find("./Armour/"+atttype) != None): - unit['Armour'][atttype] = NumericStatProcess(unit['Armour'][atttype], Template.find("./Armour/"+atttype)) - - if (Template.find("./UnitMotion") != None): - if (Template.find("./UnitMotion/WalkSpeed") != None): - unit['WalkSpeed'] = NumericStatProcess(unit['WalkSpeed'], Template.find("./UnitMotion/WalkSpeed")) - - if (Template.find("./Identity/VisibleClasses") != None): - newClasses = Template.find("./Identity/VisibleClasses").text.split(" ") - for elem in newClasses: - if (elem.find("-") != -1): - newClasses.pop(newClasses.index(elem)) - if elem in unit["Classes"]: - unit["Classes"].pop(newClasses.index(elem)) - unit["Classes"] += newClasses - - if (Template.find("./Identity/Classes") != None): - newClasses = Template.find("./Identity/Classes").text.split(" ") - for elem in newClasses: - if (elem.find("-") != -1): - newClasses.pop(newClasses.index(elem)) - if elem in unit["Classes"]: - unit["Classes"].pop(newClasses.index(elem)) - unit["Classes"] += newClasses + if unit_civ: + unit["Civ"] = unit_civ + elif Template.find("./Identity/Civ") != None: + unit["Civ"] = Template.find("./Identity/Civ").text - return unit + + if Template.find("./Health/Max") != None: + unit["HP"] = NumericStatProcess( + unit["HP"], Template.find("./Health/Max")) + + if Template.find("./Cost/BuildTime") != None: + unit["BuildTime"] = NumericStatProcess( + unit["BuildTime"], Template.find("./Cost/BuildTime") + ) + + if Template.find("./Cost/Resources") != None: + for type in list(Template.find("./Cost/Resources")): + unit["Cost"][type.tag] = NumericStatProcess( + unit["Cost"][type.tag], type) + + if Template.find("./Cost/Population") != None: + unit["Cost"]["population"] = NumericStatProcess( + unit["Cost"]["population"], Template.find("./Cost/Population") + ) + + if Template.find("./Attack/Melee") != None: + if Template.find("./Attack/Melee/RepeatTime") != None: + unit["RepeatRate"]["Melee"] = NumericStatProcess( + unit["RepeatRate"]["Melee"], + Template.find("./Attack/Melee/RepeatTime") + ) + if Template.find("./Attack/Melee/PrepareTime") != None: + unit["PrepRate"]["Melee"] = NumericStatProcess( + unit["PrepRate"]["Melee"], + Template.find("./Attack/Melee/PrepareTime") + ) + for atttype in AttackTypes: + if Template.find("./Attack/Melee/Damage/" + atttype) != None: + unit["Attack"]["Melee"][atttype] = NumericStatProcess( + unit["Attack"]["Melee"][atttype], + Template.find("./Attack/Melee/Damage/" + atttype), + ) + if Template.find("./Attack/Melee/Bonuses") != None: + for Bonus in Template.find("./Attack/Melee/Bonuses"): + Against = [] + CivAg = [] + if Bonus.find("Classes") != None \ + and Bonus.find("Classes").text != None: + Against = Bonus.find("Classes").text.split(" ") + if Bonus.find("Civ") != None and Bonus.find("Civ").text != None: + CivAg = Bonus.find("Civ").text.split(" ") + Val = float(Bonus.find("Multiplier").text) + unit["AttackBonuses"][Bonus.tag] = { + "Classes": Against, + "Civs": CivAg, + "Multiplier": Val, + } + if Template.find("./Attack/Melee/RestrictedClasses") != None: + newClasses = Template.find("./Attack/Melee/RestrictedClasses")\ + .text.split(" ") + for elem in newClasses: + if elem.find("-") != -1: + newClasses.pop(newClasses.index(elem)) + if elem in unit["Restricted"]: + unit["Restricted"].pop(newClasses.index(elem)) + unit["Restricted"] += newClasses + + if Template.find("./Attack/Ranged") != None: + unit["Ranged"] = True + if Template.find("./Attack/Ranged/MaxRange") != None: + unit["Range"] = NumericStatProcess( + unit["Range"], Template.find("./Attack/Ranged/MaxRange") + ) + if Template.find("./Attack/Ranged/Spread") != None: + unit["Spread"] = NumericStatProcess( + unit["Spread"], Template.find("./Attack/Ranged/Spread") + ) + if Template.find("./Attack/Ranged/RepeatTime") != None: + unit["RepeatRate"]["Ranged"] = NumericStatProcess( + unit["RepeatRate"]["Ranged"], + Template.find("./Attack/Ranged/RepeatTime"), + ) + if Template.find("./Attack/Ranged/PrepareTime") != None: + unit["PrepRate"]["Ranged"] = NumericStatProcess( + unit["PrepRate"]["Ranged"], + Template.find("./Attack/Ranged/PrepareTime") + ) + for atttype in AttackTypes: + if Template.find("./Attack/Ranged/Damage/" + atttype) != None: + unit["Attack"]["Ranged"][atttype] = NumericStatProcess( + unit["Attack"]["Ranged"][atttype], + Template.find("./Attack/Ranged/Damage/" + atttype), + ) + if Template.find("./Attack/Ranged/Bonuses") != None: + for Bonus in Template.find("./Attack/Ranged/Bonuses"): + Against = [] + CivAg = [] + if Bonus.find("Classes") != None \ + and Bonus.find("Classes").text != None: + Against = Bonus.find("Classes").text.split(" ") + if Bonus.find("Civ") != None and Bonus.find("Civ").text != None: + CivAg = Bonus.find("Civ").text.split(" ") + Val = float(Bonus.find("Multiplier").text) + unit["AttackBonuses"][Bonus.tag] = { + "Classes": Against, + "Civs": CivAg, + "Multiplier": Val, + } + if Template.find("./Attack/Melee/RestrictedClasses") != None: + newClasses = Template.find("./Attack/Melee/RestrictedClasses")\ + .text.split(" ") + for elem in newClasses: + if elem.find("-") != -1: + newClasses.pop(newClasses.index(elem)) + if elem in unit["Restricted"]: + unit["Restricted"].pop(newClasses.index(elem)) + unit["Restricted"] += newClasses + + if Template.find("Resistance") != None: + # Resistance lives insdie a new node Entity, e.g. + # list(ET.parse('template_unit_cavalry.xml').find('Resistance/Entity/Damage')) + + for atttype in AttackTypes: + extracted_resistance = Template.find( + "./Resistance/Entity/Damage/" + atttype + ) + if extracted_resistance != None: + unit["Resistance"][atttype] = NumericStatProcess( + unit["Resistance"][atttype], extracted_resistance + ) + + if Template.find("./UnitMotion") != None: + if Template.find("./UnitMotion/WalkSpeed") != None: + unit["WalkSpeed"] = NumericStatProcess( + unit["WalkSpeed"], Template.find("./UnitMotion/WalkSpeed") + ) + + if Template.find("./Identity/VisibleClasses") != None: + newClasses = Template.find("./Identity/VisibleClasses").text.split(" ") + for elem in newClasses: + if elem.find("-") != -1: + newClasses.pop(newClasses.index(elem)) + if elem in unit["Classes"]: + unit["Classes"].pop(newClasses.index(elem)) + unit["Classes"] += newClasses + + if Template.find("./Identity/Classes") != None: + newClasses = Template.find("./Identity/Classes").text.split(" ") + for elem in newClasses: + if elem.find("-") != -1: + newClasses.pop(newClasses.index(elem)) + if elem in unit["Classes"]: + unit["Classes"].pop(newClasses.index(elem)) + unit["Classes"] += newClasses + + return unit + def WriteUnit(Name, UnitDict): - ret = "" + ret = "" + ret += '' + Name + "" + ret += "" + str(int(UnitDict["HP"])) + "" + ret += "" + str("%.0f" % float(UnitDict["BuildTime"])) + "" + ret += "" + str("%.1f" % float(UnitDict["WalkSpeed"])) + "" - ret += "" + Name + "" + for atype in AttackTypes: + PercentValue = 1.0 - (0.9 ** float(UnitDict["Resistance"][atype])) + ret += ( + "" + + str("%.0f" % float(UnitDict["Resistance"][atype])) + + " / " + + str("%.0f" % (PercentValue * 100.0)) + + "%" + ) - ret += "" + str(int(UnitDict["HP"])) + "" - - ret += "" +str("%.0f" % float(UnitDict["BuildTime"])) + "" + attType = "Ranged" if UnitDict["Ranged"] == True else "Melee" + if UnitDict["RepeatRate"][attType] != "0": + for atype in AttackTypes: + repeatTime = float(UnitDict["RepeatRate"][attType]) / 1000.0 + ret += ( + "" + + str("%.1f" % ( + float(UnitDict["Attack"][attType][atype]) / repeatTime + )) + "" + ) - ret += "" + str("%.1f" % float(UnitDict["WalkSpeed"])) + "" + ret += ( + "" + + str("%.1f" % (float(UnitDict["RepeatRate"][attType]) / 1000.0)) + + "" + ) + else: + for atype in AttackTypes: + ret += " - " + ret += " - " - for atype in AttackTypes: - PercentValue = 1.0 - (0.9 ** float(UnitDict["Armour"][atype])) - ret += "" + str("%.0f" % float(UnitDict["Armour"][atype])) + " / " + str("%.0f" % (PercentValue*100.0)) + "%" + if UnitDict["Ranged"] == True and UnitDict["Range"] > 0: + ret += "" + str("%.1f" % float(UnitDict["Range"])) + "" + spread = float(UnitDict["Spread"]) + ret += "" + str("%.1f" % spread) + "" + else: + ret += " - - " - attType = ("Ranged" if UnitDict["Ranged"] == True else "Melee") - if UnitDict["RepeatRate"][attType] != "0": - for atype in AttackTypes: - repeatTime = float(UnitDict["RepeatRate"][attType])/1000.0 - ret += "" + str("%.1f" % (float(UnitDict["Attack"][attType][atype])/repeatTime)) + "" + for rtype in Resources: + ret += "" + str("%.0f" % + float(UnitDict["Cost"][rtype])) + "" - ret += "" + str("%.1f" % (float(UnitDict["RepeatRate"][attType])/1000.0)) + "" - else: - for atype in AttackTypes: - ret += " - " - ret += " - " + ret += "" + str("%.0f" % + float(UnitDict["Cost"]["population"])) + "" - if UnitDict["Ranged"] == True and UnitDict["Range"] > 0: - ret += "" + str("%.1f" % float(UnitDict["Range"])) + "" - spread = float(UnitDict["Spread"]) - ret += "" + str("%.1f" % spread) + "" - else: - ret += " - - " + ret += '' + for Bonus in UnitDict["AttackBonuses"]: + ret += "[" + for classe in UnitDict["AttackBonuses"][Bonus]["Classes"]: + ret += classe + " " + ret += ": %s] " % UnitDict["AttackBonuses"][Bonus]["Multiplier"] + ret += "" - for rtype in Resources: - ret += "" + str("%.0f" % float(UnitDict["Cost"][rtype])) + "" - - ret += "" + str("%.0f" % float(UnitDict["Cost"]["population"])) + "" + ret += "\n" + return ret - ret += "" - for Bonus in UnitDict["AttackBonuses"]: - ret += "[" - for classe in UnitDict["AttackBonuses"][Bonus]["Classes"]: - ret += classe + " " - ret += ': ' + str(UnitDict["AttackBonuses"][Bonus]["Multiplier"]) + "] " - ret += "" - - ret += "\n" - return ret # Sort the templates dictionary. def SortFn(A): - sortVal = 0 - for classe in SortTypes: - sortVal += 1 - if classe in A[1]["Classes"]: - break - if ComparativeSortByChamp == True and A[0].find("champion") == -1: - sortVal -= 20 - if ComparativeSortByCav == True and A[0].find("cavalry") == -1: - sortVal -= 10 - if A[1]["Civ"] != None and A[1]["Civ"] in Civs: - sortVal += 100 * Civs.index(A[1]["Civ"]) - return sortVal + sortVal = 0 + for classe in ClassesUsedForSort: + sortVal += 1 + if classe in A[1]["Classes"]: + break + if ComparativeSortByChamp == True and A[0].find("champion") == -1: + sortVal -= 20 + if ComparativeSortByCav == True and A[0].find("cavalry") == -1: + sortVal -= 10 + if A[1]["Civ"] != None and A[1]["Civ"] in Civs: + sortVal += 100 * Civs.index(A[1]["Civ"]) + return sortVal -# helper to write coloured text. -def WriteColouredDiff(file, diff, PositOrNegat): - def cleverParse(diff): - if float(diff) - int(diff) < 0.001: - return str(int(diff)) - else: - return str("%.1f" % float(diff)) +def WriteColouredDiff(file, diff, isChanged): + """helper to write coloured text. + diff value must always be computed as a unit_spec - unit_generic. + A positive imaginary part represents advantageous trait. + """ - if (PositOrNegat == "positive"): - file.write(" 0 else "0,150,0")) + ");\">" + cleverParse(diff) + "") - elif (PositOrNegat == "negative"): - file.write("" + cleverParse(diff) + "") - else: - complain + def cleverParse(diff): + if float(diff) - int(diff) < 0.001: + return str(int(diff)) + else: + return str("%.1f" % float(diff)) + + isAdvantageous = diff.imag > 0 + diff = diff.real + if diff != 0: + isChanged = True + else: + # do not change its value if one parameter is not changed (yet) + # some other parameter might be different + pass + + if diff == 0: + rgb_str = "200,200,200" + elif isAdvantageous and diff > 0: + rgb_str = "180,0,0" + elif (not isAdvantageous) and diff < 0: + rgb_str = "180,0,0" + else: + rgb_str = "0,150,0" + + file.write( + """{} + """.format( + rgb_str, cleverParse(diff) + ) + ) + return isChanged + + +def computeUnitEfficiencyDiff(TemplatesByParent, Civs): + efficiency_table = {} + for parent in TemplatesByParent: + TemplatesByParent[parent].sort(key=lambda x: Civs.index(x[1]["Civ"])) + + for tp in TemplatesByParent[parent]: + # HP + diff = -1j + (int(tp[1]["HP"]) - int(templates[parent]["HP"])) + efficiency_table[(parent, tp[0], "HP")] = diff + efficiency_table[(parent, tp[0], "HP")] = diff + + # Build Time + diff = +1j + (int(tp[1]["BuildTime"]) - + int(templates[parent]["BuildTime"])) + efficiency_table[(parent, tp[0], "BuildTime")] = diff + + # walk speed + diff = -1j + ( + float(tp[1]["WalkSpeed"]) - + float(templates[parent]["WalkSpeed"]) + ) + efficiency_table[(parent, tp[0], "WalkSpeed")] = diff + + # Resistance + for atype in AttackTypes: + diff = -1j + ( + float(tp[1]["Resistance"][atype]) + - float(templates[parent]["Resistance"][atype]) + ) + efficiency_table[(parent, tp[0], "Resistance/" + atype)] = diff + + # Attack types (DPS) and rate. + attType = "Ranged" if tp[1]["Ranged"] == True else "Melee" + if tp[1]["RepeatRate"][attType] != "0": + for atype in AttackTypes: + myDPS = float(tp[1]["Attack"][attType][atype]) / ( + float(tp[1]["RepeatRate"][attType]) / 1000.0 + ) + parentDPS = float( + templates[parent]["Attack"][attType][atype]) / ( + float(templates[parent]["RepeatRate"][attType]) / 1000.0 + ) + diff = -1j + (myDPS - parentDPS) + efficiency_table[ + (parent, tp[0], "Attack/" + attType + "/" + atype) + ] = diff + diff = -1j + ( + float(tp[1]["RepeatRate"][attType]) / 1000.0 + - float(templates[parent]["RepeatRate"][attType]) / 1000.0 + ) + efficiency_table[ + (parent, tp[0], "Attack/" + attType + "/" + atype + + "/RepeatRate") + ] = diff + # range and spread + if tp[1]["Ranged"] == True: + diff = -1j + ( + float(tp[1]["Range"]) - + float(templates[parent]["Range"]) + ) + efficiency_table[ + (parent, tp[0], "Attack/" + attType + "/Ranged/Range") + ] = diff + + diff = (float(tp[1]["Spread"]) - + float(templates[parent]["Spread"])) + efficiency_table[ + (parent, tp[0], "Attack/" + attType + "/Ranged/Spread") + ] = diff + + for rtype in Resources: + diff = +1j + ( + float(tp[1]["Cost"][rtype]) + - float(templates[parent]["Cost"][rtype]) + ) + efficiency_table[(parent, tp[0], "Resources/" + rtype)] = diff + + diff = +1j + ( + float(tp[1]["Cost"]["population"]) + - float(templates[parent]["Cost"]["population"]) + ) + efficiency_table[(parent, tp[0], "Population")] = diff + + return efficiency_table + + +def computeTemplates(LoadTemplatesIfParent): + """Loops over template XMLs and selectively insert into templates dict.""" + pwd = os.getcwd() + os.chdir(basePath) + templates = {} + for template in list(glob.glob("template_*.xml")): + if os.path.isfile(template): + found = False + for possParent in LoadTemplatesIfParent: + if hasParentTemplate(template, possParent): + found = True + break + if found == True: + templates[template] = CalcUnit(template) + # f.write(WriteUnit(template, templates[template])) + os.chdir(pwd) + return templates + + +def computeCivTemplates(template: dict, Civs: list): + """Load Civ specific templates""" + # NOTE: whether a Civ can train a certain unit is not recorded in the unit + # .xml files, and hence we have to get that info elsewhere, e.g. from the + # Civ tree. This should be delayed until this whole parser is based on the + # Civ tree itself. + + # This function must always ensure that Civ unit parenthood works as + # intended, i.e. a unit in a Civ indeed has a 'Civ' field recording its + # loyalty to that Civ. Check this when upgrading this script to keep + # up with the game engine. + pwd = os.getcwd() + os.chdir(basePath) + + CivTemplates = {} + + for Civ in Civs: + CivTemplates[Civ] = {} + # Load all templates that start with that civ indicator + # TODO: consider adding mixin/civs here too + civ_list = list(glob.glob("units/" + Civ + "/*.xml")) + for template in civ_list: + if os.path.isfile(template): + + # filter based on FilterOut + breakIt = False + for filter in FilterOut: + if template.find(filter) != -1: + breakIt = True + if breakIt: + continue + + # filter based on loaded generic templates + breakIt = True + for possParent in LoadTemplatesIfParent: + if hasParentTemplate(template, possParent): + breakIt = False + break + if breakIt: + continue + + unit = CalcUnit(template) + + # Remove variants for now + if unit["Parent"].find("template_") == -1: + continue + + # load template + CivTemplates[Civ][template] = unit + + os.chdir(pwd) + return CivTemplates + + +def computeTemplatesByParent(templates: dict, Civs: list, CivTemplates: dict): + """Get them in the array""" + # Civs:list -> CivTemplates:dict -> templates:dict -> TemplatesByParent + TemplatesByParent = {} + for Civ in Civs: + for CivUnitTemplate in CivTemplates[Civ]: + parent = CivTemplates[Civ][CivUnitTemplate]["Parent"] + + # We have the following constant equality + # templates[*]["Civ"] === gaia + # if parent in templates and templates[parent]["Civ"] == None: + if parent in templates: + if parent not in TemplatesByParent: + TemplatesByParent[parent] = [] + TemplatesByParent[parent].append( + (CivUnitTemplate, CivTemplates[Civ][CivUnitTemplate]) + ) + + # debug after CivTemplates are non-empty + return TemplatesByParent ############################################################ -############################################################ -# Create the HTML file +## Pre-compute all tables +templates = computeTemplates(LoadTemplatesIfParent) +CivTemplates = computeCivTemplates(templates, Civs) +TemplatesByParent = computeTemplatesByParent(templates, Civs, CivTemplates) -f = open(os.path.realpath(__file__).replace("unitTables.py","") + 'unit_summary_table.html', 'w') +# Not used; use it for your own custom analysis +efficiencyTable = computeUnitEfficiencyDiff( + TemplatesByParent, Civs +) -f.write("\n\n\n Unit Tables\n \n\n") -htbout(f,"h1","Unit Summary Table") -f.write("\n") - -os.chdir(basePath) ############################################################ -# Load generic templates +def writeHTML(): + """Create the HTML file""" + f = open( + os.path.realpath(__file__).replace("unitTables.py", "") + + "unit_summary_table.html", + "w", + ) -templates = {} + f.write( + """ + + + + Unit Tables + + + + """ + ) + htbout(f, "h1", "Unit Summary Table") + f.write("\n") -htbout(f,"h2", "Units") + # Write generic templates + htbout(f, "h2", "Units") + f.write( + """ + + + + + + + + + + + + + + + + + + + """ + ) + for template in templates: + f.write(WriteUnit(template, templates[template])) + f.write("
HP BuildTime Speed(walk) Resistance Attack (DPS) Costs Efficient Against
H P C H P C Rate Range Spread (/100m) F W S M P
") -f.write("\n") -f.write("") -f.write("\n") -f.write("") -f.write("\n\n") + # Write unit specialization + # Sort them by civ and write them in a table. + # + # TODO: pre-compute the diffs then render, filtering out the non-interesting + # ones + # + f.write( + """ +

Units Specializations +

-for template in list(glob.glob('template_*.xml')): - if os.path.isfile(template): - found = False - for possParent in LoadTemplatesIfParent: - if hasParentTemplate(template, possParent): - found = True - break - if found == True: - templates[template] = CalcUnit(template) - f.write(WriteUnit(template, templates[template])) +

This table compares each template to its parent, showing the +differences between the two. +
Note that like any table, you can copy/paste this in Excel (or Numbers or + ...) and sort it. +

-f.write("
HP BuildTime Speed(walk) Armour Attack (DPS) Costs Efficient Against
HPC HPCRateRangeSpread\n(/100m) FWSMP
") + + + + + + + + + + + + + + + + + + + + """ + ) + for parent in TemplatesByParent: + TemplatesByParent[parent].sort(key=lambda x: Civs.index(x[1]["Civ"])) + for tp in TemplatesByParent[parent]: + isChanged = False + ff = open( + os.path.realpath(__file__).replace("unitTables.py", "") + + ".cache", "w" + ) -############################################################ -# Load Civ specific templates + ff.write("") + ff.write( + "" + ) + ff.write( + '" + ) -CivTemplates = {} + # HP + diff = -1j + (int(tp[1]["HP"]) - int(templates[parent]["HP"])) + isChanged = WriteColouredDiff(ff, diff, isChanged) -for Civ in Civs: - CivTemplates[Civ] = {} - # Load all templates that start with that civ indicator - for template in list(glob.glob('units/' + Civ + '_*.xml')): - if os.path.isfile(template): + # Build Time + diff = +1j + (int(tp[1]["BuildTime"]) - + int(templates[parent]["BuildTime"])) + isChanged = WriteColouredDiff(ff, diff, isChanged) - # filter based on FilterOut - breakIt = False - for filter in FilterOut: - if template.find(filter) != -1: breakIt = True - if breakIt: continue + # walk speed + diff = -1j + ( + float(tp[1]["WalkSpeed"]) - + float(templates[parent]["WalkSpeed"]) + ) + isChanged = WriteColouredDiff(ff, diff, isChanged) - # filter based on loaded generic templates - breakIt = True - for possParent in LoadTemplatesIfParent: - if hasParentTemplate(template, possParent): - breakIt = False - break - if breakIt: continue + # Resistance + for atype in AttackTypes: + diff = -1j + ( + float(tp[1]["Resistance"][atype]) + - float(templates[parent]["Resistance"][atype]) + ) + isChanged = WriteColouredDiff(ff, diff, isChanged) - unit = CalcUnit(template) + # Attack types (DPS) and rate. + attType = "Ranged" if tp[1]["Ranged"] == True else "Melee" + if tp[1]["RepeatRate"][attType] != "0": + for atype in AttackTypes: + myDPS = float(tp[1]["Attack"][attType][atype]) / ( + float(tp[1]["RepeatRate"][attType]) / 1000.0 + ) + parentDPS = float( + templates[parent]["Attack"][attType][atype]) / ( + float(templates[parent]["RepeatRate"][attType]) / 1000.0 + ) + isChanged = WriteColouredDiff( + ff, -1j + (myDPS - parentDPS), isChanged + ) + isChanged = WriteColouredDiff( + ff, + -1j + + ( + float(tp[1]["RepeatRate"][attType]) / 1000.0 + - float(templates[parent]["RepeatRate"][attType]) / 1000.0 + ), + isChanged, + ) + # range and spread + if tp[1]["Ranged"] == True: + isChanged = WriteColouredDiff( + ff, + -1j + + (float(tp[1]["Range"]) - + float(templates[parent]["Range"])), + isChanged, + ) + mySpread = float(tp[1]["Spread"]) + parentSpread = float(templates[parent]["Spread"]) + isChanged = WriteColouredDiff( + ff, +1j + (mySpread - parentSpread), isChanged + ) + else: + ff.write("") + else: + ff.write("") - # Remove variants for now - if unit["Parent"].find("template_") == -1: - continue + for rtype in Resources: + isChanged = WriteColouredDiff( + ff, + +1j + + ( + float(tp[1]["Cost"][rtype]) + - float(templates[parent]["Cost"][rtype]) + ), + isChanged, + ) - # load template - CivTemplates[Civ][template] = unit + isChanged = WriteColouredDiff( + ff, + +1j + + ( + float(tp[1]["Cost"]["population"]) + - float(templates[parent]["Cost"]["population"]) + ), + isChanged, + ) -############################################################ -f.write("\n\n

Units Specializations

\n") -f.write("

This table compares each template to its parent, showing the differences between the two.
Note that like any table, you can copy/paste this in Excel (or Numbers or ...) and sort it.

") + ff.write("") + ff.write("\n") -TemplatesByParent = {} + ff.close() # to actually write into the file + with open( + os.path.realpath(__file__).replace("unitTables.py", "") + + ".cache", "r" + ) as ff: + unitStr = ff.read() -#Get them in the array -for Civ in Civs: - for CivUnitTemplate in CivTemplates[Civ]: - parent = CivTemplates[Civ][CivUnitTemplate]["Parent"] - if parent in templates and templates[parent]["Civ"] == None: - if parent not in TemplatesByParent: - TemplatesByParent[parent] = [] - TemplatesByParent[parent].append( (CivUnitTemplate,CivTemplates[Civ][CivUnitTemplate])) + if showChangedOnly: + if isChanged: + f.write(unitStr) + else: + # print the full table if showChangedOnly is false + f.write(unitStr) -#Sort them by civ and write them in a table. -f.write("
HP BuildTime Speed (/100m) Resistance Attack Costs Civ
H P C H P C Rate Range Spread F W S M P
" + + parent.replace(".xml", "").replace("template_", "") + + "' + + tp[0].replace(".xml", "").replace("units/", "") + + "" + tp[1]["Civ"] + "
\n") -f.write("") -f.write("\n") -f.write("") -f.write("\n") -for parent in TemplatesByParent: - TemplatesByParent[parent].sort(key=lambda x : Civs.index(x[1]["Civ"])) - for tp in TemplatesByParent[parent]: - f.write("") + f.write("
HP BuildTime Speed Armour Attack Costs Civ
HPC HPCRateRangeSpread FWSMP
" + parent.replace(".xml","").replace("template_","") + "
") - f.write("") - - # HP - diff = int(tp[1]["HP"]) - int(templates[parent]["HP"]) - WriteColouredDiff(f, diff, "negative") - - # Build Time - diff = int(tp[1]["BuildTime"]) - int(templates[parent]["BuildTime"]) - WriteColouredDiff(f, diff, "positive") - - # walk speed - diff = float(tp[1]["WalkSpeed"]) - float(templates[parent]["WalkSpeed"]) - WriteColouredDiff(f, diff, "negative") - - # Armor - for atype in AttackTypes: - diff = float(tp[1]["Armour"][atype]) - float(templates[parent]["Armour"][atype]) - WriteColouredDiff(f, diff, "negative") + # Table of unit having or not having some units. + f.write( + """ +

Roster Variety +

- # Attack types (DPS) and rate. - attType = ("Ranged" if tp[1]["Ranged"] == True else "Melee") - if tp[1]["RepeatRate"][attType] != "0": - for atype in AttackTypes: - myDPS = float(tp[1]["Attack"][attType][atype]) / (float(tp[1]["RepeatRate"][attType])/1000.0) - parentDPS = float(templates[parent]["Attack"][attType][atype]) / (float(templates[parent]["RepeatRate"][attType])/1000.0) - WriteColouredDiff(f, myDPS - parentDPS, "negative") - WriteColouredDiff(f, float(tp[1]["RepeatRate"][attType])/1000.0 - float(templates[parent]["RepeatRate"][attType])/1000.0, "negative") - # range and spread - if tp[1]["Ranged"] == True: - WriteColouredDiff(f, float(tp[1]["Range"]) - float(templates[parent]["Range"]), "negative") - mySpread = float(tp[1]["Spread"]) - parentSpread = float(templates[parent]["Spread"]) - WriteColouredDiff(f, mySpread - parentSpread, "positive") - else: - f.write("") - else: - f.write("") +

This table show which civilizations have units who derive from +each loaded generic template. +
Grey means the civilization has no unit derived from a generic template; +
dark green means 1 derived unit, mid-tone green means 2, bright green + means 3 or more. +
The total is the total number of loaded units for this civ, which may be + more than the total of units inheriting from loaded templates. +

+
" + tp[0].replace(".xml","").replace("units/","") + "
+ + +""" + ) + for civ in Civs: + f.write('\n") + f.write("\n") - for rtype in Resources: - WriteColouredDiff(f, float(tp[1]["Cost"][rtype]) - float(templates[parent]["Cost"][rtype]), "positive") - - WriteColouredDiff(f, float(tp[1]["Cost"]["population"]) - float(templates[parent]["Cost"]["population"]), "positive") + sortedDict = sorted(templates.items(), key=SortFn) - f.write("") + for tp in sortedDict: + if tp[0] not in TemplatesByParent: + continue + f.write("\n") + for civ in Civs: + found = 0 + for temp in TemplatesByParent[tp[0]]: + if temp[1]["Civ"] == civ: + found += 1 + if found == 1: + f.write('') + elif found == 2: + f.write('') + elif found >= 3: + f.write('') + else: + f.write('') + f.write("\n") + f.write( + '\ + \n' + ) + for civ in Civs: + count = 0 + for units in CivTemplates[civ]: + count += 1 + f.write('\n") - f.write("\n") -f.write("
Template ' + civ + "
" + tp[1]["Civ"] + "
" + tp[0] + "
Total:' + str(count) + "
") + f.write("\n") -# Table of unit having or not having some units. -f.write("\n\n

Roster Variety

\n") -f.write("

This table show which civilizations have units who derive from each loaded generic template.
Green means 1 deriving unit, blue means 2, black means 3 or more.
The total is the total number of loaded units for this civ, which may be more than the total of units inheriting from loaded templates.

") -f.write("
\n") -f.write("\n") -for civ in Civs: - f.write("\n") -f.write("\n") + f.write("
Template" + civ + "
") -sortedDict = sorted(templates.items(), key=SortFn) - -for tp in sortedDict: - if tp[0] not in TemplatesByParent: - continue - f.write("\n") - for civ in Civs: - found = 0 - for temp in TemplatesByParent[tp[0]]: - if temp[1]["Civ"] == civ: - found += 1 - if found == 1: - f.write("") - elif found == 2: - f.write("") - elif found >= 3: - f.write("") - else: - f.write("") - f.write("\n") -f.write("\n") -for civ in Civs: - count = 0 - for units in CivTemplates[civ]: count += 1 - f.write("\n") - -f.write("\n") - -f.write("
" + tp[0] +"
Total:" + str(count) + "
") - -# Add a simple script to allow filtering on sorting directly in the HTML page. -if AddSortingOverlay: - f.write("\n\ - \n\ - \n") + # Add a simple script to allow filtering on sorting directly in the HTML + # page. + if AddSortingOverlay: + f.write( + """ + + + """ + ) + + f.write("\n") + + +if __name__ == "__main__": + writeHTML()