version: 2
title: Conway's Game of Life
contributor: Matthew Cullum https://github.com/brackishman
summary: Conway's Game of Life in Quine
description: |-
  This recipe implements a generic Conway's Game of Life using standing queries for
  real-time cellular automaton evolution. The grid size, initial patterns, and
  configuration are loaded from a JSON file specified at runtime.

  Each cell evaluates its neighbors and changes state only when Conway's rules dictate
  a change, triggering cascading updates throughout the grid.

  Conway's Rules:
  1. Live cell with 2-3 live neighbors survives
  2. Dead cell with exactly 3 live neighbors becomes alive
  3. All other cells die or stay dead

  Usage: Specify JSON config file with --recipe-value config_file=path/to/config.json
  The config file schema is as follows:
  {
    "name": "My Game of Life",
    "description": "A description of this setup",
    "gridWidth": 10,
    "gridHeight": 10,
    "initialPattern": [
      {"x": 1, "y": 0, "alive": true},
      {"x": 2, "y": 1, "alive": true},
      {"x": 0, "y": 2, "alive": true},
      {"x": 1, "y": 2, "alive": true},
      {"x": 2, "y": 2, "alive": true}
    ]
  }

  In Quine you can view all cell nodes with the following query: MATCH (c:Cell) RETURN c

  Once Quine is running with this recipe, load the layout json from the UI to see the grid.
  You can create a new layout by running the generate-conways-layout.js script while Quine is running.

  Once the cell nodes are layed out, make sure to enable the bookmarklet. The javascript for the bookmarklet is in conways-gol-bookmarklet.js

  Start the game with the "▶️ START Game" quick query on any cell node, and pause it with the "⏸️ STOP Game" quick query.

# Set up grid dynamically from JSON configuration file
ingestStreams:
  - name: gol-config-ingest
    source:
      type: File
      path: $config_file
      format:
        type: Json
    query: |-
      // Extract configuration from JSON and calculate totalCells
      WITH $that.gridWidth AS gridWidth,
           $that.gridHeight AS gridHeight,
           $that.gridWidth * $that.gridHeight AS totalCells,
           $that.name AS name,
           $that.description AS description,
           $that.initialPattern AS initialPattern

      // Create all grid cells (totalCells = gridWidth * gridHeight)
      UNWIND range(0, totalCells - 1) AS cellIndex
      WITH gridWidth, gridHeight, totalCells, name, description, initialPattern,
           cellIndex % gridWidth AS x,
           cellIndex / gridWidth AS y

      // Determine if this cell should be alive based on initialPattern
      WITH x, y, gridWidth, gridHeight, totalCells, name, description,
           CASE
             WHEN any(pattern IN initialPattern WHERE pattern.x = x AND pattern.y = y AND pattern.alive = true) THEN true
             ELSE false
           END AS alive

      // Create/update the specific cell
      MATCH (cell)
      WHERE id(cell) = idFrom("cell", x, y)
      SET cell.x = x,
          cell.y = y,
          cell.alive = alive,
          cell.generation = 0,
          cell.state = "applied",
          cell: Cell

      // Create neighbor relationships within grid bounds
      WITH cell, x, y, gridWidth, gridHeight, totalCells, name, description
      UNWIND [
        [x-1, y-1], [x, y-1], [x+1, y-1],
        [x-1, y],             [x+1, y],
        [x-1, y+1], [x, y+1], [x+1, y+1]
      ] AS neighbor
      WITH cell, neighbor[0] AS nx, neighbor[1] AS ny, gridWidth, gridHeight, totalCells, name, description
      WHERE nx >= 0 AND nx < gridWidth AND ny >= 0 AND ny < gridHeight
      MATCH (neighborCell)
      WHERE id(neighborCell) = idFrom("cell", nx, ny)
      CREATE (cell)-[:NEIGHBOR]->(neighborCell)

      // Create/update ready node with configuration and connect to this cell
      WITH cell, gridWidth, gridHeight, totalCells, name, description
      MATCH (ready)
      WHERE id(ready) = idFrom("ready")
      SET ready.computingCells = 0,
          ready.applyingCells = 0,
          ready.generation = 0,
          ready.state = "stopped",
          ready.totalCells = totalCells,
          ready.gridWidth = gridWidth,
          ready.gridHeight = gridHeight,
          ready.name = name,
          ready.description = description
      CREATE (ready)-[:ACTIVATES]->(cell)

# Standing queries for two-wave Conway's Game of Life evolution (fully dynamic)
standingQueries:
  # Wave 1: Compute next state for all cells
  - name: compute-wave
    pattern:
      type: Cypher
      mode: MultipleValues
      query: >-
        MATCH (ready)-[:ACTIVATES]->(cell)
        WHERE ready.computingCells = ready.totalCells AND ready.state = "computing"
        RETURN id(cell) AS cellId
    outputs:
      - name: compute-next-state
        resultEnrichment:
          query: |-
            MATCH (cell)-[:NEIGHBOR]->(neighbor)
            WHERE id(cell) = $that.data.cellId
            WITH cell, count(CASE WHEN neighbor.alive = true THEN 1 END) AS liveNeighbors
            WITH cell, liveNeighbors, CASE
              WHEN cell.alive = false AND liveNeighbors = 3 THEN true
              WHEN cell.alive = true AND (liveNeighbors = 2 OR liveNeighbors = 3) THEN true
              ELSE false
            END AS nextAlive
            SET cell.nextAlive = nextAlive,
                cell.state = "calculated"
            WITH cell
            MATCH (ready)-[:ACTIVATES]->(cell)
            WHERE id(cell) = $that.data.cellId
            CALL int.add(ready, "computingCells", -1) YIELD result
            RETURN cell.x AS x, cell.y AS y, cell.nextAlive AS nextAlive, "calculated" AS cellState, result AS remainingCells
          parameter: that
        destinations:
          - type: StandardOut

  # Wave 2: Apply computed state changes
  - name: apply-wave
    pattern:
      type: Cypher
      mode: MultipleValues
      query: >-
        MATCH (ready)-[:ACTIVATES]->(cell)
        WHERE ready.applyingCells = ready.totalCells AND ready.state = "applying"
        RETURN id(cell) AS cellId
    outputs:
      - name: apply-state-change
        resultEnrichment:
          query: |-
            MATCH (cell)
            WHERE id(cell) = $that.data.cellId
            WITH cell, cell.alive AS oldAlive, cell.nextAlive AS newAlive
            SET cell.alive = newAlive,
                cell.updated = (oldAlive <> newAlive),
                cell.state = "applied"
            WITH cell
            MATCH (ready)-[:ACTIVATES]->(cell)
            WHERE id(cell) = $that.data.cellId
            CALL int.add(ready, "applyingCells", -1) YIELD result
            RETURN cell.x AS x, cell.y AS y, cell.alive AS alive, "applied" AS cellState, result AS remainingCells
          parameter: that
        destinations:
          - type: StandardOut

  # Wave coordination: Wave 1 complete -> Start Wave 2 (two-phase lock)
  - name: wave-1-to-2
    pattern:
      type: Cypher
      mode: MultipleValues
      query: >-
        MATCH (ready)
        WHERE ready.computingCells = 0 AND ready.applyingCells = 0 AND ready.state = "computing"
        RETURN id(ready) AS readyId
    outputs:
      - name: start-wave-2
        resultEnrichment:
          query: |-
            MATCH (ready)-[:ACTIVATES]->(cell)
            WHERE id(ready) = $that.data.readyId
            WITH ready, ready.totalCells AS TOTAL_CELLS, count(CASE WHEN cell.state = "calculated" THEN 1 END) AS calculatedCells
            WHERE calculatedCells = TOTAL_CELLS
            SET ready.applyingCells = TOTAL_CELLS,
                ready.state = "applying"
            RETURN "Starting Wave 2" AS message, TOTAL_CELLS AS cellCount, calculatedCells AS verifiedCells
          parameter: that
        destinations:
          - type: StandardOut

  # Wave coordination: Wave 2 complete -> Start next generation Wave 1 (two-phase lock)
  - name: wave-2-to-next-gen
    pattern:
      type: Cypher
      mode: MultipleValues
      query: >-
        MATCH (ready)
        WHERE ready.applyingCells = 0 AND ready.computingCells = 0 AND ready.state = "applying"
        RETURN id(ready) AS readyId
    outputs:
      - name: start-next-generation
        resultEnrichment:
          query: |-
            MATCH (ready)-[:ACTIVATES]->(cell)
            WHERE id(ready) = $that.data.readyId
            WITH ready, ready.totalCells AS TOTAL_CELLS, count(CASE WHEN cell.state = "applied" THEN 1 END) AS appliedCells
            WHERE appliedCells = TOTAL_CELLS
            CALL int.add(ready, "generation", 1) YIELD result
            SET ready.computingCells = TOTAL_CELLS,
                ready.state = "computing"
            RETURN "Starting Generation" AS message, result AS generation, TOTAL_CELLS AS cellCount, appliedCells AS verifiedCells
          parameter: that
        destinations:
          - type: StandardOut

# UI Configuration - works with any grid size
nodeAppearances:
  - predicate:
      propertyKeys: ["alive", "x", "y"]
      knownValues:
        alive: true
      dbLabel: Cell
    icon: ion-record
    color: "#FF4500"
    size: 50.0
    label:
      type: Property
      key: "x"
      prefix: "● ("
  - predicate:
      propertyKeys: ["alive", "x", "y"]
      knownValues:
        alive: false
      dbLabel: Cell
    icon: ion-record
    color: "#CCCCCC"
    size: 15.0
    label:
      type: Property
      key: "x"
      prefix: "○ ("

quickQueries:
  - predicate:
      propertyKeys: []
      knownValues: {}
    quickQuery:
      name: Refresh
      querySuffix: RETURN n
      sort:
        type: Node
  - predicate:
      propertyKeys: []
      knownValues: {}
    quickQuery:
      name: Local Properties
      querySuffix: RETURN id(n), properties(n)
      sort:
        type: Text
  - predicate:
      propertyKeys: ["x", "y"]
      knownValues: {}
      dbLabel: Cell
    quickQuery:
      name: "▶️ START Game"
      querySuffix: |-
        MATCH (ready) WHERE id(ready) = idFrom("ready")
        SET ready.computingCells = ready.totalCells, ready.state = "computing"
        RETURN n
      sort:
        type: Node
  - predicate:
      propertyKeys: ["x", "y"]
      knownValues: {}
      dbLabel: Cell
    quickQuery:
      name: "⏸️ STOP Game"
      querySuffix: |-
        MATCH (ready) WHERE id(ready) = idFrom("ready")
        SET ready.computingCells = 0, ready.applyingCells = 0, ready.state = "stopped"
        RETURN n
      sort:
        type: Node

sampleQueries:
  - name: "● Show All Cells"
    query: |-
      MATCH (c:Cell) RETURN c
  - name: "📊 Show Game Configuration"
    query: |-
      MATCH (ready) WHERE id(ready) = idFrom("ready")
      MATCH (c:Cell)
      RETURN
        ready.name AS setup,
        ready.description AS description,
        ready.gridWidth AS width,
        ready.gridHeight AS height,
        ready.totalCells AS totalCells,
        count(CASE WHEN c.alive = true THEN 1 END) AS liveCells,
        ready.generation AS currentGeneration

statusQuery:
  cypherQuery: |-
    MATCH (c:Cell)
    RETURN c
