"""
World and Universe Creation Script for Mongoose Traveller 2nd Edition

Traveller work: Copyright 2020 Mongoose Publishing. Portions reproduced with written permission from the publisher.
Software work: Copyright 2021 Guillaume Fortin-Debigaré.

Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

1. This software may not be used, copied, modified, and/or distributed for commercial purposes with or without fee without specific prior written permission from Mongoose Publishing.

2. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.

3. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"""

import random

# Parameters to be defined by the referee.
subsector_seeds = [  # set an element to an integer for a fixed subsector
    None,None,None,None,
    None,None,None,None,
    None,None,None,None,
    None,None,None,None]
subsector_density_dm = [  # -2 for rift, -1 for sparce, 1 for densely populated
    0,0,0,0,
    0,0,0,0,
    0,0,0,0,
    0,0,0,0]
subsector_hot_edge_of_habitable_zone_probability = [  # core rulebook page 218
    0.05,0.05,0.05,0.05,
    0.05,0.05,0.05,0.05,
    0.05,0.05,0.05,0.05,
    0.05,0.05,0.05,0.05]
subsector_cold_edge_of_habitable_zone_probability = [  # core rulebook page 218
    0.05,0.05,0.05,0.05,
    0.05,0.05,0.05,0.05,
    0.05,0.05,0.05,0.05,
    0.05,0.05,0.05,0.05]
subsector_unusual_atmosphere_is_panthalassic_probability = [  # core rulebook page 218
    0.1,0.1,0.1,0.1,
    0.1,0.1,0.1,0.1,
    0.1,0.1,0.1,0.1,
    0.1,0.1,0.1,0.1]

# --- CODE STARTS HERE. ---

sector_width = 4
sector_height = 4
subsector_width = 8
subsector_height = 10
boolean_str = ["No","Yes"]
size_to_diameter_str = [
    "Less than 1000 km",
    "1600 km",
    "3200 km",
    "4800 km",
    "6400 km",
    "8000 km",
    "9600 km",
    "11200 km",
    "12800 km",
    "14400 km",
    "16000 km"]
size_to_gravity_str = [
    "Negligible",
    "0.05 G",
    "0.15 G",
    "0.25 G",
    "0.35 G",
    "0.45 G",
    "0.7 G",
    "0.9 G",
    "1 G",
    "1.25 G",
    "1.4 G"]
atmosphere_to_composition_str = [
    "None",
    "Trace",
    "Very thin, tainted",
    "Very thin",
    "Thin, tainted",
    "Thin",
    "Standard",
    "Standard, tainted",
    "Dense",
    "Dense, tainted",
    "Exotic",
    "Corrosive",
    "Insidious",
    "Very dense",
    "Low",
    "Unusual"]
atmosphere_to_pressure_str = [
    "0 atm",
    "0.001 to 0.09 atm",
    "0.1 to 0.42 atm",
    "0.1 to 0.42 atm",
    "0.43 to 0.7 atm",
    "0.43 to 0.7 atm",
    "0.71 to 1.49 atm",
    "0.71 to 1.49 atm",
    "1.5 to 2.49 atm",
    "1.5 to 2.49 atm",
    "Varies",
    "Varies",
    "Varies",
    "2.5+ atm",
    "0.001 to 0.5 atm",
    "Varies"]
atmosphere_to_temperature_dm = [0,0,-2,-2,-1,-1,0,0,1,1,2,6,6,2,-1,2]
temperature_str = [
    "N/A",
    "-273.15°C to -51°C",
    "-51°C to 0°C",
    "0°C to 30°C",
    "31°C to 80°C",
    "81°C+"]
hydrographics_str = [
    "0%-5%",
    "6%-15%",
    "16%-25%",
    "26%-35%",
    "36%-45%",
    "46%-55%",
    "56%-65%",
    "66%-75%",
    "76%-85%",
    "86%-95%",
    "96%-100%"]
government_str = [
    "None",
    "Company/Corporation",
    "Participating democracy",
    "Self-perpetuating oligarchy",
    "Representative democracy",
    "Feudal technocracy",
    "Captive Government",
    "Balkanisation",
    "Civil service bureaucracy",
    "Impersonal bureaucracy",
    "Charismatic dictator",
    "Non-charismatic leader",
    "Charismatic oligarchy",
    "Religious dictatorship",
    "Religious autocracy",
    "Totalitarian oligarchy"]
faction_strength_str = [
    "obscure group",
    "fringe group",
    "minor group",
    "notable group",
    "significant",
    "overwhelming popular support"]
culture_str = [
    "Sexist",  # 11
    "Religious",
    "Artistic",
    "Ritualised",
    "Conservative",
    "Xenophobic",
    "Taboo",  # 21
    "Deceptive",
    "Liberal",
    "Honourable",
    "Influenced",  # 25
    "Fusion",  # 26, not printed
    "Barbaric",  # 31
    "Remnant",
    "Degenerate",
    "Progressive",
    "Recovering",
    "Nexus",
    "Tourist attraction",  # 41
    "Violent",
    "Peaceful",
    "Obsessed",
    "Fashion",
    "At war",
    "Unusual custom - offworlders",  # 51
    "Unusual custom - starport",
    "Unusual custom - media",
    "Unusual customs - technology",
    "Unusual customs - lifecycle",
    "Unusual customs - social standings",
    "Unusual customs - trade",  # 61
    "Unusual customs - nobility",
    "Unusual customs - sex",
    "Unusual customs - eating",
    "Unusual customs - travel",
    "Unusual custom - conspiracy"]
starport_str = ["A","B","C","D","E","X"]
atmosphere_to_minimum_tech_level = [8,8,5,5,3,0,0,3,0,3,8,9,10,5,5,8]

def roll1D():
    return random.randint(1,6)

def roll2D():
    return roll1D()+roll1D()

def rollD3():
    return random.randint(1,3)

def rollD66():
    return random.randint(0,35)

def new_sector():
    sector = []
    for col in range(0,sector_width*subsector_width):
        sector.append(sector_height*subsector_height*[False])
    return sector;

def sector_to_point_list(sector):
    point_list = []
    for x in range(0,sector_width*subsector_width):
        for y in range(0,sector_height*subsector_height):
            if sector[x][y]:
                point_list.append([x,y])
    return point_list

def measure_distance(a,b):
    horizontal = abs(a[0]-b[0])
    vertical = abs(2*(a[1]-b[1])+a[0]%2-b[0]%2)
    if horizontal >= vertical:
        return horizontal
    return horizontal+(vertical-horizontal)//2

