import math import random import time from hexgen.enums import HexEdge, Hemisphere import collections import functools from itertools import combinations class memoized(object): '''Decorator. Caches a function's return value each time it is called. If called later with the same arguments, the cached value is returned (not reevaluated). ''' def __init__(self, func): self.func = func self.cache = {} def __call__(self, *args): if not isinstance(args, collections.Hashable): # uncacheable. a list, for instance. # better to not cache than blow up. return self.func(*args) if args in self.cache: return self.cache[args] else: value = self.func(*args) self.cache[args] = value return value def __repr__(self): '''Return the function's docstring.''' return self.func.__doc__ def __get__(self, obj, objtype): '''Support instance methods.''' return functools.partial(self.__call__, obj) def blend_colors(color1, color2): return min(round((color1[0] + color2[0]) / 2), 255), \ min(round((color1[1] + color2[1]) / 2), 255), \ min(round((color1[2] + color2[2]) / 2), 255) def lighten(color, amount): return min(round(color[0] + color[0] * amount), 255), \ min(round(color[1] + color[1] * amount), 255), \ min(round(color[2] + color[2] * amount), 255) def randomize_color(color, dist=1): colors = [ color, (color[0] - dist, color[dist] - dist, color[2] - dist), (color[0] + dist, color[dist] + dist, color[2] + dist), (color[0] - dist, color[dist] + dist, color[2] - dist), (color[0] + dist, color[dist] - dist, color[2] - dist), (color[0] - dist, color[dist] + dist, color[2] - dist), (color[0] - dist, color[dist] - dist, color[2] + dist), (color[0] + dist, color[dist] - dist, color[2] + dist), (color[0] - dist, color[dist] + dist, color[2] - dist) ] return random.choice(colors) def latitude_to_number(latitude, map_size): """ Converts latitude in degrees (north is positive, south is negative) to a number corresponding to the latitude grid position """ return (map_size / 2) - ((latitude / 90) * (map_size / 2)) def pressure_at_seasons(latitude, base_pressure, pressure_diff, itcz_rise): """ latitude = latitude in degrees base_pressure = the base surface atmospheric pressure at this planet in millibars pressure_diff = the max difference in pressure at each zone itcz_rise = the rise in altitude of the ITCZ at this season at this latitude """ itcz = (-10 + itcz_rise, 10 + itcz_rise) sthz = dict(north=(20 + itcz_rise, 40 + itcz_rise), south=(-40 + itcz_rise, -20 + itcz_rise)) pf = dict(north=(50 + itcz_rise, 70 + itcz_rise), south=(-70 + itcz_rise, -50 + itcz_rise)) if itcz[0] <= latitude <= itcz[1]: # ITCZ # highest around 0 degrees final_pressure = base_pressure - (-math.pow(latitude - itcz_rise, 2) + 100) * (pressure_diff / 100) elif sthz.get('south')[0] <= latitude <= sthz.get('south')[1]: # southern STHZ # highest around -30 degrees final_pressure = base_pressure + ((-math.pow(latitude + (30 - itcz_rise), 2) + 100) / 100) * pressure_diff elif sthz.get('north')[0] <= latitude <= sthz.get('north')[1]: # northern STHZ # highest around 30 degrees final_pressure = base_pressure + ((-math.pow(latitude - (30 + itcz_rise), 2) + 100) / 100) * pressure_diff elif pf.get('south')[0] <= latitude <= pf.get('south')[1]: # southern PF # highest around -60 degrees final_pressure = base_pressure - ((-math.pow(latitude + (60 - itcz_rise), 2) + 100) / 100) * (pressure_diff / 2) elif pf.get('north')[0] <= latitude <= pf.get('north')[1]: # northern PF # highest around 60 degrees final_pressure = base_pressure - ((-math.pow(latitude - (60 + itcz_rise), 2) + 100) / 100) * (pressure_diff / 2) else: final_pressure = base_pressure + random.randint(-1, 1) return round(final_pressure) @memoized def clockwise_hex_edge(hex_edge, reverse=False): """ Given a HexEdge, return the clockwise hex edge. If reverse if True, return the anti-clockwise hex edge """ if hex_edge is HexEdge.east: return HexEdge.south_east if not reverse else HexEdge.north_east elif hex_edge is HexEdge.south_east: return HexEdge.south_west if not reverse else HexEdge.east elif hex_edge is HexEdge.south_west: return HexEdge.west if not reverse else HexEdge.south_east elif hex_edge is HexEdge.west: return HexEdge.north_west if not reverse else HexEdge.south_west elif hex_edge is HexEdge.north_west: return HexEdge.north_east if not reverse else HexEdge.west elif hex_edge is HexEdge.north_east: return HexEdge.east if not reverse else HexEdge.north_west def decide_wind(season_index, world_pressure, hexagon): """ Decide the direction and magnitude of wind at a hex season_index = 0 for end_year, 1 for mid_year world_pressure = the world's average surface air pressure hexagon = which hexagon we're making wind for """ lowest_neighbor = min(hexagon.neighbors, key=lambda h: h[1].pressure[season_index]) wind_direction = lowest_neighbor[0] neighbor = hexagon.neighbor_at(wind_direction) if neighbor.pressure[season_index] == hexagon.pressure[season_index]: return { "direction": None, "windward_hex": neighbor, "pressure_diff": 0 } if hexagon.hemisphere is Hemisphere.northern: if hexagon.pressure[season_index] <= world_pressure: # low pressure corrected_wind_direction = clockwise_hex_edge(wind_direction, True) else: # high pressure corrected_wind_direction = clockwise_hex_edge(wind_direction) else: if hexagon.pressure[season_index] <= world_pressure: # low pressure corrected_wind_direction = clockwise_hex_edge(wind_direction) else: # high pressure corrected_wind_direction = clockwise_hex_edge(wind_direction, True) windward_hex = hexagon.neighbor_at(wind_direction) pressure_diff = abs(hexagon.pressure[season_index] - windward_hex.pressure[season_index]) return { "direction": corrected_wind_direction, "windward_hex": windward_hex, "pressure_diff": pressure_diff } class Timer: def __init__(self, text, debug=True): self.text = text self.debug = debug def __enter__(self): if self.debug: print(self.text.ljust(50), end="") print('starting...') self.start = time.clock() return self def __exit__(self, *args): self.end = time.clock() self.interval = self.end - self.start if self.debug: print(self.text.ljust(50), end="") print("finished after {:0.03f} ms\n".format(self.interval * 1000)) @memoized def is_opposite_hex(my_side, other_side, strict=False): """ Given two HexEdges, are they opposite of each other? A hex is opposite of another in the following circumstances: W -> NE, E, SE NW -> E, SE, SW NE -> SE, SW, W E -> NW, W, SW SE -> NE, NW, W SW -> E, NE, NW """ opp = {} opp[HexEdge.west] = [HexEdge.east, HexEdge.north_east, HexEdge.south_east] opp[HexEdge.north_west] = [HexEdge.south_east, HexEdge.east, HexEdge.south_west] opp[HexEdge.north_east] = [HexEdge.south_west, HexEdge.south_east, HexEdge.west] opp[HexEdge.east] = [HexEdge.west, HexEdge.north_west, HexEdge.south_west] opp[HexEdge.south_east] = [HexEdge.north_west, HexEdge.north_east, HexEdge.west] opp[HexEdge.south_west] = [HexEdge.north_east, HexEdge.east, HexEdge.north_west] if strict: return other_side is opp[my_side][0] return other_side in opp[my_side] def is_isthmus(h): if h.is_water: return False water_neighbors = [h for h in h.neighbors if h[1].is_water] if len(water_neighbors) == 2: # if we have two water hexes in our neighbors, # and any one of them are opposite of another one, # we have an isthmus for one, two in combinations(water_neighbors, 2): if is_opposite_hex(one[0], two[0]): return True return False def is_peninsula(h): """ A hex is a peninsula if it has only one land neighbor """ if h.is_water: return False land_neighbors = [h for h in h.neighbors if h[1].is_land] return len(land_neighbors) == 1 def is_bay(h): if h.is_land: return False water_neighbors = [h for h in h.neighbors if h[1].is_water] return len(water_neighbors) == 1 def is_strait(h): if h.is_land: return False water_neighbors = [h for h in h.neighbors if h[1].is_water] if len(water_neighbors) == 2: return is_opposite_hex(water_neighbors[0][0], water_neighbors[1][0]) return False def first_hex_without_geoform(hexes): for y, row in enumerate(hexes): for x, col in enumerate(row): h = hexes[x][y] if h.geoform_type is None: return h return None