{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import json\n",
    "import numpy as np\n",
    "import pandas as pd\n",
    "import panel as pn\n",
    "\n",
    "pn.extension('deckgl')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "This demo was adapted from [PyDeck's Conway Game of Life example](https://github.com/uber/deck.gl/blob/66c75051d5b385db31f0a4322dff054779824783/bindings/pydeck/examples/06%20-%20Conway's%20Game%20of%20Life.ipynb), full copyright lies with the original authors.\n",
    "\n",
    "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."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Declare Game of Life logic"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import random\n",
    "\n",
    "def new_board(x, y, num_live_cells=2, num_dead_cells=3):\n",
    "    \"\"\"Initializes a board for Conway's Game of Life\"\"\"\n",
    "    board = []\n",
    "    for i in range(0, y):\n",
    "        # Defaults to a 3:2 dead cell:live cell ratio\n",
    "        board.append([random.choice([0] * num_dead_cells + [1] * num_live_cells) for _ in range(0, x)])\n",
    "    return board\n",
    "\n",
    "        \n",
    "def get(board, x, y):\n",
    "    \"\"\"Return the value at location (x, y) on a board, wrapping around if out-of-bounds\"\"\"\n",
    "    return board[y % len(board)][x % len(board[0])]\n",
    "\n",
    "\n",
    "def assign(board, x, y, value):\n",
    "    \"\"\"Assigns a value at location (x, y) on a board, wrapping around if out-of-bounds\"\"\"\n",
    "    board[y % len(board)][x % len(board[0])] = value\n",
    "\n",
    "\n",
    "def count_neighbors(board, x, y):\n",
    "    \"\"\"Counts the number of living neighbors a cell at (x, y) on a board has\"\"\"\n",
    "    return sum([\n",
    "        get(board, x - 1, y),\n",
    "        get(board, x + 1, y),\n",
    "        get(board, x, y - 1),\n",
    "        get(board, x, y + 1),\n",
    "        get(board, x + 1, y + 1),\n",
    "        get(board, x + 1, y - 1),\n",
    "        get(board, x - 1, y + 1),\n",
    "        get(board, x - 1, y - 1)])\n",
    "\n",
    "\n",
    "def process_life(board):\n",
    "    \"\"\"Creates the next iteration from a passed state of Conway's Game of Life\"\"\"\n",
    "    next_board = new_board(len(board[0]), len(board))\n",
    "    for y in range(0, len(board)):\n",
    "        for x in range(0, len(board[y])):\n",
    "            num_neighbors = count_neighbors(board, x, y)\n",
    "            is_alive = get(board, x, y) == 1\n",
    "            if num_neighbors < 2 and is_alive:\n",
    "                assign(next_board, x, y, 0)\n",
    "            elif 2 <= num_neighbors <= 3 and is_alive:\n",
    "                assign(next_board, x, y, 1)\n",
    "            elif num_neighbors > 3 and is_alive:\n",
    "                assign(next_board, x, y, 0)\n",
    "            elif num_neighbors == 3 and not is_alive:\n",
    "                assign(next_board, x, y, 1)\n",
    "            else:\n",
    "                assign(next_board, x, y, 0)\n",
    "    return next_board"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Set up DeckGL JSON"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "points = {\n",
    "    '@@type': 'PointCloudLayer',\n",
    "    'data': [],\n",
    "    'getColor': '@@=color',\n",
    "    'getPosition': '@@=position',\n",
    "    'getRadius': 40,\n",
    "    'id': '0558257e-1a5c-43d7-bd98-1fba69981c6c'\n",
    "}\n",
    "\n",
    "board_json = {\n",
    "    \"initialViewState\": {\n",
    "        \"bearing\": 44,\n",
    "        \"latitude\": 0.01,\n",
    "        \"longitude\": 0.01,\n",
    "        \"pitch\": 45,\n",
    "        \"zoom\": 13\n",
    "    },\n",
    "    \"layers\": [points],\n",
    "    \"mapStyle\": \"\",\n",
    "    \"views\": [\n",
    "        {\n",
    "            \"@@type\": \"MapView\",\n",
    "            \"controller\": True\n",
    "        }\n",
    "    ]\n",
    "}"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Declare callbacks to periodically update the board"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "PINK = [155, 155, 255, 245]\n",
    "PURPLE = [255, 155, 255, 245]\n",
    "\n",
    "SCALING_FACTOR = 1000.0\n",
    "\n",
    "def convert_board_to_df(board):\n",
    "    \"\"\"Makes the board matrix into a list for easier processing\"\"\"\n",
    "    rows = []\n",
    "    for x in range(0, len(board[0])):\n",
    "        for y in range(0, len(board)):\n",
    "            rows.append([[x / SCALING_FACTOR, y / SCALING_FACTOR], PURPLE if board[y][x] else PINK])\n",
    "    return pd.DataFrame(rows, columns=['position', 'color'])\n",
    "\n",
    "def run_gol(event=None):\n",
    "    global board\n",
    "    board = process_life(board)\n",
    "    records = convert_board_to_df(board)\n",
    "    points['data'] = records\n",
    "    gol.param.trigger('object')\n",
    "    \n",
    "def reset_board(event):\n",
    "    global board\n",
    "    board = new_board(30, 30)\n",
    "    run_gol()\n",
    "\n",
    "def toggle_periodic_callback(event):\n",
    "    if event.new:\n",
    "        periodic_toggle.name = 'Stop'\n",
    "        periodic_toggle.button_type = 'danger'\n",
    "        periodic_cb.start()\n",
    "    else:\n",
    "        periodic_toggle.name = 'Run'\n",
    "        periodic_toggle.button_type = 'success'\n",
    "        periodic_cb.stop()\n",
    "        \n",
    "def update_period(event):\n",
    "    periodic_cb.period = event.new"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Set up Panel and callbacks"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "board = new_board(30, 30)\n",
    "\n",
    "gol = pn.pane.DeckGL(board_json, width=600, height=600)\n",
    "\n",
    "run_gol()\n",
    "\n",
    "periodic_toggle = pn.widgets.Toggle(\n",
    "    name='Run', value=False, button_type='primary', align='end', width=50\n",
    ")\n",
    "periodic_toggle.param.watch(toggle_periodic_callback, 'value')\n",
    "\n",
    "period = pn.widgets.Spinner(name=\"Period (ms)\", value=500, step=50, start=50,\n",
    "                            align='end', width=100)\n",
    "period.param.watch(update_period, 'value')\n",
    "\n",
    "reset = pn.widgets.Button(name='Reset', button_type='warning', width=60, align='end')\n",
    "reset.on_click(reset_board)\n",
    "\n",
    "periodic_cb = gol.add_periodic_callback(run_gol, start=False, period=period.value)\n",
    "\n",
    "pn.Column(\n",
    "    '## Game of Life (using Deck.GL)',\n",
    "    gol,\n",
    "    pn.Row(period, periodic_toggle, reset)\n",
    ").servable()"
   ]
  }
 ],
 "metadata": {
  "language_info": {
   "name": "python",
   "pygments_lexer": "ipython3"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
