Example #1
0
def parse(lines, filename, headings=None):
    """ headings: Specify the headings explicitly. Otherwise they are read from the first line in the file. """
    reader = csv.reader(lines, dialect=ParadoxDialect)

    if headings is None:
        headings = next(reader)

    result = pyradox.Tree()

    for row_index, row_tokens in enumerate(reader):
        if len(row_tokens) == 0: continue  # skip blank lines

        if len(row_tokens) != len(headings):
            warnings.warn_explicit(
                'Row length (%d) should be same as headings length (%d).' %
                (len(row_tokens), len(headings)), ParseWarning, filename,
                row_index + 2)

        # first column is the key
        key = pyradox.token.make_primitive(row_tokens[0],
                                           default_token_type='str')
        tree_row = pyradox.Tree()
        result.append(key, tree_row)

        for col_index in range(min(len(headings), len(row_tokens))):
            heading = headings[col_index]
            row_token = row_tokens[col_index]
            value = pyradox.token.make_primitive(row_token,
                                                 default_token_type='str')
            tree_row.append(heading, value)

    return result
Example #2
0
def get_hulls(beta = False):
    game = 'HoI4_beta' if beta else 'HoI4'
    
    equipments = pyradox.parse_merge('common/units/equipment', game = game, filter_pattern = "ship_hull", merge_levels = 1)['equipments']
    
    archetypes = pyradox.Tree()
    # Compute archetypes.
    for key, hull in equipments.items():
        if hull['is_archetype']:
            hull = copy.deepcopy(hull)
            hull['module_slots'].resolve_references()
            archetypes[key] = hull
    
    hulls = pyradox.Tree()
    # Compute final hulls.
    for key, hull in equipments.items():
        if hull['is_buildable'] is False: continue
        
        hull = copy.deepcopy(hull)
        
        archetype = archetypes[hull['archetype']]
        hull.inherit(archetype)
        hull.weak_update(archetype)
        
        if 'parent' in hull:
            parent = hulls[hull['parent']]
            hull.inherit(parent)
        
        hull['module_slots'].resolve_references()
        hull['is_archetype'] = False
        hull['is_buildable'] = True
        
        hulls[key] = hull
    
    return archetypes, hulls
Example #3
0
def compute_effects(k, v):
    result = '<ul>'

    def equipment_bonus(equipment_bonuses):
        result = ''
        for equipment_type, effects in equipment_bonuses.items():
            equipment_type_name = (
                pyradox.yml.get_localisation(equipment_type, game='HoI4')
                or pyradox.format.human_title(equipment_type)
                or pyradox.format.human_title(equipment_type))
            for effect_key, magnitude in effects.items():
                effect_name = pyradox.yml.get_localisation(
                    'modifier_' + effect_key,
                    game='HoI4') or pyradox.format.human_title(effect_key)
                magnitude_string = compute_magnitude_string(
                    effect_key, magnitude)
                result += '<li>%s %s: %s</li>' % (
                    equipment_type_name, effect_name, magnitude_string)
        return result

    if 'research_bonus' in v:
        for category, magnitude in v['research_bonus'].items():
            category_string = pyradox.yml.get_localisation(
                category + '_research',
                game='HoI4') or (pyradox.format.human_title(category) +
                                 ' Research Time')
            magnitude_string = compute_magnitude_string(
                category, -magnitude, False)
            result += '<li>%s: %s</li>' % (category_string, magnitude_string)

    if 'equipment_bonus' in v:
        result += equipment_bonus(v['equipment_bonus'])

    for trait_key in v.find_all('traits'):
        trait = traits[trait_key]
        subresult = ''
        for effect_key, magnitude in trait.items():
            if effect_key in ['sprite', 'random', 'ai_will_do']: continue
            elif effect_key == 'equipment_bonus':
                subresult += equipment_bonus(magnitude)
            else:
                effect_name = pyradox.yml.get_localisation(
                    'modifier_' + effect_key,
                    game='HoI4') or pyradox.format.human_title(effect_key)
                magnitude_string = compute_magnitude_string(
                    effect_key, magnitude)
                subresult += '<li>%s: %s</li>' % (effect_name,
                                                  magnitude_string)
        if trait_key not in trait_result.keys():
            row = pyradox.Tree()
            row['name'] = pyradox.yml.get_localisation(trait_key,
                                                       game='HoI4').replace(
                                                           '\\n', ' ')
            row['type'] = v['type']
            row['text'] = '<ul>' + subresult + '</ul>'

            trait_result[trait_key] = row
        result += subresult
    result += '</ul>'
    return result
