Deckgl Game Of Life#

This demo was adapted from PyDeck’s Conway Game of Life example, full copyright lies with the original authors.

This modified example demonstrates how to display and update a DeckGL pane with a periodic callback. The board lives in a reactive expression and the pane binds to a pure function that turns the board into a deck.gl spec, so each tick only assigns a new board.

import numpy as np
import pandas as pd
import panel as pn

pn.extension('deckgl', sizing_mode="stretch_width")

Declare Game of Life logic#

The board is a 2D array that wraps around at its edges, so the neighbour count is the sum of the board shifted in all eight directions.

SIZE = 30

def new_board(size=SIZE, live_fraction=0.4):
    """Initializes a board for Conway's Game of Life"""
    return (np.random.default_rng().random((size, size)) < live_fraction).astype(int)

def process_life(board):
    """Creates the next iteration from a passed state of Conway's Game of Life"""
    neighbors = sum(
        np.roll(board, (dy, dx), axis=(0, 1))
        for dy in (-1, 0, 1) for dx in (-1, 0, 1)
        if (dy, dx) != (0, 0)
    )
    return ((neighbors == 3) | ((board == 1) & (neighbors == 2))).astype(int)

Convert the board to a DeckGL spec#

PINK = [155, 155, 255, 245]
PURPLE = [255, 155, 255, 245]

SCALING_FACTOR = 1000.0

def board_to_spec(board):
    ys, xs = np.indices(board.shape)
    cells = pd.DataFrame({
        'position': [[x / SCALING_FACTOR, y / SCALING_FACTOR] for x, y in zip(xs.ravel(), ys.ravel())],
        'color': [PURPLE if alive else PINK for alive in board.ravel()],
    })
    return {
        "initialViewState": {
            "bearing": 44,
            "latitude": 0.01,
            "longitude": 0.01,
            "pitch": 45,
            "zoom": 13
        },
        "layers": [{
            '@@type': 'PointCloudLayer',
            'data': cells,
            'getColor': '@@=color',
            'getPosition': '@@=position',
            'getRadius': 40,
            'id': 'cells',
        }],
        "mapStyle": None,
        "views": [{"@@type": "MapView", "controller": True}]
    }

Set up Panel and callbacks#

A single periodic callback advances the board. It is created stopped, and its running and period parameters drive the toggle and the period input directly.

board = pn.rx(new_board())

def step():
    board.rx.value = process_life(board.rx.value)

def reset_board(event):
    board.rx.value = new_board()

periodic_cb = pn.state.add_periodic_callback(step, period=500, start=False)
running = periodic_cb.param.running.rx()

gol = pn.ui.DeckGL(board.rx.pipe(board_to_spec), height=400)

reset = pn.ui.Button(label='Reset', color='warning', width=80, align='end')
reset.on_click(reset_board)

settings = pn.ui.Row(
    pn.ui.IntInput.from_param(periodic_cb.param.period, label="Period (ms)", description="", step=50, start=50, width=150),
    pn.ui.Toggle.from_param(
        periodic_cb.param.running, description="", label=running.rx.where('Stop', 'Run'),
        color=running.rx.where('warning', 'primary'), align='end', width=80,
    ),
    reset,
)

description = pn.ui.Markdown("""
**Conway's Game of Life** is a classic demonstration of *emergence*, where higher level patterns form from a few simple rules.
Fantastic patterns emerge when the game is let to run long enough.

The **rules** here, to borrow from [Wikipedia](https://en.wikipedia.org/wiki/Conway%27s_Game_of_Life), are as follows:

- Any live cell with fewer than two live neighbours dies, as if by underpopulation.
- Any live cell with two or three live neighbours lives on to the next generation.
- Any live cell with more than three live neighbours dies, as if by overpopulation.
- Any dead cell with exactly three live neighbours becomes a live cell, as if by reproduction.
""")

page = pn.ui.Page(
    main=[pn.ui.Container(pn.ui.Column(description, gol, settings))],
    title="Deck.gl - Game of Life",
)

page

Served App#

if pn.state.served:
    page.servable()