Merge of doom
[koperkapel.git] / koperkapel / generators / maps.py
1 """ Procedural map generation for levels """
2
3 import random
4 import math
5 import json
6 import os
7
8 ATTRIBUTE_MAP = {
9     ' ': {'floor': {'base': 'floor',
10                     'behaviour': ['walk', 'fly'],
11                     },
12           },
13     'o': {'tunnels': {'base': 'underground',
14                       'behaviour': [],
15                       }
16           },
17     '-': {'tunnels': {'base': 'tunnel',
18                       'behaviour': ['walk', ],
19                       },
20           },
21 }
22
23
24 class Room:
25     def __init__(self, coordinates, region):
26         """
27         """
28         self.coordinates = [coordinates]
29         self.region = region
30         self.max_connections = 1
31         self.passages = []
32         self.tunnels = []
33
34     def is_linked(self):
35         """
36         Check if the room is linked to another room
37         :return: Whether the room has any links or not
38         """
39         return len(self.passages) + len(self.tunnels) > 0
40
41     def add_region(self, coordinates):
42         """
43         Add a new region into an existing room
44         :param coordinates: region coordinates to be added to room
45         :return:
46         """
47         self.coordinates.append(coordinates)
48
49     def connect_rooms(self, other_rooms):
50         """ Find the nearest rooms to this room
51         :param other_rooms: list of Rooms objects that we are searching
52         :return:
53         """
54         distance = []
55         other_tile = []
56         this_tile = []
57         target_rooms = []
58         for coord in self.coordinates:
59             for room in other_rooms:
60                 if self == room:
61                     continue
62                 for new_coord in room.coordinates:
63                     distance.append(
64                         math.sqrt((coord[0] - new_coord[0]) ** 2 +
65                                   (coord[1] - new_coord[1]) ** 2))
66                     other_tile.append(new_coord)
67                     this_tile.append(coord)
68                     target_rooms.append(room)
69
70         sorted_indices = [i[0] for i in sorted(enumerate(distance),
71                                                key=lambda x:x[0])]
72         for index in sorted_indices:
73             if len(self.passages) + len(self.tunnels) >= self.max_connections:
74                 break
75             if not target_rooms[index].is_linked():
76                 self.link_passage(this_tile[index], other_tile[index])
77                 target_rooms[index].link_passage(
78                     other_tile[index], this_tile[index])
79
80     def link_passage(self, local_tile, foreign_tile):
81         """ Link a passage between two rooms
82         :param local_tile: tile in this room to which we wish to link
83         :param foreign_tile: tile in another room to which we wish to link
84         :return:
85         """
86         self.passages.append([local_tile, foreign_tile])
87
88     def render_region(self, region, room_dist, region_size, tile_map, x, y):
89         """ Check if a region is in this room and return the required tiles
90         :param region: Region that we wish to render
91         :param room_dist: Tile separation distance from other rooms
92         :param region_size: Region size in tiles
93         :param tile_map: Tile map to update
94         :param x: X coordinate
95         :param y: Y coordinate
96         :return:
97         """
98         if region in self.coordinates:
99             print(self.coordinates)
100             for ht in range(room_dist, region_size - room_dist):
101                 for wt in range(room_dist, region_size - room_dist):
102                     tile_map[x + ht][y + wt] = ' '
103
104                     # if w == 0:
105                     #     w_dist = self.dist_from_other_rooms
106                     # elif self.region_map[h][w-1] == region_selected:
107                     #     w_dist = 0
108                     # else:
109                     #     w_dist = self.dist_from_other_rooms
110                     #
111                     # if w + 1 == self.width:
112                     #     e_dist = self.region_size - self.dist_from_other_rooms
113                     # elif self.region_map[h][w+1] == region_selected:
114                     #     e_dist = self.region_size
115                     # else:
116                     #     e_dist = self.region_size - self.dist_from_other_rooms
117                     #
118                     # if h == 0:
119                     #     n_dist = self.dist_from_other_rooms
120                     # elif self.region_map[h-1][w] == region_selected:
121                     #     n_dist = 0
122                     # else:
123                     #     n_dist = self.dist_from_other_rooms
124                     #
125                     # if h + 1 == self.height:
126                     #     s_dist = self.region_size - self.dist_from_other_rooms
127                     # elif self.region_map[h+1][w] == region_selected:
128                     #     s_dist = self.region_size
129                     # else:
130                     #     s_dist = self.region_size - self.dist_from_other_rooms
131                     #
132                     # for wt in range(w_dist, e_dist):
133                     #     for ht in range(n_dist, s_dist):
134                     #         self.map[h * self.region_size + ht]\
135                     #             [w * self.region_size + wt] = ' '
136
137 def random_cardinal():
138     """Return a random cardinal direction for random walks."""
139     return random.choice([(0, 1), (0, -1), (1, 0), (-1, 0)])
140
141
142 class LevelGenerator:
143     width = 0
144     height = 0
145     no_rooms = 0
146     rooms = []
147     map = None
148     map2 = None
149     dist_from_other_rooms = 0
150     region_map = None
151     regions = 0
152     region_size = 0
153
154     def __init__(self, width, height, no_rooms, dist_from_other_rooms,
155                  region_size):
156         """ Initialize the level parameters
157         """
158         self.width = width
159         self.height = height
160         self.no_rooms = no_rooms
161         self.dist_from_other_rooms = dist_from_other_rooms
162         self.region_size = region_size
163         self.region_coordinates = []
164
165     def generate(self):
166         """ Generate a random level map
167         """
168         self.generate_rooms()
169         regions_selected = random.sample(range(self.regions),
170                                          min(self.regions, self.no_rooms))
171         row = ['#' for x in range(self.width * self.region_size)]
172         self.map = [row[:] for x in range(self.height * self.region_size)]
173         self.map2 = [row[:] for x in range(self.height * self.region_size)]
174         print('Regions: %s' % str(regions_selected))
175         for region in regions_selected:
176             self.rooms[region].connect_rooms(
177                 [self.rooms[i] for i in regions_selected])
178             self.generate_room(region)
179         region_coordinates_selected = [p for p in self.region_coordinates if
180                                        p[0] in regions_selected]
181         print('Coords: %s' % str(region_coordinates_selected))
182         for coord in region_coordinates_selected:
183             print(str(coord))
184             self.rooms[coord[0]].render_region(
185                 coord[1], self.dist_from_other_rooms, self.region_size,
186                 self.map2, coord[1][0] * self.region_size,
187                 coord[1][1] * self.region_size)
188         # self.generate_underlayer()
189
190     def generate_rooms(self):
191         """ Generate a random level region map
192         """
193         row = [0 for x in range(self.width)]
194         self.region_map = [row[:] for x in range(self.height)]
195         for h in range(self.height):
196             for w in range(self.width):
197                 random_number = random.randint(0, 2)
198                 increment_region = False
199                 if w == h == 0:
200                     update_value = self.regions
201                     increment_region = True
202                 elif h == 0:
203                     if random_number > 1:
204                         update_value = self.region_map[h][w - 1]
205                     else:
206                         update_value = self.regions
207                         increment_region = True
208                 elif w == 0:
209                     if random_number > 1:
210                         update_value = self.region_map[h - 1][w]
211                     else:
212                         update_value = self.regions
213                         increment_region = True
214                 else:
215                     if random_number > 1:
216                         update_value = self.region_map[h - 1][w]
217                     elif random_number > 0:
218                         update_value = self.region_map[h][w - 1]
219                     else:
220                         update_value = self.regions
221                         increment_region = True
222                 self.region_map[h][w] = update_value
223                 if increment_region:
224                     r = Room([h, w], update_value)
225                     self.rooms.append(r)
226                     self.region_coordinates.append([update_value, [h, w]])
227                     self.regions += 1
228                 else:
229                     for r in self.rooms:
230                         if r.region == update_value:
231                             r.add_region([h, w])
232                     self.region_coordinates.append([update_value, [h, w]])
233
234     def generate_underlayer(self):
235         """Generate a small mess of tunnels to have something."""
236         width = len(self.map[0])
237         height = len(self.map)
238         row = ['o' for x in range(width)]
239         self.underlayer = [row[:] for x in range(height)]
240         # we create a set of biased random walks to create the tunnel network
241         for walk in range(random.randint(3, 6)):
242             x = width // 2 + random.randint(-8, 8)
243             y = height // 2 + random.randint(-8, 8)
244             dir_x, dir_y = random_cardinal()
245             max_steps = random.randint(40, width * height // 4)
246             for step in range(20, max_steps):
247                 if 0 < x < width - 1:
248                     if 0 < y < height - 1:
249                         self.underlayer[y][x] = '-'
250                 if random.random() > 0.7:
251                    dir_x, dir_y = random_cardinal()
252                 x += dir_x
253                 y += dir_y
254
255     def generate_tiles(self, region_selected):
256         """Generate a small mess of tunnels to have something."""
257         width = len(self.map[0])
258         height = len(self.map)
259         row = ['o' for x in range(width)]
260         self.underlayer = [row[:] for x in range(height)]
261         # we create a set of biased random walks to create the tunnel network
262         for walk in range(random.randint(3, 6)):
263             x = width // 2 + random.randint(-8, 8)
264             y = height // 2 + random.randint(-8, 8)
265             dir_x, dir_y = random_cardinal()
266             max_steps = random.randint(40, width * height // 4)
267             for step in range(20, max_steps):
268                 if 0 < x < width - 1:
269                     if 0 < y < height - 1:
270                         self.underlayer[y][x] = '-'
271                 if random.random() > 0.7:
272                    dir_x, dir_y = random_cardinal()
273                 x += dir_x
274                 y += dir_y
275
276     def generate_room(self, region_selected):
277         """
278         :param region_selected:
279         :return:
280         """
281         for h in range(self.height):
282             for w in range(self.width):
283                 if self.region_map[h][w] == region_selected:
284                     if w == 0:
285                         w_dist = self.dist_from_other_rooms
286                     elif self.region_map[h][w-1] == region_selected:
287                         w_dist = 0
288                     else:
289                         w_dist = self.dist_from_other_rooms
290
291                     if w + 1 == self.width:
292                         e_dist = self.region_size - self.dist_from_other_rooms
293                     elif self.region_map[h][w+1] == region_selected:
294                         e_dist = self.region_size
295                     else:
296                         e_dist = self.region_size - self.dist_from_other_rooms
297
298                     if h == 0:
299                         n_dist = self.dist_from_other_rooms
300                     elif self.region_map[h-1][w] == region_selected:
301                         n_dist = 0
302                     else:
303                         n_dist = self.dist_from_other_rooms
304
305                     if h + 1 == self.height:
306                         s_dist = self.region_size - self.dist_from_other_rooms
307                     elif self.region_map[h+1][w] == region_selected:
308                         s_dist = self.region_size
309                     else:
310                         s_dist = self.region_size - self.dist_from_other_rooms
311
312                     for wt in range(w_dist, e_dist):
313                         for ht in range(n_dist, s_dist):
314                             self.map[h * self.region_size + ht]\
315                                 [w * self.region_size + wt] = str(region_selected)
316
317     def display(self):
318         file = open('map.txt', 'w')
319         print('-----------------')
320         for l in self.map:
321             print(''.join(l))
322             file.write(''.join(l))
323             file.write('\n')
324         print('-----------------')
325         for l in self.map2:
326             print(''.join(l))
327             file.write(''.join(l))
328             file.write('\n')
329         print('-----------------')
330         try:
331             for l in self.underlayer:
332                 print(''.join(l))
333                 file.write(''.join(l))
334                 file.write('\n')
335         except AttributeError:
336             pass
337         file.close()
338         for l in self.region_map:
339             # self._to_json()
340             print(l)
341
342     def _to_json(self):
343         level = {}
344         level['tileset'] = 'bunker'
345         level['tiles'] = []
346         for l, lu in zip(self.map, self.underlayer):
347             row = []
348             for t1, t2 in zip(l, lu):
349                 tile = ATTRIBUTE_MAP[t1].copy()
350                 tile.update(ATTRIBUTE_MAP[t2])
351                 row.append(tile)
352             level['tiles'].append(row)
353         # FIXME: Do a lot better here
354         # Crude hack so the level is written into the levels folder
355         name = os.path.join(os.path.dirname(__file__), '..', 'levels', 'map.json')
356         f = open(name, 'w')
357         json.dump(level, f)
358         f.close()
359
360
361 if __name__ == '__main__':
362     while True:
363         level = LevelGenerator(width=4, height=3, no_rooms=4,
364                                dist_from_other_rooms=0, region_size=3)
365         level.generate()
366         level.display()
367         input("Press Enter to continue...")