Created
January 24, 2025 15:42
-
-
Save CoderCowMoo/ecbfa613559f3159a40412a9488d2a9e to your computer and use it in GitHub Desktop.
Game of life in a single html
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!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