Example #4
0
def make_ship(hull, design):
    ship = copy.deepcopy(hull)

    ship['design_modules'] = pyradox.Tree()

    for slot_key, module_key, module in design:
        ship['design_modules'].append(slot_key, module_key)

    # resource costs
    for slot_key, module_key,module in design:
        if 'build_cost_resources' in module:
            for resource, amount in module['build_cost_resources'].items():
                ship['resources'][resource] = (
                    (ship['resources'][resource] or 0)
                    + amount)

    ship['total_resources'] = sum(ship['resources'].values())

    # add_stats
    for slot_key, module_key, module in design:
        if 'add_stats' in module:
            for stat, amount in module['add_stats'].items():
                ship[stat] = (ship[stat] or 0) + amount

    # average stats
    average_dict = {}

    for slot_key, module_key, module in design:
        if 'add_average_stats' in module:
            for stat, amount in module['add_average_stats'].items():
                if stat in average_dict:
                    prev_total, prev_count = average_dict[stat]
                else:
                    prev_total, prev_count = 0.0, 0
                average_dict[stat] = (prev_total + amount, prev_count + 1)

    for stat, (total, count) in average_dict.items():
        ship[stat] = (ship[stat] or 0) + total / count
    
    # multiply stats
    multiply_dict = {}

    for slot_key, module_key, module in design:
        if 'multiply_stats' in module:
            for stat, amount in module['multiply_stats'].items():
                if stat not in multiply_dict: multiply_dict[stat] = 0.0
                multiply_dict[stat] += amount

    for stat, amount in multiply_dict.items():
        ship[stat] *= (1.0 + amount)

    return ship
Example #5
0
 def __init__(self, token_data, filename, start_pos, is_top_level):
     self.token_data = token_data          # The tokenized version of the file. List of (token_type, token_string, token_line_number) tuples.
     self.filename = filename            # File the tree is being parsed from. Used for warning and error messages.
     self.is_top_level = is_top_level        # True iff this tree is the top level of the file.
 
     self.result = pyradox.Tree() # The resulting tree.
     
     self.pos = start_pos                 # Current token position.
     self.pending_comments = []           # Comments pending assignment.
     self.key = None                     # The key currently being processed.
     self.key_string = None               # The original token string for that key.
     self.operator = None                # The operator currently being processed. Usually '='.
     self.in_group = False               # Whether the parser is currently inside a group.
     self.next = self.process_key         # The next case to execute.
Example #6
0
def parse_merge(path, game=None, filter_pattern = None, merge_levels = 0, apply_defines = False, *args, **kwargs):
    """Given a directory, return a Tree as if all .txt files in the directory were a single file"""
    path, game = pyradox.config.combine_path_and_game(path, game)
    
    result = pyradox.Tree()
    for filename in os.listdir(path):
        fullpath = os.path.join(path, filename)
        if os.path.isfile(fullpath):
            if should_parse(fullpath, filename, filter_pattern):
                tree = parse_file(fullpath, game = game, *args, **kwargs)
                if apply_defines:
                    tree = tree.apply_defines()
                result.merge(tree, merge_levels)
    return result
Example #7
0
def generate_row(decision_id, decision):
    row = pyradox.Tree()

    row['id'] = decision_id
    if 'has_tech' in decision['available']:
        tech = decision['available']['has_tech']
        row['tech_type'] = tech[:-1]
        row['tech_level'] = tech[-1]
    else:
        row['tech_type'] = ''
        row['tech_level'] = ''

    row['cic'] = decision['modifier']['civilian_factory_use']

    if isinstance(decision['days_remove'], int):
        row['time'] = decision['days_remove']
    else:
        # TODO: determine from code?
        row['time'] = 30

    if 'fire_only_once' in decision:
        pass
    else:
        row['repeatable'] = decision['visible'].find('value',
                                                     recurse=True,
                                                     default=1)

    for effect_key, effect in decision['remove_effect'].items():
        if not isinstance(effect_key, int): continue
        if 'add_resource' in effect:
            row['state'] = effect_key
            row['resource'] = effect['add_resource']['type']
            row['amount'] = effect['add_resource']['amount']

    row['state_name'] = hoi4.state.get_state_name(row['state'])
    row['owner_name'] = hoi4.state.get_state_owner(row['state'])['name']
    row['cost'] = row['cic'] * row['time'] * 5

    return row
Example #8
0
import re
import os

import pyradox


def compute_country_tag_and_name(filename):
    m = re.match('.*([A-Z]{3})\s*-\s*(.*)\.txt$', filename)
    return m.group(1), m.group(2)


economics = pyradox.txt.parse_file(
    os.path.join(pyradox.get_game_directory('HoI4'), 'common', 'ideas',
                 '_economic.txt'))['ideas']

result = pyradox.Tree()

for filename, country in pyradox.txt.parse_dir(
        os.path.join(pyradox.get_game_directory('HoI4'), 'history',
                     'countries')):
    country = country.at_time('1936.1.1')
    tag, name = compute_country_tag_and_name(filename)
    country['tag'] = tag
    ruling_party = country['set_politics']['ruling_party']
    country['name'] = pyradox.yml.get_localisation('%s_%s' %
                                                   (tag, ruling_party),
                                                   game='HoI4')
    result[tag] = country

    if 'add_ideas' in country:
        for idea in country['add_ideas']:
Example #9
0
                elif key == 'max_level':
                    max_level = value
                elif 'level_cost_' in key:
                    level = int(key[len('level_cost_'):])
                    if level <= 2:
                        costs[level - 1] = value
                else:
                    bonus_key = key
                    base_value = value
            bonuses[bonus_key] = (power_type, base_value, max_level,
                                  tuple(costs))

result = '{|class = "wikitable sortable"\n'
result += '! Idea group !! Linear cost !! Base cost !! Adjusted for<br/>early ideas !! Max level ratio !! Final cost !! Bonuses unaccounted for\n'

result_tree = pyradox.Tree()

for file_name, file_data in pyradox.txt.parse_dir(
        os.path.join(pyradox.get_game_directory('EU4'), 'common', 'ideas')):
    for idea_set_name, idea_set in file_data.items():

        index = 0
        base_cost = 0.0
        early_cost = 0.0
        total_linear_cost = 0.0
        unaccounted = []  # not appearing in designer
        level_counts = {
            'adm': 0.0,
            'dip': 0.0,
            'mil': 0.0,
        }
Example #10
0
def units_at_year(year):
    units = hoi4.load.get_units()

    # archetype_key -> best equipment
    equipment_models = {}

    for unit_key, unit_data in units.items():
        unit_data["year"] = year
        unit_data["last_upgrade"] = default_year
        if "active" not in unit_data.keys(): unit_data["active"] = True

    for tech_key, tech in techs.items():
        if not isinstance(tech, pyradox.Tree): continue
        if (tech["start_year"] or year) > year: continue

        if 'folder' in tech and 'doctrine' in tech['folder']['name']:
            continue  # ignore doctrines
        if "enable_subunits" in tech:
            for unit_key in tech.find_all("enable_subunits"):
                units[unit_key]["active"] = True
                units[unit_key]["last_upgrade"] = max(
                    units[unit_key]["last_upgrade"], tech["start_year"])

        if tech["allow"] and tech["allow"]["always"] == False:
            continue  # ignore unallowed techs, but allow subunits through

        if "enable_equipments" in tech:
            for equipment_key in tech.find_all("enable_equipments"):
                equipment = equipments[equipment_key]
                if "archetype" in equipment:
                    archetype_key = equipment["archetype"]
                    equipment_models[archetype_key] = equipments[equipment_key]
                equipment_models[equipment_key] = equipments[equipment_key]
                # TODO: drop ordering assumption?

        # non-equipment modifiers
        for unit_key, unit_data in units.items():
            for tech_unit_key, stats in tech.items():
                if tech_unit_key == unit_key or tech_unit_key in unit_data.find_all(
                        'categories'):
                    units[unit_key]["last_upgrade"] = max(
                        units[unit_key]["last_upgrade"], tech["start_year"])
                    for stat_key, stat_value in stats.items():
                        if (not type(stat_value) is pyradox.Tree):
                            unit_data[stat_key] = (unit_data[stat_key]
                                                   or 0.0) + stat_value

    # fill in equipment
    for unit_key, unit_data in units.items():
        unit_data["equipments"] = pyradox.Tree()
        for archetype_key in unit_data["need"]:
            if archetype_key in equipment_models:
                equipment = equipment_models[archetype_key]
                unit_data["equipments"][archetype_key] = equipment
                unit_data["last_upgrade"] = max(unit_data["last_upgrade"],
                                                equipment["year"])
                if not equipments[archetype_key]["is_archetype"]:
                    print(
                        "Warning: non-archetype equipment %s defined for %s" %
                        (archetype_key, unit_key))
            else:
                unit_data["equipments"][archetype_key] = False

    return units
