Skip to content

Instantly share code, notes, and snippets.

@vladris
Last active June 11, 2022 17:17
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save vladris/bda4ca4d5e815ecacec3b35ed1fc39a7 to your computer and use it in GitHub Desktop.
Save vladris/bda4ca4d5e815ecacec3b35ed1fc39a7 to your computer and use it in GitHub Desktop.
Game of Life visualization
// Game of Life cell matrix wrapped in an object
type Matrix = {
m: boolean[][]
wrap: boolean;
};
// Makes matrix of desired width x height plus an initial configuration of "on" cells and wrap-around flag
function makeMatrix(width: number, height: number, initialConfig: [number, number][], wrap: boolean) {
let m = new Array<boolean[]>(height);
// If not wrap-around, add some padding
if (!wrap) {
height += 4;
width += 4;
}
for (let i = 0; i < height; i++) {
m[i] = new Array<boolean>(width);
for (let j = 0; j < width; j++) {
m[i][j] = false;
}
}
for (let coord in initialConfig) {
m[initialConfig[coord][0]][initialConfig[coord][1]] = true;
}
return { m: m, wrap: wrap };
}
// Counts "on" neighbors without wrap-around
function neighborsNoWrap(matrix: Matrix, i: number, j: number) {
let m = matrix.m;
let result = 0;
if (i > 0) {
if (j > 0) {
result += +m[i - 1][j - 1];
}
result += +m[i - 1][j]
if (j < m[0].length - 1) {
result += +m[i - 1][j + 1];
}
}
if (j > 0) {
result += +m[i][j - 1];
}
if (j < m[0].length - 1) {
result += +m[i][j + 1];
}
if (i < m.length - 1) {
if (j > 0) {
result += +m[i + 1][j - 1];
}
result += +m[i + 1][j]
if (j < m[0].length - 1) {
result += +m[i + 1][j + 1];
}
}
return result;
}
// Counts "on" neighbors with wrap-around
function neighborsWrap(matrix: Matrix, i: number, j: number) {
const m = matrix.m;
// Wraparound
const fst_i = i != 0 ? i - 1 : m.length - 1;
const fst_j = j != 0 ? j - 1 : m[0].length - 1;
const lst_i = i + 1 != m.length ? i + 1 : 0;
const lst_j = j + 1 != m[0].length ? j + 1 : 0;
return +m[fst_i][fst_j] + +m[fst_i][j] + +m[fst_i][lst_j] +
+m[i][fst_j] + +m[i][lst_j] +
+m[lst_i][fst_j] + +m[lst_i][j] + +m[lst_i][lst_j];
}
// Computes next state and update matrix
function step(matrix: Matrix) {
let m2 = new Array<Array<boolean>>(matrix.m.length);
const neighbors = matrix.wrap ? neighborsWrap : neighborsNoWrap;
for (let i = 0; i < matrix.m.length; i++) {
m2[i] = new Array<boolean>(matrix.m[0].length);
for (let j = 0; j < matrix.m[0].length; j++) {
const n = neighbors(matrix, i, j);
if (n == 3) {
m2[i][j] = true;
}
else if (n == 2 && matrix.m[i][j]) {
m2[i][j] = true;
}
else {
m2[i][j] = false;
}
}
}
matrix.m = m2;
}
// Parameters for drawing: a 2D context, width, height, and cell size in pixels
type DrawParams = {
ctx: CanvasRenderingContext2D,
width: number,
height: number,
cellSize: number,
}
// Draws the cell grid
function drawGrid(params: DrawParams) {
params.ctx.clearRect(0, 0, params.width * params.cellSize, params.height * params.cellSize);
params.ctx.strokeStyle = "lightGray"
params.ctx.fillStyle = "black";
params.ctx.rect(0, 0, params.width * params.cellSize, params.height * params.cellSize);
params.ctx.stroke();
for (let i = 0; i < params.height; i++) {
params.ctx.moveTo(0, i * params.cellSize);
params.ctx.lineTo(params.width * params.cellSize, i * params.cellSize);
params.ctx.stroke();
}
for (let j = 0; j < params.width; j++) {
params.ctx.moveTo(j * params.cellSize, 0);
params.ctx.lineTo(j * params.cellSize, params.height * params.cellSize);
params.ctx.stroke();
}
}
// Draws matrix state
function drawState(params: DrawParams, matrix: Matrix) {
for (let i = 0; i < params.height; i++) {
for (let j = 0; j < params.width; j++) {
if (matrix.m[i][j]) {
params.ctx.fillRect(j * params.cellSize, i * params.cellSize, params.cellSize, params.cellSize);
}
}
}
}
// Draws grid and state
function draw(params: DrawParams, matrix: Matrix) {
drawGrid(params);
drawState(params, matrix);
}
// Keeps track of current running interval & play/stop button
let running: { intervalId: number, button: HTMLButtonElement } = { intervalId: 0, button: undefined };
// Starts animation
function start(params: DrawParams, matrix: Matrix) {
return setInterval(() => {
draw(params, matrix);
step(matrix);
}, 200);
}
// Stops currently running animation
function stopRunning() {
if (running.intervalId === 0) {
return;
}
// Stop interval and update button to say "Play"
clearInterval(running.intervalId);
running.button.textContent = "Play";
// Nothing running anymore
running = { intervalId: 0, button: undefined };
}
// Animates cellular automata in target div
function animate(width: number, height: number, initialConfig: [number, number][], targetDiv: string, wrap: boolean = true) {
let m = makeMatrix(width, height, initialConfig, wrap);
const cellSize = 10;
const div = document.getElementById(targetDiv) as HTMLDivElement;
// Create play button
const button = document.createElement("button");
button.style.display = "block";
button.textContent = "Play";
button.onclick = () => {
// If stopped (button reads "Play") stop currently running animation and start this one
if (button.textContent === "Play") {
stopRunning();
running = { intervalId: start({ ctx, width, height, cellSize }, m), button: button };
button.textContent = "Stop";
}
// Else this is the running animation, so just stop it
else {
stopRunning();
}
};
// Create canvas to render in
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
canvas.width = width * cellSize;
canvas.height = height * cellSize;
// Append new elements to div
div.appendChild(button);
div.appendChild(canvas);
// Draw initial state
draw({ctx, width, height, cellSize}, m);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment