Conway’s Game of Life

This version of Conway’s Game of Life speeds everything up by using controlling a cell’s visibility through its alpha value, and handing the drawing logic off to the graphics card.

Grid-based games can take a while to render the program uses classic raster-based graphics. Every cell has to be re-drawn every single frame. If the cells are complex at all, that adds to the rendering time.

In this program, we create all cells in the grid to begin with. (This does causes the program to pause a while at start-up.)

After the sprites are created, we turn the cells on and off by their alpha value. We can update the entire grid by simply sending a list of alpha values to the graphics card. This significantly improves drawing time.

conway_alpha.py
  1"""
  2Conway's Game of Life
  3
  4This code shows how to set up sprites in a grid, and then use their
  5'alpha' value to quickly turn them on and off.
  6
  7After installing the `arcade` package version 2.4.4+, this program can be run by
  8typing:
  9python -m arcade.examples.conway_alpha
 10"""
 11
 12import arcade
 13from arcade import SpriteCircle, SpriteList
 14import random
 15
 16# Set how many rows and columns we will have
 17ROW_COUNT = 70
 18COLUMN_COUNT = 128
 19
 20# This sets the WIDTH and HEIGHT of each grid location
 21CELL_WIDTH = 15
 22CELL_HEIGHT = 15
 23
 24# This sets the margin between each cell
 25# and on the edges of the screen.
 26CELL_MARGIN = 0
 27
 28# Do the math to figure out our screen dimensions
 29WINDOW_WIDTH = (CELL_WIDTH + CELL_MARGIN) * COLUMN_COUNT + CELL_MARGIN
 30WINDOW_HEIGHT = (CELL_HEIGHT + CELL_MARGIN) * ROW_COUNT + CELL_MARGIN
 31WINDOW_TITLE = "Conway's Game of Life"
 32
 33# Colors and alpha values
 34ALIVE_COLOR = arcade.color.BISTRE
 35BACKGROUND_COLOR = arcade.color.ANTIQUE_WHITE
 36ALPHA_ON = 255
 37ALPHA_OFF = 0
 38
 39
 40def create_grids(
 41    cell_size: tuple[int, int] = (CELL_WIDTH, CELL_HEIGHT), cell_margin: int = CELL_MARGIN
 42):
 43    """
 44    Create a 2D and 1D grid of sprites. We use the 1D SpriteList for drawing,
 45    and the 2D list for accessing via grid. Both lists point to the same set of
 46    sprites.
 47    """
 48    # One dimensional list of all sprites in the two-dimensional sprite list
 49    grid_sprites_one_dim: SpriteList[SpriteCircle] = SpriteList()
 50
 51    # This will be a two-dimensional grid of sprites to mirror the two
 52    # dimensional grid of numbers. This points to the SAME sprites that are
 53    # in grid_sprite_list, just in a 2d manner.
 54    grid_sprites_two_dim: list[list[SpriteCircle]] = []
 55
 56    # Calculate values we'll re-use below
 57    cell_width, cell_height = cell_size
 58    half_width = cell_width // 2
 59    half_height = cell_height // 2
 60
 61    x_step = cell_width + cell_margin
 62    y_step = cell_height + cell_margin
 63
 64    center_offset_x = half_width + cell_margin
 65    center_offset_y = half_height + cell_margin
 66
 67    # Fit sprites into the cell size
 68    radius = min(half_width, half_height)
 69
 70    # Create a list of sprites to represent each grid location
 71    for row in range(ROW_COUNT):
 72        grid_sprites_two_dim.append([])
 73
 74        for column in range(COLUMN_COUNT):
 75            # Position the sprite
 76            x = column * x_step + center_offset_x
 77            y = row * y_step + center_offset_y
 78
 79            # Make the sprite as a soft circle
 80            sprite = SpriteCircle(radius, ALIVE_COLOR, True, center_x=x, center_y=y)
 81
 82            # Add the sprite to both lists
 83            grid_sprites_one_dim.append(sprite)
 84            grid_sprites_two_dim[row].append(sprite)
 85
 86    return grid_sprites_one_dim, grid_sprites_two_dim
 87
 88
 89def randomize_grid(grid: arcade.SpriteList):
 90    """Randomize the grid to alive/dead"""
 91    for cell in grid:
 92        pick = random.randrange(2)
 93        if pick:
 94            cell.alpha = ALPHA_ON
 95        else:
 96            cell.alpha = ALPHA_OFF
 97
 98
 99class GameView(arcade.View):
100    """
101    Main application class.
102    """
103
104    def __init__(self):
105        """
106        Set up the application.
107        """
108        super().__init__()
109
110        self.background_color = BACKGROUND_COLOR
111
112        # We need two layers. One holds the current state of our grid, the other
113        # holds the next frame's state. We flip back and forth between the two.
114        grid_sprites_one_dim1, grid_sprites_two_dim1 = create_grids()
115        grid_sprites_one_dim2, grid_sprites_two_dim2 = create_grids()
116
117        self.layers_grid_sprites_one_dim = [grid_sprites_one_dim1, grid_sprites_one_dim2]
118        self.layers_grid_sprites_two_dim = [grid_sprites_two_dim1, grid_sprites_two_dim2]
119
120        self.cur_layer = 0
121        randomize_grid(self.layers_grid_sprites_one_dim[0])
122
123    def reset(self):
124        """Reset the grid"""
125        randomize_grid(self.layers_grid_sprites_one_dim[0])
126
127    def on_draw(self):
128        """Render the screen."""
129        # Clear all pixels in the window
130        self.clear()
131        self.layers_grid_sprites_one_dim[0].draw()
132
133    def on_key_press(self, symbol: int, modifiers: int):
134        """Handle key press events"""
135        if symbol == arcade.key.SPACE:
136            self.reset()
137        elif symbol == arcade.key.ESCAPE:
138            self.window.close()
139
140    def on_update(self, delta_time: float):
141        """Update the grid"""
142
143        # Flip layers
144        if self.cur_layer == 0:
145            layer1 = self.layers_grid_sprites_two_dim[0]
146            layer2 = self.layers_grid_sprites_two_dim[1]
147            self.cur_layer = 1
148        else:
149            layer1 = self.layers_grid_sprites_two_dim[1]
150            layer2 = self.layers_grid_sprites_two_dim[0]
151            self.cur_layer = 0
152
153        # Count the neighbors that are alive
154        for row in range(ROW_COUNT):
155            for column in range(COLUMN_COUNT):
156                live_neighbors = 0
157                # -1 -1
158                if row > 0 and column > 0 and layer1[row - 1][column - 1].alpha == ALPHA_ON:
159                    live_neighbors += 1
160                # -1  0
161                if row > 0 and layer1[row - 1][column].alpha == ALPHA_ON:
162                    live_neighbors += 1
163                # -1 +1
164                if (
165                    row > 0
166                    and column < COLUMN_COUNT - 1
167                    and layer1[row - 1][column + 1].alpha == ALPHA_ON
168                ):
169                    live_neighbors += 1
170                #  0 +1
171                if column < COLUMN_COUNT - 1 and layer1[row][column + 1].alpha == ALPHA_ON:
172                    live_neighbors += 1
173                # +1 +1
174                if (
175                    row < ROW_COUNT - 1
176                    and column < COLUMN_COUNT - 1
177                    and layer1[row + 1][column + 1].alpha == ALPHA_ON
178                ):
179                    live_neighbors += 1
180                # +1  0
181                if row < ROW_COUNT - 1 and layer1[row + 1][column].alpha == ALPHA_ON:
182                    live_neighbors += 1
183                # +1 -1
184                if (
185                    row < ROW_COUNT - 1
186                    and column > 0
187                    and layer1[row + 1][column - 1].alpha == ALPHA_ON
188                ):
189                    live_neighbors += 1
190                #  0 -1
191                if column > 0 and layer1[row][column - 1].alpha == ALPHA_ON:
192                    live_neighbors += 1
193
194                """
195                Implement Conway's game of life rules
196
197                Any live cell with two or three live neighbours survives.
198                Any dead cell with three live neighbours becomes a live cell.
199                All other live cells die in the next generation. Similarly,
200                all other dead cells stay dead.
201                """
202                # Shortcut the cell sprites
203                l1_sprite = layer1[row][column]
204                l2_sprite = layer2[row][column]
205
206                if l1_sprite.alpha == ALPHA_ON and (live_neighbors == 2 or live_neighbors == 3):
207                    if l2_sprite.alpha == ALPHA_OFF:
208                        l2_sprite.alpha = ALPHA_ON
209                elif l1_sprite.alpha == ALPHA_OFF and live_neighbors == 3:
210                    if l2_sprite.alpha == ALPHA_OFF:
211                        l2_sprite.alpha = ALPHA_ON
212                else:
213                    if l2_sprite.alpha == ALPHA_ON:
214                        l2_sprite.alpha = ALPHA_OFF
215
216
217def main():
218    """Main function"""
219    # Create a window class. This is what actually shows up on screen
220    window = arcade.Window(WINDOW_WIDTH, WINDOW_HEIGHT, WINDOW_TITLE)
221    window.center_window()
222
223    # Create and setup the GameView
224    game = GameView()
225
226    # Show GameView on screen
227    window.show_view(game)
228
229    # Start the arcade game loop
230    arcade.run()
231
232
233if __name__ == "__main__":
234    main()