Example #11
0
import _initpath
import os

import pyradox

result_tree = pyradox.Tree()

for group_name, group_data in pyradox.txt.parse_merge(
        os.path.join(pyradox.get_game_directory('EU4'), 'common',
                     'ideas')).items():
    if 'trigger' not in group_data: continue
    trigger = group_data['trigger']
    for tag in trigger.find_all('tag'):
        result_tree[tag] = group_name
    if 'or' in trigger:
        for tag in trigger['or'].find_all('tag'):
            result_tree[tag] = group_name

outfile = open('out/idea_map.txt', mode='w')
outfile.write(str(result_tree))
outfile.close()
Example #12
0
commander_type_keys = {
    'create_field_marshal': 'Field Marshal',
    'create_corps_commander': 'General',
    'create_navy_leader': 'Admiral',
}

columns = (
    ('Country', '{{flag|%(country)s}}', None),
    ('Name', '%(name)s', None),
    ('Type', lambda k, v: commander_type_keys[k], None),
    ('Skill', '%(skill)d', None),
    ('Traits', list_commander_traits, None),
)

commanders = pyradox.Tree()

for filename, country in pyradox.txt.parse_dir(
        os.path.join(pyradox.get_game_directory('HoI4'), 'history',
                     'countries')):
    tag, _ = compute_country_tag_and_name(filename)
    ruling_party = country['set_politics']['ruling_party']
    country_name = pyradox.yml.get_localisation('%s_%s' % (tag, ruling_party),
                                                game='HoI4')
    for commander_type_key in commander_type_keys.keys():
        for leader in country.find_all(commander_type_key):
            leader['country'] = country_name
            commanders.append(commander_type_key, leader)

