Before diving into development, thoroughly reading the Quadrille API documentation is key to efficient coding, preventing mistakes, and ensuring optimal performance. By acquainting yourself with the API’s nuances, you set the stage for informed decision-making, seamless collaboration, and a smoother development journey.

The following demo illustrates the game of life using a pentadecathlon pattern as its initial seed, rendered as a surface texture on p5 3D shapes in WEBGL mode:

(mouse drag to navigate; [1..7] keys change shape, other keys reset)

The program uses three quadrille instances: game, next and pattern. While the game quadrille visualizes the current state of the game, the next quadrille calculates its subsequent iteration, and the pattern quadrille establishes the game’s initial seed. Cells filled with the life value represent living cells (i.e., fill() resurrects a dead cell), while empty cells denote the dead ones (i.e., clear() kills a living cell).

To render the game as a texture, a p5.Graphics object is initialized with graphics = createGraphics(game.width * Quadrille.cellLength, game.height * Quadrille.cellLength), which is then set as the rendering context using drawQuadrille(game, { graphics, outline: 'magenta' }) and subsequently applied as a texture using texture(graphics).

code
Quadrille.cellLength = 20
let game, pattern
let life
let graphics
let mode = 1
const shapes = {
  1: () => plane(width, height),
  2: () => sphere(100),
  3: () => torus(100, 50),
  4: () => box(200),
  5: () => cylinder(100, 200, 24, 1, false, false),
  6: () => cone(100, 200, 24, 1, false),
  7: () => ellipsoid(100, 180)
}

function setup() {
  game = createQuadrille(20, 20)
  life = color('lime')
  pattern = createQuadrille(3, 16252911n, life)
  game = Quadrille.or(game, pattern, 6, 8)
  createCanvas(game.width  * Quadrille.cellLength,
               game.height * Quadrille.cellLength,
               WEBGL)
  graphics = createGraphics(game.width  * Quadrille.cellLength,
                            game.height * Quadrille.cellLength)
  update()
}

function draw() {
  background('yellow')
  orbitControl()
  frameCount % 20 === 0 && update()
  noStroke()
  shapes[mode] ? shapes[mode]() : shapes[1]()
}

function update() {
  const next = game.clone()
  visitQuadrille(game, (row, col) => {
                       const order = game.ring(row, col).order
                       game.isFilled(row, col) ?
                       (order < 3 || order > 4) && next.clear(row, col) :
                       order === 3 && next.fill(row, col, life) })
  game = next
  graphics.background('blue')
  drawQuadrille(game, { graphics, outline: 'magenta' })
  texture(graphics)
}

function keyPressed() {
  mode = +key
  game.clear()
  game = Quadrille.or(game, pattern, 6, 8)
}

API references

Patterns

The initial seed is defined using a BigInt encoding a quadrille filling pattern, i.e., pattern = createQuadrille(3, 16252911n, life) which is equivalent to:

pattern = createQuadrille([[life, life, life],
                           [life, null, life],
                           [life, life, life],
                           [life, life, life],
                           [life, life, life],
                           [life, life, life],
                           [life, null, life],
                           [life, life, life]
                           ])
pattern.toBigInt() // 16252911n

Rules

The game of life rules are applied to each cell by:

const next = game.clone()
visitQuadrille(game, (row, col) => {
                       const order = game.ring(row, col).order
                       game.isFilled(row, col) ?
                       (order < 3 || order > 4) && next.clear(row, col) :
                       order === 3 && next.fill(row, col, life) })
game = next

The (arrow) anonymous function first compute the order of the game ring (of dimension 1) centered at (row, col): const order = game.ring(row, col).order, and then use it to apply the game of life rules to the next quadrille:

  1. Any live cell (game.isFilled(row, col)) with less than two or more than three live neighbors dies: (order < 3 || order > 4) && next.clear(row, col).
  2. Any dead cell with three live neighbors becomes a live cell: order === 3 && next.fill(row, col, life) }).

Future work

Possible improvements of the demo include, but are not limited to:

It’s also advisable to compare and contrast the game of life implementation introduced here with the one presented in the nature of code which employs raw p5 commands.

References