-3

I have a 2D cellular automaton open source project here, where the automaton is basically this:

export interface CA {
  i: Uint8ClampedArray;
  load: Uint8ClampedArray;
  max: number;
  move: Array<RuleType> | Array<(n: Uint8ClampedArray) => number>;
  n: Uint8ClampedArray;
  save: Uint8ClampedArray;
  size: number;
}

export type RuleType = {
  make: number;
  test: Array<(r: number) => boolean>;
};

type CA2DPropsType = {
  h: number;
  max: number;
  move: Array<RuleType> | Array<(n: Uint8ClampedArray) => number>;
  w: number;
};

export class CA2D implements CA {
  load: Uint8ClampedArray;

  save: Uint8ClampedArray;

  i: Uint8ClampedArray;

  n: Uint8ClampedArray;

  w: number;

  h: number;

  size: number;

  // 0 means it doesn't care
  move: Array<RuleType> | Array<(n: Uint8ClampedArray) => number>;

  max: number;

  constructor({ w, h, move, max }: CA2DPropsType) {
    this.w = w;
    this.h = h;
    this.max = max;
    this.size = w * h;
    this.i = new Uint8ClampedArray(9);
    this.n = new Uint8ClampedArray(9);
    this.load = new Uint8ClampedArray(this.size);
    this.save = new Uint8ClampedArray(this.size);
    this.move = move;
  }

  update() {
    update2d(this);
  }

  seed() {
    seed(this);
  }
}

function loadNeighborhood2d(ca: CA2D, x: number, y: number) {
  let x1;
  let x2;
  let x3;
  if (x === 0) {
    x1 = ca.w - 1;
    x2 = x;
    x3 = x + 1;
  } else if (x === ca.w - 1) {
    x1 = x - 1;
    x2 = x;
    x3 = 0;
  } else {
    x1 = x - 1;
    x2 = x;
    x3 = x + 1;
  }

  let y1;
  let y2;
  let y3;
  if (y === 0) {
    y1 = ca.h - 1;
    y2 = y;
    y3 = y + 1;
  } else if (y === ca.h - 1) {
    y1 = y - 1;
    y2 = y;
    y3 = 0;
  } else {
    y1 = y - 1;
    y2 = y;
    y3 = y + 1;
  }

  let i00 = y1 * ca.h + x1;
  let i01 = y1 * ca.h + x2;
  let i02 = y1 * ca.h + x3;
  let i10 = y2 * ca.h + x1;
  let i11 = y2 * ca.h + x2;
  let i12 = y2 * ca.h + x3;
  let i20 = y3 * ca.h + x1;
  let i21 = y3 * ca.h + x2;
  let i22 = y3 * ca.h + x3;

  // indexes
  ca.i[0] = i00; // upper left
  ca.i[1] = i01;
  ca.i[2] = i02;
  ca.i[3] = i10;
  ca.i[4] = i11; // middle
  ca.i[5] = i12;
  ca.i[6] = i20;
  ca.i[7] = i21;
  ca.i[8] = i22; // lower right

  // neighborhoods
  ca.n[0] = ca.save[i00]; // upper left
  ca.n[1] = ca.save[i01];
  ca.n[2] = ca.save[i02];
  ca.n[3] = ca.save[i10];
  ca.n[4] = ca.save[i11]; // middle
  ca.n[5] = ca.save[i12];
  ca.n[6] = ca.save[i20];
  ca.n[7] = ca.save[i21];
  ca.n[8] = ca.save[i22]; // lower right
}

export function update2d(ca: CA2D) {
  let y = 0;
  while (y < ca.h) {
    let x = 0;
    while (x < ca.w) {
      loadNeighborhood2d(ca, x, y);
      loadUpdate(y * ca.h + x, ca);

      x++;
    }
    y++;
  }

  saveUpdates(ca);
}

export function loadUpdate(p: number, ca: CA): void {
  let k = 0;
  ruleLoop: while (k < ca.move.length) {
    let rule = ca.move[k++];
    if (typeof rule === "function") {
      const make = rule(ca.n);
      if (make >= 0) {
        ca.load[p] = make;
        break ruleLoop;
      }
    } else {
      let i = 0;
      const { test, make } = rule;

      while (i < ca.n.length) {
        const v = ca.n[i];
        const t = test[i++];
        if (!t(v)) {
          continue ruleLoop;
        }
      }

      ca.load[p] = make;
      break ruleLoop;
    }
  }
}

export function randomIntBetween(min: number, max: number) {
  // min and max included
  return Math.floor(Math.random() * (max - min + 1) + min);
}

export function saveUpdates(ca: CA) {
  let i = 0;
  while (i < ca.load.length) {
    ca.save[i] = ca.load[i];
    i++;
  }
}

export function seed(ca: CA) {
  let i = 0;
  while (i < ca.save.length) {
    const rand = randomIntBetween(0, ca.max);
    ca.save[i++] = rand;
  }
}

I am generating a Gif from it in the terminal like this:

import { any, CA2D, CA2DRenderer, eq, wait } from './src/index.js'
import CanvasGifEncoder from '@pencil.js/canvas-gif-encoder'
import GIFEncoder from 'gif-encoder-2'
import fs from 'fs'
import { createCanvas } from 'canvas'
import { writeFile } from 'fs'
import _ from 'lodash'

const COLOR = {
  black: 'rgba(40, 40, 40, 0)',
  blue: 'rgba(56, 201, 247)',
  green: 'hsl(165, 92%, 44%)',
  greenLight: 'hsl(165, 92%, 79%)',
  purple: 'rgba(121, 85, 243, 0.8)',
  purpleLight: 'hsl(254, 87%, 70%)',
  red: 'rgba(238, 56, 96)',
  white: 'rgba(255, 255, 255)',
  white2: 'rgba(244, 244, 244)',
  white3: 'rgba(222, 222, 222)',
  yellow: 'rgba(246, 223, 104)',
}

export const caProps = {
  h: 60,
  max: 1,
  move: [
    (n: Uint8ClampedArray) => {
      const sum = _.sum(n)
      const isLive = n[4] === 1
      if (isLive && sum >= 3 && sum <= 4) {
        return 1
      } else {
        return -1
      }
    },
    (n: Uint8ClampedArray) => {
      const sum = _.sum(n)
      const isDead = n[4] === 0
      if (isDead && sum === 3) {
        return 0
      } else {
        return -1
      }
    },
    (n: Uint8ClampedArray) => {
      const isLive = n[4] === 1
      return isLive ? 0 : -1
    },
  ],
  w: 60,
}

const ca = new CA2D(caProps)

ca.seed()

// const saveByteArray = (function () {
//   var a = document.createElement('a')
//   document.body.appendChild(a)
//   a.style.display = 'none'
//   return function (data: Array<BlobPart>, name: string) {
//     var blob = new Blob(data, { type: 'octet/stream' }),
//       url = window.URL.createObjectURL(blob)
//     a.href = url
//     a.download = name
//     a.click()
//     window.URL.revokeObjectURL(url)
//   }
// })()

start()

async function start() {
  const canvas = createCanvas(500, 500)
  const renderer = new CA2DRenderer(canvas)
  renderer.setDimensions({
    gap: 1,
    width: 500,
    rowSize: ca.h,
    columnSize: ca.w,
  })

  const encoder = new GIFEncoder(500, 500)
  encoder.setDelay(100)
  encoder.start()

  // document.body.appendChild(renderer.canvas)

  const colors = [COLOR.white2, COLOR.green, COLOR.purple, COLOR.red]

  renderer.draw(ca, colors)
  if (renderer.context) encoder.addFrame(renderer.context)

  let i = 0
  while (i < 5) {
    console.log(i)
    await wait(20)
    ca.update()
    renderer.draw(ca, colors)
    if (renderer.context) encoder.addFrame(renderer.context)
    i++
  }
  encoder.finish()

  const buffer = encoder.out.getData()

  fs.writeFileSync('example.gif', buffer)

  // saveByteArray([buffer], 'example.gif')
}

You can run the project like this:

git clone git@github.com:lancejpollard/ca.js.git
npm install
npm run test
node dist.test/test

Notice the rules in the second code snippet:

[
  (n: Uint8ClampedArray) => {
    const sum = _.sum(n)
    const isLive = n[4] === 1
    if (isLive && sum >= 3 && sum <= 4) {
      return 1
    } else {
      return -1
    }
  },
  (n: Uint8ClampedArray) => {
    const sum = _.sum(n)
    const isDead = n[4] === 0
    if (isDead && sum === 3) {
      return 0
    } else {
      return -1
    }
  },
  (n: Uint8ClampedArray) => {
    const isLive = n[4] === 1
    return isLive ? 0 : -1
  },
]

It gets a neighborhood array (including self), and then implements the 3 rules on the Wiki page:

  1. Any live cell with two or three live neighbours survives.
  2. Any dead cell with three live neighbours becomes a live cell.
  3. All other live cells die in the next generation. Similarly, all other dead cells stay dead.

I am getting it stabilizing after 5 generations though, what did I do wrong?

enter image description here

What do I need to change in the rules or the CA2D implementation to get it working? I don't think the CA2D implementation needs to change, I think I got that right, but maybe I misinterpreted the rules?

Lance
  • 75,200
  • 93
  • 289
  • 503
  • Can you please clarify what you *expected* to happen? There is nothing wrong for game of life to quickly degrade to stable configurations. Maybe try plane (or other self-replicating setup)? – Alexei Levenkov Jan 17 '23 at 18:39

1 Answers1

2

The main issue is in the second rule: if the if condition is true -- when the cell is dead and has three live neighbors -- the cell should become alive and so the return value should be 1 instead 0.

Less of an issue if the grid is a square, but the formula for converting an x, y pair to an index is wrong. The y coordinate should be multiplied by the width of the grid, not the height. You have this mistake in more than one spot in the code.

trincot
  • 317,000
  • 35
  • 244
  • 286