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 neighbor_x < 0 or neighbor_y < 0 or neighbor_y >= height or neighbor_x >= width:
 73                # Edges are considered alive. Makes map more likely to appear naturally closed.
 74                alive_count += 1
 75            elif grid[neighbor_y][neighbor_x] == 1:
 76                alive_count += 1
 77    return alive_count
 78
 79
 80def do_simulation_step(old_grid):
 81    """ Run a step of the cellular automaton. """
 82    height = len(old_grid)
 83    width = len(old_grid[0])
 84    new_grid = create_grid(width, height)
 85    for x in range(width):
 86        for y in range(height):
 87            alive_neighbors = count_alive_neighbors(old_grid, x, y)
 88            if old_grid[y][x] == 1:
 89                if alive_neighbors < DEATH_LIMIT:
 90                    new_grid[y][x] = 0
 91                else:
 92                    new_grid[y][x] = 1
 93            else:
 94                if alive_neighbors > BIRTH_LIMIT:
 95                    new_grid[y][x] = 1
 96                else:
 97                    new_grid[y][x] = 0
 98    return new_grid
 99
100
101class InstructionView(arcade.View):
102    """ View to show instructions """
103
104    def __init__(self):
105        super().__init__()
106        self.frame_count = 0
107
108    def on_show_view(self):
109        """ This is run once when we switch to this view """
110        self.window.background_color = arcade.csscolor.DARK_SLATE_BLUE
111
112        # Reset the viewport, necessary if we have a scrolling game and we need
113        # to reset the viewport back to the start so we can see what we draw.
114        self.window.default_camera.use()
115
116    def on_draw(self):
117        """ Draw this view """
118        self.clear()
119        arcade.draw_text("Loading...", self.window.width / 2, self.window.height / 2,
120                         arcade.color.BLACK, font_size=50, anchor_x="center")
121
122    def on_update(self, dt):
123        if self.frame_count == 0:
124            self.frame_count += 1
125            return
126
127        """ If the user presses the mouse button, start the game. """
128        game_view = GameView()
129        game_view.setup()
130        self.window.show_view(game_view)
131
132
133class GameView(arcade.View):
134    """
135    Main application class.
136    """
137
138    def __init__(self):
139        super().__init__()
140
141        self.grid = None
142        self.wall_list = None
143        self.player_list = None
144        self.player_sprite = None
145        self.draw_time = 0
146        self.processing_time = 0
147        self.physics_engine = None
148
149        # Track the current state of what key is pressed
150        self.left_pressed = False
151        self.right_pressed = False
152        self.up_pressed = False
153        self.down_pressed = False
154
155        # Create the cameras. One for the GUI, one for the sprites.
156        # We scroll the 'sprite world' but not the GUI.
157        self.camera_sprites = arcade.camera.Camera2D()
158        self.camera_gui = arcade.camera.Camera2D()
159
160        self.window.background_color = arcade.color.BLACK
161
162        self.sprite_count_text = None
163        self.draw_time_text = None
164        self.processing_time_text = None
165
166    def setup(self):
167        self.wall_list = arcade.SpriteList(use_spatial_hash=True)
168        self.player_list = arcade.SpriteList()
169
170        # Create cave system using a 2D grid
171        self.grid = create_grid(GRID_WIDTH, GRID_HEIGHT)
172        initialize_grid(self.grid)
173        for step in range(NUMBER_OF_STEPS):
174            self.grid = do_simulation_step(self.grid)
175
176        texture = arcade.load_texture(":resources:images/tiles/grassCenter.png")
177        # Create sprites based on 2D grid
178        # Each grid location is a sprite.
179        for row in range(GRID_HEIGHT):
180            for column in range(GRID_WIDTH):
181                if self.grid[row][column] == 1:
182                    wall = arcade.BasicSprite(texture, scale=SPRITE_SCALING)
183                    wall.center_x = column * SPRITE_SIZE + SPRITE_SIZE / 2
184                    wall.center_y = row * SPRITE_SIZE + SPRITE_SIZE / 2
185                    self.wall_list.append(wall)
186
187        # Set up the player
188        self.player_sprite = arcade.Sprite(
189            ":resources:images/animated_characters/female_person/femalePerson_idle.png",
190            scale=SPRITE_SCALING)
191        self.player_list.append(self.player_sprite)
192
193        # Randomly place the player. If we are in a wall, repeat until we aren't.
194        placed = False
195        while not placed:
196
197            # Randomly position
198            max_x = int(GRID_WIDTH * SPRITE_SIZE)
199            max_y = int(GRID_HEIGHT * SPRITE_SIZE)
200            self.player_sprite.center_x = random.randrange(max_x)
201            self.player_sprite.center_y = random.randrange(max_y)
202
203            # Are we in a wall?
204            walls_hit = arcade.check_for_collision_with_list(self.player_sprite, self.wall_list)
205            if len(walls_hit) == 0:
206                # Not in a wall! Success!
207                placed = True
208
209        self.scroll_to_player(1.0)
210
211        self.physics_engine = arcade.PhysicsEngineSimple(self.player_sprite,
212                                                         self.wall_list)
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    def on_draw(self):
235        """ Render the screen. """
236
237        # Start timing how long this takes
238        draw_start_time = timeit.default_timer()
239
240        # This command should happen before we start drawing. It will clear
241        # the screen to the background color, and erase what we drew last frame.
242        self.clear()
243
244        # Select the camera we'll use to draw all our sprites
245        self.camera_sprites.use()
246
247        # Draw the sprites
248        self.wall_list.draw(pixelated=True)
249        self.player_list.draw()
250
251        # Select the (unscrolled) camera for our GUI
252        self.camera_gui.use()
253
254        self.sprite_count_text.draw()
255        output = f"Drawing time: {self.draw_time:.3f}"
256        self.draw_time_text.text = output
257        self.draw_time_text.draw()
258
259        output = f"Processing time: {self.processing_time:.3f}"
260        self.processing_time_text.text = output
261        self.processing_time_text.draw()
262
263        self.draw_time = timeit.default_timer() - draw_start_time
264
265    def update_player_speed(self):
266
267        # Calculate speed based on the keys pressed
268        self.player_sprite.change_x = 0
269        self.player_sprite.change_y = 0
270
271        if self.up_pressed and not self.down_pressed:
272            self.player_sprite.change_y = MOVEMENT_SPEED
273        elif self.down_pressed and not self.up_pressed:
274            self.player_sprite.change_y = -MOVEMENT_SPEED
275        if self.left_pressed and not self.right_pressed:
276            self.player_sprite.change_x = -MOVEMENT_SPEED
277        elif self.right_pressed and not self.left_pressed:
278            self.player_sprite.change_x = MOVEMENT_SPEED
279
280    def on_key_press(self, key, modifiers):
281        """Called whenever a key is pressed. """
282
283        if key == arcade.key.UP:
284            self.up_pressed = True
285        elif key == arcade.key.DOWN:
286            self.down_pressed = True
287        elif key == arcade.key.LEFT:
288            self.left_pressed = True
289        elif key == arcade.key.RIGHT:
290            self.right_pressed = True
291
292    def on_key_release(self, key, modifiers):
293        """Called when the user releases a key. """
294
295        if key == arcade.key.UP:
296            self.up_pressed = False
297        elif key == arcade.key.DOWN:
298            self.down_pressed = False
299        elif key == arcade.key.LEFT:
300            self.left_pressed = False
301        elif key == arcade.key.RIGHT:
302            self.right_pressed = False
303
304    def scroll_to_player(self, speed=CAMERA_SPEED):
305        """
306        Scroll the window to the player.
307
308        if CAMERA_SPEED is 1, the camera will immediately move to the desired position.
309        Anything between 0 and 1 will have the camera move to the location with a smoother
310        pan.
311        """
312
313        position = (self.player_sprite.center_x, self.player_sprite.center_y)
314        arcade.camera.controllers.simple_follow_2D(speed, position, self.camera_sprites.view_data)
315
316    def on_resize(self, width: int, height: int):
317        """
318        Resize window
319        Handle the user grabbing the edge and resizing the window.
320        """
321        super().on_resize(width, height)
322        self.camera_sprites.match_screen(and_projection=True)
323        self.camera_gui.match_screen(and_projection=True)
324
325    def on_update(self, delta_time):
326        """ Movement and game logic """
327
328        start_time = timeit.default_timer()
329
330        # Call update on all sprites (The sprites don't do much in this
331        # example though.)
332        self.update_player_speed()
333        self.physics_engine.update()
334
335        # Scroll the screen to the player
336        self.scroll_to_player()
337
338        # Save the time it took to do this.
339        self.processing_time = timeit.default_timer() - start_time
340
341
342def main():
343    window = arcade.Window(WINDOW_WIDTH, WINDOW_HEIGHT, WINDOW_TITLE, resizable=True)
344    start_view = InstructionView()
345    window.show_view(start_view)
346    arcade.run()
347
348
349if __name__ == "__main__":
350    main()