Skip to content

Commit 47e2319

Browse files
committed
updates to map generators
1 parent 2270c2c commit 47e2319

File tree

4 files changed

+417
-12
lines changed

4 files changed

+417
-12
lines changed

ants/mapgen/heightmap.py

+127
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
#!/usr/bin/env python
2+
from map import *
3+
from random import randint
4+
from collections import defaultdict
5+
6+
class HeightMapMap(Map):
7+
def __init__(self, options={}):
8+
super(HeightMapMap, self).__init__(options)
9+
self.name = 'height_map'
10+
self.rows = options.get('rows', (40,200))
11+
self.cols = options.get('cols', (40,200))
12+
self.players = options.get('players', (2,12))
13+
self.land = options.get('land', (75, 85))
14+
15+
def generate(self):
16+
rows = self.get_random_option(self.rows)
17+
cols = self.get_random_option(self.cols)
18+
19+
# calc max players that can be tiled
20+
row_max = rows//16
21+
col_max = cols//16
22+
player_max = row_max*col_max
23+
24+
# fix dimensions for tiling evenly
25+
players =self.get_random_option((self.players[0],
26+
min(player_max, self.players[1])))
27+
# pick random grid size
28+
divs = [(i, players//i) for i in range(1,min(players+1, row_max+1))
29+
if players % i == 0 # look fox divisors of players
30+
#and i < row_max # ensure grid rows < row_max
31+
and players//i < col_max # ensure grid cols < col_max
32+
]
33+
if len(divs) == 0:
34+
return self.generate()
35+
row_sym, col_sym = choice(divs)
36+
37+
# fix dimensions for even tiling
38+
rows //= row_sym
39+
cols //= col_sym
40+
41+
land = self.get_random_option(self.land)
42+
43+
44+
# initialize height map
45+
height_map = [[0]*cols for _ in range(rows)]
46+
47+
# cut and lift
48+
iterations = 100
49+
for _ in range(iterations):
50+
row = randint(0, rows-1)
51+
col = randint(0, cols-1)
52+
radius = randint(5, (rows+cols)/4)
53+
radius2 = radius**2
54+
for d_row in range(-radius, radius+1):
55+
for d_col in range(-radius, radius+1):
56+
h_row = (row + d_row) % rows
57+
h_col = (col + d_col) % cols
58+
if self.euclidean_distance2((row, col), (h_row, h_col), (rows, cols)) <= radius2:
59+
height_map[h_row][h_col] += 1
60+
61+
# create histogram
62+
histo = defaultdict(int)
63+
for height_row in height_map:
64+
for height in height_row:
65+
histo[height] += 1
66+
67+
# find sea level
68+
map_area = rows * cols
69+
land_area = 0
70+
for height in sorted(histo.keys(), reverse=True):
71+
land_area += histo[height]
72+
if 1.0 * land_area / map_area >= 1.0 * land / 100:
73+
break
74+
75+
# initialize map
76+
self.map = [[LAND]*cols for _ in range(rows)]
77+
78+
# place water
79+
for row in range(rows):
80+
for col in range(cols):
81+
if height_map[row][col] < height:
82+
self.map[row][col] = WATER
83+
self.fill_small_areas()
84+
85+
# check too make sure too much wasn't filled in
86+
areas = self.section(0)
87+
land_area = len(areas[0][0])
88+
percent = 1.0 * land_area / map_area
89+
#print(land_area, map_area, percent)
90+
if percent < 1.0 * land / 100:
91+
return self.generate()
92+
93+
# place player start
94+
while self.map[row][col] != LAND:
95+
row = randint(0, rows-1)
96+
col = randint(0, cols-1)
97+
self.map[row][col] = ANTS
98+
99+
# tile map
100+
t_rows = rows * row_sym
101+
t_cols = cols * col_sym
102+
ant = 0
103+
map = [[LAND]*t_cols for _ in range(t_rows)]
104+
for t_row in range(t_rows):
105+
for t_col in range(t_cols):
106+
row = t_row % rows
107+
col = t_col % cols
108+
map[t_row][t_col] = self.map[row][col]
109+
if self.map[row][col] == ANTS:
110+
map[t_row][t_col] = ant
111+
ant += 1
112+
self.map = map
113+
self.make_wider()
114+
115+
def main():
116+
new_map = HeightMapMap()
117+
new_map.generate()
118+
119+
# check that all land area is accessable
120+
while new_map.allowable() != None:
121+
#print(new_map.allowable())
122+
new_map.generate()
123+
124+
new_map.toText()
125+
126+
if __name__ == '__main__':
127+
main()

ants/mapgen/map.py

+225
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
#!/usr/bin/env python
2+
import sys
3+
from random import randint, choice, seed
4+
from collections import deque
5+
from itertools import product
6+
from sys import maxint
7+
8+
MY_ANT = 0
9+
ANTS = 0
10+
DEAD = -1
11+
LAND = -2
12+
FOOD = -3
13+
WATER = -4
14+
UNSEEN = -5
15+
MAP_RENDER = 'abcdefghijklmnopqrstuvwxyz?%*.!'
16+
17+
AIM = {'n': (-1, 0),
18+
'e': (0, 1),
19+
's': (1, 0),
20+
'w': (0, -1)}
21+
22+
class Map(object):
23+
def __init__(self, options={}):
24+
super(Map, self).__init__()
25+
self.name = "blank"
26+
self.map = [[]]
27+
self.random_seed = options.get('seed', None)
28+
if self.random_seed == None:
29+
self.random_seed = randint(-maxint-1, maxint)
30+
seed(self.random_seed)
31+
32+
def generate(self):
33+
raise Exception("Not Implemented")
34+
35+
def get_random_option(self, option):
36+
if type(option) == tuple:
37+
if len(option) == 2:
38+
return randint(*option)
39+
elif len(option) == 1:
40+
return option[0]
41+
elif len(option) == 0:
42+
raise Exception("Invalid option: 0 length tuple")
43+
else:
44+
return choice(option)
45+
elif type(option) in (list, set):
46+
if len(option) > 0:
47+
return choice(option)
48+
else:
49+
raise Exception("Invalid option: 0 length list")
50+
elif type(option) in (int, float, str):
51+
return option
52+
else:
53+
raise Exception("Invalid option: type {0} not supported".format(type(option)))
54+
55+
def toPNG(self, fd=sys.stdout):
56+
raise Exception("Not Implemented")
57+
58+
def toText(self, fd=sys.stdout):
59+
players = set()
60+
for row in self.map:
61+
for c in row:
62+
if c >= ANTS:
63+
players.add(c)
64+
fd.write("# map_type {0}\n# random_seed {1}\nplayers {2}\nrows {3}\ncols {4}\n"
65+
.format(self.name,
66+
self.random_seed,
67+
len(players),
68+
len(self.map),
69+
len(self.map[0])))
70+
for row in self.map:
71+
fd.write("m {0}\n".format(''.join([MAP_RENDER[c] for c in row])))
72+
73+
def manhatten_distance(self, loc1, loc2, size):
74+
rows, cols = size
75+
row1, col1 = loc1
76+
row2, col2 = loc2
77+
row1 = row1 % rows
78+
row2 = row2 % rows
79+
col1 = col1 % cols
80+
col2 = col2 % cols
81+
d_col = min(abs(col1 - col2), cols - abs(col1 - col2))
82+
d_row = min(abs(row1 - row2), rows - abs(row1 - row2))
83+
return d_row + d_col
84+
85+
def euclidean_distance2(self, loc1, loc2, size):
86+
rows, cols = size
87+
row1, col1 = loc1
88+
row2, col2 = loc2
89+
row1 = row1 % rows
90+
row2 = row2 % rows
91+
col1 = col1 % cols
92+
col2 = col2 % cols
93+
d_col = min(abs(col1 - col2), cols - abs(col1 - col2))
94+
d_row = min(abs(row1 - row2), rows - abs(row1 - row2))
95+
return d_row**2 + d_col**2
96+
97+
def destination(self, loc, direction, size):
98+
rows, cols = size
99+
row, col = loc
100+
d_row, d_col = AIM[direction]
101+
return ((row + d_row) % rows, (col + d_col) % cols)
102+
103+
def section(self, block_size=1):
104+
rows = len(self.map)
105+
cols = len(self.map[0])
106+
visited = [[False] * cols for _ in range(rows)]
107+
108+
def is_block_free(loc):
109+
row, col = loc
110+
for d_row in range(-block_size, block_size+1):
111+
for d_col in range(-block_size, block_size+1):
112+
h_row = (row + d_row) % rows
113+
h_col = (col + d_col) % cols
114+
if self.map[h_row][h_col] == WATER:
115+
return False
116+
return True
117+
118+
def mark_block(loc, m, ilk):
119+
row, col = loc
120+
for d_row in range(-block_size, block_size+1):
121+
for d_col in range(-block_size, block_size+1):
122+
h_row = (row + d_row) % rows
123+
h_col = (col + d_col) % cols
124+
m[h_row][h_col] = ilk
125+
126+
def find_open_spot():
127+
for row, col in product(range(rows), range(cols)):
128+
if is_block_free((row, col)) and not visited[row][col]:
129+
return (row, col)
130+
else:
131+
return None
132+
133+
# list of contiguous areas
134+
areas = []
135+
136+
# flood fill map for each separate area
137+
while find_open_spot():
138+
# maintain lists of visited and seen squares
139+
# visited will not overlap, but seen may
140+
area_visited = [[False] * cols for _ in range(rows)]
141+
area_seen = [[False] * cols for _ in range(rows)]
142+
143+
squares = deque()
144+
row, col = find_open_spot()
145+
146+
#seen_area = open_block((row, col))
147+
squares.appendleft((row, col))
148+
149+
while len(squares) > 0:
150+
row, col = squares.pop()
151+
visited[row][col] = True
152+
area_visited[row][col] = True
153+
area_seen[row][col] = True
154+
for d_row, d_col in ((1,0), (0,1), (-1,0), (0,-1)):
155+
s_row = (row + d_row) % rows
156+
s_col = (col + d_col) % cols
157+
if not visited[s_row][s_col] and is_block_free((s_row, s_col)):
158+
visited[s_row][s_col] = True
159+
mark_block((s_row, s_col), area_seen, True)
160+
squares.appendleft((s_row, s_col))
161+
162+
# check percentage filled
163+
#areas.append(1.0 * seen_area / land_area)
164+
visited_list = []
165+
seen_list = []
166+
for row in range(rows):
167+
for col in range(cols):
168+
if area_visited[row][col]:
169+
visited_list.append((row, col))
170+
elif area_seen[row][col]:
171+
seen_list.append((row, col))
172+
areas.append([visited_list, seen_list])
173+
174+
# sort by largest area first
175+
areas.sort(key=lambda area: len(area[0]), reverse=True)
176+
return areas
177+
178+
def fill_small_areas(self):
179+
# keep largest contiguous area as land, fill the rest with water
180+
count = 0
181+
areas = self.section(0)
182+
for area in areas[1:]:
183+
for row, col in area[0]:
184+
self.map[row][col] = WATER
185+
count += 1
186+
#print("fill {0}".format(count))
187+
188+
def make_wider(self):
189+
# make sure the map has more columns than rows
190+
rows = len(self.map)
191+
cols = len(self.map[0])
192+
if rows > cols:
193+
map = [[LAND] * rows for _ in range(cols)]
194+
for row in range(rows):
195+
for col in range(cols):
196+
map[col][row] = self.map[row][col]
197+
self.map = map
198+
199+
def allowable(self):
200+
# all squares must be accessable from all other squares
201+
# fill small areas can fix this
202+
areas = self.section(0)
203+
if len(areas) > 1:
204+
return "Map not 100% accessable"
205+
land_area = len(areas[0][0])
206+
207+
# 66% of the map must not be blockable
208+
# or must be accessable by a 3x3 block
209+
areas = self.section(1)
210+
area_visited, area_seen = areas[0]
211+
if 1.0 * (len(area_seen) + len(area_visited)) / land_area < 0.66:
212+
return "Map is too blockable"
213+
214+
# all starting ants must be in the largest area
215+
ants = {}
216+
rows = len(self.map)
217+
cols = len(self.map[0])
218+
for row in range(rows):
219+
for col in range(cols):
220+
if self.map[row][col] >= ANTS:
221+
if (row, col) not in area_seen and (row, col) not in area_visited:
222+
return "Starting ants not in unblockable area"
223+
ants[(row, col)] = self.map[row][col]
224+
225+
return None

0 commit comments

Comments
 (0)