Skip to content

Instantly share code, notes, and snippets.

@viktorbezdek
Created September 16, 2024 11:59
Show Gist options
  • Save viktorbezdek/99379d17a5bf252eda0cce933b62c8d2 to your computer and use it in GitHub Desktop.
Save viktorbezdek/99379d17a5bf252eda0cce933b62c8d2 to your computer and use it in GitHub Desktop.
type Cell = 0 | 1;
type Grid = Cell[][];
// Utility to transpose a matrix (swap rows and columns)
const transpose = <T>(grid: T[][]): T[][] =>
grid[0].map((_, i) => grid.map(row => row[i]));
// Create a function to get neighboring cells of a given coordinate
const neighbors = (grid: Grid) => ([x, y]: [number, number]): [number, number][] =>
[-1, 0, 1]
.flatMap(dx => [-1, 0, 1].map(dy => [x + dx, y + dy]))
.filter(([nx, ny]) => (nx !== x || ny !== y) && inBounds(grid)([nx, ny]));
// Check if coordinates are within bounds of the grid
const inBounds = (grid: Grid) => ([x, y]: [number, number]): boolean =>
x >= 0 && y >= 0 && x < grid.length && y < grid[0].length;
// Count live neighbors for a cell at (x, y)
const liveNeighborCount = (grid: Grid) => ([x, y]: [number, number]): number =>
neighbors(grid)([x, y])
.map(([nx, ny]) => grid[nx][ny])
.reduce((sum, val) => sum + val, 0);
// Rule for determining the next cell state based on live neighbors
const nextCellState = (grid: Grid) => ([x, y]: [number, number]): Cell => {
const currentState = grid[x][y];
const liveNeighbors = liveNeighborCount(grid)([x, y]);
return currentState === 1
? (liveNeighbors === 2 || liveNeighbors === 3 ? 1 : 0)
: (liveNeighbors === 3 ? 1 : 0);
};
// Function to compute next generation for a given grid
const nextGeneration = (grid: Grid): Grid =>
grid.map((row, x) =>
row.map((_, y) => nextCellState(grid)([x, y]))
);
// Utility to print grid (for debugging/testing)
const printGrid = (grid: Grid): string =>
grid.map(row => row.join(' ')).join('\n');
// Example usage: Initial grid configuration
const initialGrid: Grid = [
[0, 1, 0, 0],
[0, 0, 1, 0],
[1, 1, 1, 0],
[0, 0, 0, 0]
];
// Run Conway's Game of Life generations
const runGenerations = (grid: Grid, generations: number): void => {
let currentGrid = grid;
for (let i = 0; i < generations; i++) {
console.log(`Generation ${i}:\n${printGrid(currentGrid)}\n`);
currentGrid = nextGeneration(currentGrid);
}
};
// Run the simulation for N generations
runGenerations(initialGrid, 5);
// Conway's Game of Life - Functional Point-Free Implementation in TypeScript
type Grid = Set<string>
type Coordinate = string
// Utility Functions
const parseCell = ([x, y]: [number, number]): Coordinate => `${x},${y}`
const neighborDeltas: [number, number][] = [
[-1, -1], [-1, 0], [-1, 1],
[ 0, -1], [ 0, 1],
[ 1, -1], [ 1, 0], [ 1, 1],
]
const getNeighbors = (cell: Coordinate): Coordinate[] =>
neighborDeltas
.map(([dx, dy]) => cell.split(',').map(Number))
.map(([x, y]) => [x + dx, y + dy] as [number, number])
.map(parseCell)
const expandGrid = (grid: Grid): Grid =>
new Set(
[...grid]
.flatMap(getNeighbors)
.concat([...grid])
)
const countNeighbors = (grid: Grid) => (cell: Coordinate): number =>
getNeighbors(cell).filter(grid.has).length
const shouldBeAlive = (grid: Grid) => (cell: Coordinate): boolean =>
(grid.has(cell) && [2, 3].includes(countNeighbors(grid)(cell))) ||
(!grid.has(cell) && countNeighbors(grid)(cell) === 3)
const nextGeneration = (grid: Grid): Grid =>
Array.from(expandGrid(grid))
.filter(shouldBeAlive(grid))
.reduce((acc, cell) => acc.add(cell), new Set<string>())
const renderGrid = (grid: Grid): string => {
const cells = Array.from(grid).map(cell => cell.split(',').map(Number) as [number, number])
const xs = cells.map(([x]) => x)
const ys = cells.map(([, y]) => y)
const minX = Math.min(...xs)
const maxX = Math.max(...xs)
const minY = Math.min(...ys)
const maxY = Math.max(...ys)
const lines = []
for (let y = minY; y <= maxY; y++) {
let line = ''
for (let x = minX; x <= maxX; x++) {
line += grid.has(`${x},${y}`) ? '█' : ' '
}
lines.push(line)
}
return lines.join('\n')
}
// Initial Configuration (Glider)
const initialGrid: Grid = new Set([
parseCell([1, 0]),
parseCell([2, 1]),
parseCell([0, 2]),
parseCell([1, 2]),
parseCell([2, 2]),
])
// Run the Game
const run = (grid: Grid, iterations: number): void => {
console.clear()
console.log(renderGrid(grid))
if (iterations > 0) {
setTimeout(() => run(nextGeneration(grid), iterations - 1), 500)
}
}
// Start the Game with 20 iterations
run(initialGrid, 20)
type Grid = ReadonlyArray<ReadonlyArray<number>>;
// Compose functions from right to left
const compose = (...fns: Function[]) => (x: any) =>
fns.reduceRight((v, f) => f(v), x);
// Pipe functions from left to right
const pipe = (...fns: Function[]) => (x: any) =>
fns.reduce((v, f) => f(v), x);
// Map function in point-free style
const map = (fn: Function) => (arr: any[]) => arr.map(fn);
// Reduce function in point-free style
const reduce = (fn: Function, init: any) => (arr: any[]) =>
arr.reduce(fn, init);
// Sum function in point-free style
const add = (a: number) => (b: number) => a + b;
const sum = reduce((a: number, b: number) => a + b, 0);
// Get the value at position (x, y) in the grid, or 0 if out of bounds
const getCellValue = (grid: Grid) => (x: number) => (y: number): number =>
grid[y] && grid[y][x] !== undefined ? grid[y][x] : 0;
// Get the coordinates of neighboring cells for position (x, y)
const getNeighborCoords = (x: number) => (y: number): [number, number][] =>
[
[x - 1, y - 1],
[x, y - 1],
[x + 1, y - 1],
[x - 1, y],
// [x, y], // Exclude the cell itself
[x + 1, y],
[x - 1, y + 1],
[x, y + 1],
[x + 1, y + 1],
];
// Count the live neighbors for cell at position (x, y)
const countLiveNeighbors = (grid: Grid) => (x: number) => (y: number): number =>
pipe(
getNeighborCoords(x),
map(([nx, ny]) => getCellValue(grid)(nx)(ny)),
sum
)(y);
// Determine the next state of a cell based on the Game of Life rules
const computeNextCellState = (grid: Grid) => (x: number) => (y: number): number =>
((liveNeighbors: number) => {
const currentState = getCellValue(grid)(x)(y);
return (currentState === 1 && (liveNeighbors === 2 || liveNeighbors === 3)) ||
(currentState === 0 && liveNeighbors === 3)
? 1
: 0;
})(countLiveNeighbors(grid)(x)(y));
// Compute the next state of the entire grid
const computeNextGrid = (grid: Grid): Grid =>
grid.map((row, y) =>
row.map((_, x) => computeNextCellState(grid)(x)(y))
);
// Generate the Game of Life for N generations
const iterateLife = (grid: Grid) => (generations: number): Grid[] =>
generations === 0
? [grid]
: [grid, ...iterateLife(computeNextGrid(grid))(generations - 1)];
// Example usage
const initialGrid: Grid = [
[0, 1, 0],
[0, 1, 0],
[0, 1, 0],
];
const generations = iterateLife(initialGrid)(5);
// Output the grid states for each generation
generations.forEach((gen, index) => {
console.log(`Generation ${index}:`);
gen.forEach(row => console.log(row.join(' ')));
console.log('');
});
type Cell = 0 | 1;
type Grid = Cell[][];
const size = 20;
const range = (n: number) => [...Array(n).keys()];
const initial = () =>
range(size).map(() =>
range(size).map(() => Math.random() > 0.5 ? 1 : 0)
);
const neighbors = ([x, y]: [number, number]): [number, number][] =>
[[-1, -1], [-1, 0], [-1, 1], [0, -1], [0, 1], [1, -1], [1, 0], [1, 1]]
.map(([dx, dy]) => [(x + dx + size) % size, (y + dy + size) % size]);
const countAlive = (grid: Grid) => ([x, y]: [number, number]): number =>
neighbors([x, y]).reduce((sum, [nx, ny]) => sum + grid[nx][ny], 0);
const nextState = (grid: Grid) => (x: number) => (y: number): Cell =>
(grid[x][y] === 1 && [2, 3].includes(countAlive(grid)([x, y]))) ||
(grid[x][y] === 0 && countAlive(grid)([x, y]) === 3)
? 1
: 0;
const evolve = (grid: Grid): Grid =>
range(size).map(x => range(size).map(nextState(grid)(x)));
const render = (grid: Grid): string =>
grid.map(row => row.map(cell => cell ? '◼' : '◻').join('')).join('\n');
const game = (grid: Grid) => {
console.clear();
console.log(render(grid));
setTimeout(() => game(evolve(grid)), 100);
};
game(initial());
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment