aityx Demo Day: Cellfie

Welcome to the first aityx Demo Day. In this series of videos we build better AI applications by showing aityx features and techniques, with a little software engineering along the way. We start with a classic of computer science, Conway’s Game of Life, called Cellfie here, and with the feature it needs most: rules.

aityx Demo Day: Cellfie. The rules of Life, the patterns they make, and every aityx call that decided them (8 minutes).

Key takeaways

  • One call per generation. Cellfie sends the board to aityx and gets back which cells die, which survive and which are born. The app only paints the answer.
  • The rules live in the decision model. Conway’s rules, HighLife and Life without death are rows in a table, switched on from the app. Nothing about Life is hardcoded.
  • aityx names the patterns. A pattern catalogue in the same model matches each cluster of cells to a still life, an oscillator or a spaceship.
  • Every call is on screen. The side panel shows each request and response, with “Jump to” links to the parts worth reading.
  • The rules are followed exactly. For the same board and the same rules, aityx returns the same answer every time.
  • Play it now at aityx.ai/demos/cellfie, or read the code in aityx-labs/aityx-demos.

Conway and the Game of Life

John Horton Conway was a brilliant mathematician who worked on game theory and many other subjects, including cellular automata: grids of simple cells and simple rules that give rise to complex patterns. They have found uses in biology, mathematics and the study of society. Conway died in 2020, during the COVID-19 pandemic, and his work is well worth exploring. Cellfie opens with a short note in his memory.

The rules of his Game of Life fit in a few lines. Each generation, every cell looks at its eight neighbours:

  • A dead cell with exactly three live neighbours is born.
  • A live cell with two or three live neighbours lives on.
  • A live cell with fewer than two dies of loneliness, and one with more than three dies of overcrowding.

The Rules menu also has two variations: HighLife, where a dead cell with six neighbours is born too, and Life without death, where no cell ever dies. The video plays with Conway’s rules.

Step one generation

Random start puts a pattern on the board: a soup of random cells, a denser soup, islands, a glider fleet, a zoo with one of each known pattern, a glider gun or a methuselah. You can also click to add your own cells.

Press Step and the first generation plays. Lone cells have no neighbours, so most of a random soup disappears at once, and a few cells are born where three neighbours meet. The first aityx call appears in the side panel.

That call is a decision, and it uses one aityx feature: the rules engine. Classification, Jev and AI models come in future Demo Days. Life needs only rules. aityx decided which cells died, which survived and which were born, and then went further: it grouped the live cells into clusters and matched each cluster against a catalogue of known patterns.

Read the call

The request is long, so the panel has “Jump to” links to the parts worth reading:

  • The board’s situations: every distinct situation a cell is in (alive or dead, and how many live neighbours), with how many cells share it. The request stays this size however big the board is.
  • The clusters and their shapes: each group of live cells, with its size and a normalised shape.
  • The rules that are on, the rules of Life, the pattern catalogue and the kinds of pattern: the decision model itself.
  • What is asked: the answers the app wants back.

The response has links too: which clusters matched which patterns, the cells that are alive in the next generation, and the usage of the call, including the tokens it consumed.

Play, and watch the patterns

Press Play and the game starts at one generation every three seconds, slow enough to read each call. The Acceleration slider speeds it up, as fast as aityx answers. In the recording each call took a little over 200 ms.

As the generations pass, the patterns aityx recognises are labelled on the board, and a card announces each one as it appears. They come in three kinds:

  • Still lifes never change: the block, four cells in a square, the beehive and the boat.
  • Oscillators cycle through phases. The blinker, three cells in a row, flips between horizontal and vertical.
  • Spaceships move across the board. The glider, the smallest one, crosses diagonally one cell every four generations.

A dense soup produces all three kinds within a few dozen generations.

The glider gun

If you like gliders, try the glider fleet, or the glider gun: Gosper’s glider gun, a formation that produces gliders by itself. Two shuttles bounce back and forth between blocks, and each collision sends out a new glider, again and again. On a board that wraps around, the gliders would come back and hit the gun. Cellfie’s board is infinite, so they keep flying away.

Rules that are followed exactly

What makes the demo interesting is how strictly the rules are followed. A generative model asked to play Life can hallucinate, producing cells and patterns that no rule allows and that sit outside your business rules. Here every birth, every death and every named pattern comes from the decision model, generation after generation, and for the same board and the same rules aityx returns the same answer.

You can check it yourself. Click any generation in the panel to see what was sent to aityx and what came back: the clusters that matched, and the pattern each one matched, glider or otherwise.

One call, annotated

Here is one real call from the live demo: the glider gun at generation 44, with a glider on its way out. This is the board aityx answered with.

Generation 45 of Gosper's glider gun in Cellfie: a block labelled at each end, the gun's two shuttles in the middle, and a glider labelled below them, flying away. Green cells were just born and grey ones just died.
Generation 45 of the glider gun. aityx named the two blocks and the glider; the shuttles in the middle are not in the catalogue.

The request is the decision model followed by the board. The model is the same in every call, so most of it is shortened here:

// POST https://api.aityx.ai/v1/systemone (the key goes in a header, never in the body)
{
  "decision": {
    "name": "Cellfie · Conway's Game of Life, one generation",
    "types": { /* Situation, Cluster, Pattern, Kind, Fate, Rules */ },
    "inputs": { /* generation, rules, situations, clusters: read from "state" below */ },
    "functions": {
      // The rules of Life: a decision table, one row per rule, each with its why
      "fate": {
        "body": [
          ["no_death", "highlife", "conway", "alive", "neighbours", "=>", "# why"],
          /* rows 1 and 2: Life without death and HighLife, switched off in this call */
          ["-", "-", "true", "true",  "< 2",    "dies_lonely",  "Underpopulation: a live cell with fewer than two live neighbours dies."],
          ["-", "-", "true", "true",  "[2..3]", "survives",     "Survival: a live cell with two or three live neighbours lives on to the next generation."],
          ["-", "-", "true", "true",  "> 3",    "dies_crowded", "Overpopulation: a live cell with more than three live neighbours dies."],
          ["-", "-", "true", "false", "3",      "is_born",      "Reproduction: a dead cell with exactly three live neighbours becomes a live cell."]
          /* rows 7 and 8: a live cell no enabled rule keeps alive dies; a dead cell no rule brings to life stays empty */
        ]
      },
      // The pattern catalogue: 37 rows matched on size and shape, then a catch-all
      "identify": { /* e.g. ["5", "3", "3", "\"..o/o.o/.oo\", \"..o/oo./.oo\"", "glider", "Glider, 2 phases of its 4-generation cycle."] */ },
      // Still life, oscillator, spaceship or methuselah, for each pattern
      "kind_of": { /* … */ }
    },
    "decisions": { /* the answers asked for: fates, alive_next, the counts, patterns, kinds, recognized */ }
  },

  // The board: the only part that changes from one call to the next
  "state": {
    "generation": 44,
    "rules": { "conway": true, "highlife": false, "no_death": false },
    // 14 situations stand for all 221 cells in play: 61 alive, and 160 dead cells next to them
    "situations": [
      { "alive": true,  "neighbours": 0, "cells": 4 },
      { "alive": true,  "neighbours": 1, "cells": 7 },
      { "alive": true,  "neighbours": 2, "cells": 11 },
      { "alive": true,  "neighbours": 3, "cells": 17 },
      { "alive": true,  "neighbours": 4, "cells": 12 },
      { "alive": true,  "neighbours": 5, "cells": 8 },
      { "alive": true,  "neighbours": 6, "cells": 2 },
      { "alive": false, "neighbours": 1, "cells": 69 },
      { "alive": false, "neighbours": 2, "cells": 59 },
      { "alive": false, "neighbours": 3, "cells": 19 }, // the only dead cells that can be born
      { "alive": false, "neighbours": 4, "cells": 6 },
      { "alive": false, "neighbours": 5, "cells": 4 },
      { "alive": false, "neighbours": 6, "cells": 1 },
      { "alive": false, "neighbours": 7, "cells": 2 }
    ],
    // Each group of touching live cells, its shape normalised: o alive, . dead, / next row
    "clusters": [
      { "cells": 12, "width": 7, "height": 6, "shape": "...o.../.oo.oo./......./o.....o/......./oo.o.oo" },
      { "cells": 23, "width": 8, "height": 7, "shape": "....o.../..oooo../.o.o.oo./o..o.ooo/.o.o.oo./..oooo../....o..." },
      { "cells": 4,  "width": 2, "height": 2, "shape": "oo/oo" },
      { "cells": 4,  "width": 2, "height": 2, "shape": "oo/oo" },
      { "cells": 13, "width": 7, "height": 4, "shape": "......o/.o.oo../o.oooo./oo..oo." },
      { "cells": 5,  "width": 3, "height": 3, "shape": "..o/oo./.oo" }
    ]
  }
}

The response answers in the order the board was sent, and its receipt says which row of which table produced each answer:

{
  "answers": {
    // One fate per situation, in the order they were sent
    "fates": {
      "type": "value",
      "value": ["dies_lonely", "dies_lonely", "survives", "survives", "dies_crowded", "dies_crowded", "dies_crowded",
                "stays_empty", "stays_empty", "is_born", "stays_empty", "stays_empty", "stays_empty", "stays_empty"],
      "confidence": 1 // rules, not a guess
    },
    // What the app paints: the cells of situations 3, 4 and 10 are alive next
    "alive_next": {
      "type": "value",
      "value": [false, false, true, true, false, false, false, false, false, true, false, false, false, false],
      "confidence": 1
    },
    "population":      { "type": "number", "number": 61, "confidence": 1 },
    "population_next": { "type": "number", "number": 47, "confidence": 1 },
    "births":          { "type": "number", "number": 19, "confidence": 1 }, // the 19 dead cells with three neighbours
    "deaths":          { "type": "number", "number": 33, "confidence": 1 },
    "survivors":       { "type": "number", "number": 28, "confidence": 1 }, // 11 + 17
    "next_generation": { "type": "number", "number": 45, "confidence": 1 },
    // One pattern per cluster: two blocks and a glider; the gun's shuttles are unknown
    "patterns": {
      "type": "value",
      "value": ["unknown", "unknown", "block", "block", "unknown", "glider"],
      "confidence": 1
    },
    "kinds": {
      "type": "value",
      "value": ["unknown", "unknown", "still_life", "still_life", "unknown", "spaceship"],
      "confidence": 1
    },
    "recognized": { "type": "number", "number": 3, "confidence": 1 }
  },
  "receipt": {
    "functions": {
      // The row of the rules of Life that fired (F) for each situation: 3 lonely, 4 survives, 5 crowded, 6 born, 8 empty
      "fate": [
        { "rule": 3, "rows": "xxF...++" }, { "rule": 3, "rows": "xxF...++" },
        { "rule": 4, "rows": "xxxF..++" }, { "rule": 4, "rows": "xxxF..++" },
        { "rule": 5, "rows": "xxxxF.++" }, { "rule": 5, "rows": "xxxxF.++" }, { "rule": 5, "rows": "xxxxF.++" },
        { "rule": 8, "rows": "xxxxxxxF" }, { "rule": 8, "rows": "xxxxxxxF" },
        { "rule": 6, "rows": "xxxxxF.+" },
        { "rule": 8, "rows": "xxxxxxxF" }, { "rule": 8, "rows": "xxxxxxxF" }, { "rule": 8, "rows": "xxxxxxxF" }, { "rule": 8, "rows": "xxxxxxxF" }
      ],
      // The catalogue row that matched each cluster: 1 is the block, 28 the glider, 38 the catch-all
      "identify": [
        { "rule": 38, "rows": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxF" },
        { "rule": 38, "rows": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxF" },
        { "rule": 1,  "rows": "F....................................+" },
        { "rule": 1,  "rows": "F....................................+" },
        { "rule": 38, "rows": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxF" },
        { "rule": 28, "rows": "xxxxxxxxxxxxxxxxxxxxxxxxxxxF.........+" }
      ],
      "kind_of": [ /* … */ ]
    },
    "decisions": { /* how each answer was worked out */ },
    "provenance": { /* where each input came from: the state */ }
  },
  "usage": { "input_tokens": 4164, "output_tokens": 1279 }
}

Fourteen situations in, fourteen fates out: that is all it takes to compute the next generation, however many cells are on the board. And every block, glider or unknown cluster points back to the exact catalogue row that named it.

Your turn

Cellfie is a simple demo: rules and a turn-based simulation, where each generation is one aityx call. The same pattern works for your business, your games or whatever you are building. The app collects the facts of the moment, aityx applies the rules, and every answer comes with the request and response that explain it.

  1. Play Cellfie at aityx.ai/demos/cellfie. Get a key in the console with your email, no approval needed.
  2. Change the rules. Switch on HighLife or Life without death and watch the same board evolve differently.
  3. Read the model in aityx-labs/aityx-demos, next to the calls the app shows.
  4. Meet the other demos in Playing with aityx, and write your own decision with Building with aityx.

See you at the next Demo Day.


Play Cellfie at aityx.ai/demos/cellfie, read the code in aityx-labs/aityx-demos, and read Building with aityx to write your own decision. Questions, capacity needs or feedback: [email protected].