out = open("out/military_commanders.txt", "w", encoding="utf-8")
out.write(
Example #13
0
import hoi4
import re
import os
import hoi4


import pyradox

game = 'HoI4'

def compute_country_tag_and_name(filename):
    m = re.match('.*([A-Z]{3})\s*-\s*(.*)\.txt$', filename)
    return m.group(1), m.group(2)

countries = {}
total = pyradox.Tree()

for filename, country in pyradox.txt.parse_dir(os.path.join(pyradox.get_game_directory('HoI4'), 'history', 'countries')):
    tag, name = compute_country_tag_and_name(filename)
    country['tag'] = tag
    ruling_party = country['set_politics']['ruling_party'] or 'neutrality'
    country['name'] = pyradox.yml.get_localisation('%s_%s' % (tag, ruling_party), game = 'HoI4')
    countries[tag] = country

states = pyradox.txt.parse_merge(os.path.join(pyradox.get_game_directory('HoI4'), 'history', 'states'))
state_categories = pyradox.txt.parse_merge(os.path.join(pyradox.get_game_directory(game), 'common', 'state_category'),
                                         verbose=False, merge_levels = 1)

state_categories = state_categories['state_categories']

for state in states.values():
Example #14
0
import _initpath
import pyradox
import os
import copy
import math

import stellaris.weapon

csv_data = pyradox.csv.parse_file(
    ['common', 'component_templates', 'weapon_components.csv'],
    game='Stellaris')

txt_data = pyradox.parse_merge(['common', 'component_templates'],
                               game='Stellaris')

total_data = pyradox.Tree()

for weapon in txt_data.find_all('weapon_component_template'):
    if weapon['hidden']: continue
    key = weapon['key']
    total_data[key] = weapon + csv_data[key]

column_specs = [
    ('Weapon/Role', stellaris.weapon.icon_and_name_and_role),
    ('Size', stellaris.weapon.slot_string),
    ('{{icon|minerals}}<br/>Cost', '%(cost)d'),
    ('{{icon|power}}<br/>Power', lambda k, v: str(abs(v['power']))),
    ('{{icon|damage}}<br/>Average<br/>damage',
     lambda k, v: '%0.1f' % stellaris.weapon.average_damage(v)),
    ('{{icon|time}}<br/>Cooldown', '%(cooldown)d'),
    ('{{icon|weapons range}}<br/>Range', '%(range)d'),
Example #15
0
    'theorist',
]

military_chief_types = [
    'army_chief',
    'navy_chief',
    'air_chief',
]

military_high_command_types = [
    'high_command',
]

types_to_tabulate = military_chief_types

result = pyradox.Tree()

idea_data = pyradox.txt.parse_merge(os.path.join(
    pyradox.get_game_directory('HoI4'), 'common', 'ideas'),
                                    merge_levels=2)['ideas']
for idea_type in types_to_tabulate:
    ideas = idea_data[idea_type]
    type_name = pyradox.yml.get_localisation(idea_type, game='HoI4')
    for idea_key, idea in ideas.items():
        if idea_key == 'designer': continue
        row = pyradox.Tree()
        row['type'] = type_name

        if 'allowed' not in idea:
            row['country'] = 'Generic'
        elif 'tag' in idea['allowed']:
Example #16
0
import hoi4

import pyradox
import os.path
import json

all_years = pyradox.Tree()

unit_type = 'land'

for year in hoi4.unitstats.unit_type_years[unit_type]:
    units = hoi4.unitstats.units_at_year(year)
    all_years += units

with open("out/%s_units_by_year.txt" % unit_type, "w") as out_file:
    columns = [("Unit", hoi4.unitstats.compute_unit_name)
               ] + hoi4.unitstats.base_columns[unit_type] + [
                   ("Unit", hoi4.unitstats.compute_unit_name)
               ]
    tables = {
        year: pyradox.Tree()
        for year in hoi4.unitstats.unit_type_years[unit_type]
    }
    for unit_key, unit in all_years.items():
        if unit["year"] in hoi4.unitstats.unit_type_years[
                unit_type] and hoi4.unitstats.compute_unit_type(
                    unit) == unit_type and hoi4.unitstats.is_availiable(unit):
            tables[unit["year"]].append(unit_key, unit)
    for year in hoi4.unitstats.unit_type_years[unit_type]:
        out_file.write("== %d ==\n" % year)
        out_file.write(
Example #17
0
    territory_costs[province['owner']] += province_cost(province)
    
result = '{|class = "wikitable sortable"\n'
result += '! Country !! Tag !! Territory cost !! Ruler cost !! Government cost !! Technology cost !! Total cost \n'

for tag in sorted(government_costs.keys()):
    if territory_costs[tag] == 0: continue
    result += '|-\n'
    total_cost = sum(government_costs[tag]) + territory_costs[tag]
    result += '| %s || %s || %d || %0.1f || %d || %d || %d \n' % (
        load.country.get_country_name(tag), tag, territory_costs[tag],
        government_costs[tag][0], government_costs[tag][1], government_costs[tag][2],
        total_cost)

result += '|}\n'
print(result)

result_tree = pyradox.Tree()

for tag in sorted(government_costs.keys()):
    nation_tree = pyradox.Tree()
    nation_tree['territory'] = territory_costs[tag]
    nation_tree['ruler'] = government_costs[tag][0]
    nation_tree['government'] = government_costs[tag][1]
    nation_tree['technology'] = government_costs[tag][2]
    result_tree[tag] = nation_tree

outfile = open('out/non_idea_costs.txt', mode = 'w')
outfile.write(str(result_tree))
outfile.close()
Example #18
0
import _initpath
import pyradox
import os
import copy
import math
import stellaris.weapon

txt_data = pyradox.parse_merge(['common', 'component_templates'],
                               game='Stellaris',
                               apply_defines=True)

default_values = pyradox.Tree()
default_values['hull_damage'] = 1.0
default_values['armor_damage'] = 1.0
default_values['shield_damage'] = 1.0
default_values['armor_penetration'] = 0.0
default_values['shield_penetration'] = 0.0
default_values['tracking'] = 0.0
default_values['prerequisites'] = 'Ship part strike craft scout 1'

result_data = pyradox.Tree()

for strike_craft in txt_data.find_all('strike_craft_component_template'):
    if strike_craft['hidden']: continue
    key = strike_craft['key']
    strike_craft.weak_update(default_values)
    strike_craft['size'] = 'hangar'
    result_data[key] = strike_craft

column_specs = [
    ('Weapon/Role', stellaris.weapon.icon_and_name_and_role),
Example #19
0
            row['state'] = effect_key
            row['resource'] = effect['add_resource']['type']
            row['amount'] = effect['add_resource']['amount']

    row['state_name'] = hoi4.state.get_state_name(row['state'])
    row['owner_name'] = hoi4.state.get_state_owner(row['state'])['name']
    row['cost'] = row['cic'] * row['time'] * 5

    return row


# Load resource decisions.
decisions = pyradox.txt.parse_file(
    ['common', 'decisions', 'resource_prospecting.txt'], game=game)

table = pyradox.Tree()

for decision_id, decision in decisions['prospect_for_resources'].items():
    row = generate_row(decision_id, decision)
    table.append(decision_id, row)

columns = (
    ('State', '%(state_name)s'),
    ('Owner (1936)', '{{flag|%(owner_name)s}}'),
    ('Tech type', '%(tech_type)s'),
    ('Tech level', '%(tech_level)s'),
    ('{{icon|cic}}', '%(cic)s'),
    ('{{icon|time}}', '%(time)s'),
    ('{{icon|construction cost}}', '%(cost)d'),
    ('Resource', '{{icon|%(resource)s}} %(resource)s'),
    ('Amount', '%(amount)d'),
Example #20
0
import pyradox


from unitstats import *

files = {}
for unit_type in base_columns.keys():
    files[unit_type] = open("out/%s_units_by_unit.txt" % unit_type, "w")

unit_data = {}

for year in range(1936, 1948):
    unit_data[year] = units_at_year(year)

by_unit = pyradox.Tree()

for year, data in unit_data.items():
    for unit, stats in data.items():
        if unit not in by_unit.keys(): by_unit[unit] = pyradox.Tree()
        by_unit[unit][year] = stats

for unit_type, unit_file in files.items():
    for unit, data in by_unit.items():
        if data[1936]["type"] != unit_type: continue
        unit_file.write("== %s ==\n" % pyradox.format.human_string(unit, True))
        unit_file.write(pyradox.table.make_table(data, 'wiki', base_columns[unit_type], lambda k, v: v["active"]))
        unit_file.write("=== Derived statistics ===\n")
        unit_file.write(pyradox.table.make_table(data, 'wiki', derived_columns[unit_type], lambda k, v: v["active"]))

for unit_file in files.values():
import _initpath
import os
import re
import collections

import pyradox


import pyradox

start_date = pyradox.Date('1444.11.11')

counts = pyradox.Tree() # province counts

# parse all files in a directory, producing instances of pyradox.Tree
for filename, data in pyradox.txt.parse_dir(os.path.join(pyradox.get_game_directory('EU4'), 'history', 'provinces')):
    # pyradox.Tree has many dict methods, such as .keys()
    if 'base_tax' not in data.keys(): continue
    
    trade_good = 'unknown'
    for curr_good in data.find_walk('trade_goods'):
        if curr_good != 'unknown':
            trade_good = curr_good
        
    if trade_good not in counts: counts[trade_good] = 1
    else: counts[trade_good] += 1
        
print([(key, counts[key]) for key in counts.keys()])
Example #22
0
    ('ship_hull_pre_dreadnought', 'heavy_then_light', 1932),
    ('ship_hull_super_heavy_1', 'heavy_then_light', 1940),
    ('ship_hull_super_heavy_1', 'heavy_then_light', 1944),
    ('ship_hull_submarine_1', 'torpedo', 1932),
    ('ship_hull_submarine_2', 'torpedo', 1936),
    ('ship_hull_submarine_3', 'torpedo', 1940),
    ('ship_hull_submarine_4', 'torpedo', 1944),
    ('ship_hull_cruiser_submarine', 'torpedo', 1940),
    ('ship_hull_carrier_1', 'carrier_fast', 1936),
    ('ship_hull_carrier_2', 'carrier_fast', 1940),
    ('ship_hull_carrier_3', 'carrier_fast', 1944),
    ('ship_hull_carrier_conversion_ca', 'carrier_fast', 1936),
    ('ship_hull_carrier_conversion_bb', 'carrier_fast', 1936),
]

models = pyradox.Tree()
for hull_key, strategy_key, year in model_specs:
    hull = hulls[hull_key]
    strategy = strategies[strategy_key]
    ship = design_ship(hull, strategy, year=year)

    ship['model_year'] = year

    model_key = '%s, %s, %d' % (hull_key, strategy_key, year)
    ship['model_id'] = model_key
    models[model_key] = ship

print('List of hulls:')
for hull_key in hulls:
    if 'hull' in hull_key: print(hull_key)