Deckgl game of life

Download this notebook from GitHub (right-click to download).


In [ ]:
import json
import numpy as np
import pandas as pd
import panel as pn

pn.extension('deckgl')

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 by modifying the JSON representation and triggering an update.

Declare Game of Life logic

In [ ]:
import random

def new_board(x, y, num_live_cells=2, num_dead_cells=3):
    """Initializes a board for Conway's Game of Life"""
    board = []
    for i in range(0, y):
        # Defaults to a 3:2 dead cell:live cell ratio
        board.append([random.choice([0] * num_dead_cells + [1] * num_live_cells) for _ in range(0, x)])
    return board

        
def get(board, x, y):
    """Return the value at location (x, y) on a board, wrapping around if out-of-bounds"""
    return board[y % len(board)][x % len(board[0])]


def assign(board, x, y, value):
    """Assigns a value at location (x, y) on a board, wrapping around if out-of-bounds"""
    board[y % len(board)][x % len(board[0])] = value


def count_neighbors(board, x, y):
    """Counts the number of living neighbors a cell at (x, y) on a board has"""
    return sum([
        get(board, x - 1, y),
        get(board, x + 1, y),
        get(board, x, y - 1),
        get(board, x, y + 1),
        get(board, x + 1, y + 1),
        get(board, x + 1, y - 1),
        get(board, x - 1, y + 1),
        get(board, x - 1, y - 1)])


def process_life(board):
    """Creates the next iteration from a passed state of Conway's Game of Life"""
    next_board = new_board(len(board[0]), len(board))
    for y in range(0, len(board)):
        for x in range(0, len(board[y])):
            num_neighbors = count_neighbors(board, x, y)
            is_alive = get(board, x, y) == 1
            if num_neighbors < 2 and is_alive:
                assign(next_board, x, y, 0)
            elif 2 <= num_neighbors <= 3 and is_alive:
                assign(next_board, x, y, 1)
            elif num_neighbors > 3 and is_alive:
                assign(next_board, x, y, 0)
            elif num_neighbors == 3 and not is_alive:
                assign(next_board, x, y, 1)
            else:
                assign(next_board, x, y, 0)
    return next_board

Set up DeckGL JSON

In [ ]:
points = {
    '@@type': 'PointCloudLayer',
    'data': [],
    'getColor': '@@=color',
    'getPosition': '@@=position',
    'getRadius': 40,
    'id': '0558257e-1a5c-43d7-bd98-1fba69981c6c'
}

board_json = {
    "initialViewState": {
        "bearing": 44,
        "latitude": 0.01,
        "longitude": 0.01,
        "pitch": 45,
        "zoom": 13
    },
    "layers": [points],
    "mapStyle": "",
    "views": [
        {
            "@@type": "MapView",
            "controller": True
        }
    ]
}

Declare callbacks to periodically update the board

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

SCALING_FACTOR = 1000.0

def convert_board_to_df(board):
    """Makes the board matrix into a list for easier processing"""
    rows = []
    for x in range(0, len(board[0])):
        for y in range(0, len(board)):
            rows.append([[x / SCALING_FACTOR, y / SCALING_FACTOR], PURPLE if board[y][x] else PINK])
    return pd.DataFrame(rows, columns=['position', 'color'])

def run_gol(event=None):
    global board
    board = process_life(board)
    records = convert_board_to_df(board)
    points['data'] = records
    gol.param.trigger('object')
    
def reset_board(event):
    global board
    board = new_board(30, 30)
    run_gol()

def toggle_periodic_callback(event):
    if event.new:
        periodic_toggle.name = 'Stop'
        periodic_toggle.button_type = 'danger'
        periodic_cb.start()
    else:
        periodic_toggle.name = 'Run'
        periodic_toggle.button_type = 'success'
        periodic_cb.stop()
        
def update_period(event):
    periodic_cb.period = event.new

Set up Panel and callbacks

In [ ]:
board = new_board(30, 30)

gol = pn.pane.DeckGL(board_json, width=600, height=600)

run_gol()

periodic_toggle = pn.widgets.Toggle(
    name='Run', value=False, button_type='primary', align='end', width=50
)
periodic_toggle.param.watch(toggle_periodic_callback, 'value')

period = pn.widgets.Spinner(name="Period (ms)", value=500, step=50, start=50,
                            align='end', width=100)
period.param.watch(update_period, 'value')

reset = pn.widgets.Button(name='Reset', button_type='warning', width=60, align='end')
reset.on_click(reset_board)

periodic_cb = gol.add_periodic_callback(run_gol, start=False, period=period.value)

pn.Column(
    '## Game of Life (using Deck.GL)',
    gol,
    pn.Row(period, periodic_toggle, reset)
).servable()

Download this notebook from GitHub (right-click to download).