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()