{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Conway's Game of Life\n",
    "\n",
    "Conway's Game of Life is a classic demonstration of [emergence](https://en.wikipedia.org/wiki/Emergence), where higher level patterns form from a few simple rules. Fantastic patterns emerge when the game is let to run long enough.\n",
    "\n",
    "The rules here, to borrow from [Wikipedia](https://en.wikipedia.org/wiki/Conway%27s_Game_of_Life), are as follows:\n",
    "\n",
    "> 1. Any live cell with fewer than two live neighbours dies, as if by underpopulation.\n",
    "> 2. Any live cell with two or three live neighbours lives on to the next generation.\n",
    "> 3. Any live cell with more than three live neighbours dies, as if by overpopulation.\n",
    "> 4. Any dead cell with exactly three live neighbours becomes a live cell, as if by reproduction.\n",
    "\n",
    "Below is a simple Conway's Game of Life implementation:"
   ]
  },
  {
   "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": [
    "## A text-based example\n",
    "\n",
    "To plot a simple version of Conway's Game of Life, we can use a print function:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from IPython.display import clear_output\n",
    "import time\n",
    "\n",
    "def draw_board(board):\n",
    "    res = ''\n",
    "    for row in board:\n",
    "        for col in row:\n",
    "            if col == 1:\n",
    "                res += '* '\n",
    "            else:\n",
    "                res += '  '\n",
    "        res += '\\n'\n",
    "    return res\n",
    "\n",
    "board = new_board(20, 20)\n",
    "\n",
    "NUM_ITERATIONS = 100\n",
    "\n",
    "for i in range(0, NUM_ITERATIONS):\n",
    "    print('Iteration ' + str(i + 1))\n",
    "    board = process_life(board)\n",
    "    res = draw_board(board)\n",
    "    print(res)\n",
    "    time.sleep(0.1)\n",
    "    clear_output(wait=True)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## pydeck implementation\n",
    "\n",
    "We can use either the [PointCloudLayer](https://deck.gl/showcases/gallery/point-cloud-layer) or [ScatterplotLayer](https://deck.gl/showcases/gallery/scatterplot-layer) from deck.gl to visualize the game."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "scrolled": true
   },
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "import pandas as pd\n",
    "import pydeck as deck\n",
    "\n",
    "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",
    "board = new_board(30, 30)\n",
    "records = convert_board_to_df(board)\n",
    "layer = deck.Layer(\n",
    "    'PointCloudLayer',\n",
    "    records,\n",
    "    get_position='position',\n",
    "    get_color='color',\n",
    "    get_radius=40)\n",
    "view_state = deck.ViewState(latitude=0.00, longitude=0.00, zoom=13, bearing=44, pitch=45)\n",
    "r = deck.Deck(layers=[layer], initial_view_state=view_state, map_style='')\n",
    "r.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "To play the game over time, we call `update` in a loop."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "NUM_ITERATIONS = 100\n",
    "display(r.show())\n",
    "for i in range(0, NUM_ITERATIONS):\n",
    "    board = process_life(board)\n",
    "    records = convert_board_to_df(board)\n",
    "    layer.data = records\n",
    "    r.update()\n",
    "    time.sleep(0.1)"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.7.7"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