def point_to_string(point):
    x = point[0]
    y = point[1]
    return "%s%s%02d%02d" % (chr(ord("A")+x//subsector_width),chr(ord("A")+y//subsector_height),x%subsector_width+1,y%subsector_height+1)

def append_potential_trade_routes(sector_demand,sector_supply,routes):
    demand_points = sector_to_point_list(sector_demand)
    supply_points = sector_to_point_list(sector_supply)
    for point_demand in demand_points:
        for point_supply in supply_points:
            distance = measure_distance(point_demand,point_supply)
            if distance > 0 and distance <= 4:
                point_a = point_demand
                point_b = point_supply
                if point_demand[0] > point_supply[0] or (point_demand[0] == point_supply[0] and point_demand[1] > point_supply[1]):
                    point_a = point_supply
                    point_b = point_demand
                try:
                    routes.index([point_a,point_b])
                except ValueError:
                    routes.append([point_a,point_b])
    return routes

def validate_potential_trade_route(position,destination,helium_left,jumps_left):
    x = position[0]
    y = position[1]
    if sector_helium_supply[x][y]:
        helium_left = 2
    elif helium_left == 0:
        return False
    if position == destination:
        return True
    if jumps_left == 0:
        return False
    helium_left -= 1
    jumps_left -= 1
    potential_jump_points = []
    if y > 0:
        potential_jump_points.append([x,y-1])
    if y < sector_height*subsector_height-1:
        potential_jump_points.append([x,y+1])
    if x > 0:
        potential_jump_points.append([x-1,y])
        if x%2 == 0:
            if y > 0:
                potential_jump_points.append([x-1,y-1])
        else:
            if y < sector_height*subsector_height-1:
                potential_jump_points.append([x-1,y+1])
    if x < sector_width*subsector_width-1:
        potential_jump_points.append([x+1,y])
        if x%2 == 0:
            if y > 0:
                potential_jump_points.append([x+1,y-1])
        else:
            if y < sector_height*subsector_height-1:
                potential_jump_points.append([x+1,y+1])
    for point in potential_jump_points:
        if validate_potential_trade_route(point,destination,helium_left,jumps_left):
            return True
    return False

sector_communication_point = new_sector()
sector_helium_supply = new_sector()
sector_material_demand = new_sector()
sector_material_supply = new_sector()
sector_food_demand = new_sector()
sector_food_supply = new_sector()
for s_x in range(0,sector_width):
    for s_y in range(0,sector_height):
        random.seed(subsector_seeds[s_x+s_y*sector_width])
        for ss_x in range(0,subsector_width):
            for ss_y in range(0,subsector_height):
                if (roll1D()+subsector_density_dm[s_x+s_y*sector_width] >= 4):
                    print("%s%s%02d%02d" % (chr(ord("A")+s_x),chr(ord("A")+s_y),ss_x+1,ss_y+1))
                    x = subsector_width*s_x+ss_x
                    y = subsector_height*s_y+ss_y
                    gas_giant = 0
                    if (roll2D() < 10):
                        gas_giant = 1
                        sector_helium_supply[x][y] = True
                    print("Gas giant: " + boolean_str[gas_giant])
                    size = roll2D()-2
                    print("Primary world:") 
                    print("- Diameter: %s" % (size_to_diameter_str[size]))
                    print("- Gravity: %s" % (size_to_gravity_str[size]))
                    panthalassic = False
                    panthalassic_str = ""
                    atmosphere = roll2D()-7+size
                    if atmosphere < 0:
                        atmosphere = 0
                    if atmosphere >= 15:
                        atmosphere = 15
                        if random.random() < subsector_unusual_atmosphere_is_panthalassic_probability[s_x+s_y*sector_width]:
                            panthalassic = True
                            panthalassic_str = " - Panthalassic"
                        else:
                            panthalassic_str = " - Not panthalassic"
                    print("- Atmosphere composition: %s%s" % (atmosphere_to_composition_str[atmosphere],panthalassic_str))
                    print("- Atmospheric pressure at sea level: %s" % (atmosphere_to_pressure_str[atmosphere]))
                    temperature = 0
                    if atmosphere >= 2:
                        edge_of_habitable_zone = random.random()
                        edge_of_habitable_zone_dm = 0
                        if edge_of_habitable_zone < 1.0-subsector_hot_edge_of_habitable_zone_probability[s_x+s_y*sector_width]:
                            edge_of_habitable_zone_dm = 4
                        elif edge_of_habitable_zone < subsector_cold_edge_of_habitable_zone_probability[s_x+s_y*sector_width]:
                            edge_of_habitable_zone_dm = -4
                        temperature_roll = roll2D()+atmosphere_to_temperature_dm[atmosphere]+edge_of_habitable_zone_dm
                        if temperature_roll <= 2:
                            temperature = 1  # Frozen
                        elif temperature_roll <= 4:
                            temperature = 2  # Cold
                        elif temperature_roll <= 9:
                            temperature = 3  # Temperate
                        elif temperature_roll <= 11:
                            temperature = 4  # Hot
                        else:
                            temperature = 5  # Boiling
                    print("- Average temperature: %s" % temperature_str[temperature])
                    hydrographics = 0
                    if size >= 2:
                        hydrographics_dm = -7+atmosphere
                        if atmosphere <= 0 or (atmosphere >= 10 and atmosphere <= 12):
                            hydrographics_dm = -4
                        if atmosphere != 13 and not panthalassic:
                            if temperature == 4:
                                hydrographics_dm += -2
                            elif temperature == 5:
                                hydrographics_dm += -6
                        hydrographics = roll2D()+hydrographics_dm
                    if hydrographics < 0:
                        hydrographics = 0
                    elif hydrographics > 10:
                        hydrographics = 10
                    print("- Sea/surface ratio: %s" % (hydrographics_str[hydrographics]))
                    population = roll2D()-2
                    government = 0
                    law_level = 0
                    tech_level = 0
                    if population == 0:
                        print("- Population: 0")
                    else:
                        print("- Population: ~10^%d" % (population))
                        government = roll2D()-7+population
                        if government < 0:
                            government = 0
                        elif government > 15:
                            government = 15
                        print("- Government: %s" % (government_str[government]))
                        factions = rollD3()
                        if government == 0 or government == 7:
                            factions += 1
                        elif government >= 10:
                            factions += -1
                        print("- Number of factions: %d" % (factions))
                        for faction in range(1,factions+1):
                            faction_form = roll2D()-7+population
                            if faction_form < 0:
                                faction_form = 0
                            elif faction_form > 15:
                                faction_form = 15
                            print("- - Faction #%d: %s, %s" % (faction,government_str[faction_form],faction_strength_str[roll2D()//2-1]))
                        print("- Culture:")
                        cultures_to_roll = 1
                        cultures_all_influenced = False
                        culture_influenced = False
                        culture_fusion = False
                        while cultures_to_roll > 0:
                            culture = rollD66()
                            if culture == 10:  # D66 == 25
                                culture_influenced = True
                                if not culture_fusion:
                                    cultures_all_influenced = True
                            elif culture == 11:  ## D66 == 26
                                culture_fusion = True
                                cultures_to_roll += 1
                            else:
                                culture_influenced_str = ""
                                if culture_influenced:
                                    culture_influenced_str = "%s - " % (culture_str[10])  # D66 == 26
                                print("- - %s%s" % (culture_influenced_str,culture_str[culture]))
                                culture_influenced = cultures_all_influenced
                                cultures_to_roll -= 1
                        law_level = roll2D()-7+government
                        if law_level < 0:
                            law_level = 0
                        print("- Law level: %d" % (law_level))
                        starport_dm = 0
                        if population >= 10:
                            starport_dm = 2
                        elif population >= 8:
                            starport_dm = 1
                        elif population <= 2:
                            starport_dm = -2
                        elif population <= 4:
                            starport_dm = -1
                        starport = roll2D()+starport_dm
                        if starport <= 2:
                            starport = 2
                        elif starport >= 11:
                            starport = 11
                        starport = (12-starport)//2
                        print("- Starport:")
                        print("- - Class: %s" % (starport_str[starport]))
                        if starport == 0:  # A
                            sector_communication_point[x][y] = True
                        if starport != 5:  # not X
                            sector_helium_supply[x][y] = True
                        base_naval = 0
                        base_scout = 0
                        base_research = 0
                        base_tas = 0
                        if starport == 0:  # A
                            if roll2D() >= 8:
                                base_naval = 1
                            if roll2D() >= 10:
                                base_scout = 1
                            if roll2D() >= 8:
                                base_research = 1
                            base_tas = 1
                        elif starport == 1:  # B
                            if roll2D() >= 8:
                                base_naval = 1
                            if roll2D() >= 8:
                                base_scout = 1
                            if roll2D() >= 10:
                                base_research = 1
                            base_tas = 1
                        elif starport == 2:  # C
                            if roll2D() >= 8:
                                base_scout = 1
                            if roll2D() >= 10:
                                base_research = 1
                            if roll2D() >= 10:
                                base_tas = 1
                        elif starport == 3:  # D
                            if roll2D() >= 7:
                                base_scout = 1
                        print("- - Naval base: %s" % (boolean_str[base_naval]))
                        if base_naval:
                            sector_communication_point[x][y] = True
                        print("- - Scout base: %s" % (boolean_str[base_scout]))
                        print("- - Research base: %s" % (boolean_str[base_research]))
                        print("- - Traveller's Aid Society (TAS) Hostel: %s" % (boolean_str[base_tas]))
                        tech_level_dm = 0
                        if starport == 0:  # A
                            tech_level_dm += 6
                        elif starport == 1:  # B
                            tech_level_dm += 4
                        elif starport == 2:  # C
                            tech_level_dm += 2
                        elif starport == 5:  # X
                            tech_level_dm += -4
                        if size <= 1:
                            tech_level_dm += 2
                        elif size <= 4:
                            tech_level_dm += 1
                        if atmosphere <= 3 or atmosphere >= 10:
                            tech_level_dm += 1
                        if hydrographics == 0 or hydrographics == 9:
                            tech_level_dm += 1
                        elif hydrographics == 10:
                            tech_level_dm += 2
                        if (population >= 1 and population <= 5) or population == 8:
                            tech_level_dm += 1
                        elif population == 9:
                            tech_level_dm += 2
                        elif population == 10:
                            tech_level_dm += 4
                        if government == 0 or government == 5:
                            tech_level_dm += 1
                        elif government == 7:
                            tech_level_dm += 2
                        elif government == 13 or government == 14:
                            tech_level_dm += -2
                        tech_level = roll1D()+tech_level_dm
                    print("- Technology level: %d" % (tech_level))
                    print("- Minimum tech level to sustain life: %d" % (atmosphere_to_minimum_tech_level[atmosphere]))
                    travel_code_str = "None"
                    if atmosphere >= 10 or government == 0 or government == 7 or government == 10 or law_level == 0 or law_level >= 9:
                        travel_code_str = "Amber"
                    print("- Travel code: %s" % (travel_code_str))
                    print("- Trade codes:")
                    if atmosphere >= 4 and atmosphere <= 9 and hydrographics <= 4 and hydrographics <= 8 and population >= 5 and population <= 7:
                        print("- - Agricultural")
                        sector_food_supply[x][y] = True
                    if size == 0 and atmosphere == 0 and hydrographics == 0:
                        print("- - Asteroid")
                        sector_material_supply[x][y] = True
                    if population == 0 and government == 0 and law_level == 0:
                        print("- - Barren")
                    if atmosphere >= 2 and hydrographics == 0:
                        print("- - Desert")
                        sector_material_supply[x][y] = True
                    if atmosphere >= 10 and hydrographics >= 1:
                        print("- - Fluid oceans")
                    if size >= 6 and size <= 8 and (atmosphere == 5 or atmosphere == 6 or atmosphere == 8) and hydrographics >= 5 and hydrographics <= 7:
                        print("- - Garden")
                        sector_food_supply[x][y] = True
                    if population >= 9:
                        print("- - High population")
                        sector_food_demand[x][y] = True
                    if tech_level >= 12:
                        print("- - High tech")
                        sector_material_demand[x][y] = True
                    if atmosphere <= 1 and hydrographics >= 1:
                        print("- - Ice-capped")
                        sector_material_supply[x][y] = True
                    if (atmosphere <= 2 or atmosphere == 4 or atmosphere == 7 or atmosphere == 9) and population >= 9:
                        print("- - Industrial")
                        sector_material_demand[x][y] = True
                    if population <= 3:
                        print("- - Low population")
                    if tech_level <= 5:
                        print("- - Low tech")
                    if atmosphere <= 3 and hydrographics <= 3 and population >= 6:
                        print("- - Non-agricultural")
                    if population <= 6:
                        print("- - Non-industrial")
                        sector_material_supply[x][y] = True
                    if atmosphere >= 2 and atmosphere <= 5 and hydrographics <= 3:
                        print("- - Poor")
                    if (atmosphere == 6 or atmosphere == 8) and population >= 6 and population <= 8 and government >= 4 and government <= 9:
                        print("- - Rich")
                        sector_food_demand[x][y] = True
                    if atmosphere == 0:
                        print("- - Vacuum")
                    if hydrographics >= 10:
                        print("- - Water world")
                        sector_food_supply[x][y] = True
                    print("")
print("Communication routes:")
communication_points_available = sector_to_point_list(sector_communication_point)
if len(communication_points_available) >= 2:
    communication_points_linked = [communication_points_available.pop(0)]
    while len(communication_points_available) > 0:
        best_distance = measure_distance(communication_points_linked[0],communication_points_available[0])
        for point_available in communication_points_available:
            for point_linked in communication_points_linked:
                distance = measure_distance(point_linked,point_available)
                if distance < best_distance:
                    best_distance = distance
        for point_available in communication_points_available:
            point_used = False
            for point_linked in communication_points_linked:
                distance = measure_distance(point_linked,point_available)
                if distance == best_distance:
                    print("- %s-%s" % (point_to_string(point_linked),point_to_string(point_available)))
                    point_used = True
            if point_used:
                communication_points_linked.append(point_available)
                communication_points_available.remove(point_available)
print("")
print("Trade routes:")
potential_trade_routes = append_potential_trade_routes(sector_food_demand,sector_food_supply,append_potential_trade_routes(sector_material_demand,sector_material_supply,[]))
for pair in potential_trade_routes:
    if validate_potential_trade_route(pair[0],pair[1],1,4):
        print("- %s-%s" % (point_to_string(pair[0]),point_to_string(pair[1])))
print("")
print("To complete manually:")
print("- Define exact numbers for ranges and approximations")
print("- Clarify composition of unusual non-panthalassic atmospheres (core rulebook page 218)")
print("- Validate cultures (core rulebook page 220)")
print("- Validate Amber travel codes (core rulebook pages 215 and 227)")
print("- Add Red travel codes (core rulebook page 215)")
print("- Validate routes (core rulebook page 215)")
print("- Add as many additional details as desired")