Skip to content

Instantly share code, notes, and snippets.

@CoderCowMoo
Created January 24, 2025 15:42
Show Gist options
  • Save CoderCowMoo/ecbfa613559f3159a40412a9488d2a9e to your computer and use it in GitHub Desktop.
Save CoderCowMoo/ecbfa613559f3159a40412a9488d2a9e to your computer and use it in GitHub Desktop.
Game of life in a single html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Game of Life Canvas</title>
<style>
body {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background-color: #f0f0f0;
}
canvas {
border: 1px solid black;
}
#startButton {
margin: 10px;
}
</style>
</head>
<body>
<canvas id="gameCanvas" width="600" height="600"></canvas>
<div>
<button id="startButton" style="display: block;">Start</button>
<button id="clearButton" style="display: block; margin: 10px;">Clear</button>
</div>
<div>
<text>FPS: </text>
<input id="fps" type="number" min="0" max="240" step="1" value="15">
</div>
<script>
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
const cellSize = 10;
const gridWidth = canvas.width / cellSize;
const gridHeight = canvas.height / cellSize;
class Grid {
constructor(rows, columns) {
this.rows = rows
this.columns = columns
// We do row major in this household
this.grid = Array(rows).fill().map(() => Array(columns).fill(false));
console.log(this.grid);
}
getValue(x, y) {
return this.grid[x][y];
}
setValue(x,y,value) {
this.grid[x][y] = value;
}
getNumNeighbours(x, y) {
// so how do I do this smartly, so that im not naively getting eight lines of x,y
// what im doing now is so stupid and naive, im going to leave it in. I thought
// I was being smart
let neighbours = 0;
// for (let x1 = x-1; x < x+2; x1++) {
// for (let y1 = y-1; y < y+2; y1++) {
// neighbours += this.getValue(x1,y1) ? 1 : 0;
// }
// }
// wasnt handling edge cases
for (let x1 = Math.max(0, x-1); x1 <= Math.min(this.rows-1, x+1); x1++) {
for (let y1 = Math.max(0, y-1); y1 <= Math.min(this.columns-1, y+1); y1++) {
if (x1 !== x || y1 !== y) {
neighbours += this.getValue(x1, y1) ? 1 : 0
}
}
}
return neighbours;
}
copyFrom(otherGrid) {
for (let x = 0; x < this.rows; x++) {
for (let y = 0; y < this.columns; y++) {
this.setValue(x, y, otherGrid.getValue(x, y))
}
}
}
clearGrid() {
for (let x = 0; x < this.rows; x++) {
for (let y = 0; y < this.columns; y++) {
this.setValue(x, y, false)
}
}
}
fill_grid_random() {
for (let x = 0; x < this.rows; x++) {
for (let y = 0; y < this.columns; y++) {
this.grid[x][y] = Math.random() < 0.5;
}
}
}
}
function drawGridCanvas() {
ctx.strokeStyle = '#ddd';
ctx.lineWidth = 0.5;
for (let x = 0; x <= canvas.width; x += cellSize) {
ctx.beginPath();
ctx.moveTo(x, 0);
ctx.lineTo(x, canvas.height);
ctx.stroke();
}
for (let y = 0; y <= canvas.height; y += cellSize) {
ctx.beginPath();
ctx.moveTo(0, y);
ctx.lineTo(canvas.width, y);
ctx.stroke();
}
}
function fillGridCanvas(grid) {
// first smart thing i have ever done i think
prevstyle = ctx.fillStyle;
ctx.fillStyle = 'black';
for (let x = 0; x < grid.rows; x++) {
for (let y = 0; y < grid.columns; y++) {
if (grid.getValue(x, y) == true) {
ctx.fillRect(x*cellSize, y*cellSize, cellSize, cellSize);
}
}
}
ctx.fillStyle = prevstyle;
}
function clearCanvas() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
drawGridCanvas();
}
function gameOfLife(grid, grid2) {
// only thing left to do is to do the game logic.
// so how does game of life work? first we need to go over each cell
for (let x = 0; x < grid.rows; x++) {
for (let y = 0; y < grid.columns; y++) {
// so we need to getValue from grid and setValue's at grid2
// additionally getNumNeighbours from grid, never from grid2.
let neighbours = grid.getNumNeighbours(x, y);
if (grid.getValue(x,y) == true) {
if (neighbours < 2) {
grid2.setValue(x, y, false);
}
else if (neighbours >= 2 && neighbours <= 3) {
grid2.setValue(x, y, true);
} else {
grid2.setValue(x, y, false);
}
} else if (grid.getValue(x, y) == false && neighbours == 3) {
// cell is coming alive
grid2.setValue(x, y, true);
} else {
grid2.setValue(x, y, false);
}
}
}
// at the end i have to clone grid2 into grid
grid.copyFrom(grid2);
// i think i need to clear grid2
// apparently not
}
function init() {
// so we want to have two grids for the game of life actually
grid2 = new Grid(canvas.width / cellSize, canvas.height / cellSize);
grid = new Grid(canvas.width / cellSize, canvas.height / cellSize);
drawGridCanvas();
fillGridCanvas(grid);
// add mouse event listeners courtesy of deepseek
let isMouseDown = false;
// START MOUSE LISTENER -------------------------------------------
canvas.addEventListener('mousedown', (event) =>{
isMouseDown = true;
const x = Math.floor(event.offsetX / cellSize);
const y = Math.floor(event.offsetY / cellSize);
if (x >= 0 && x < canvas.width && y >= 0 && y < canvas.height) {
grid.setValue(x,y, !grid.getValue(x,y));
fillGridCanvas(grid);
}
})
canvas.addEventListener('mousemove', (event) => {
if (isMouseDown) {
const x = Math.floor(event.offsetX / cellSize);
const y = Math.floor(event.offsetY / cellSize);
grid.setValue(x, y, true); // Set cell to alive
drawGridCanvas();
fillGridCanvas(grid);
}
});
canvas.addEventListener('mouseup', () => {
isMouseDown = false;
});
canvas.addEventListener('mouseleave', () => {
isMouseDown = false;
});
// END MOUSE LISTENER -------------------------------------------
// i wanna try make it so that it only starts on user input, so they can draw beforehand
let isRunning = false;
const button = document.getElementById("startButton");
// deepseek suggested a three function approach, toggleSimulation
// startSimulation and stopSimulation
let intervalID;
function toggleSimulation() {
// some dumbass (me) put the toggle here, so it would behave weirdly.
// thanks deepseek for seting me straight
if (isRunning) {
intervalID = stopSimulation(intervalID);
} else {
intervalID = startSimulation();
console.log(intervalID)
}
isRunning = !isRunning;
button.textContent = isRunning ? "Stop" : "Start";
}
function startSimulation() {
// we'll need to get the fps first, then set an interval for
// gameOfLife, and I think return the interval
const fps = parseInt(document.getElementById("fps").value);
let interval = 1000/fps;
let intervalID = setInterval(() => {
clearCanvas();
gameOfLife(grid, grid2);
fillGridCanvas(grid);
}, interval)
return intervalID
}
function stopSimulation(intervalID) {
// here we just clear the interval i believe
if (intervalID) {
clearInterval(intervalID)
}
return null;
}
button.addEventListener("click", toggleSimulation);
let clearButton = document.getElementById("clearButton");
clearButton.addEventListener("click", ()=> {
grid.clearGrid();
clearCanvas();
fillGridCanvas(grid);
})
}
init();
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment