glider gun

[github]

gosper's gun firing a glider every 30 generations

generative 56×20 10 fps 2.5 kB

use it

html

<script type="module" src="https://ascii.rest/ascii.js"></script>

<ascii-art piece="glider-gun"></ascii-art>

No install and no build step. ascii.js defines the <ascii-art> tag, loads the piece from ascii.rest, plays it while it is on screen, and holds the first frame for anyone who prefers reduced motion.

react

// npm install github:bas3line/ascii
import { Ascii } from "ascii.rest/react";
import { gliderGun } from "ascii.rest/pieces";

<Ascii piece={gliderGun} />

A client component, so it works in the Next.js app router as it is. Pass the name instead, piece="glider-gun", to fetch the piece only when it mounts.

astro

---
// npm install github:bas3line/ascii
import Ascii from "ascii.rest/astro";
---

<Ascii piece="glider-gun" />

Renders the first frame on the server, so the page is whole before any script runs, then plays the piece once it loads.

typescript

// npm install github:bas3line/ascii
import { mount } from "ascii.rest";
import { gliderGun } from "ascii.rest/pieces";

const el = document.querySelector<HTMLPreElement>("pre")!;
const stop = mount(el, gliderGun);

mount plays a piece in a <pre>, or on a <canvas> for the coloured ones, and returns a function that stops it. Every piece, option and frame is typed.

glider-gun.ts

[js] [github]
/*
 * glider gun: Gosper's glider gun in Conway's life, two cells to a character
 * so every cell is square. It fires a glider every 30 generations, and an
 * eater below the frame swallows each one, so the field repeats exactly.
 */
import type { Frame, Meta } from "../types.ts";

export const meta = {
  name: "glider gun",
  category: "generative",
  note: "gosper's gun firing a glider every 30 generations",
  cols: 56,
  rows: 20,
  fps: 10,
} satisfies Meta;

const GUN = [
  "........................o",
  "......................o.o",
  "............oo......oo............oo",
  "...........o...o....oo............oo",
  "oo........o.....o...oo",
  "oo........o...o.oo....o.o",
  "..........o.....o.......o",
  "...........o...o",
  "............oo",
];
const EATER = ["oo", "o", ".ooo", "...o"];
const PERIOD = 30;

export default function gliderGun(): Frame {
  const { cols, rows, fps } = meta;
  const PAD = 4;
  const GX = 2, GY = 2; // the gun's top left in the picture, in cells
  const K = 30; // how far down the glider lane the eater sits, past the frame
  const W = cols + 2 * PAD + 8;
  const H = rows * 2 + 2 * PAD + 8;
  let cur = new Uint8Array(W * H);
  let nxt = new Uint8Array(W * H);
  const place = (shape: string[], x0: number, y0: number) =>
    shape.forEach((row, y) => [...row].forEach((c, x) => c === "o" && (cur[(PAD + y0 + y) * W + PAD + x0 + x] = 1)));
  place(GUN, GX, GY);
  place(EATER, GX + 28 + K, GY + 15 + K);

  // The outer ring is never written, so it stays dead.
  const step = () => {
    for (let y = 1; y < H - 1; y++) {
      for (let x = 1; x < W - 1; x++) {
        const i = y * W + x;
        const n = cur[i - W - 1] + cur[i - W] + cur[i - W + 1] + cur[i - 1] + cur[i + 1] + cur[i + W - 1] + cur[i + W] + cur[i + W + 1];
        nxt[i] = n === 3 || (n === 2 && cur[i]) ? 1 : 0;
      }
    }
    [cur, nxt] = [nxt, cur];
  };

  // One character holds a cell over the cell below it.
  const GLYPH = [" ", "▀", "▄", "█"];
  const text = () => {
    const lines: string[] = [];
    for (let r = 0; r < rows; r++) {
      let line = "";
      const a = (PAD + 2 * r) * W + PAD;
      for (let c = 0; c < cols; c++) line += GLYPH[cur[a + c] + 2 * cur[a + W + c]];
      lines.push(line);
    }
    return lines.join("\n");
  };

  // Run until the lane is full of gliders, then keep one period of frames.
  for (let g = 0; g < 4 * (K + 40) + 60; g++) step();
  const cycle: string[] = [];
  for (let g = 0; g < PERIOD; g++) {
    cycle.push(text());
    step();
  }

  return (t) => cycle[Math.floor(t * fps + 1e-6) % PERIOD];
}