Search code examples
javascripthtmlconways-game-of-life

Conway's Game of Life - Gliders not moving


So I've implemented a simple Game of Life in Javascript and canvas, and I thought it was working perfectly (fixed timestep, a temporary 'next board' to store changes until they're needed, etc.) but when I added a 'glider' pattern it didn't behave as expected. They shift slightly but then stop.

I've gone over the code a hundred times and can't see anything wrong, but I'm sure it's a simple error I'm making somewhere. Code below. Any advice much appreciated!

UPDATE:

I was failing to deep copy the array, as Jonas pointed out below. I've fixed that now, and the simulation now works as the Game of Life is supposed to. (Thanks Jonas!)

Updated code below. Unfortunately the glider issue is still there - they move correctly for the first frame of the simulation and then stop completely. If anyone can spot the remaining error I'd be very grateful.

let canvas = document.getElementById('canvas');
let ctx = canvas.getContext('2d');

const tableSize = 64;
const cellSize = 4;

let tickDelay = 60;

let table = [];
let loop;

let deadChance = 0.5;

const colors = {
	alive: '#f2b630',
	dead: '#333'
};

function init() {
	
	//build table
	table = [];
	
	for (let y = 0; y < tableSize; y++) {
		let row = [];
		for (let x = 0; x < tableSize; x++) {
			
			let randomAlive = true;
			if (Math.random() > deadChance) {
				randomAlive = false;
			}
			let cell = new Cell(x, y, randomAlive);
			row.push(cell);
		}
		table.push(row);
	}
	
}

function tick() {
	
	console.log("tick");
	
	table = table.map(row => row.map(cell => cell.tick()));
	
	render();
}

function render() {
	
	for (let y = 0; y < tableSize; y++) {
		for (let x = 0; x < tableSize; x++) {
			table[x][y].draw();
		}
	}
}

function start() {
	console.log("Starting");
	loop = setInterval(tick, tickDelay);
}

function stop() {
	console.log("Stopping");
	clearInterval(loop);
}

function reset() {
	console.log("Resetting");
	clearInterval(loop);
	init();
	render();
}

class Cell {
	
	constructor(x, y, isAlive) {
		//The x and y values are table indices, not pixel values
		this.x = x;
		this.y = y;
		this.isAlive = isAlive;
	}
	
	tick() {
		
		let currentNeighbours = getNeighbours(this.x, this.y);
		
		let numAliveNeighbours = 0;
		
		for (let i = 0; i < currentNeighbours.length; i++) {
			if (currentNeighbours[i].isAlive) {
				numAliveNeighbours++;
			}
		}

		
		switch (numAliveNeighbours) {
			case 0: this.makeDead(); break;
			case 1: this.makeDead(); break;
			case 2: break;
			case 3: this.makeAlive(); break;
			case 4: this.makeDead(); break;
			case 5: this.makeDead(); break;
			case 6: this.makeDead(); break;
			case 7: this.makeDead(); break;
			case 8: this.makeDead(); break;
		}
	
		return new Cell(this.x, this.y, this.isAlive);
	}
	
	draw() {
		
		if (this.isAlive) {
			ctx.fillStyle = colors.alive;
		} else {
			ctx.fillStyle = colors.dead;
		}
		
		let margin = 1;

		ctx.fillRect(this.x * cellSize + (this.x * margin), this.y * cellSize + (this.y * margin), cellSize, cellSize);
	}
	
	makeAlive() {
		this.isAlive = true;
	}
	
	makeDead() {
		this.isAlive = false;
	}
}

//Helper functions

function getNeighbours(x, y) {
	
	//return a list of all eight neighbours of this cell in North-East-South-West (NESW) order
	let result = [];
	
	//wrap at the edges of the table for each neighbour
	
	let targetX;
	let targetY;
	
	//get NORTH neighbour
	targetX = x;
	targetY = y-1;
	if (targetY < 0)
		targetY = tableSize-1;
	
	result.push(table[targetX][targetY]);
	
	//get NORTHEAST neighbour
	targetX = x+1;
	targetY = y-1;
	if (targetY < 0)
		targetY = tableSize-1;
	if (targetX > tableSize-1)
		targetX = 0;
	
	result.push(table[targetX][targetY]);
	
	//get EAST neighbour
	targetX = x+1;
	targetY = y;
	if (targetX >= tableSize)
		targetX = 0;
	
	result.push(table[targetX][targetY]);
	
	//get SOUTHEAST neighbour
	targetX = x+1;
	targetY = y+1;
	if (targetY > tableSize-1)
		targetY = 0;
	if (targetX > tableSize-1)
		targetX = 0;
	
	result.push(table[targetX][targetY]);
	
	//get SOUTH neighbour
	targetX = x;
	targetY = y+1;
	if (targetY >= tableSize)
		targetY = 0;
	
	result.push(table[targetX][targetY]);
	
	//get SOUTHWEST neighbour
	targetX = x-1;
	targetY = y+1;
	if (targetY > tableSize-1)
		targetY = 0;
	if (targetX < 0)
		targetX = tableSize-1;
	
	result.push(table[targetX][targetY]);
	
	//get WEST neighbour
	targetX = x-1;
	targetY = y;
	if (targetX < 0)
		targetX = tableSize-1;
	
	result.push(table[targetX][targetY]);
	
	//get NORTHWEST neighbour
	targetX = x-1;
	targetY = y-1;
	if (targetY < 0)
		targetY = tableSize-1;
	if (targetX < 0)
		targetX = tableSize-1;
	
	result.push(table[targetX][targetY]);
	
	return result;
}

//Patterns

function pattern() {
	
	//Set up the board using a random preset pattern
	console.log("Creating pattern");
	clearInterval(loop);
	
	//build dead table
	table = [];
	
	for (let y = 0; y < tableSize; y++) {
		let row = [];
		for (let x = 0; x < tableSize; x++) {
			
			let cell = new Cell(x, y, false);
			row.push(cell);
		}
		table.push(row);
	}
	
	//add living cells for patterns
	
	//Blinker
	
	table[1][0].isAlive = true;
	table[2][0].isAlive = true;
	table[3][0].isAlive = true;
	
	/*
	//Glider
	
	table[1][1].isAlive = true;
	table[2][2].isAlive = true;
	table[2][3].isAlive = true;
	table[3][2].isAlive = true;
	table[3][1].isAlive = true;
	
	
	table[12][12].isAlive = true;
	table[13][13].isAlive = true;
	table[14][13].isAlive = true;
	table[13][14].isAlive = true;
	table[12][14].isAlive = true;
	*/
	
	render();
	
}

//Build board and render initial state
init();
render();
html {
	background: slategray;
}

.game {
	background: #ddc;
	border-radius: 2px;
	
    padding-left: 0;
    padding-right: 0;
    margin-left: auto;
    margin-right: auto;
	margin-top: 10%;
    display: block;
    
}

h1 {
	color: white;
	text-align: center;
	font-family: sans-serif;
}

button {
	text-align: center;
	
	padding: 12px;
	border-radius: 2px;
	font-size: 1.2em;
    margin-left: auto;
    margin-right: auto;
	margin-top: 12px;
    display: block;
}

.controls {
	display: flex;
	width: 300px;
	margin: auto;
}
<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8" />
        <title>Conway's Game of Life</title>
        <link rel="stylesheet" href="css/styles.css">
    </head>
	
    <body>
		<h1>Conway's Game of Life</h1>
		<canvas id="canvas" class="game" width="319px" height="319px"></canvas>
		
		<div class="controls">
			<button onclick='start()'>Start</button>
			<button onclick='stop()'>Stop</button>
			<button onclick='reset()'>Reset</button>
			<button onclick='pattern()'>Pattern</button>
		</div>
		
		<script src="js/game.js"></script>
    </body>
</html>


Solution

  • Throughout the code, you're accessing the grid by x, then y coordinate. In order for that to work, the grid should be defined as an array of columns, not array of rows.

    As a quick fix, I simply swapped x and y when defining the grid in init() and pattern(). You'll probably want to rename your variables to reflect the intention.


    There's a big issue with this part of the tick function. You're changing the value of isAlive property of a tile before the other tiles were checked for their future states.

    switch (numAliveNeighbours) {
       case 0: this.makeDead(); break;
       case 1: this.makeDead(); break;
       case 2: break;
       case 3: this.makeAlive(); break;
       case 4: this.makeDead(); break;
       case 5: this.makeDead(); break;
       case 6: this.makeDead(); break;
       case 7: this.makeDead(); break;
       case 8: this.makeDead(); break;
    }     
    return new Cell(this.x, this.y, this.isAlive);
    

    I fixed it with the following one liner as a personal preference, you can keep the switch statement as long as you're not directly changing the existing tile.

    const isAlive = this.isAlive ? (numAliveNeighbours === 2 || numAliveNeighbours === 3) : (numAliveNeighbours === 3)
    return new Cell(this.x, this.y, isAlive);
    

    let canvas = document.getElementById('canvas');
    let ctx = canvas.getContext('2d');
    
    const tableSize = 64;
    const cellSize = 4;
    
    let tickDelay = 60;
    
    let table = [];
    let loop;
    
    let deadChance = 0.5;
    
    const colors = {
    	alive: '#f2b630',
    	dead: '#333'
    };
    
    function init() {
    	
    	//build table
    	table = [];
    	
    	for (let y = 0; y < tableSize; y++) {
    		let row = [];
    		for (let x = 0; x < tableSize; x++) {
    			
    			let randomAlive = true;
    			if (Math.random() > deadChance) {
    				randomAlive = false;
    			}
    			let cell = new Cell(y, x, randomAlive);
    			row.push(cell);
    		}
    		table.push(row);
    	}
    	
    }
    
    function tick() {
    	
    	//console.log("tick");
    	
    	table = table.map(row => row.map(cell => cell.tick()));
    	
    	render();
    }
    
    function render() {
    	
    	for (let y = 0; y < tableSize; y++) {
    		for (let x = 0; x < tableSize; x++) {
    			table[x][y].draw();
    		}
    	}
    }
    
    function start() {
    	console.log("Starting");
    	loop = setInterval(tick, tickDelay);
    }
    
    function stop() {
    	console.log("Stopping");
    	clearInterval(loop);
    }
    
    function reset() {
    	console.log("Resetting");
    	clearInterval(loop);
    	init();
    	render();
    }
    
    class Cell {
    	
    	constructor(x, y, isAlive) {
    		//The x and y values are table indices, not pixel values
    		this.x = x;
    		this.y = y;
    		this.isAlive = isAlive;
    	}
    	
    	tick() {
    		
    		let currentNeighbours = getNeighbours(this.x, this.y);
    		
    		let numAliveNeighbours = 0;
    		
    		for (let i = 0; i < currentNeighbours.length; i++) {
    			if (currentNeighbours[i].isAlive) {
    				numAliveNeighbours++;
    			}
    		}
        
            const isAlive = this.isAlive ? (numAliveNeighbours === 2 || numAliveNeighbours === 3) : (numAliveNeighbours === 3)
    
            return new Cell(this.x, this.y, isAlive);
    
    	}
    	
    	draw() {
    		
    		if (this.isAlive) {
    			ctx.fillStyle = colors.alive;
    		} else {
    			ctx.fillStyle = colors.dead;
    		}
    		
    		let margin = 1;
    
    		ctx.fillRect(this.x * cellSize + (this.x * margin), this.y * cellSize + (this.y * margin), cellSize, cellSize);
    	}
    	
    	makeAlive() {
    		this.isAlive = true;
    	}
    	
    	makeDead() {
    		this.isAlive = false;
    	}
    }
    
    //Helper functions
    
    function getNeighbours(x, y) {
    	
    	//return a list of all eight neighbours of this cell in North-East-South-West (NESW) order
    	let result = [];
    	
    	//wrap at the edges of the table for each neighbour
    	
    	let targetX;
    	let targetY;
    	
    	//get NORTH neighbour
    	targetX = x;
    	targetY = y-1;
    	if (targetY < 0)
    		targetY = tableSize-1;
    	
    	result.push(table[targetX][targetY]);
    	
    	//get NORTHEAST neighbour
    	targetX = x+1;
    	targetY = y-1;
    	if (targetY < 0)
    		targetY = tableSize-1;
    	if (targetX > tableSize-1)
    		targetX = 0;
    	
    	result.push(table[targetX][targetY]);
    	
    	//get EAST neighbour
    	targetX = x+1;
    	targetY = y;
    	if (targetX >= tableSize)
    		targetX = 0;
    	
    	result.push(table[targetX][targetY]);
    	
    	//get SOUTHEAST neighbour
    	targetX = x+1;
    	targetY = y+1;
    	if (targetY > tableSize-1)
    		targetY = 0;
    	if (targetX > tableSize-1)
    		targetX = 0;
    	
    	result.push(table[targetX][targetY]);
    	
    	//get SOUTH neighbour
    	targetX = x;
    	targetY = y+1;
    	if (targetY >= tableSize)
    		targetY = 0;
    	
    	result.push(table[targetX][targetY]);
    	
    	//get SOUTHWEST neighbour
    	targetX = x-1;
    	targetY = y+1;
    	if (targetY > tableSize-1)
    		targetY = 0;
    	if (targetX < 0)
    		targetX = tableSize-1;
    	
    	result.push(table[targetX][targetY]);
    	
    	//get WEST neighbour
    	targetX = x-1;
    	targetY = y;
    	if (targetX < 0)
    		targetX = tableSize-1;
    	
    	result.push(table[targetX][targetY]);
    	
    	//get NORTHWEST neighbour
    	targetX = x-1;
    	targetY = y-1;
    	if (targetY < 0)
    		targetY = tableSize-1;
    	if (targetX < 0)
    		targetX = tableSize-1;
    	
    	result.push(table[targetX][targetY]);
    	
    	return result;
    }
    
    //Patterns
    
    function pattern() {
    	
    	//Set up the board using a random preset pattern
    	console.log("Creating pattern");
    	clearInterval(loop);
    	
    	//build dead table
    	table = [];
    	
    	for (let y = 0; y < tableSize; y++) {
    		let row = [];
    		for (let x = 0; x < tableSize; x++) {
    			
    			let cell = new Cell(y, x, false);
    			row.push(cell);
    		}
    		table.push(row);
    	}
    	
    	//add living cells for patterns
    	
    
    	//Glider
    	
    	table[1][1].isAlive = true;
    	table[2][2].isAlive = true;
    	table[2][3].isAlive = true;
    	table[3][2].isAlive = true;
    	table[3][1].isAlive = true;
    
    	
    	render();
    	
    }
    
    //Build board and render initial state
    pattern();
    render();
    html {
    	background: slategray;
    }
    
    .game {
    	background: #ddc;
    	border-radius: 2px;
    	
        padding-left: 0;
        padding-right: 0;
        margin-left: auto;
        margin-right: auto;
    	margin-top: 10%;
        display: block;
        
    }
    
    h1 {
    	color: white;
    	text-align: center;
    	font-family: sans-serif;
    }
    
    button {
    	text-align: center;
    	
    	padding: 12px;
    	border-radius: 2px;
    	font-size: 1.2em;
        margin-left: auto;
        margin-right: auto;
    	margin-top: 12px;
        display: block;
    }
    
    .controls {
    	display: flex;
    	width: 300px;
    	margin: auto;
    }
    <!DOCTYPE html>
    <html>
        <head>
            <meta charset="UTF-8" />
            <title>Conway's Game of Life</title>
            <link rel="stylesheet" href="css/styles.css">
        </head>
    	
        <body>
    		<h1>Conway's Game of Life</h1>
    		<canvas id="canvas" class="game" width="319px" height="319px"></canvas>
    		
    		<div class="controls">
    			<button onclick='start()'>Start</button>
    			<button onclick='stop()'>Stop</button>
    			<button onclick='reset()'>Reset</button>
    			<button onclick='pattern()'>Pattern</button>
    		</div>
    		
    		<script src="js/game.js"></script>
        </body>
    </html>