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