Procedural Caves - Cellular Automata

Screen shot of cellular automata to generate caves
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()