Procedural Caves - Cellular Automata

procedural_caves_cellular.py
1"""
2This example procedurally develops a random cave based on cellular automata.
3
4For more information, see:
5https://gamedevelopment.tutsplus.com/tutorials/generate-random-cave-levels-using-cellular-automata--gamedev-9664
6
7If Python and Arcade are installed, this example can be run from the command line with:
8python -m arcade.examples.procedural_caves_cellular
9"""
10
11import random
12import arcade
13import timeit
14
15# Sprite scaling. Make this larger, like 0.5 to zoom in and add
16# 'mystery' to what you can see. Make it smaller, like 0.1 to see
17# more of the map.
18SPRITE_SCALING = 0.25
19SPRITE_SIZE = 128 * SPRITE_SCALING
20
21# How big the grid is
22GRID_WIDTH = 450
23GRID_HEIGHT = 400
24
25# Parameters for cellular automata
26CHANCE_TO_START_ALIVE = 0.4
27DEATH_LIMIT = 3
28BIRTH_LIMIT = 4
29NUMBER_OF_STEPS = 4
30
31# How fast the player moves
32MOVEMENT_SPEED = 5
33
34# How close the player can get to the edge before we scroll.
35VIEWPORT_MARGIN = 300
36
37# How big the window is
38WINDOW_WIDTH = 800
39WINDOW_HEIGHT = 600
40WINDOW_TITLE = "Procedural Caves Cellular Automata Example"
41
42# How fast the camera pans to the player. 1.0 is instant.
43CAMERA_SPEED = 0.1
44
45
46def create_grid(width, height):
47 """ Create a two-dimensional grid of specified size. """
48 return [[0 for _x in range(width)] for _y in range(height)]
49
50
51def initialize_grid(grid):
52 """ Randomly set grid locations to on/off based on chance. """
53 height = len(grid)
54 width = len(grid[0])
55 for row in range(height):
56 for column in range(width):
57 if random.random() <= CHANCE_TO_START_ALIVE:
58 grid[row][column] = 1
59
60
61def count_alive_neighbors(grid, x, y):
62 """ Count neighbors that are alive. """
63 height = len(grid)
64 width = len(grid[0])
65 alive_count = 0
66 for i in range(-1, 2):
67 for j in range(-1, 2):
68 neighbor_x = x + i
69 neighbor_y = y + j
70 if i == 0 and j == 0:
71 continue
72 elif (
73 neighbor_x < 0 or
74 neighbor_y < 0 or
75 neighbor_y >= height or
76 neighbor_x >= width
77 ):
78 # Edges are considered alive. Makes map more likely to appear naturally closed.
79 alive_count += 1
80 elif grid[neighbor_y][neighbor_x] == 1:
81 alive_count += 1
82 return alive_count
83
84
85def do_simulation_step(old_grid):
86 """ Run a step of the cellular automaton. """
87 height = len(old_grid)
88 width = len(old_grid[0])
89 new_grid = create_grid(width, height)
90 for x in range(width):
91 for y in range(height):
92 alive_neighbors = count_alive_neighbors(old_grid, x, y)
93 if old_grid[y][x] == 1:
94 if alive_neighbors < DEATH_LIMIT:
95 new_grid[y][x] = 0
96 else:
97 new_grid[y][x] = 1
98 else:
99 if alive_neighbors > BIRTH_LIMIT:
100 new_grid[y][x] = 1
101 else:
102 new_grid[y][x] = 0
103 return new_grid
104
105
106class InstructionView(arcade.View):
107 """ View to show instructions """
108
109 def __init__(self):
110 super().__init__()
111 self.frame_count = 0
112
113 def on_show_view(self):
114 """ This is run once when we switch to this view """
115 self.window.background_color = arcade.csscolor.DARK_SLATE_BLUE
116
117 # Reset the viewport, necessary if we have a scrolling game and we need
118 # to reset the viewport back to the start so we can see what we draw.
119 self.window.default_camera.use()
120
121 def on_draw(self):
122 """ Draw this view """
123 self.clear()
124 arcade.draw_text("Loading...", self.window.width // 2, self.window.height // 2,
125 arcade.color.BLACK, font_size=50, anchor_x="center")
126
127 def on_update(self, dt):
128 if self.frame_count == 0:
129 self.frame_count += 1
130 return
131
132 """ If the user presses the mouse button, start the game. """
133 game_view = GameView()
134 game_view.setup()
135 self.window.show_view(game_view)
136
137
138class GameView(arcade.View):
139 """
140 Main application class.
141 """
142
143 def __init__(self):
144 super().__init__()
145
146 self.grid = None
147 self.wall_list = None
148 self.player_list = None
149 self.player_sprite = None
150 self.draw_time = 0
151 self.processing_time = 0
152 self.physics_engine = None
153
154 # Track the current state of what key is pressed
155 self.left_pressed = False
156 self.right_pressed = False
157 self.up_pressed = False
158 self.down_pressed = False
159
160 self.sprite_count_text = None
161 self.draw_time_text = None
162 self.processing_time_text = None
163
164 # Create the cameras. One for the GUI, one for the sprites.
165 # We scroll the 'sprite world' but not the GUI.
166 self.camera_sprites = arcade.camera.Camera2D()
167 self.camera_gui = arcade.camera.Camera2D()
168
169 self.window.background_color = arcade.color.BLACK
170
171 def setup(self):
172 self.wall_list = arcade.SpriteList(use_spatial_hash=True)
173 self.player_list = arcade.SpriteList()
174
175 # Create cave system using a 2D grid
176 self.grid = create_grid(GRID_WIDTH, GRID_HEIGHT)
177 initialize_grid(self.grid)
178 for step in range(NUMBER_OF_STEPS):
179 self.grid = do_simulation_step(self.grid)
180
181 texture = arcade.load_texture(":resources:images/tiles/grassCenter.png")
182 # Create sprites based on 2D grid
183 # Each grid location is a sprite.
184 for row in range(GRID_HEIGHT):
185 for column in range(GRID_WIDTH):
186 if self.grid[row][column] == 1:
187 wall = arcade.BasicSprite(texture, scale=SPRITE_SCALING)
188 wall.center_x = column * SPRITE_SIZE + SPRITE_SIZE / 2
189 wall.center_y = row * SPRITE_SIZE + SPRITE_SIZE / 2
190 self.wall_list.append(wall)
191
192 # Set up the player
193 self.player_sprite = arcade.Sprite(
194 ":resources:images/animated_characters/female_person/femalePerson_idle.png",
195 scale=SPRITE_SCALING)
196 self.player_list.append(self.player_sprite)
197
198 # Randomly place the player. If we are in a wall, repeat until we aren't.
199 placed = False
200 while not placed:
201
202 # Randomly position
203 max_x = int(GRID_WIDTH * SPRITE_SIZE)
204 max_y = int(GRID_HEIGHT * SPRITE_SIZE)
205 self.player_sprite.center_x = random.randrange(max_x)
206 self.player_sprite.center_y = random.randrange(max_y)
207
208 # Are we in a wall?
209 walls_hit = arcade.check_for_collision_with_list(self.player_sprite, self.wall_list)
210 if len(walls_hit) == 0:
211 # Not in a wall! Success!
212 placed = True
213
214 # Draw info on the screen
215 sprite_count = len(self.wall_list)
216 output = f"Sprite Count: {sprite_count:,}"
217 self.sprite_count_text = arcade.Text(output,
218 20,
219 self.window.height - 20,
220 arcade.color.WHITE, 16)
221
222 output = "Drawing time:"
223 self.draw_time_text = arcade.Text(output,
224 20,
225 self.window.height - 40,
226 arcade.color.WHITE, 16)
227
228 output = "Processing time:"
229 self.processing_time_text = arcade.Text(output,
230 20,
231 self.window.height - 60,
232 arcade.color.WHITE, 16)
233
234 self.physics_engine = arcade.PhysicsEngineSimple(self.player_sprite,
235 self.wall_list)
236
237 self.scroll_to_player(1.0)
238
239 def on_draw(self):
240 """ Render the screen. """
241
242 # Start timing how long this takes
243 draw_start_time = timeit.default_timer()
244
245 # This command should happen before we start drawing. It will clear
246 # the screen to the background color, and erase what we drew last frame.
247 self.clear()
248
249 # Select the camera we'll use to draw all our sprites
250 self.camera_sprites.use()
251
252 # Draw the sprites
253 self.wall_list.draw(pixelated=True)
254 self.player_list.draw()
255
256 # Select the (unscrolled) camera for our GUI
257 self.camera_gui.use()
258
259 self.sprite_count_text.draw()
260 output = f"Drawing time: {self.draw_time:.3f}"
261 self.draw_time_text.text = output
262 self.draw_time_text.draw()
263
264 output = f"Processing time: {self.processing_time:.3f}"
265 self.processing_time_text.text = output
266 self.processing_time_text.draw()
267
268 self.draw_time = timeit.default_timer() - draw_start_time
269
270 def update_player_speed(self):
271
272 # Calculate speed based on the keys pressed
273 self.player_sprite.change_x = 0
274 self.player_sprite.change_y = 0
275
276 if self.up_pressed and not self.down_pressed:
277 self.player_sprite.change_y = MOVEMENT_SPEED
278 elif self.down_pressed and not self.up_pressed:
279 self.player_sprite.change_y = -MOVEMENT_SPEED
280 if self.left_pressed and not self.right_pressed:
281 self.player_sprite.change_x = -MOVEMENT_SPEED
282 elif self.right_pressed and not self.left_pressed:
283 self.player_sprite.change_x = MOVEMENT_SPEED
284
285 def on_key_press(self, key, modifiers):
286 """Called whenever a key is pressed. """
287
288 if key == arcade.key.UP:
289 self.up_pressed = True
290 elif key == arcade.key.DOWN:
291 self.down_pressed = True
292 elif key == arcade.key.LEFT:
293 self.left_pressed = True
294 elif key == arcade.key.RIGHT:
295 self.right_pressed = True
296
297 def on_key_release(self, key, modifiers):
298 """Called when the user releases a key. """
299
300 if key == arcade.key.UP:
301 self.up_pressed = False
302 elif key == arcade.key.DOWN:
303 self.down_pressed = False
304 elif key == arcade.key.LEFT:
305 self.left_pressed = False
306 elif key == arcade.key.RIGHT:
307 self.right_pressed = False
308
309 def scroll_to_player(self, camera_speed):
310 """
311 Scroll the window to the player.
312
313 if camera_speed is 1, the camera will immediately move to the desired position.
314 Anything between 0 and 1 will have the camera move to the location with a smoother
315 pan.
316 """
317
318 position = (self.player_sprite.center_x, self.player_sprite.center_y)
319 self.camera_sprites.position = arcade.math.lerp_2d(
320 self.camera_sprites.position,
321 position,
322 camera_speed,
323 )
324
325 def on_resize(self, width: int, height: int):
326 """
327 Resize window
328 Handle the user grabbing the edge and resizing the window.
329 """
330 super().on_resize(width, height)
331 self.camera_sprites.match_window()
332 self.camera_gui.match_window()
333
334 def on_update(self, delta_time):
335 """ Movement and game logic """
336
337 start_time = timeit.default_timer()
338
339 # Call update on all sprites (The sprites don't do much in this
340 # example though.)
341 self.update_player_speed()
342 self.physics_engine.update()
343
344 # Scroll the screen to the player
345 self.scroll_to_player(camera_speed=CAMERA_SPEED)
346
347 # Save the time it took to do this.
348 self.processing_time = timeit.default_timer() - start_time
349
350
351def main():
352 """ Main function """
353 # Create a window class. This is what actually shows up on screen
354 window = arcade.Window(WINDOW_WIDTH, WINDOW_HEIGHT, WINDOW_TITLE)
355
356 # Create the InstructionView
357 game = InstructionView()
358
359 # Show GameView on screen
360 window.show_view(game)
361
362 # Start the arcade game loop
363 arcade.run()
364
365
366if __name__ == "__main__":
367 main()