Pen Settings

HTML

CSS

CSS Base

Vendor Prefixing

Add External Stylesheets/Pens

Any URLs added here will be added as <link>s in order, and before the CSS in the editor. You can use the CSS from another Pen by using its URL and the proper URL extension.

+ add another resource

JavaScript

Babel includes JSX processing.

Add External Scripts/Pens

Any URL's added here will be added as <script>s in order, and run before the JavaScript in the editor. You can use the URL of any other Pen and it will include the JavaScript from that Pen.

+ add another resource

Packages

Add Packages

Search for and use JavaScript packages from npm here. By selecting a package, an import statement will be added to the top of the JavaScript editor for this package.

Behavior

Auto Save

If active, Pens will autosave every 30 seconds after being saved once.

Auto-Updating Preview

If enabled, the preview panel updates automatically as you code. If disabled, use the "Run" button to update.

Format on Save

If enabled, your code will be formatted when you actively save your Pen. Note: your code becomes un-folded during formatting.

Editor Settings

Code Indentation

Want to change your Syntax Highlighting theme, Fonts and more?

Visit your global Editor Settings.

HTML

              
                <div id="wrapper">
  <canvas id="gameCanvas" width="1004" height="680"></canvas>
  <div>
      <p><button id="decreaseSpeed">slow</button><cite id="generations">Generation: <span id="generation">0</span></cite><button id="increaseSpeed">fast</button></p>
      <!-- <button id="restart" onclick="universe.init()">Restart</button> -->
      <div id="legend">
          <ul id="icon-legend"></ul>
          <ul id="color-legend"></ul>
      </div>
  </div>
</div>
              
            
!

CSS

              
                #wrapper { with: 1008px; margin-top: 20px; }
body { background: #001F2B; color: white; }
button { background-color: #FFBA20; border: 0; margin-right: 10px; padding: 10px; border-radius: 10px; cursor: pointer; }
button:hover { opacity: .5; }
#startGame, #restart { background: #0047AA; }
div { font-size: 15px; font-family: arial; font-weight: bold; text-align: center; }
cite { font-style: normal; margin: 0 20px 0 13px; }
#legend { width: 1000px; padding: 20px; color: #01B8FF; border: 1px solid #00415A; margin: 20px auto 0 auto; text-align: left; }
#legend ul { display: grid; grid-template-columns: repeat(8, 1fr); row-gap: 15px; list-style-type: none; padding: 0; }
#legend ul:last-child { margin-bottom: 0; }
#legend #icon-legend { text-align: center; grid-template-columns: repeat(2, 1fr); }
#legend li span { display: inline-block; width: 17px; height: 17px; margin-right: 10px; }

              
            
!

JS

              
                // TODO: Add a Start button
// TODO: Hide icons at GAME OVER
// TODO: Add population counts by age
// TODO: Track the population by age on a graph

const width = 60,
	height = 40,
	cellSize = 17,
	maxHistory = 100,
	eventDuration = 5000,
	eventTypeChallenge = 'challenge',
	eventTypeAbundance = 'abundance',
	challengeProbability = 0.01,
	abundanceProbability = 0.02,
	boonIcon = '👶',
	baneIcon = '💀';


let generationsWrapper = document.getElementById('generations');
let generationElement = document.getElementById('generation');

class Utilities {
	getRandomNumber(min, max) {
		if (typeof min !== 'number' || typeof max !== 'number' || !Number.isInteger(min) || !Number.isInteger(max) || !Number.isFinite(min) || !Number.isFinite(max))
			throw new Error('min and max must be finite numbers');
	  
		if (min > max)
			throw new Error('Invalid range: min is greater than max');
	  
		if (min === max)
			return min;            
	  
		const randomNumber = Math.floor(Math.random() * (max - min + 1)) + min;
		return randomNumber;
	  }

	getRandomEventType() {
		const rand = Math.random();
		if (rand < challengeProbability)
			return eventTypeChallenge;
		else if (rand < abundanceProbability)
			return eventTypeAbundance;
	}
}
class Universe {
    constructor() {
        this.universe = Array.from({ length: height }, () => Array(width).fill(0));
        this.stagnation = Array.from({ length: height }, () => Array(width).fill(0));
        this.ages = Array.from({ length: height }, () => Array(width).fill(0));
        this.activeEvents = [];
        this.universeHistory = [];
        this.gameRunning = true;
        this.count = 0;
    }

    hasRepeatedState() {
		const currentState = this.universe.map(row => row.join('')).join('');
		const repeatedState = this.universeHistory.indexOf(currentState) !== -1;
		this.universeHistory.push(currentState);
		if (this.universeHistory.length > maxHistory) {
			this.universeHistory.shift(); // Remove the oldest state
		}
		return repeatedState;
	}

    decayRandomCells(percentage = 0.05) {
		for (let y = 0; y < height; y++) {
			for (let x = 0; x < width; x++) {
				if (this.universe[y][x] === 1 && Math.random() < percentage) {
					this.universe[y][x] = 0; // Decay the cell
					this.ages[y][x] = 0;  // Reset age
				}
			}
		}
	}

    decayCellsDuringChallenge(x, y) {
		const cellsToDecay = Math.floor(Math.random() * 5) + 1; // Random number between 1 to 5
		for (let i = 0; i < cellsToDecay; i++) {
			let offsetX = Math.floor(Math.random() * 3) - 1; // Generate a number between -1 to 1
			let offsetY = Math.floor(Math.random() * 3) - 1; // Generate a number between -1 to 1
	
			// Make sure the coordinates are within the universe's boundaries
			const newX = (x + offsetX + width) % width;
			const newY = (y + offsetY + height) % height;
	
			this.universe[newY][newX] = 0;  // Kill the cell
			this.ages[newY][newX] = 0;  // Reset age
		}
	}

	decayStagnantCells(newUniverse, newAges) {
		// Decay cells that have been static for too long
		for (let y = 0; y < height; y++) {
			for (let x = 0; x < width; x++) {
				if (this.stagnation[y][x] >= 50 && this.stagnation[y][x] <= 75 && Math.random() < 0.05) {
					newUniverse[y][x] = 0;  // Decay the cell
					newAges[y][x] = 0;  // Reset age
					this.stagnation[y][x] = 0;  // Reset stagnation counter
				}
			}
		}
	}

	simulateGrowth(x, y) {
		let newCells = Math.floor(Math.random() * 6) + 10; // Random number between 10 to 15
		for (let i = 0; i < newCells; i++) {
			let offsetX = Math.floor(Math.random() * 3) - 1; // Generate a number between -1 to 1
			let offsetY = Math.floor(Math.random() * 3) - 1; // Generate a number between -1 to 1
	
			// Make sure the coordinates are within the universe's boundaries
			const newX = (x + offsetX + width) % width;
			const newY = (y + offsetY + height) % height;
	
			this.universe[newY][newX] = 1;  // Birth the cell
			this.ages[newY][newX] = 1;  // Set age to 1
		}
	}

	addEvent(type, x, y, duration) {
		this.activeEvents.push({ type, x, y, duration });
	}

    setRandomEvent() {
		const randX = utilities.getRandomNumber(0, width);
		const randY = utilities.getRandomNumber(0, height);

		const eventType = utilities.getRandomEventType();

		if (eventType === eventTypeChallenge) {
			this.addEvent(eventTypeChallenge, randX, randY, eventDuration);
			this.decayCellsDuringChallenge(randX, randY);
		} else if (eventType === eventTypeAbundance) {
			this.addEvent(eventTypeAbundance, randX, randY, eventDuration);
			this.simulateGrowth(randX, randY);
		}
	}

	countLivingCells() {
		let totalAlive = 0;
		for (let y = 0; y < height; y++) {
			for (let x = 0; x < width; x++) {
				totalAlive += this.universe[y][x];
			}
		}
		return totalAlive;
	}

	checkForStagnation(y, x, newUniverse) {
		if (this.universe[y][x] === newUniverse[y][x]) {
			this.stagnation[y][x]++;
		} else {
			this.stagnation[y][x] = 0;
		}
	}		

	countNeighboringCells(newUniverse, newAges) {
		const 	neighborCountThreshold = 3,
				neighborCountAliveThreshold = 2,
				MAX_AGE = 10;

		for (let i = 0; i < height * width; i++) {
			const y = (i / width) | 0;
    		const x = i % width;
			let n = 0;
			for (let y1 = y - 1; y1 <= y + 1; y1++) {
				for (let x1 = x - 1; x1 <= x + 1; x1++) {
					if (this.universe[(y1 + height) % height][(x1 + width) % width] > 0) {
						n++;
					}
				}
			}
			
			const isAlive = this.universe[y][x] > 0;
			
			if (isAlive) n--;
			
			if (n === neighborCountThreshold || (n === neighborCountAliveThreshold && isAlive))
				newUniverse[y][x] = 1;
			else
				newUniverse[y][x] = 0;

			if (newUniverse[y][x] === 1)
				newAges[y][x] = this.ages[y][x] + 1;
			else
				newAges[y][x] = 0;

			if (newAges[y][x] > MAX_AGE)
				newAges[y][x] = 0;  // Reset age if it exceeds 10

			this.checkForStagnation(y, x, newUniverse);
		}
	}

    develop() {
		let newUniverse = Array.from({ length: height }, () => Array(width).fill(0));
		let newAges = Array.from({ length: height }, () => Array(width).fill(0));
		let totalAlive = 0;
	
		this.countNeighboringCells(newUniverse, newAges);
		this.decayStagnantCells(newUniverse, newAges);

		this.universe = newUniverse;
		this.ages = newAges;
	
		totalAlive = this.countLivingCells();
	
		// If no cells are alive, update the game's status
		if (totalAlive === 0) {
			this.gameRunning = false;
			generationsWrapper.innerText = `Mass Extinction at Generation #${this.count}!`;
		}
	
		if (this.hasRepeatedState() && this.gameRunning) this.decayRandomCells();
	}

	init() {
		this.count = 0;
		for (let y = 0; y < height; y++) {
			for (let x = 0; x < width; x++) {
				if (Math.random() < 0.125) {
					this.universe[y][x] = 1;
					this.ages[y][x] = 1;
				} else {
					this.universe[y][x] = 0;
					this.ages[y][x] = 0;
				}
			}
		}
		generationsWrapper.innerHTML = `Generation: <span id='generation'>0</span>`;
		generationElement = document.getElementById('generation');
		graphics.show();
	}
}
class Graphics {
    constructor(canvas, universe) {
        this.canvas = canvas;
        this.ctx = canvas.getContext('2d');
        this.universe = universe;
    }

    drawEventIcon(event) {
		this.ctx.font = `${cellSize}px Arial`;
		const adjustedX = event.x * cellSize - 3;
		const adjustedY = (event.y + 1) * cellSize - 2;
	
		// Calculate the alpha value based on the remaining duration of the event
		let alphaValue = event.duration / eventDuration; 
	
		// Adjust alphaValue based on speed.
		const speedFactor = Math.pow(1.5, -game.speed); // The exponential function makes it fade faster as speed decreases
		alphaValue = alphaValue * speedFactor;
	
		if (alphaValue > 1) {
			alphaValue = 1; // Ensure alpha value doesn't exceed 1
		}
		
		if (alphaValue > 0.5) {
			// Fade in for the first half of the duration
			alphaValue = 2 * (alphaValue - 0.5);
		} else {
			// Fade out for the second half
			alphaValue = 2 * alphaValue;
		}
	
		// Adjust alpha based on speed
		const fadeFactor = Math.exp(1 - game.speed / 10); // speed is divided by 10 assuming it ranges between 0 and 10
		alphaValue = Math.pow(alphaValue, fadeFactor);
	
		this.ctx.globalAlpha = alphaValue; // Set the alpha value
	
		if (event.type === 'challenge') {
			this.ctx.fillText(baneIcon, adjustedX, adjustedY);
		} else if (event.type === 'abundance') {
			this.ctx.fillText(boonIcon, adjustedX, adjustedY);
		}
	
		this.ctx.globalAlpha = 1; // Reset the alpha value
    }

    lerp(start, end, amt) {
		return start * (1 - amt) + end * amt;
    }

    getColorForAge(age) {
		function colorToRgb(color) {
			// Get RGB values from CSS color
			let tempElem = document.createElement('div');
			tempElem.style.color = color;
			document.body.appendChild(tempElem);
	
			let colors = window.getComputedStyle(tempElem).color.match(/\d+/g).map(Number);
			document.body.removeChild(tempElem);
	
			return colors;
		}
	
		function interpolateColors(color1, color2, factor) {
			let result = [];
			let col1 = colorToRgb(color1);
			let col2 = colorToRgb(color2);
			
			for (let i = 0; i < 3; i++) {
				result.push(Math.round(graphics.lerp(col1[i], col2[i], factor)));
			}
	
			return 'rgb(' + result[0] + ', ' + result[1] + ', ' + result[2] + ')';
		}
	
		if (age === 1) return 'lightblue';  // Baby
		if (age === 2) return interpolateColors('lightblue', 'blue', 0.5); // Pr-Teen
		if (age === 3) return 'blue';  // Teen
		if (age >= 4 && age <= 5) return interpolateColors('blue', 'black', (age - 3) * 0.5); // Young Adult
		if (age === 6) return 'black';  // Adult
		if (age >= 7 && age <= 8) return interpolateColors('black', 'red', (age - 6) * 0.5); // Mature
		if (age === 9) return 'red';  // Elderly
		if (age === 10) return 'pink';  // Dying
	
		return null;  // Dead or undefined age        
    }

    drawGameOverText() {
		const fontSize = 48;
		const x = this.canvas.width / 2;
		const y = this.canvas.height / 2;
	
		this.ctx.font = `${fontSize}px Arial`;
		this.ctx.textAlign = 'center';
		this.ctx.textBaseline = 'middle';
	
		// Set stroke properties
		this.ctx.strokeStyle = '#00415A';
		this.ctx.lineWidth = 5;
	
		// Set the fill color
		this.ctx.fillStyle = '#FFFFFF';
	
		// Draw the stroke (outline) for the text
		this.ctx.strokeText('GAME OVER', x, y);
	
		// Draw the text itself
		this.ctx.fillText('GAME OVER', x, y);
	}

    show() {
		game.ctx.clearRect(0, 0, game.canvas.width, game.canvas.height);

		// Draw cells
		for (let y = 0; y < height; y++) {
			for (let x = 0; x < width; x++) {
				if (universe.universe[y][x]) {
					game.ctx.fillStyle = this.getColorForAge(universe.ages[y][x]);
					game.ctx.fillRect(x * cellSize, y * cellSize, cellSize, cellSize);
				}
			}
		}
	
		// Draw grid
		game.ctx.strokeStyle = '#00415A';
		game.ctx.lineWidth = 1;
		
		for (let y = 0; y <= height; y++) {
			game.ctx.moveTo(0, y * cellSize);
			game.ctx.lineTo(game.canvas.width, y * cellSize);
		}
		for (let x = 0; x <= width; x++) {
			game.ctx.moveTo(x * cellSize, 0);
			game.ctx.lineTo(x * cellSize, game.canvas.height);
		}
		game.ctx.stroke();
	
		generationElement.innerText = universe.count;
	
		if (!universe.gameRunning) this.drawGameOverText();
	}
}
class Game {
    constructor() {
        this.canvas = document.getElementById('gameCanvas');
        this.ctx = this.canvas.getContext('2d');
        this.restart = document.getElementById('restart');
        this.increaseSpeedBtn = document.getElementById('increaseSpeed');
        this.decreaseSpeedBtn = document.getElementById('decreaseSpeed');
        this.speed = 50;
        this.gameRunning = true;
        this.count = 0;
        this.activeEvents = [];
        this.initEventListeners();
    }

    loop() {
        universe.setRandomEvent();

        // Update event durations and filter out expired events
        this.activeEvents = universe.activeEvents.map(event => ({
            ...event,
            duration: event.duration - this.speed
        })).filter(event => event.duration > 0);

        graphics.show();
        this.activeEvents.forEach(graphics.drawEventIcon.bind(graphics));

        if (universe.gameRunning) {
            universe.develop();
            universe.count++;
            setTimeout(this.loop.bind(this), this.speed);
        }
    }

    initEventListeners() {
        this.increaseSpeedBtn.addEventListener('click', () => {
            if (this.speed > 10) {
                this.speed -= 10;
            }
        });

        this.decreaseSpeedBtn.addEventListener('click', () => {
            this.speed += 10;
        });

        /*
        this.restart.addEventListener('click', () => {
            this.gameRunning = true;
            this.activeEvents = [];
            universe.init(); // Assuming 'init' is a method of the Game class
            this.loop();
        });
      */
    }
}

const utilities = new Utilities();
const game = new Game();
const universe = new Universe();
const graphics = new Graphics(game.canvas, universe);

function populateLegend() {
    const iconLegend = document.getElementById('icon-legend');
    const colorLegend = document.getElementById('color-legend');

    // Add icons to the legend
    const icons = [
        { icon: boonIcon, description: 'A sudden population increase (abundant food, etc.)' },
        { icon: baneIcon, description: 'A sudden population reduction (famine, epidemic, disaster, etc.)' }
    ];

    icons.forEach(item => {
        const li = document.createElement('li');
        li.innerHTML = `<span>${item.icon}</span> ${item.description}`;
        iconLegend.appendChild(li);
    });

    // Add colors to the legend
    const colors = [
        { color: 'lightblue', description: 'Born' },
        { color: graphics.getColorForAge(2), description: 'Pre-Teen' },
        { color: 'blue', description: 'Teen' },
        { color: graphics.getColorForAge(4), description: 'Young Adult' },
        { color: 'black', description: 'Adult' },
        { color: graphics.getColorForAge(7), description: 'Mature' },
        { color: 'red', description: 'Elderly' },
        { color: 'pink', description: 'Dying' }
    ];

    colors.forEach(item => {
        const li = document.createElement('li');
        li.innerHTML = `<span style='background-color: ${item.color}'>${String.fromCharCode('&#9632;')}</span> ${item.description}`;
        colorLegend.appendChild(li);
    });
}

universe.init();
game.loop();
populateLegend();
              
            
!
999px

Console