Update: more science and kid friendly
This commit is contained in:
@@ -1,299 +1,209 @@
|
||||
// Initialize the game when the page loads
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// Create game with 80x60 grid and 10px cells
|
||||
const game = new GameOfLife(80, 60, 10);
|
||||
const canvas = document.getElementById('lifeCanvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
const cols = 80;
|
||||
const rows = 60;
|
||||
const cellSize = canvas.width / cols;
|
||||
let grid = makeGrid();
|
||||
let running = false;
|
||||
let generation = 0;
|
||||
let births = 0;
|
||||
let deaths = 0;
|
||||
let timer = null;
|
||||
let speed = 8;
|
||||
|
||||
const themeTokens = {
|
||||
sprout: ['oklch(58% 0.16 145)', 'oklch(72% 0.14 170)'],
|
||||
nebula: ['oklch(60% 0.18 285)', 'oklch(70% 0.14 235)'],
|
||||
sunny: ['oklch(70% 0.15 65)', 'oklch(74% 0.12 35)']
|
||||
};
|
||||
|
||||
function makeGrid() {
|
||||
return Array.from({ length: rows }, () => Array(cols).fill(0));
|
||||
}
|
||||
|
||||
function randomize(chance = 0.28) {
|
||||
grid = grid.map(row => row.map(() => (Math.random() < chance ? 1 : 0)));
|
||||
generation = 0;
|
||||
births = 0;
|
||||
deaths = 0;
|
||||
updateLesson('A fresh universe appeared. Press Play and watch which clusters survive the first few generations.');
|
||||
updateStats();
|
||||
draw();
|
||||
}
|
||||
|
||||
function sparkles() {
|
||||
grid = makeGrid();
|
||||
const centerX = Math.floor(cols / 2);
|
||||
const centerY = Math.floor(rows / 2);
|
||||
const points = [[0,0], [1,0], [-1,0], [0,1], [0,-1], [2,1], [-2,-1], [4,0], [4,1], [4,-1], [-4,0], [-4,1], [-4,-1], [7,2], [8,2], [7,3], [8,3]];
|
||||
points.forEach(([x, y]) => { grid[centerY + y][centerX + x] = 1; });
|
||||
generation = 0;
|
||||
births = 0;
|
||||
deaths = 0;
|
||||
updateLesson('Sparkles loaded. Step slowly and look for parts that blink while other parts float away.');
|
||||
updateStats();
|
||||
draw();
|
||||
}
|
||||
|
||||
function neighbors(x, y) {
|
||||
let total = 0;
|
||||
for (let dy = -1; dy <= 1; dy++) {
|
||||
for (let dx = -1; dx <= 1; dx++) {
|
||||
if (dx === 0 && dy === 0) continue;
|
||||
const nx = (x + dx + cols) % cols;
|
||||
const ny = (y + dy + rows) % rows;
|
||||
total += grid[ny][nx];
|
||||
}
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
function step() {
|
||||
const next = makeGrid();
|
||||
births = 0;
|
||||
deaths = 0;
|
||||
for (let y = 0; y < rows; y++) {
|
||||
for (let x = 0; x < cols; x++) {
|
||||
const n = neighbors(x, y);
|
||||
const alive = grid[y][x] === 1;
|
||||
const nextAlive = alive ? (n === 2 || n === 3) : n === 3;
|
||||
next[y][x] = nextAlive ? 1 : 0;
|
||||
if (!alive && nextAlive) births++;
|
||||
if (alive && !nextAlive) deaths++;
|
||||
}
|
||||
}
|
||||
grid = next;
|
||||
generation++;
|
||||
updateStats();
|
||||
draw();
|
||||
}
|
||||
|
||||
function draw() {
|
||||
const styles = getComputedStyle(document.documentElement);
|
||||
const cellA = styles.getPropertyValue('--cell-a').trim();
|
||||
const cellB = styles.getPropertyValue('--cell-b').trim();
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
ctx.fillStyle = styles.getPropertyValue('--surface').trim();
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
ctx.strokeStyle = styles.getPropertyValue('--grid').trim();
|
||||
ctx.lineWidth = 0.7;
|
||||
for (let x = 0; x <= cols; x++) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x * cellSize, 0);
|
||||
ctx.lineTo(x * cellSize, canvas.height);
|
||||
ctx.stroke();
|
||||
}
|
||||
for (let y = 0; y <= rows; y++) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(0, y * cellSize);
|
||||
ctx.lineTo(canvas.width, y * cellSize);
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
const gradient = ctx.createLinearGradient(0, 0, canvas.width, canvas.height);
|
||||
gradient.addColorStop(0, cellA);
|
||||
gradient.addColorStop(1, cellB);
|
||||
ctx.fillStyle = gradient;
|
||||
ctx.shadowColor = cellA;
|
||||
ctx.shadowBlur = 8;
|
||||
for (let y = 0; y < rows; y++) {
|
||||
for (let x = 0; x < cols; x++) {
|
||||
if (grid[y][x]) {
|
||||
const inset = 1.4;
|
||||
ctx.beginPath();
|
||||
ctx.roundRect(x * cellSize + inset, y * cellSize + inset, cellSize - inset * 2, cellSize - inset * 2, 3);
|
||||
ctx.fill();
|
||||
}
|
||||
}
|
||||
}
|
||||
ctx.shadowBlur = 0;
|
||||
}
|
||||
|
||||
function population() {
|
||||
return grid.reduce((sum, row) => sum + row.reduce((rowSum, cell) => rowSum + cell, 0), 0);
|
||||
}
|
||||
|
||||
function updateStats() {
|
||||
document.getElementById('generation').textContent = generation;
|
||||
document.getElementById('population').textContent = population();
|
||||
document.getElementById('births').textContent = births;
|
||||
document.getElementById('deaths').textContent = deaths;
|
||||
}
|
||||
|
||||
function updateLesson(text) {
|
||||
document.getElementById('lessonText').textContent = text;
|
||||
}
|
||||
|
||||
function play() {
|
||||
running = true;
|
||||
document.getElementById('playBtn').textContent = 'Pause';
|
||||
document.getElementById('runStatus').textContent = 'Running - universe is alive';
|
||||
loop();
|
||||
}
|
||||
|
||||
function pause() {
|
||||
running = false;
|
||||
document.getElementById('playBtn').textContent = 'Play';
|
||||
document.getElementById('runStatus').textContent = 'Paused - ready to experiment';
|
||||
if (timer) window.clearTimeout(timer);
|
||||
}
|
||||
|
||||
function loop() {
|
||||
if (!running) return;
|
||||
step();
|
||||
timer = window.setTimeout(() => requestAnimationFrame(loop), 620 - speed * 45);
|
||||
}
|
||||
|
||||
function setTheme(name) {
|
||||
const [a, b] = themeTokens[name];
|
||||
document.documentElement.style.setProperty('--cell-a', a);
|
||||
document.documentElement.style.setProperty('--cell-b', b);
|
||||
document.documentElement.style.setProperty('--accent', a);
|
||||
document.querySelectorAll('.theme').forEach(button => {
|
||||
button.setAttribute('aria-pressed', String(button.dataset.theme === name));
|
||||
});
|
||||
updateLesson(name === 'sprout' ? 'Sprout mode keeps the science-lab look: calm, clear, and easy to read.' : name === 'nebula' ? 'Nebula mode feels more arcade-like. It is great for spotting moving patterns.' : 'Sunny mode is warm and playful for younger scientists presenting to family.');
|
||||
draw();
|
||||
}
|
||||
|
||||
canvas.addEventListener('pointerdown', event => {
|
||||
pause();
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const scaleX = canvas.width / rect.width;
|
||||
const scaleY = canvas.height / rect.height;
|
||||
const x = Math.floor((event.clientX - rect.left) * scaleX / cellSize);
|
||||
const y = Math.floor((event.clientY - rect.top) * scaleY / cellSize);
|
||||
if (x >= 0 && x < cols && y >= 0 && y < rows) {
|
||||
grid[y][x] = grid[y][x] ? 0 : 1;
|
||||
births = 0;
|
||||
deaths = 0;
|
||||
updateLesson('You drew on the board. Press Step once to test your prediction before running it fast.');
|
||||
updateStats();
|
||||
draw();
|
||||
}
|
||||
});
|
||||
|
||||
class GameOfLife {
|
||||
constructor(width, height, cellSize = 10) {
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
this.cellSize = cellSize;
|
||||
this.grid = this.createGrid();
|
||||
this.nextGrid = this.createGrid();
|
||||
this.running = false;
|
||||
this.generation = 0;
|
||||
this.births = 0;
|
||||
this.deaths = 0;
|
||||
this.animationId = null;
|
||||
this.speed = 10;
|
||||
|
||||
// Canvas setup
|
||||
this.canvas = document.getElementById('gameCanvas');
|
||||
this.ctx = this.canvas.getContext('2d');
|
||||
this.canvas.width = width * cellSize;
|
||||
this.canvas.height = height * cellSize;
|
||||
|
||||
// Event listeners
|
||||
this.setupEventListeners();
|
||||
this.setupPresets();
|
||||
|
||||
// Initial draw
|
||||
this.draw();
|
||||
this.updateStats();
|
||||
}
|
||||
|
||||
createGrid() {
|
||||
return Array(this.height).fill().map(() => Array(this.width).fill(0));
|
||||
}
|
||||
|
||||
randomize() {
|
||||
for (let y = 0; y < this.height; y++) {
|
||||
for (let x = 0; x < this.width; x++) {
|
||||
this.grid[y][x] = Math.random() > 0.7 ? 1 : 0;
|
||||
}
|
||||
}
|
||||
this.generation = 0;
|
||||
this.draw();
|
||||
}
|
||||
|
||||
setupEventListeners() {
|
||||
// Start/Pause/Reset buttons
|
||||
document.getElementById('startBtn').addEventListener('click', () => this.start());
|
||||
document.getElementById('pauseBtn').addEventListener('click', () => this.pause());
|
||||
document.getElementById('resetBtn').addEventListener('click', () => this.reset());
|
||||
document.getElementById('randomBtn').addEventListener('click', () => this.randomize());
|
||||
|
||||
// Speed control
|
||||
document.getElementById('speed').addEventListener('input', (e) => {
|
||||
this.speed = parseInt(e.target.value);
|
||||
});
|
||||
|
||||
// Canvas click to toggle cells
|
||||
this.canvas.addEventListener('click', (e) => {
|
||||
if (this.running) return;
|
||||
|
||||
const rect = this.canvas.getBoundingClientRect();
|
||||
const x = Math.floor((e.clientX - rect.left) / this.cellSize);
|
||||
const y = Math.floor((e.clientY - rect.top) / this.cellSize);
|
||||
|
||||
if (x >= 0 && x < this.width && y >= 0 && y < this.height) {
|
||||
this.grid[y][x] = this.grid[y][x] ? 0 : 1;
|
||||
this.draw();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
setupPresets() {
|
||||
document.querySelector('[data-preset="glider"]').addEventListener('click', () => {
|
||||
this.reset();
|
||||
// Glider pattern
|
||||
this.grid[5][5] = 1;
|
||||
this.grid[6][6] = 1;
|
||||
this.grid[6][7] = 1;
|
||||
this.grid[5][7] = 1;
|
||||
this.grid[4][7] = 1;
|
||||
this.draw();
|
||||
});
|
||||
|
||||
document.querySelector('[data-preset="spaceship"]').addEventListener('click', () => {
|
||||
this.reset();
|
||||
// Lightweight spaceship
|
||||
this.grid[10][10] = 1;
|
||||
this.grid[10][13] = 1;
|
||||
this.grid[11][9] = 1;
|
||||
this.grid[11][13] = 1;
|
||||
this.grid[12][9] = 1;
|
||||
this.grid[12][10] = 1;
|
||||
this.grid[12][11] = 1;
|
||||
this.grid[12][12] = 1;
|
||||
this.draw();
|
||||
});
|
||||
|
||||
document.querySelector('[data-preset="pulsar"]').addEventListener('click', () => {
|
||||
this.reset();
|
||||
// Pulsar pattern
|
||||
const pulsarPoints = [
|
||||
[2,4],[2,5],[2,6],[2,10],[2,11],[2,12],
|
||||
[4,2],[4,7],[4,9],[4,14],
|
||||
[5,2],[5,7],[5,9],[5,14],
|
||||
[6,2],[6,7],[6,9],[6,14],
|
||||
[7,4],[7,5],[7,6],[7,10],[7,11],[7,12],
|
||||
[9,4],[9,5],[9,6],[9,10],[9,11],[9,12],
|
||||
[10,2],[10,7],[10,9],[10,14],
|
||||
[11,2],[11,7],[11,9],[11,14],
|
||||
[12,2],[12,7],[12,9],[12,14],
|
||||
[14,4],[14,5],[14,6],[14,10],[14,11],[14,12]
|
||||
];
|
||||
|
||||
pulsarPoints.forEach(([y, x]) => {
|
||||
this.grid[y][x] = 1;
|
||||
});
|
||||
|
||||
this.draw();
|
||||
});
|
||||
|
||||
document.querySelector('[data-preset="gosper"]').addEventListener('click', () => {
|
||||
this.reset();
|
||||
// Gosper glider gun
|
||||
const gunPoints = [
|
||||
[5,1],[5,2],[6,1],[6,2],
|
||||
[5,11],[6,11],[7,11],
|
||||
[4,12],[8,12],
|
||||
[3,13],[9,13],
|
||||
[3,14],[9,14],
|
||||
[6,15],
|
||||
[4,16],[8,16],
|
||||
[5,17],[6,17],[7,17],
|
||||
[6,18],
|
||||
[3,21],[4,21],[5,21],
|
||||
[3,22],[4,22],[5,22],
|
||||
[2,23],[6,23],
|
||||
[1,25],[2,25],[6,25],[7,25],
|
||||
[3,35],[4,35],[3,36],[4,36]
|
||||
];
|
||||
|
||||
gunPoints.forEach(([y, x]) => {
|
||||
this.grid[y][x] = 1;
|
||||
});
|
||||
|
||||
this.draw();
|
||||
});
|
||||
}
|
||||
|
||||
countNeighbors(x, y) {
|
||||
let count = 0;
|
||||
for (let dy = -1; dy <= 1; dy++) {
|
||||
for (let dx = -1; dx <= 1; dx++) {
|
||||
if (dx === 0 && dy === 0) continue;
|
||||
const nx = (x + dx + this.width) % this.width;
|
||||
const ny = (y + dy + this.height) % this.height;
|
||||
count += this.grid[ny][nx];
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
update() {
|
||||
// Reset counters
|
||||
this.births = 0;
|
||||
this.deaths = 0;
|
||||
|
||||
// Compute next generation
|
||||
for (let y = 0; y < this.height; y++) {
|
||||
for (let x = 0; x < this.width; x++) {
|
||||
const neighbors = this.countNeighbors(x, y);
|
||||
const cell = this.grid[y][x];
|
||||
let nextState;
|
||||
|
||||
// Apply Conway's Game of Life rules
|
||||
if (cell === 1) {
|
||||
nextState = (neighbors === 2 || neighbors === 3) ? 1 : 0;
|
||||
if (nextState === 0) this.deaths++;
|
||||
} else {
|
||||
nextState = (neighbors === 3) ? 1 : 0;
|
||||
if (nextState === 1) this.births++;
|
||||
}
|
||||
|
||||
this.nextGrid[y][x] = nextState;
|
||||
}
|
||||
}
|
||||
|
||||
// Swap grids
|
||||
[this.grid, this.nextGrid] = [this.nextGrid, this.grid];
|
||||
this.generation++;
|
||||
|
||||
// Update stats display
|
||||
this.updateStats();
|
||||
}
|
||||
|
||||
draw() {
|
||||
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
||||
|
||||
// Draw live cells with gradient
|
||||
const gradient = this.ctx.createLinearGradient(0, 0, this.canvas.width, this.canvas.height);
|
||||
gradient.addColorStop(0, '#4facfe');
|
||||
gradient.addColorStop(1, '#00f2fe');
|
||||
|
||||
this.ctx.fillStyle = gradient;
|
||||
|
||||
for (let y = 0; y < this.height; y++) {
|
||||
for (let x = 0; x < this.width; x++) {
|
||||
if (this.grid[y][x] === 1) {
|
||||
this.ctx.fillRect(
|
||||
x * this.cellSize,
|
||||
y * this.cellSize,
|
||||
this.cellSize - 1,
|
||||
this.cellSize - 1
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Draw grid lines
|
||||
this.ctx.strokeStyle = 'rgba(255, 255, 255, 0.05)';
|
||||
this.ctx.lineWidth = 0.5;
|
||||
|
||||
// Vertical lines
|
||||
for (let x = 0; x <= this.width; x++) {
|
||||
this.ctx.beginPath();
|
||||
this.ctx.moveTo(x * this.cellSize, 0);
|
||||
this.ctx.lineTo(x * this.cellSize, this.canvas.height);
|
||||
this.ctx.stroke();
|
||||
}
|
||||
|
||||
// Horizontal lines
|
||||
for (let y = 0; y <= this.height; y++) {
|
||||
this.ctx.beginPath();
|
||||
this.ctx.moveTo(0, y * this.cellSize);
|
||||
this.ctx.lineTo(this.canvas.width, y * this.cellSize);
|
||||
this.ctx.stroke();
|
||||
}
|
||||
}
|
||||
|
||||
updateStats() {
|
||||
document.getElementById('generation').textContent = this.generation;
|
||||
document.getElementById('population').textContent = this.getPopulation();
|
||||
document.getElementById('births').textContent = this.births;
|
||||
document.getElementById('deaths').textContent = this.deaths;
|
||||
}
|
||||
|
||||
getPopulation() {
|
||||
let count = 0;
|
||||
for (let y = 0; y < this.height; y++) {
|
||||
for (let x = 0; x < this.width; x++) {
|
||||
count += this.grid[y][x];
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
gameLoop() {
|
||||
this.update();
|
||||
this.draw();
|
||||
|
||||
// Adjust speed (higher value = faster)
|
||||
const speedFactor = (21 - this.speed) * 20;
|
||||
setTimeout(() => {
|
||||
if (this.running) {
|
||||
this.animationId = requestAnimationFrame(() => this.gameLoop());
|
||||
}
|
||||
}, speedFactor);
|
||||
}
|
||||
|
||||
start() {
|
||||
if (!this.running) {
|
||||
this.running = true;
|
||||
this.gameLoop();
|
||||
}
|
||||
}
|
||||
|
||||
pause() {
|
||||
this.running = false;
|
||||
if (this.animationId) {
|
||||
cancelAnimationFrame(this.animationId);
|
||||
this.animationId = null;
|
||||
}
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.pause();
|
||||
this.grid = this.createGrid();
|
||||
this.nextGrid = this.createGrid();
|
||||
this.generation = 0;
|
||||
this.births = 0;
|
||||
this.deaths = 0;
|
||||
this.updateStats();
|
||||
this.draw();
|
||||
}
|
||||
}
|
||||
document.getElementById('playBtn').addEventListener('click', () => running ? pause() : play());
|
||||
document.getElementById('stepBtn').addEventListener('click', () => { pause(); step(); updateLesson('One generation advanced. Did the population go up, down, or stay balanced?'); });
|
||||
document.getElementById('clearBtn').addEventListener('click', () => { pause(); grid = makeGrid(); generation = 0; births = 0; deaths = 0; updateLesson('The board is clear. Draw a creature, then test if it survives.'); updateStats(); draw(); });
|
||||
document.getElementById('randomBtn').addEventListener('click', () => randomize(0.28));
|
||||
document.getElementById('sparkBtn').addEventListener('click', sparkles);
|
||||
document.getElementById('speed').addEventListener('input', event => {
|
||||
speed = Number(event.target.value);
|
||||
document.getElementById('speedLabel').textContent = speed;
|
||||
});
|
||||
document.querySelectorAll('.theme').forEach(button => button.addEventListener('click', () => setTheme(button.dataset.theme)));
|
||||
|
||||
if (!CanvasRenderingContext2D.prototype.roundRect) {
|
||||
CanvasRenderingContext2D.prototype.roundRect = function roundRect(x, y, width, height, radius) {
|
||||
this.moveTo(x + radius, y);
|
||||
this.arcTo(x + width, y, x + width, y + height, radius);
|
||||
this.arcTo(x + width, y + height, x, y + height, radius);
|
||||
this.arcTo(x, y + height, x, y, radius);
|
||||
this.arcTo(x, y, x + width, y, radius);
|
||||
return this;
|
||||
};
|
||||
}
|
||||
|
||||
randomize(0.18);
|
||||
|
||||
Reference in New Issue
Block a user