Add a sensible license
[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_coords(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, coords, room_dist, region_size, tile_map, x, y):
89         """ Check if a region is in this room and return the required tiles
90         :param coords: Coordinates of the 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 coords in self.coordinates:
99             x_pre_room_dist = room_dist
100             x_post_room_dist = region_size - room_dist
101             y_pre_room_dist = room_dist
102             y_post_room_dist = region_size - room_dist
103             if [x - 1, y] in self.coordinates:
104                 y_pre_room_dist = 0
105             if [x + 1, y] in self.coordinates:
106                 y_post_room_dist = region_size
107             if [x, y - 1] in self.coordinates:
108                 x_pre_room_dist = 0
109             if [x, y + 1] in self.coordinates:
110                 x_post_room_dist = region_size
111
112             for ht in range(y_pre_room_dist, y_post_room_dist):
113                 for wt in range(x_pre_room_dist, x_post_room_dist):
114                     tile_map[(x * region_size) + ht][(y * region_size) + wt] =\
115                                              str(self.region)
116             for p in self.passages:
117                 print(p)
118                 x_regions = p[0][0] - p[1][0]
119                 y_regions = p[0][1] - p[1][1]
120                 if p[0][0] < p[1][0]:
121                     x_direction = -1
122                 elif p[0][0] < p[1][0]:
123                     x_direction = 0
124                 else:
125                     x_direction = 1
126                 if p[0][1] < p[1][1]:
127                     y_direction = -1
128                 elif p[0][1] < p[1][1]:
129                     y_direction = 0
130                 else:
131                     y_direction = 1
132                 for ht in range(0, region_size, x_direction):
133                     tile_map[(p[0][0] * region_size) + int(region_size / 2)]\
134                         [(p[0][1] * region_size) + int(region_size / 2) + ht] = 'p'
135                 for wt in range(0, y_regions, y_direction):
136                     tile_map[(p[0][0] * region_size) + int(region_size / 2) + wt]\
137                         [(p[0][1] * region_size) + int(region_size / 2)] = 'p'
138
139
140 def random_cardinal():
141     """Return a random cardinal direction for random walks."""
142     return random.choice([(0, 1), (0, -1), (1, 0), (-1, 0)])
143
144
145 class LevelGenerator:
146     width = 0
147     height = 0
148     no_rooms = 0
149     rooms = []
150     map = None
151     map2 = None
152     dist_from_other_rooms = 0
153     region_map = None
154     regions = 0
155     region_size = 0
156
157     def __init__(self, width, height, no_rooms, dist_from_other_rooms,
158                  region_size):
159         """ Initialize the level parameters
160         """
161         self.width = width
162         self.height = height
163         self.no_rooms = no_rooms
164         self.dist_from_other_rooms = dist_from_other_rooms
165         self.region_size = region_size
166         self.region_coordinates = []
167
168     def generate(self):
169         """ Generate a random level map
170         """
171         self.generate_rooms()
172         regions_selected = random.sample(range(self.regions),
173                                          min(self.regions, self.no_rooms))
174         row = ['#' for x in range(self.width * self.region_size)]
175         self.map = [row[:] for x in range(self.height * self.region_size)]
176         self.map2 = [row[:] for x in range(self.height * self.region_size)]
177         for region in regions_selected:
178             self.rooms[region].connect_rooms(
179                 [self.rooms[i] for i in regions_selected])
180         region_coordinates_selected = [p for p in self.region_coordinates if
181                                        p[0] in regions_selected]
182         for coord in region_coordinates_selected:
183             self.rooms[coord[0]].render_region(
184                 coord[1], self.dist_from_other_rooms, self.region_size,
185                 self.map2, coord[1][0], coord[1][1])
186         # self.generate_underlayer()
187
188     def generate_rooms(self):
189         """ Generate a random level region map
190         """
191         row = [0 for x in range(self.width)]
192         self.region_map = [row[:] for x in range(self.height)]
193         for h in range(self.height):
194             for w in range(self.width):
195                 random_number = random.randint(0, 2)
196                 increment_region = False
197                 if w == h == 0:
198                     update_value = self.regions
199                     increment_region = True
200                 elif h == 0:
201                     if random_number > 1:
202                         update_value = self.region_map[h][w - 1]
203                     else:
204                         update_value = self.regions
205                         increment_region = True
206                 elif w == 0:
207                     if random_number > 1:
208                         update_value = self.region_map[h - 1][w]
209                     else:
210                         update_value = self.regions
211                         increment_region = True
212                 else:
213                     if random_number > 1:
214                         update_value = self.region_map[h - 1][w]
215                     elif random_number > 0:
216                         update_value = self.region_map[h][w - 1]
217                     else:
218                         update_value = self.regions
219                         increment_region = True
220                 self.region_map[h][w] = update_value
221                 if increment_region:
222                     r = Room([h, w], update_value)
223                     self.rooms.append(r)
224                     self.region_coordinates.append([update_value, [h, w]])
225                     self.regions += 1
226                 else:
227                     for r in self.rooms:
228                         if r.region == update_value:
229                             r.add_coords([h, w])
230                     self.region_coordinates.append([update_value, [h, w]])
231
232     def generate_underlayer(self):
233         """Generate a small mess of tunnels to have something."""
234         width = len(self.map[0])
235         height = len(self.map)
236         row = ['o' for x in range(width)]
237         self.underlayer = [row[:] for x in range(height)]
238         # we create a set of biased random walks to create the tunnel network
239         for walk in range(random.randint(3, 6)):
240             x = width // 2 + random.randint(-8, 8)
241             y = height // 2 + random.randint(-8, 8)
242             dir_x, dir_y = random_cardinal()
243             max_steps = random.randint(40, width * height // 4)
244             for step in range(20, max_steps):
245                 if 0 < x < width - 1:
246                     if 0 < y < height - 1:
247                         self.underlayer[y][x] = '-'
248                 if random.random() > 0.7:
249                    dir_x, dir_y = random_cardinal()
250                 x += dir_x
251                 y += dir_y
252
253     def generate_tiles(self, region_selected):
254         """Generate a small mess of tunnels to have something."""
255         width = len(self.map[0])
256         height = len(self.map)
257         row = ['o' for x in range(width)]
258         self.underlayer = [row[:] for x in range(height)]
259         # we create a set of biased random walks to create the tunnel network
260         for walk in range(random.randint(3, 6)):
261             x = width // 2 + random.randint(-8, 8)
262             y = height // 2 + random.randint(-8, 8)
263             dir_x, dir_y = random_cardinal()
264             max_steps = random.randint(40, width * height // 4)
265             for step in range(20, max_steps):
266                 if 0 < x < width - 1:
267                     if 0 < y < height - 1:
268                         self.underlayer[y][x] = '-'
269                 if random.random() > 0.7:
270                    dir_x, dir_y = random_cardinal()
271                 x += dir_x
272                 y += dir_y
273
274     def display(self):
275         file = open('map.txt', 'w')
276         print('-----------------')
277         for l in self.map2:
278             print(''.join(l))
279             file.write(''.join(l))
280             file.write('\n')
281         print('-----------------')
282         try:
283             for l in self.underlayer:
284                 print(''.join(l))
285                 file.write(''.join(l))
286                 file.write('\n')
287         except AttributeError:
288             pass
289         file.close()
290         for l in self.region_map:
291             # self._to_json()
292             print(l)
293
294     def _to_json(self):
295         level = {}
296         level['tileset'] = 'bunker'
297         level['tiles'] = []
298         for l, lu in zip(self.map, self.underlayer):
299             row = []
300             for t1, t2 in zip(l, lu):
301                 tile = ATTRIBUTE_MAP[t1].copy()
302                 tile.update(ATTRIBUTE_MAP[t2])
303                 row.append(tile)
304             level['tiles'].append(row)
305         # FIXME: Do a lot better here
306         # Crude hack so the level is written into the levels folder
307         name = os.path.join(os.path.dirname(__file__), '..', 'levels', 'map.json')
308         f = open(name, 'w')
309         json.dump(level, f)
310         f.close()
311
312
313 if __name__ == '__main__':
314     level = LevelGenerator(width=4, height=3, no_rooms=4,
315                            dist_from_other_rooms=1, region_size=5)
316     level.generate()
317     level.display()