def setUp(self): params = default_params self.size = 50 params['size'] = self.size self.heightmap = Heightmap(params) self.grid = Grid(self.heightmap, params) self.h1 = self.grid.find_hex(0, 0) self.h4 = self.grid.find_hex(1, 0) self.h9 = self.grid.find_hex(2, 2) self.e1 = self.h1.edge_south_east self.e2 = self.h4.edge_north_west self.e3 = self.h9.edge_north_west
class TestEdge(TestCase): def setUp(self): params = default_params self.size = 50 params['size'] = self.size self.heightmap = Heightmap(params) self.grid = Grid(self.heightmap, params) self.h1 = self.grid.find_hex(0, 0) self.h4 = self.grid.find_hex(1, 0) self.h9 = self.grid.find_hex(2, 2) self.e1 = self.h1.edge_south_east self.e2 = self.h4.edge_north_west self.e3 = self.h9.edge_north_west def test_calculate(self): self.assertTrue(isinstance(self.e1, Edge), "e1 is not an Edge, calculate() " "may not be working") def test_sides(self): self.assertTrue(self.e1.side == HexSide.south_east, "Edge has wrong HexSide enum") def test_init(self): self.assertTrue( self.e1.one == self.h1, "'one' value for Hex 1 Edge 1 is not Hex 1, was {}".format( self.e1.one)) self.assertTrue( self.e1.two == self.h4, "'two' value for Hex 2 Edge 1 is not Hex 4, was {}".format( self.e1.two)) def test_equality(self): """ Tests that the __eq__ function is working correctly """ self.assertTrue(self.e1 == self.e1, "E1 equals E1") self.assertTrue( self.e1 == self.e2, "Hex 1 Edge SE and Hex 4 Edge NW should be equal, was \n{}\n{}". format(self.e1, self.e2)) self.assertFalse( self.e1 == self.e3, "Hex 1 Edge SE and Hex 9 Edge NW should not be equal")
class TestEdge(TestCase): def setUp(self): params = default_params self.size = 50 params['size'] = self.size self.heightmap = Heightmap(params) self.grid = Grid(self.heightmap, params) self.h1 = self.grid.find_hex(0, 0) self.h4 = self.grid.find_hex(1, 0) self.h9 = self.grid.find_hex(2, 2) self.e1 = self.h1.edge_south_east self.e2 = self.h4.edge_north_west self.e3 = self.h9.edge_north_west def test_calculate(self): self.assertTrue(isinstance(self.e1, Edge), "e1 is not an Edge, calculate() " "may not be working") def test_sides(self): self.assertTrue(self.e1.side == HexSide.south_east, "Edge has wrong HexSide enum") def test_init(self): self.assertTrue(self.e1.one == self.h1, "'one' value for Hex 1 Edge 1 is not Hex 1, was {}".format(self.e1.one)) self.assertTrue(self.e1.two == self.h4, "'two' value for Hex 2 Edge 1 is not Hex 4, was {}".format(self.e1.two)) def test_equality(self): """ Tests that the __eq__ function is working correctly """ self.assertTrue(self.e1 == self.e1, "E1 equals E1") self.assertTrue(self.e1 == self.e2, "Hex 1 Edge SE and Hex 4 Edge NW should be equal, was \n{}\n{}" .format(self.e1, self.e2)) self.assertFalse(self.e1 == self.e3, "Hex 1 Edge SE and Hex 9 Edge NW should not be equal")
def __init__(self, params, debug=False): """ initialize """ self.params = default_params self.params.update(params) if debug: print("Making world with params:") for key, value in params.items(): print("\t{}:\t{}".format(key, value)) self.debug = debug if type(params.get("random_seed")) is int: random.seed(params.get("random_seed")) self.heightmap = Heightmap(self.params) self.hex_grid = Grid(self.heightmap, self.params) self.rivers = [] self.rivers_sources = [] print("Computing hex distances") if self.debug else False self._get_distances() if self.params.get("hydrosphere"): self._generate_rivers() # give coastal land hexes moisture based on how close to the coast they are print("Making coastal moisture") if self.debug else False for y, row in enumerate(self.hex_grid.grid): for x, col in enumerate(row): hex = self.hex_grid.grid[x][y] if hex.is_land: if hex.distance <= 5: hex.moisture += 1 if hex.distance <= 3: hex.moisture += random.randint(1, 3) if hex.distance <= 1: hex.moisture += random.randint(1, 6) # generate aquifers num_aquifers = random.randint(5, 25) if self.params.get("hydrosphere") is False or self.params.get("sea_percent") == 100: num_aquifers = 0 print("Making {} aquifers".format(num_aquifers)) if self.debug else False aquifers = [] while len(aquifers) < num_aquifers: rx = random.randint(0, len(self.hex_grid.grid) - 1) ry = random.randint(0, len(self.hex_grid.grid) - 1) hex = self.hex_grid.grid[rx][ry] if hex.is_land and hex.moisture < 5: aquifers.append(hex) for hex in aquifers: # print("Aquifer at ", hex) r1 = hex.bubble(distance=3) for hex in r1: if hex.is_land: hex.moisture += random.randint(0, 2) r2 = hex.bubble(distance=2) for hex in r2: if hex.is_land: hex.moisture += 1 r3 = hex.surrounding for hex in r3: if hex.is_land: hex.moisture += 1 # decide terrain features print("Making terrain features") if self.debug else False # craters only form in barren planets with a normal or lower atmosphere if self.params.get("craters") is True: # decide number of craters num_craters = random.randint(0, 15) print("Making {} craters".format(num_craters)) craters = [] while len(craters) < num_craters: size = random.randint(1, 3) craters.append(dict(hex=random.choice(self.hex_grid.hexes), size=size, depth=10 * size)) for crater in craters: center_hex = crater.get("hex") size = crater.get("size") depth = crater.get("depth") hexes = [] if size >= 1: hexes = center_hex.surrounding for h in hexes: h.add_feature(HexFeature.crater) h.altitude = center_hex.altitude - 5 h.altitude = max(h.altitude, 0) if size >= 2: hexes = center_hex.bubble(distance=2) for h in hexes: h.add_feature(HexFeature.crater) h.altitude = center_hex.altitude - 10 h.altitude = max(h.altitude, 0) if size >= 3: hexes = center_hex.bubble(distance=3) for h in hexes: h.add_feature(HexFeature.crater) h.altitude = center_hex.altitude - 15 h.altitude = max(h.altitude, 0) for h in hexes[: round(len(hexes) / 3)]: for i in h.surrounding: if i.has_feature(HexFeature.crater) is False: i.add_feature(HexFeature.crater) i.altitude = center_hex.altitude - 20 i.altitude = max(i.altitude, 0) # volcanoes if self.params.get("volcanoes"): num_volcanoes = random.randint(0, 10) print("Making {} volcanoes".format(num_volcanoes)) volcanoes = [] while len(volcanoes) < num_volcanoes: center_hex = random.choice(self.hex_grid.hexes) if center_hex.altitude > 50: size = random.randint(1, 5) height = random.randint(30, 70) volcanoes.append(dict(hex=center_hex, size=size, height=height)) for volcano in volcanoes: height = volcano.get("height") size = volcano.get("size") center_hex = volcano.get("hex") print("\tVolcano: Size: {}, Height: {}".format(size, height)) size_list = list(range(size)) size_list.reverse() hexes = [] for i in size_list: i += 1 this_height = round(height / i) if i == 1: l = center_hex.surrounding + [center_hex] else: l = center_hex.bubble(distance=i) for h in l: hexes.append(h) h.altitude = center_hex.altitude + this_height h.add_feature(HexFeature.volcano) last_altitude = 0 for h in hexes[: round(len(hexes) / 2)]: for i in h.surrounding: if i.has_feature(HexFeature.volcano) is False: i.add_feature(HexFeature.volcano) i.altitude += 5 i.altitude = min(i.altitude, 255) last_altitude = i.altitude center_hex.altitude += last_altitude + 5 # lava flow def step(active_hex): if active_hex.altitude < 50: return else: active_hex.add_feature(HexFeature.lava_flow) found = [] for i in active_hex.surrounding: if i.altitude <= active_hex.altitude and i.has_feature(HexFeature.lava_flow) is False: found.append(i) found.sort(key=lambda x: x.altitude) if len(found) > 1: step(found[0]) if len(found) > 2: step(found[1]) step(center_hex) self.territories = [] self.generate_territories() self.generate_resources() print("Done") if self.debug else False
class MapGen: """ generates a heightmap as an array of integers between 1 and 255 using the diamond-square algorithm""" def __init__(self, params, debug=False): """ initialize """ self.params = default_params self.params.update(params) if debug: print("Making world with params:") for key, value in params.items(): print("\t{}:\t{}".format(key, value)) self.debug = debug if type(params.get("random_seed")) is int: random.seed(params.get("random_seed")) self.heightmap = Heightmap(self.params) self.hex_grid = Grid(self.heightmap, self.params) self.rivers = [] self.rivers_sources = [] print("Computing hex distances") if self.debug else False self._get_distances() if self.params.get("hydrosphere"): self._generate_rivers() # give coastal land hexes moisture based on how close to the coast they are print("Making coastal moisture") if self.debug else False for y, row in enumerate(self.hex_grid.grid): for x, col in enumerate(row): hex = self.hex_grid.grid[x][y] if hex.is_land: if hex.distance <= 5: hex.moisture += 1 if hex.distance <= 3: hex.moisture += random.randint(1, 3) if hex.distance <= 1: hex.moisture += random.randint(1, 6) # generate aquifers num_aquifers = random.randint(5, 25) if self.params.get("hydrosphere") is False or self.params.get("sea_percent") == 100: num_aquifers = 0 print("Making {} aquifers".format(num_aquifers)) if self.debug else False aquifers = [] while len(aquifers) < num_aquifers: rx = random.randint(0, len(self.hex_grid.grid) - 1) ry = random.randint(0, len(self.hex_grid.grid) - 1) hex = self.hex_grid.grid[rx][ry] if hex.is_land and hex.moisture < 5: aquifers.append(hex) for hex in aquifers: # print("Aquifer at ", hex) r1 = hex.bubble(distance=3) for hex in r1: if hex.is_land: hex.moisture += random.randint(0, 2) r2 = hex.bubble(distance=2) for hex in r2: if hex.is_land: hex.moisture += 1 r3 = hex.surrounding for hex in r3: if hex.is_land: hex.moisture += 1 # decide terrain features print("Making terrain features") if self.debug else False # craters only form in barren planets with a normal or lower atmosphere if self.params.get("craters") is True: # decide number of craters num_craters = random.randint(0, 15) print("Making {} craters".format(num_craters)) craters = [] while len(craters) < num_craters: size = random.randint(1, 3) craters.append(dict(hex=random.choice(self.hex_grid.hexes), size=size, depth=10 * size)) for crater in craters: center_hex = crater.get("hex") size = crater.get("size") depth = crater.get("depth") hexes = [] if size >= 1: hexes = center_hex.surrounding for h in hexes: h.add_feature(HexFeature.crater) h.altitude = center_hex.altitude - 5 h.altitude = max(h.altitude, 0) if size >= 2: hexes = center_hex.bubble(distance=2) for h in hexes: h.add_feature(HexFeature.crater) h.altitude = center_hex.altitude - 10 h.altitude = max(h.altitude, 0) if size >= 3: hexes = center_hex.bubble(distance=3) for h in hexes: h.add_feature(HexFeature.crater) h.altitude = center_hex.altitude - 15 h.altitude = max(h.altitude, 0) for h in hexes[: round(len(hexes) / 3)]: for i in h.surrounding: if i.has_feature(HexFeature.crater) is False: i.add_feature(HexFeature.crater) i.altitude = center_hex.altitude - 20 i.altitude = max(i.altitude, 0) # volcanoes if self.params.get("volcanoes"): num_volcanoes = random.randint(0, 10) print("Making {} volcanoes".format(num_volcanoes)) volcanoes = [] while len(volcanoes) < num_volcanoes: center_hex = random.choice(self.hex_grid.hexes) if center_hex.altitude > 50: size = random.randint(1, 5) height = random.randint(30, 70) volcanoes.append(dict(hex=center_hex, size=size, height=height)) for volcano in volcanoes: height = volcano.get("height") size = volcano.get("size") center_hex = volcano.get("hex") print("\tVolcano: Size: {}, Height: {}".format(size, height)) size_list = list(range(size)) size_list.reverse() hexes = [] for i in size_list: i += 1 this_height = round(height / i) if i == 1: l = center_hex.surrounding + [center_hex] else: l = center_hex.bubble(distance=i) for h in l: hexes.append(h) h.altitude = center_hex.altitude + this_height h.add_feature(HexFeature.volcano) last_altitude = 0 for h in hexes[: round(len(hexes) / 2)]: for i in h.surrounding: if i.has_feature(HexFeature.volcano) is False: i.add_feature(HexFeature.volcano) i.altitude += 5 i.altitude = min(i.altitude, 255) last_altitude = i.altitude center_hex.altitude += last_altitude + 5 # lava flow def step(active_hex): if active_hex.altitude < 50: return else: active_hex.add_feature(HexFeature.lava_flow) found = [] for i in active_hex.surrounding: if i.altitude <= active_hex.altitude and i.has_feature(HexFeature.lava_flow) is False: found.append(i) found.sort(key=lambda x: x.altitude) if len(found) > 1: step(found[0]) if len(found) > 2: step(found[1]) step(center_hex) self.territories = [] self.generate_territories() self.generate_resources() print("Done") if self.debug else False def generate_resources(self): print("Placing resources") ratings = HexResourceRating.list() types = HexResourceType.list() combined = [] for r in ratings: for t in types: combined.append(dict(rating=r, type=t)) for h in self.hex_grid.hexes: for resource in combined: chance = (resource.get("rating").rarity * resource.get("type").rarity * self.hex_grid.size / 1000) / ( math.pow(self.hex_grid.size, 2) ) given = random.uniform(0, 1) if given <= chance: h.resource = resource def generate_territories(self): """ Makes territories """ # select number of territories to place land_percent = 100 - self.params.get("sea_percent") num_territories = self.params.get("num_territories") # give each a land pixel to start print("Making {} territories".format(num_territories)) if self.debug else False c = 0 if num_territories == 0: return while len(self.territories) < num_territories: rx = random.randint(0, len(self.hex_grid.grid) - 1) ry = random.randint(0, len(self.hex_grid.grid) - 1) hex_s = self.hex_grid.grid[rx][ry] if hex_s.is_land: color = random.randint(0, 255), random.randint(0, 255), random.randint(0, 255) self.territories.append(Territory(self.hex_grid, hex_s, c, color)) c += 1 # loop over each, adding hexes total_hexes = self.hex_grid.size * self.hex_grid.size count = 0 while count < total_hexes: # i in range(0, 15): count = 0 # print("Start: {} < {}".format(count, total_hexes)) territories = self.territories random.shuffle(territories) for t in territories: frontier = t.frontier for f in frontier: if f.is_owned is False: f.territory = t t.members.append(f) t.last_added.append(f) count += t.size # print("End: {} < {}".format(count, total_hexes)) # remove water hexes for t in self.territories: members = t.members t.members = [h for h in t.members if h.is_land] water_hexes = (h for h in members if h.is_water) for h in water_hexes: h.territory = None # merge territories print("Merging barren territories") if self.params.get("num_territories") > 0: top = [] bottom = [] for t in self.territories: avg_x = round(sum([i.x for i in t.members]) / len(t.members)) if t.avg_temp < 0 and (avg_x / self.hex_grid.size) < 0.5: top.append(t) elif t.avg_temp < 0 and (avg_x / self.hex_grid.size) >= 0.5: bottom.append(t) pick_top = None pick_bottom = None if len(top) > 0: print("Merging {} territories from the top of the map".format(len(top))) pick_top = random.choice(top) top.remove(pick_top) for t in self.territories: if t in top: pick_top.members += t.members t.members = [] if len(bottom) > 0: print("Merging {} territories from the bottom of the map".format(len(bottom))) pick_bottom = random.choice(bottom) bottom.remove(pick_bottom) for t in self.territories: if t in bottom: pick_bottom.members += t.members t.members = [] if len(top) > 0: for h in pick_top.members: h.territory = pick_top if len(bottom) > 0: for h in pick_bottom.members: h.territory = pick_bottom self.territories = [t for t in self.territories if t is not None] print( "{} empty territories being deleted".format(len([t for t in self.territories if len(t.members) == 0])) ) self.territories = [t for t in self.territories if len(t.members) > 0] print("There are now {} territories".format(len(self.territories))) print("Splitting territories into contiguous blocks") if self.debug else False for t in self.territories: t.find_groups() def _get_distances(self): """ Gets the distances each land pixel is to the coastline. TODO: Make this more efficient """ if not self.params.get("hydrosphere"): # we don't care about distances otherwise return for y, row in enumerate(self.hex_grid.grid): for x, col in enumerate(row): h = self.hex_grid.grid[x][y] if h.is_land: count = 1 numbers = [] east = h.hex_east while east.is_land is True and count < self.hex_grid.size * 2: east = east.hex_east count += 1 numbers.append(count) count = 1 west = h.hex_west while west.is_land is True and count < self.hex_grid.size * 2: west = west.hex_west count += 1 numbers.append(count) count = 1 north_east = h.hex_north_east while north_east.is_land is True and count < self.hex_grid.size * 2: north_east = north_east.hex_north_east count += 1 numbers.append(count) count = 1 north_west = h.hex_north_west while north_west.is_land is True and count < self.hex_grid.size * 2: north_west = north_west.hex_north_west count += 1 numbers.append(count) count = 1 south_west = h.hex_south_west while south_west.is_land is True and count < self.hex_grid.size * 2: south_west = south_west.hex_south_west count += 1 numbers.append(count) count = 1 south_east = h.hex_south_east while south_east.is_land is True and count < self.hex_grid.size * 2: south_east = south_east.hex_south_east count += 1 numbers.append(count) h.distance = min(numbers) def _generate_rivers(self): """ For each river source edge: If the "down" hex of the left edge is the "one" or "two" hexes of this edge, the left edge is invalid If the "down" hex of the right edge is the "one" or "two" hexes of this edge, the right edge is invalid If two are valid: E = lowest slope edge If E.down is below sea level, end here otherwise add this edge as a river segment else if one is valid: E = valid edge If E.down is below sea level, end here otherwise add this edge as a river segment else if both are invalid: Make a lake at the lowest of the "one" or "two" hexes of this edge Make a new river source edge at an random edge pointing out from this lake that has a direction pointing out from the lake """ land_percent = 100 - self.params.get("sea_percent") num_rivers = self.params.get("num_rivers") print("Making {} rivers".format(num_rivers)) if self.debug else False while len(self.rivers_sources) < num_rivers: rx = random.randint(0, self.hex_grid.size - 1) ry = random.randint(0, self.hex_grid.size - 1) hex_s = self.hex_grid.find_hex(rx, ry) if hex_s.is_inland and hex_s.altitude > self.hex_grid.sealevel + 35: if hex_s.temperature < 0: # don't place rivers above +35 altitude when the temperature is below zero continue random_side = random.choice(list(HexSide)) # print("Placing river source at {}, {}".format(rx, ry)) self.rivers_sources.append(RiverSegment(self.hex_grid, rx, ry, random_side, True)) print("Placed river sources") if self.debug else False for r in self.rivers_sources: # loop over each source segment segment = r # river segment we are looking at finished = False last_unselected = None # we stop only when one of two things happen: # - a lake is formed # - we reached sea level while finished is False: # print("Segment: {}, {}".format(segment.x, segment.y)) side_one, side_two = segment.side.branching(segment.edge.direction) down = segment.edge.down # down-slope hex of this segment # add the moisture to the one and two hexes in a 3 radius one = segment.edge.one.bubble(distance=3) two = segment.edge.two.bubble(distance=3) both = list(set(one + two)) for hex in both: if hex.is_land: hex.moisture += 1 three = segment.edge.one.surrounding four = segment.edge.two.surrounding both = list(set(three + four)) for hex in both: if hex.is_land: hex.moisture += 1 # find the two Edges from the sides found branching edge_one = down.get_edge(side_one) edge_two = down.get_edge(side_two) # check if either of the edges are valid river segment locations one_valid = True two_valid = True if edge_one.down == segment.edge.one or edge_one.down == segment.edge.two: one_valid = False if edge_two.down == segment.edge.one or edge_two.down == segment.edge.two: two_valid = False if self.is_river(edge_one): one_valid = False elif self.is_river(edge_two): two_valid = False if one_valid and two_valid: # print("\tBoth are valid edges") if edge_one.down.altitude < edge_two.down.altitude: selected = edge_one selected_side = side_one last_unselected = edge_two, side_two else: selected = edge_two selected_side = side_two last_unselected = edge_one, side_one if selected.down.altitude < self.hex_grid.sealevel: finished = True segment.next = RiverSegment(self.hex_grid, selected.one.x, selected.one.y, selected_side, False) segment = segment.next elif one_valid is True and two_valid is False: # print("\tOne is Valid") selected = edge_one last_unselected = edge_two, side_two if selected.down.altitude < self.hex_grid.sealevel: finished = True segment.next = RiverSegment(self.hex_grid, selected.one.x, selected.one.y, side_one, False) segment = segment.next elif one_valid is False and two_valid is True: # print("\tTwo is valid") selected = edge_two last_unselected = edge_one, side_one if selected.down.altitude < self.hex_grid.sealevel: finished = True segment.next = RiverSegment(self.hex_grid, selected.one.x, selected.one.y, side_two, False) segment = segment.next else: # import ipdb; ipdb.set_trace() # segment.x = last_unselected[0].one.x # segment.y = last_unselected[0].one.y # segment.side = last_unselected[1] # segment.is_source = True # finished = True # print("huh?") # both edges are invalid, make lake at one or two if segment.edge.one.altitude < segment.edge.two.altitude: lake = segment.edge.one else: lake = segment.edge.two lake.add_feature(HexFeature.lake) # moisture around lake increases surrounding = lake.surrounding for hex in surrounding: if hex.is_land: hex.moisture += 3 # print("\tMade a lake at {}, {}".format(segment.x, segment.y)) # # # make a new source river at an outer edge of the lake # chosen_edge = random.choice(lake.outer_edges) # self.rivers_sources.append(RiverSegment(self.hex_grid, chosen_edge.one.x, chosen_edge.one.y, chosen_edge.side, True)) finished = True final = [] for r in self.rivers_sources: # remove rivers that are too small if r.size > 2: final.append(r) while r.next is not None: # print("Segment: ", r.next) final.append(r.next) r = r.next self.rivers = final def is_river(self, edge): """ Determines if an edge has a river :param edge: Edge :return: Boolean """ for r in self.rivers_sources: while r.next is not None: if r.edge == edge: return True r = r.next return False def find_river(self, x, y): """ Finds river segments at an hex's x and y coordinates. Returns a list of EdgeSides representing where the river segments are """ seg = [] for s in self.rivers: if s.x == x and s.y == y: seg.append(s.side) return seg
def __init__(self, params, debug=False): """ initialize """ self.params = default_params self.params.update(params) if debug: print("Making world with params:") for key, value in params.items(): print("\t{}:\t{}".format(key, value)) self.debug = debug if type(params.get('random_seed')) is int: random.seed(params.get('random_seed')) with Timer("Building Heightmap", self.debug): self.heightmap = Heightmap(self.params, self.debug) self.hex_grid = Grid(self.heightmap, self.params) if self.debug is True: print("\tAverage Height: {}".format(self.hex_grid.average_height)) print("\tHighest Height: {}".format(self.hex_grid.highest_height)) print("\tLowest Height: {}".format(self.hex_grid.lowest_height)) print("Making calendar") self.calendar = Calendar(self.params.get('year_length'), self.params.get('day_length')) self.rivers = [] self.rivers_sources = [] with Timer("Computing hex distances", self.debug): self._get_distances() self._generate_pressure() if self.params.get('hydrosphere'): self._generate_rivers() # give coastal land hexes moisture based on how close to the coast they are # TODO: replace with more realistic model print("Making coastal moisture") if self.debug else False for y, row in enumerate(self.hex_grid.grid): for x, col in enumerate(row): hex = self.hex_grid.grid[x][y] if hex.is_land: if hex.distance <= 5: hex.moisture += 1 if hex.distance <= 3: hex.moisture += random.randint(1, 3) if hex.distance <= 1: hex.moisture += random.randint(1, 6) # generate aquifers num_aquifers = random.randint(5, 25) if self.params.get('hydrosphere') is False or self.params.get( 'sea_percent') == 100: num_aquifers = 0 print( "Making {} aquifers".format(num_aquifers)) if self.debug else False aquifers = [] while len(aquifers) < num_aquifers: rx = random.randint(0, len(self.hex_grid.grid) - 1) ry = random.randint(0, len(self.hex_grid.grid) - 1) hex = self.hex_grid.grid[rx][ry] if hex.is_land and hex.moisture < 5: aquifers.append(hex) for hex in aquifers: # print("Aquifer at ", hex) r1 = hex.bubble(distance=3) for hex in r1: if hex.is_land: hex.moisture += random.randint(0, 2) r2 = hex.bubble(distance=2) for hex in r2: if hex.is_land: hex.moisture += 1 r3 = hex.surrounding for hex in r3: if hex.is_land: hex.moisture += 1 # decide terrain features print("Making terrain features") if self.debug else False # craters only form in barren planets with a normal or lower atmosphere if self.params.get('craters') is True: # decide number of craters num_craters = random.randint(0, 15) print("Making {} craters".format(num_craters)) craters = [] while len(craters) < num_craters: size = random.randint(1, 3) craters.append( dict(hex=random.choice(self.hex_grid.hexes), size=size, depth=10 * size)) for crater in craters: center_hex = crater.get('hex') size = crater.get('size') depth = crater.get('depth') hexes = [] if size >= 1: hexes = center_hex.surrounding for h in hexes: h.add_feature(HexFeature.crater) h.altitude = center_hex.altitude - 5 h.altitude = max(h.altitude, 0) if size >= 2: hexes = center_hex.bubble(distance=2) for h in hexes: h.add_feature(HexFeature.crater) h.altitude = center_hex.altitude - 10 h.altitude = max(h.altitude, 0) if size >= 3: hexes = center_hex.bubble(distance=3) for h in hexes: h.add_feature(HexFeature.crater) h.altitude = center_hex.altitude - 15 h.altitude = max(h.altitude, 0) for h in hexes[:round(len(hexes) / 3)]: for i in h.surrounding: if i.has_feature(HexFeature.crater) is False: i.add_feature(HexFeature.crater) i.altitude = center_hex.altitude - 20 i.altitude = max(i.altitude, 0) # volcanoes if self.params.get('volcanoes'): num_volcanoes = random.randint(0, 10) print("Making {} volcanoes".format(num_volcanoes)) volcanoes = [] while len(volcanoes) < num_volcanoes: center_hex = random.choice(self.hex_grid.hexes) if center_hex.altitude > 50: size = random.randint(1, 5) height = random.randint(30, 70) volcanoes.append( dict(hex=center_hex, size=size, height=height)) for volcano in volcanoes: height = volcano.get('height') size = volcano.get('size') center_hex = volcano.get('hex') print("\tVolcano: Size: {}, Height: {}".format(size, height)) size_list = list(range(size)) size_list.reverse() hexes = [] for i in size_list: i += 1 this_height = round(height / i) if i == 1: l = center_hex.surrounding + [center_hex] else: l = center_hex.bubble(distance=i) for h in l: hexes.append(h) h.altitude = center_hex.altitude + this_height h.add_feature(HexFeature.volcano) last_altitude = 0 for h in hexes[:round(len(hexes) / 2)]: for i in h.surrounding: if i.has_feature(HexFeature.volcano) is False: i.add_feature(HexFeature.volcano) i.altitude += 5 i.altitude = min(i.altitude, 255) last_altitude = i.altitude center_hex.altitude += last_altitude + 5 # lava flow def step(active_hex): if active_hex.altitude < 50: return else: active_hex.add_feature(HexFeature.lava_flow) found = [] for i in active_hex.surrounding: if i.altitude <= active_hex.altitude and i.has_feature( HexFeature.lava_flow) is False: found.append(i) found.sort(key=lambda x: x.altitude) if len(found) > 1: step(found[0]) if len(found) > 2: step(found[1]) step(center_hex) self.territories = [] self.generate_territories() self.generate_resources() self.geoforms = [] self._determine_landforms() print("Done") if self.debug else False
class MapGen: """ generates a heightmap as an array of integers between 1 and 255 using the diamond-square algorithm""" def __init__(self, params, debug=False): """ initialize """ self.params = default_params self.params.update(params) if debug: print("Making world with params:") for key, value in params.items(): print("\t{}:\t{}".format(key, value)) self.debug = debug if type(params.get('random_seed')) is int: random.seed(params.get('random_seed')) with Timer("Building Heightmap", self.debug): self.heightmap = Heightmap(self.params, self.debug) self.hex_grid = Grid(self.heightmap, self.params) if self.debug is True: print("\tAverage Height: {}".format(self.hex_grid.average_height)) print("\tHighest Height: {}".format(self.hex_grid.highest_height)) print("\tLowest Height: {}".format(self.hex_grid.lowest_height)) print("Making calendar") self.calendar = Calendar(self.params.get('year_length'), self.params.get('day_length')) self.rivers = [] self.rivers_sources = [] with Timer("Computing hex distances", self.debug): self._get_distances() self._generate_pressure() if self.params.get('hydrosphere'): self._generate_rivers() # give coastal land hexes moisture based on how close to the coast they are # TODO: replace with more realistic model print("Making coastal moisture") if self.debug else False for y, row in enumerate(self.hex_grid.grid): for x, col in enumerate(row): hex = self.hex_grid.grid[x][y] if hex.is_land: if hex.distance <= 5: hex.moisture += 1 if hex.distance <= 3: hex.moisture += random.randint(1, 3) if hex.distance <= 1: hex.moisture += random.randint(1, 6) # generate aquifers num_aquifers = random.randint(5, 25) if self.params.get('hydrosphere') is False or self.params.get( 'sea_percent') == 100: num_aquifers = 0 print( "Making {} aquifers".format(num_aquifers)) if self.debug else False aquifers = [] while len(aquifers) < num_aquifers: rx = random.randint(0, len(self.hex_grid.grid) - 1) ry = random.randint(0, len(self.hex_grid.grid) - 1) hex = self.hex_grid.grid[rx][ry] if hex.is_land and hex.moisture < 5: aquifers.append(hex) for hex in aquifers: # print("Aquifer at ", hex) r1 = hex.bubble(distance=3) for hex in r1: if hex.is_land: hex.moisture += random.randint(0, 2) r2 = hex.bubble(distance=2) for hex in r2: if hex.is_land: hex.moisture += 1 r3 = hex.surrounding for hex in r3: if hex.is_land: hex.moisture += 1 # decide terrain features print("Making terrain features") if self.debug else False # craters only form in barren planets with a normal or lower atmosphere if self.params.get('craters') is True: # decide number of craters num_craters = random.randint(0, 15) print("Making {} craters".format(num_craters)) craters = [] while len(craters) < num_craters: size = random.randint(1, 3) craters.append( dict(hex=random.choice(self.hex_grid.hexes), size=size, depth=10 * size)) for crater in craters: center_hex = crater.get('hex') size = crater.get('size') depth = crater.get('depth') hexes = [] if size >= 1: hexes = center_hex.surrounding for h in hexes: h.add_feature(HexFeature.crater) h.altitude = center_hex.altitude - 5 h.altitude = max(h.altitude, 0) if size >= 2: hexes = center_hex.bubble(distance=2) for h in hexes: h.add_feature(HexFeature.crater) h.altitude = center_hex.altitude - 10 h.altitude = max(h.altitude, 0) if size >= 3: hexes = center_hex.bubble(distance=3) for h in hexes: h.add_feature(HexFeature.crater) h.altitude = center_hex.altitude - 15 h.altitude = max(h.altitude, 0) for h in hexes[:round(len(hexes) / 3)]: for i in h.surrounding: if i.has_feature(HexFeature.crater) is False: i.add_feature(HexFeature.crater) i.altitude = center_hex.altitude - 20 i.altitude = max(i.altitude, 0) # volcanoes if self.params.get('volcanoes'): num_volcanoes = random.randint(0, 10) print("Making {} volcanoes".format(num_volcanoes)) volcanoes = [] while len(volcanoes) < num_volcanoes: center_hex = random.choice(self.hex_grid.hexes) if center_hex.altitude > 50: size = random.randint(1, 5) height = random.randint(30, 70) volcanoes.append( dict(hex=center_hex, size=size, height=height)) for volcano in volcanoes: height = volcano.get('height') size = volcano.get('size') center_hex = volcano.get('hex') print("\tVolcano: Size: {}, Height: {}".format(size, height)) size_list = list(range(size)) size_list.reverse() hexes = [] for i in size_list: i += 1 this_height = round(height / i) if i == 1: l = center_hex.surrounding + [center_hex] else: l = center_hex.bubble(distance=i) for h in l: hexes.append(h) h.altitude = center_hex.altitude + this_height h.add_feature(HexFeature.volcano) last_altitude = 0 for h in hexes[:round(len(hexes) / 2)]: for i in h.surrounding: if i.has_feature(HexFeature.volcano) is False: i.add_feature(HexFeature.volcano) i.altitude += 5 i.altitude = min(i.altitude, 255) last_altitude = i.altitude center_hex.altitude += last_altitude + 5 # lava flow def step(active_hex): if active_hex.altitude < 50: return else: active_hex.add_feature(HexFeature.lava_flow) found = [] for i in active_hex.surrounding: if i.altitude <= active_hex.altitude and i.has_feature( HexFeature.lava_flow) is False: found.append(i) found.sort(key=lambda x: x.altitude) if len(found) > 1: step(found[0]) if len(found) > 2: step(found[1]) step(center_hex) self.territories = [] self.generate_territories() self.generate_resources() self.geoforms = [] self._determine_landforms() print("Done") if self.debug else False def generate_resources(self): print("Placing resources") ratings = HexResourceRating.list() types = HexResourceType.list() combined = [] for r in ratings: for t in types: combined.append(dict(rating=r, type=t)) for h in self.hex_grid.hexes: for resource in combined: chance = (resource.get('rating').rarity * resource.get('type').rarity * self.hex_grid.size / 1000) / (math.pow(self.hex_grid.size, 2)) given = random.uniform(0, 1) if given <= chance: h.resource = resource def generate_territories(self): """ Makes territories """ # select number of territories to place land_percent = 100 - self.params.get('sea_percent') num_territories = self.params.get('num_territories') # give each a land pixel to start print("Making {} territories".format( num_territories)) if self.debug else False c = 0 if num_territories == 0: return while len(self.territories) < num_territories: rx = random.randint(0, len(self.hex_grid.grid) - 1) ry = random.randint(0, len(self.hex_grid.grid) - 1) hex_s = self.hex_grid.grid[rx][ry] if hex_s.is_land: color = random.randint(0, 255), random.randint( 0, 255), random.randint(0, 255) self.territories.append( Territory(self.hex_grid, hex_s, c, color)) c += 1 # loop over each, adding hexes total_hexes = self.hex_grid.size * self.hex_grid.size count = 0 while count < total_hexes: # i in range(0, 15): count = 0 # print("Start: {} < {}".format(count, total_hexes)) territories = self.territories random.shuffle(territories) for t in territories: frontier = t.frontier for f in frontier: if f.is_owned is False: f.territory = t t.members.append(f) t.last_added.append(f) count += t.size # print("End: {} < {}".format(count, total_hexes)) # remove water hexes for t in self.territories: members = t.members t.members = [h for h in t.members if h.is_land] water_hexes = (h for h in members if h.is_water) for h in water_hexes: h.territory = None # merge territories print("Merging barren territories") if self.params.get('num_territories') > 0: top = [] bottom = [] for t in self.territories: avg_x = round(sum([i.x for i in t.members]) / len(t.members)) if t.avg_temp < 0 and (avg_x / self.hex_grid.size) < 0.5: top.append(t) elif t.avg_temp < 0 and (avg_x / self.hex_grid.size) >= 0.5: bottom.append(t) pick_top = None pick_bottom = None if len(top) > 0: print("Merging {} territories from the top of the map".format( len(top))) pick_top = random.choice(top) top.remove(pick_top) for t in self.territories: if t in top: pick_top.members += t.members t.members = [] if len(bottom) > 0: print( "Merging {} territories from the bottom of the map".format( len(bottom))) pick_bottom = random.choice(bottom) bottom.remove(pick_bottom) for t in self.territories: if t in bottom: pick_bottom.members += t.members t.members = [] if len(top) > 0: for h in pick_top.members: h.territory = pick_top if len(bottom) > 0: for h in pick_bottom.members: h.territory = pick_bottom self.territories = [t for t in self.territories if t is not None] print("{} empty territories being deleted".format( len([t for t in self.territories if len(t.members) == 0]))) self.territories = [ t for t in self.territories if len(t.members) > 0 ] print("There are now {} territories".format(len(self.territories))) print("Splitting territories into contiguous blocks" ) if self.debug else False for t in self.territories: t.find_groups() def _get_distances(self): """ Gets the distances each land pixel is to the coastline. TODO: Make this more efficient """ if not self.params.get('hydrosphere'): # we don't care about distances otherwise return for y, row in enumerate(self.hex_grid.grid): for x, col in enumerate(row): h = self.hex_grid.grid[x][y] if h.is_land: count = 1 numbers = [] east = h.hex_east while east.is_land is True and count < self.hex_grid.size * 2: east = east.hex_east count += 1 numbers.append(count) count = 1 west = h.hex_west while west.is_land is True and count < self.hex_grid.size * 2: west = west.hex_west count += 1 numbers.append(count) count = 1 north_east = h.hex_north_east while north_east.is_land is True and count < self.hex_grid.size * 2: north_east = north_east.hex_north_east count += 1 numbers.append(count) count = 1 north_west = h.hex_north_west while north_west.is_land is True and count < self.hex_grid.size * 2: north_west = north_west.hex_north_west count += 1 numbers.append(count) count = 1 south_west = h.hex_south_west while south_west.is_land is True and count < self.hex_grid.size * 2: south_west = south_west.hex_south_west count += 1 numbers.append(count) count = 1 south_east = h.hex_south_east while south_east.is_land is True and count < self.hex_grid.size * 2: south_east = south_east.hex_south_east count += 1 numbers.append(count) h.distance = min(numbers) def _generate_pressure(self): with Timer("Generating pressure", self.debug): base_pressure = self.hex_grid.params.get('surface_pressure') with Timer(" calculating pressure zones", self.debug): # calcualte pressure caused by pressure zones pressure_diff = random.randint(3, 5) for y, row in enumerate(self.hex_grid.grid): for x, col in enumerate(row): h = self.hex_grid.grid[x][y] # end_year is winter, mid_year is summer if h.is_land: max_shift = round(h.distance / 2) end_year = pressure_at_seasons( h.latitude, base_pressure, pressure_diff, -max_shift) mid_year = pressure_at_seasons( h.latitude, base_pressure, pressure_diff, max_shift) else: max_shift = min( 6, 0.005 * round(self.hex_grid.sealevel - h.latitude)) end_year = pressure_at_seasons( h.latitude, base_pressure, pressure_diff, -max_shift) mid_year = pressure_at_seasons( h.latitude, base_pressure, pressure_diff, max_shift) h.pressure = (end_year, mid_year) # sort all hexes by land and water, lowest to highest with Timer(" sorting hexes into groups", self.debug): land_hexes = [h for h in self.hex_grid.hexes if h.is_land] water_hexes = [h for h in self.hex_grid.hexes if not h.is_land] land_hexes.sort(key=lambda x: x.altitude, reverse=True) water_hexes.sort(key=lambda x: x.altitude) def decide_change(h, incr): if h.is_land: if h.hemisphere is Hemisphere.northern: # winter / increase # summer / decrease return (h.pressure[0] + incr, h.pressure[1] - incr) elif h.hemisphere is Hemisphere.southern: # winter / decrease # summer / increase return (h.pressure[0] - incr, h.pressure[1] + incr) else: if h.hemisphere is Hemisphere.northern: # winter / decrease # summer / increase return (h.pressure[0] - incr, h.pressure[1] + incr) elif h.hemisphere is Hemisphere.southern: # winter / increase # summer / decrease return (h.pressure[0] + incr, h.pressure[1] - incr) # return (h.pressure[0], h.pressure[1]) def brush(percent, incr): matching_land_hexes = land_hexes[ 0:round(len(land_hexes) * percent)] matching_water_hexes = water_hexes[ 0:round(len(water_hexes) * percent)] for h in matching_land_hexes: for h in h.bubble(3): h.pressure = decide_change(h, incr * h.zone.incr) for h in matching_water_hexes: for h in h.bubble(3): h.pressure = decide_change(h, incr * h.zone.incr) brush(0.80, 0.05) brush(0.30, 0.10) brush(0.10, 0.10) # decide wind directions # Wind consists of a HexEdge direction and a magnitude that is equal to the difference in pressure # Wind direction is always to the neighbor with the lowest pressure, # deflected by the following rules: # Northern Hemisphere: # high pressure areas: clockwise # low pressure areas: counter-clockwise # Southern Hemisphere: # high pressure areas: counter-clockwise # low pressure areas: clockwise with Timer("Generating wind", self.debug): for y, row in enumerate(self.hex_grid.grid): for x, col in enumerate(row): h = self.hex_grid.grid[x][y] h.wind = (decide_wind(0, base_pressure, h), decide_wind(1, base_pressure, h)) # IDEA 1 # If hex is warmer than downstream: increase temperature downstream 20 hexes # If hex is colder than downstream: decrease temperature downstream 20 hexes # downstream hexes are neighboring hexes that have lower pressure # magnitude depends on pressure difference # Visit each hex. Steps: N*N*20 where N is map size # IDEA 2 # going downstream, bring each hex's temperature close to the starting hex's temperature # less and less each loop, with loop 1 being very close and loop 20 being barely changed # IDEA 3 # Visiting every hex, start a loop downstream until you reach a hex you have already visited # in this loop, averaging every hex's temperature with the last visited hex's temperature # weighted average favoring hex's base temperature when wind is less strong def windgust(season_index, starting_hex, loops=20): downstream_hex = starting_hex.wind[season_index].get( 'windward_hex') temp_change = ((20 / (loops + 1)) / 20) * 10 if starting_hex.base_temperature[ season_index] > downstream_hex.base_temperature[ season_index]: # increase temperature at downstream hex downstream_hex.wind_temp_effect[season_index] = temp_change / 2 # decrease temperature at this hex starting_hex.wind_temp_effect[season_index] = -(temp_change / 2) else: # decrease temperature at downstream hex downstream_hex.wind_temp_effect[season_index] = -(temp_change / 2) # increase temperature at this hex starting_hex.wind_temp_effect[season_index] = temp_change / 2 # go on to the next hex in line if loops != 0: next_hex = downstream_hex.wind[season_index].get( 'windward_hex') windgust(season_index, next_hex, loops - 1) with Timer("Generating Temperature Changes", self.debug): for y, row in enumerate(self.hex_grid.grid): for x, col in enumerate(row): h = self.hex_grid.grid[x][y] # end year windgust(0, h) # mid year windgust(1, h) def _generate_rivers(self): """ For each river source edge: If the "down" hex of the left edge is the "one" or "two" hexes of this edge, the left edge is invalid If the "down" hex of the right edge is the "one" or "two" hexes of this edge, the right edge is invalid If two are valid: E = lowest slope edge If E.down is below sea level, end here otherwise add this edge as a river segment else if one is valid: E = valid edge If E.down is below sea level, end here otherwise add this edge as a river segment else if both are invalid: Make a lake at the lowest of the "one" or "two" hexes of this edge Make a new river source edge at an random edge pointing out from this lake that has a direction pointing out from the lake """ land_percent = 100 - self.params.get('sea_percent') num_rivers = self.params.get('num_rivers') print("Making {} rivers".format(num_rivers)) if self.debug else False while len(self.rivers_sources) < num_rivers: rx = random.randint(0, self.hex_grid.size - 1) ry = random.randint(0, self.hex_grid.size - 1) hex_s = self.hex_grid.find_hex(rx, ry) if hex_s.is_inland and hex_s.altitude > self.hex_grid.sealevel + 35: # if hex_s.temperature < 0: # TODO: Determine when to not place rivers at hight latitudes # # don't place rivers above +35 altitude when the temperature is below zero # continue random_side = random.choice(list(HexSide)) #print("Placing river source at {}, {}".format(rx, ry)) self.rivers_sources.append( RiverSegment(self.hex_grid, rx, ry, random_side, True)) print("Placed river sources") if self.debug else False for r in self.rivers_sources: # loop over each source segment segment = r # river segment we are looking at finished = False last_unselected = None # we stop only when one of two things happen: # - a lake is formed # - we reached sea level while finished is False: # print("Segment: {}, {}".format(segment.x, segment.y)) side_one, side_two = segment.side.branching( segment.edge.direction) down = segment.edge.down # down-slope hex of this segment # add the moisture to the one and two hexes in a 3 radius one = segment.edge.one.bubble(distance=3) two = segment.edge.two.bubble(distance=3) both = list(set(one + two)) for hex in both: if hex.is_land: hex.moisture += 1 three = segment.edge.one.surrounding four = segment.edge.two.surrounding both = list(set(three + four)) for hex in both: if hex.is_land: hex.moisture += 1 # find the two Edges from the sides found branching edge_one = down.get_edge(side_one) edge_two = down.get_edge(side_two) # check if either of the edges are valid river segment locations one_valid = True two_valid = True if edge_one.down == segment.edge.one or edge_one.down == segment.edge.two: one_valid = False if edge_two.down == segment.edge.one or edge_two.down == segment.edge.two: two_valid = False if self.is_river(edge_one): one_valid = False elif self.is_river(edge_two): two_valid = False if one_valid and two_valid: # print("\tBoth are valid edges") if edge_one.down.altitude < edge_two.down.altitude: selected = edge_one selected_side = side_one last_unselected = edge_two, side_two else: selected = edge_two selected_side = side_two last_unselected = edge_one, side_one if selected.down.altitude < self.hex_grid.sealevel: finished = True segment.next = RiverSegment(self.hex_grid, selected.one.x, selected.one.y, selected_side, False) segment = segment.next elif one_valid is True and two_valid is False: # print("\tOne is Valid") selected = edge_one last_unselected = edge_two, side_two if selected.down.altitude < self.hex_grid.sealevel: finished = True segment.next = RiverSegment(self.hex_grid, selected.one.x, selected.one.y, side_one, False) segment = segment.next elif one_valid is False and two_valid is True: # print("\tTwo is valid") selected = edge_two last_unselected = edge_one, side_one if selected.down.altitude < self.hex_grid.sealevel: finished = True segment.next = RiverSegment(self.hex_grid, selected.one.x, selected.one.y, side_two, False) segment = segment.next else: # import ipdb; ipdb.set_trace() # segment.x = last_unselected[0].one.x # segment.y = last_unselected[0].one.y # segment.side = last_unselected[1] # segment.is_source = True # finished = True # print("huh?") # both edges are invalid, make lake at one or two # if segment.edge.one.altitude < segment.edge.two.altitude: # lake = segment.edge.one # else: # lake = segment.edge.two # lake.add_feature(HexFeature.lake) # moisture around lake increases # surrounding = lake.surrounding # for hex in surrounding: # if hex.is_land: # hex.moisture += 3 # print("\tMade a lake at {}, {}".format(segment.x, segment.y)) # make a new source river at an outer edge of the lake # chosen_edge = random.choice(lake.outer_edges) # self.rivers_sources.append(RiverSegment(self.hex_grid, chosen_edge.one.x, chosen_edge.one.y, chosen_edge.side, True)) finished = True final = [] for r in self.rivers_sources: # remove rivers that are too small if r.size > 2: final.append(r) while r.next is not None: # print("Segment: ", r.next) final.append(r.next) r = r.next for r in final: r.edge.is_river = True self.rivers = final def _determine_landforms(self): # single hex geoforms with Timer("Finding geographic features", self.debug): with Timer("\tPlacing initial geoforms", self.debug): for y, row in enumerate(self.hex_grid.grid): for x, col in enumerate(row): h = self.hex_grid.grid[x][y] # Isthmus if is_isthmus(h): h.geoform_type = GeoformType.isthmus # Bays if is_bay(h): h.geoform_type = GeoformType.bay # Straits if is_strait(h): h.geoform_type = GeoformType.strait # Peninsula if is_peninsula(h): h.geoform_type = GeoformType.peninsula if h.geoform_type is not None: self.geoforms.append( Geoform(set([h]), h.geoform_type)) def flood(found, current, hex_type): """ Do a flood fill at this hex over all hexes of this type without geoforms """ if current.geoform_type is not None: return set() if current in found: return set() neighbors = [h[1] for h in current.neighbors] found.add(current) for neighbor in neighbors: if neighbor.type is hex_type: found.update(flood(found, neighbor, hex_type)) return found def give_geoform(hexes, geoform_type): for h in hexes: h.geoform_type = geoform_type # loop until every f*****g hex has a geoform # import ipdb; ipdb.set_trace() with Timer("\tFinding contiguous geoforms", self.debug): sys.setrecursionlimit(10000) current = first_hex_without_geoform(self.hex_grid.grid) while current is not None: if current.is_land: # try to find continents hexes = flood(set(), current, current.type) if len(hexes) < 25: geotype = GeoformType.small_island elif len(hexes) < 100: geotype = GeoformType.large_island else: geotype = GeoformType.continent else: # try to find oceans hexes = flood(set(), current, current.type) if len(hexes) < 3: geotype = GeoformType.lake elif len(hexes) < 100: geotype = GeoformType.sea else: geotype = GeoformType.ocean give_geoform(hexes, geotype) # hexes is a set of hexes self.geoforms.append(Geoform(hexes, geotype)) # find a new hex current = first_hex_without_geoform(self.hex_grid.grid) # now we have geoforms # FIND NEIGHBORING GEOFORMS def calculate_neighbors(): """ recalculate neighboring geoforms """ for geoform in self.geoforms: geoform.neighbors.clear() for h in geoform.hexes: ng = [ n[1].geoform for n in h.neighbors if n[1].geoform is not geoform ] geoform.neighbors.update(ng) assert geoform not in geoform.neighbors, 'A Geoform should not be in its own neighbors set' calculate_neighbors() with Timer("\tMerging geoforms", self.debug): # MERGE GEOFORMS # merge all neighboring geoforms of like type for geoform in self.geoforms: for neighbor in geoform.neighbors: if geoform.type is neighbor.type: # remove neighbor print('Merging {} '.format(geoform.type)) geoform.merge(neighbor) calculate_neighbors() # if an island is next to a continent or island separated by an isthmus, # and that island doesn't have any other isthmuses # the island becomes a peninsula for geoform in self.geoforms: if geoform.type is GeoformType.isthmus: islands = geoform.neighbor_of_type( GeoformType.small_island) land_form = geoform.neighbor_of_types([ GeoformType.continent, GeoformType.small_island, GeoformType.large_island ]) if len(islands) == 1 and len(land_form) == 1 and \ len(land_form[0].neighbors) >= 2: other_isthmuses = len(islands[0].neighbor_of_type( GeoformType.isthmus)) > 1 # check to see if this island has other isthmuses # if it does, exclude it if other_isthmuses is False: print( 'Merging island + isthmus into peninsula') islands[0].merge( geoform ) # merge the island and the isthmus islands[ 0].type = GeoformType.peninsula # change island to peninsula calculate_neighbors() # small islands separated by an isthmus to a large island should # be merged into the large island for geoform in self.geoforms: if geoform.type is GeoformType.small_island: large_islands = geoform.neighbor_of_type( GeoformType.large_island) if len(large_islands) > 0: print('Merging small island into large island') large_islands[0].merge(geoform) calculate_neighbors() # islands separated by an isthmus with a continent should be merged # TODO: maybe large islands should be a new continent for geoform in self.geoforms: if geoform.type is GeoformType.large_island or \ geoform.type is GeoformType.small_island: isthmuses = geoform.neighbor_of_type( GeoformType.isthmus) continents = set() for i in isthmuses: continents.update( i.neighbor_of_type(GeoformType.continent)) continents = list(continents) if len(continents) == 1: # one continent neighbor print('Merging island into continent') continents[0].merge(geoform) elif len(continents) > 1: # multiple continents are neighbors print( 'Merging island and other continents into one continent' ) continents[0].merge(geoform) for c in continents[1:]: continents[0].merge(c) calculate_neighbors() # if a peninsula is next to a isthmus, merge them into one peninsula for geoform in self.geoforms: if geoform.type is GeoformType.peninsula: isthmuses = geoform.neighbor_of_type( GeoformType.isthmus) if len(isthmuses) == 1: print('Merging isthmus into peninsula') geoform.merge(isthmuses[0]) # a peninsula of size 2 with no neighbors is an island if geoform.size == 2 and len(geoform.neighbors) == 0: geoform.type = GeoformType.small_island calculate_neighbors() # remove old geoforms print("Deleting {} geoforms".format( len([g for g in self.geoforms if g.to_delete is True]))) self.geoforms = [g for g in self.geoforms if g.to_delete is False] print("There is now {} geoforms".format(len(self.geoforms))) def is_river(self, edge): """ Determines if an edge has a river :param edge: Edge :return: Boolean """ for r in self.rivers_sources: while r.next is not None: if r.edge == edge: return True r = r.next return False def find_river(self, x, y): """ Finds river segments at an hex's x and y coordinates. Returns a list of EdgeSides representing where the river segments are """ seg = [] for s in self.rivers: if s.x == x and s.y == y: seg.append(s.side) return seg def export(self, filename): """ Export the map data as a JSON file """ with Timer("Compiling data into dictionary", self.debug): params = copy.copy(self.params) params['map_type'] = params.get('map_type').to_dict() params['ocean_type'] = params.get('ocean_type').to_dict() data = { "parameters": params, "details": { "size": self.hex_grid.size, "sea_level": self.hex_grid.sealevel, "avg_height": self.hex_grid.average_height, "max_height": self.hex_grid.highest_height, "min_height": self.hex_grid.lowest_height }, "hexes": [], "geoforms": [] } def edge_dict(edge): return dict(is_river=edge.is_river, is_coast=edge.is_coast, direction=edge.direction.name) for x, row in enumerate(self.hex_grid.grid): row_data = [] for y, col in enumerate(row): h = self.hex_grid.find_hex(x, y) color_temperature = ((h.color_temperature[0][0] + h.color_temperature[1][0]) / 2, (h.color_temperature[0][1] + h.color_temperature[1][1]) / 2, (h.color_temperature[0][2] + h.color_temperature[1][2]) / 2) temperature = round( (h.temperature[0] + h.temperature[1]) / 2, 2) row_data.append({ "id": h.id.hex, "x": x, "y": y, "altitude": h.altitude, "temperature": temperature, "moisture": h.moisture, "biome": h.biome.to_dict(), "type": h.type.name, "is_inland": h.is_inland, "is_coast": h.is_coast, "geoform": h.geoform.id.hex, "colors": { "satellite": h.color_satellite, "terrain": h.color_terrain, "temperature": color_temperature, "biome": h.color_biome, "rivers": h.color_rivers }, "edges": { "east": edge_dict(h.edge_east), "north_east": edge_dict(h.edge_north_east), "north_west": edge_dict(h.edge_north_west), "west": edge_dict(h.edge_west), "south_west": edge_dict(h.edge_south_west), "south_east": edge_dict(h.edge_south_east) } }) data['hexes'].append(row_data) for geoform in self.geoforms: data['geoforms'].append(geoform.to_dict()) with open(filename, 'w') as outfile: with Timer("Writing data to JSON file", self.debug): json.dump(data, outfile) return data
class MapGen: """ generates a heightmap as an array of integers between 1 and 255 using the diamond-square algorithm""" def __init__(self, params, debug=False): """ initialize """ self.params = default_params self.params.update(params) if debug: print("Making world with params:") for key, value in params.items(): print("\t{}:\t{}".format(key, value)) self.debug = debug if type(params.get('random_seed')) is int: random.seed(params.get('random_seed')) self.heightmap = Heightmap(self.params) self.hex_grid = Grid(self.heightmap, self.params) self.rivers = [] self.rivers_sources = [] print("Computing hex distances") if self.debug else False self._get_distances() if self.params.get('hydrosphere'): self._generate_rivers() # give coastal land hexes moisture based on how close to the coast they are print("Making coastal moisture") if self.debug else False for y, row in enumerate(self.hex_grid.grid): for x, col in enumerate(row): hex = self.hex_grid.grid[x][y] if hex.is_land: if hex.distance <= 5: hex.moisture += 1 if hex.distance <= 3: hex.moisture += random.randint(1, 3) if hex.distance <= 1: hex.moisture += random.randint(1, 6) # generate aquifers num_aquifers = random.randint(5, 25) if self.params.get('hydrosphere') is False or self.params.get('sea_percent') == 100: num_aquifers = 0 print("Making {} aquifers".format(num_aquifers)) if self.debug else False aquifers = [] while len(aquifers) < num_aquifers: rx = random.randint(0, len(self.hex_grid.grid) - 1) ry = random.randint(0, len(self.hex_grid.grid) - 1) hex = self.hex_grid.grid[rx][ry] if hex.is_land and hex.moisture < 5: aquifers.append(hex) for hex in aquifers: # print("Aquifer at ", hex) r1 = hex.bubble(distance=3) for hex in r1: if hex.is_land: hex.moisture += random.randint(0, 2) r2 = hex.bubble(distance=2) for hex in r2: if hex.is_land: hex.moisture += 1 r3 = hex.surrounding for hex in r3: if hex.is_land: hex.moisture += 1 # decide terrain features print("Making terrain features") if self.debug else False # craters only form in barren planets with a normal or lower atmosphere if self.params.get('craters') is True: # decide number of craters num_craters = random.randint(0, 15) print("Making {} craters".format(num_craters)) craters = [] while len(craters) < num_craters: size = random.randint(1, 3) craters.append(dict(hex=random.choice(self.hex_grid.hexes), size=size, depth= 10 * size)) for crater in craters: center_hex = crater.get('hex') size = crater.get('size') depth = crater.get('depth') hexes = [] if size >= 1: hexes = center_hex.surrounding for h in hexes: h.add_feature(HexFeature.crater) h.altitude = center_hex.altitude - 5 h.altitude = max(h.altitude, 0) if size >= 2: hexes = center_hex.bubble(distance=2) for h in hexes: h.add_feature(HexFeature.crater) h.altitude = center_hex.altitude - 10 h.altitude = max(h.altitude, 0) if size >= 3: hexes = center_hex.bubble(distance=3) for h in hexes: h.add_feature(HexFeature.crater) h.altitude = center_hex.altitude - 15 h.altitude = max(h.altitude, 0) for h in hexes[:round(len(hexes)/3)]: for i in h.surrounding: if i.has_feature(HexFeature.crater) is False: i.add_feature(HexFeature.crater) i.altitude = center_hex.altitude - 20 i.altitude = max(i.altitude, 0) # volcanoes if self.params.get('volcanoes'): num_volcanoes = random.randint(0, 10) print("Making {} volcanoes".format(num_volcanoes)) volcanoes = [] while len(volcanoes) < num_volcanoes: center_hex = random.choice(self.hex_grid.hexes) if center_hex.altitude > 50: size = random.randint(1, 5) height = random.randint(30, 70) volcanoes.append(dict(hex=center_hex, size=size, height=height)) for volcano in volcanoes: height = volcano.get('height') size = volcano.get('size') center_hex = volcano.get('hex') print("\tVolcano: Size: {}, Height: {}".format(size, height)) size_list = list(range(size)) size_list.reverse() hexes = [] for i in size_list: i += 1 this_height = round(height / i) if i == 1: l = center_hex.surrounding + [center_hex] else: l = center_hex.bubble(distance=i) for h in l: hexes.append(h) h.altitude = center_hex.altitude + this_height h.add_feature(HexFeature.volcano) last_altitude = 0 for h in hexes[:round(len(hexes)/2)]: for i in h.surrounding: if i.has_feature(HexFeature.volcano) is False: i.add_feature(HexFeature.volcano) i.altitude += 5 i.altitude = min(i.altitude, 255) last_altitude = i.altitude center_hex.altitude += last_altitude + 5 # lava flow def step(active_hex): if active_hex.altitude < 50: return else: active_hex.add_feature(HexFeature.lava_flow) found = [] for i in active_hex.surrounding: if i.altitude <= active_hex.altitude and i.has_feature(HexFeature.lava_flow) is False: found.append(i) found.sort(key=lambda x: x.altitude) if len(found) > 1: step(found[0]) if len(found) > 2: step(found[1]) step(center_hex) self.territories = [] self.generate_territories() self.generate_resources() print("Done") if self.debug else False def generate_resources(self): print("Placing resources") ratings = HexResourceRating.list() types = HexResourceType.list() combined = [] for r in ratings: for t in types: combined.append(dict(rating=r, type=t)) for h in self.hex_grid.hexes: for resource in combined: chance = (resource.get('rating').rarity * resource.get('type').rarity * self.hex_grid.size / 1000 ) / (math.pow(self.hex_grid.size, 2)) given = random.uniform(0, 1) if given <= chance: h.resource = resource def generate_territories(self): """ Makes territories """ # select number of territories to place land_percent = 100 - self.params.get('sea_percent') num_territories = self.params.get('num_territories') # give each a land pixel to start print("Making {} territories".format(num_territories)) if self.debug else False c = 0 if num_territories == 0: return while len(self.territories) < num_territories: rx = random.randint(0, len(self.hex_grid.grid) - 1) ry = random.randint(0, len(self.hex_grid.grid) - 1) hex_s = self.hex_grid.grid[rx][ry] if hex_s.is_land: color = random.randint(0, 255), random.randint(0, 255), random.randint(0, 255) self.territories.append(Territory(self.hex_grid, hex_s, c, color)) c += 1 # loop over each, adding hexes total_hexes = self.hex_grid.size * self.hex_grid.size count = 0 while count < total_hexes: # i in range(0, 15): count = 0 # print("Start: {} < {}".format(count, total_hexes)) territories = self.territories random.shuffle(territories) for t in territories: frontier = t.frontier for f in frontier: if f.is_owned is False: f.territory = t t.members.append(f) t.last_added.append(f) count += t.size # print("End: {} < {}".format(count, total_hexes)) # remove water hexes for t in self.territories: members = t.members t.members = [h for h in t.members if h.is_land] water_hexes = (h for h in members if h.is_water) for h in water_hexes: h.territory = None # merge territories print("Merging barren territories") if self.params.get('num_territories') > 0: top = [] bottom = [] for t in self.territories: avg_x = round(sum([i.x for i in t.members]) / len(t.members)) if t.avg_temp < 0 and (avg_x / self.hex_grid.size) < 0.5: top.append(t) elif t.avg_temp < 0 and (avg_x / self.hex_grid.size) >= 0.5: bottom.append(t) pick_top = None pick_bottom = None if len(top) > 0: print("Merging {} territories from the top of the map".format( len(top) )) pick_top = random.choice(top) top.remove(pick_top) for t in self.territories: if t in top: pick_top.members += t.members t.members = [] if len(bottom) > 0: print("Merging {} territories from the bottom of the map".format( len(bottom) )) pick_bottom = random.choice(bottom) bottom.remove(pick_bottom) for t in self.territories: if t in bottom: pick_bottom.members += t.members t.members = [] if len(top) > 0: for h in pick_top.members: h.territory = pick_top if len(bottom) > 0: for h in pick_bottom.members: h.territory = pick_bottom self.territories = [t for t in self.territories if t is not None] print("{} empty territories being deleted".format(len([t for t in self.territories if len(t.members) == 0]))) self.territories = [t for t in self.territories if len(t.members) > 0] print("There are now {} territories".format(len(self.territories))) print("Splitting territories into contiguous blocks") if self.debug else False for t in self.territories: t.find_groups() def _get_distances(self): """ Gets the distances each land pixel is to the coastline. TODO: Make this more efficient """ if not self.params.get('hydrosphere'): # we don't care about distances otherwise return for y, row in enumerate(self.hex_grid.grid): for x, col in enumerate(row): h = self.hex_grid.grid[x][y] if h.is_land: count = 1 numbers = [] east = h.hex_east while east.is_land is True and count < self.hex_grid.size * 2: east = east.hex_east count += 1 numbers.append(count) count = 1 west = h.hex_west while west.is_land is True and count < self.hex_grid.size * 2: west = west.hex_west count += 1 numbers.append(count) count = 1 north_east = h.hex_north_east while north_east.is_land is True and count < self.hex_grid.size * 2: north_east = north_east.hex_north_east count += 1 numbers.append(count) count = 1 north_west = h.hex_north_west while north_west.is_land is True and count < self.hex_grid.size * 2: north_west = north_west.hex_north_west count += 1 numbers.append(count) count = 1 south_west = h.hex_south_west while south_west.is_land is True and count < self.hex_grid.size * 2: south_west = south_west.hex_south_west count += 1 numbers.append(count) count = 1 south_east = h.hex_south_east while south_east.is_land is True and count < self.hex_grid.size * 2: south_east = south_east.hex_south_east count += 1 numbers.append(count) h.distance = min(numbers) def _generate_rivers(self): """ For each river source edge: If the "down" hex of the left edge is the "one" or "two" hexes of this edge, the left edge is invalid If the "down" hex of the right edge is the "one" or "two" hexes of this edge, the right edge is invalid If two are valid: E = lowest slope edge If E.down is below sea level, end here otherwise add this edge as a river segment else if one is valid: E = valid edge If E.down is below sea level, end here otherwise add this edge as a river segment else if both are invalid: Make a lake at the lowest of the "one" or "two" hexes of this edge Make a new river source edge at an random edge pointing out from this lake that has a direction pointing out from the lake """ land_percent = 100 - self.params.get('sea_percent') num_rivers = self.params.get('num_rivers') print("Making {} rivers".format(num_rivers)) if self.debug else False while len(self.rivers_sources) < num_rivers: rx = random.randint(0, self.hex_grid.size - 1) ry = random.randint(0, self.hex_grid.size - 1) hex_s = self.hex_grid.find_hex(rx, ry) if hex_s.is_inland and hex_s.altitude > self.hex_grid.sealevel + 35: if hex_s.temperature < 0: # don't place rivers above +35 altitude when the temperature is below zero continue random_side = random.choice(list(HexSide)) #print("Placing river source at {}, {}".format(rx, ry)) self.rivers_sources.append(RiverSegment(self.hex_grid, rx, ry, random_side, True)) print("Placed river sources") if self.debug else False for r in self.rivers_sources: # loop over each source segment segment = r # river segment we are looking at finished = False last_unselected = None # we stop only when one of two things happen: # - a lake is formed # - we reached sea level while finished is False: # print("Segment: {}, {}".format(segment.x, segment.y)) side_one, side_two = segment.side.branching(segment.edge.direction) down = segment.edge.down # down-slope hex of this segment # add the moisture to the one and two hexes in a 3 radius one = segment.edge.one.bubble(distance=3) two = segment.edge.two.bubble(distance=3) both = list(set(one + two)) for hex in both: if hex.is_land: hex.moisture += 1 three = segment.edge.one.surrounding four = segment.edge.two.surrounding both = list(set(three + four)) for hex in both: if hex.is_land: hex.moisture += 1 # find the two Edges from the sides found branching edge_one = down.get_edge(side_one) edge_two = down.get_edge(side_two) # check if either of the edges are valid river segment locations one_valid = True two_valid = True if edge_one.down == segment.edge.one or edge_one.down == segment.edge.two: one_valid = False if edge_two.down == segment.edge.one or edge_two.down == segment.edge.two: two_valid = False if self.is_river(edge_one): one_valid = False elif self.is_river(edge_two): two_valid = False if one_valid and two_valid: # print("\tBoth are valid edges") if edge_one.down.altitude < edge_two.down.altitude: selected = edge_one selected_side = side_one last_unselected = edge_two, side_two else: selected = edge_two selected_side = side_two last_unselected = edge_one, side_one if selected.down.altitude < self.hex_grid.sealevel: finished = True segment.next = RiverSegment(self.hex_grid, selected.one.x, selected.one.y, selected_side, False) segment = segment.next elif one_valid is True and two_valid is False: # print("\tOne is Valid") selected = edge_one last_unselected = edge_two, side_two if selected.down.altitude < self.hex_grid.sealevel: finished = True segment.next = RiverSegment(self.hex_grid, selected.one.x, selected.one.y, side_one, False) segment = segment.next elif one_valid is False and two_valid is True: # print("\tTwo is valid") selected = edge_two last_unselected = edge_one, side_one if selected.down.altitude < self.hex_grid.sealevel: finished = True segment.next = RiverSegment(self.hex_grid, selected.one.x, selected.one.y, side_two, False) segment = segment.next else: # import ipdb; ipdb.set_trace() # segment.x = last_unselected[0].one.x # segment.y = last_unselected[0].one.y # segment.side = last_unselected[1] # segment.is_source = True # finished = True # print("huh?") # both edges are invalid, make lake at one or two if segment.edge.one.altitude < segment.edge.two.altitude: lake = segment.edge.one else: lake = segment.edge.two lake.add_feature(HexFeature.lake) # moisture around lake increases surrounding = lake.surrounding for hex in surrounding: if hex.is_land: hex.moisture += 3 # print("\tMade a lake at {}, {}".format(segment.x, segment.y)) # # # make a new source river at an outer edge of the lake # chosen_edge = random.choice(lake.outer_edges) # self.rivers_sources.append(RiverSegment(self.hex_grid, chosen_edge.one.x, chosen_edge.one.y, chosen_edge.side, True)) finished = True final = [] for r in self.rivers_sources: # remove rivers that are too small if r.size > 2: final.append(r) while r.next is not None: # print("Segment: ", r.next) final.append(r.next) r = r.next self.rivers = final def is_river(self, edge): """ Determines if an edge has a river :param edge: Edge :return: Boolean """ for r in self.rivers_sources: while r.next is not None: if r.edge == edge: return True r = r.next return False def find_river(self, x, y): """ Finds river segments at an hex's x and y coordinates. Returns a list of EdgeSides representing where the river segments are """ seg = [] for s in self.rivers: if s.x == x and s.y == y: seg.append(s.side) return seg