Conway's Game of Life · seed-2.0-code
Run Conway's Game of Life on a full-screen canvas grid with click-and-drag to toggle cells, plus play/pause, step, randomize, and clear controls.
transcript · 6 messages
Run Conway's Game of Life on a full-screen canvas grid with click-and-drag to toggle cells, plus play/pause, step, randomize, and clear controls.
thinking
Okay, let's tackle this Conway's Game of Life implementation. The user wants a full-screen canvas with click-and-drag to toggle cells, plus play/pause, step, randomize, and clear controls. Hmm, let's start by breaking down the requirements.
First, the file structure. I need to create a single index.html with inline CSS and JavaScript, no external resources. Let's start with the HTML skeleton: a canvas element and some controls.
For the canvas, it should be full-screen, so I'll set the body to margin 0, padding 0, overflow hidden. The canvas will take up the entire window except for the controls bar at the top.
Controls: I need buttons for Play/Pause, Step, Randomize, Clear. Also, maybe a speed slider? Wait, the user didn't mention that, but maybe it's useful. Let me check the requirements again: play/pause, step, randomize, clear. Oh, maybe also a generation counter? That would be helpful to see how many generations have passed.
Now, the Game of Life logic. The standard rules:
- Any live cell with fewer than two live neighbors dies (underpopulation).
- Any live cell with two or three live neighbors lives on to the next generation.
- Any live cell with more than three live neighbors dies (overpopulation).
- Any dead cell with exactly three live neighbors becomes a live cell (birth).
Implementation steps:
1. Set up the canvas and calculate grid dimensions. Let's use a cell size, like 10px, so the number of cells is width / cellSize and height / cellSize. But we need to adjust the canvas size to fit exactly, so maybe round down or use a fixed cell size. Wait, let's pick a cell size, say 15px, and then set the canvas to be (window.innerWidth) x (window.innerHeight - control bar height). Wait, let's make the control bar fixed at the top, with height, say, 50px. So the canvas height will be window.innerHeight - 50.
Wait, but we need to handle window resizing. Oh, the user didn't mention that, but it's a nice touch. But let's see, when the window is resized, the grid should probably reset? Or maybe preserve the existing cells? Hmm, maybe for simplicity, when resizing, we reset the grid. Or maybe adjust. But let's think: if the user has a pattern, resizing the window would change the grid size, which might truncate or expand. Maybe for now, just set the grid size once when the page loads, and then if the window is resized, we can handle it with a resize event that recalculates. But let's proceed step by step.
First, let's outline the code structure in index.html:
- <style> tag with all the CSS.
- <body> with a controls div and a canvas.
- <script> tag with all the JS.
Let's start writing the CSS. The body will have margin: 0, padding: 0, font-family: sans-serif, overflow: hidden. The controls will be a flex container, background: #333, color: white, padding: 10px, align-items: center, gap: 10px, height: 50px. Buttons: padding 8px 16px, border none, border-radius 4px, cursor: pointer. Play button can be green when paused, red when playing. Wait, play/pause: let's have the button text change. When playing, show "Pause" and have a red background; when paused, "Play" with green.
The canvas: display: block, background: #000. So cells are white, background is black.
Now, JS:
Variables:
- canvas: the canvas element.
- ctx: 2D context.
- cellSize: let's pick 10px? Or 15px? Let's use 15px for better visibility.
- gridWidth, gridHeight: number of cells in x and y directions.
- grid: 2D array (or 1D array for performance) representing the current state.
- nextGrid: 2D array for next generation.
- isPlaying: boolean to track play state.
- animationId: to store the requestAnimationFrame ID.
- generation: counter.
- isMouseDown: boolean to track if mouse is pressed for dragging.
- lastToggledCell: to prevent toggling the same cell multiple times while dragging.
Wait, click-and-drag to toggle cells. So when the user presses the mouse button down on a cell, it toggles, and then as they drag, it toggles cells under the mouse, but only if they're different from the last toggled cell (so you don't toggle the same cell multiple times as you move the mouse slightly).
So steps for JS:
1. Initialize the canvas and context.
2. Calculate gridWidth and gridHeight based on canvas size and cellSize.
3. Create grid and nextGrid arrays.
4. Add event listeners:
- mousedown: start dragging, toggle cell under mouse.
- mousemove: if dragging, toggle cell under mouse (if different from last).
- mouseup: stop dragging.
- mouseleave: stop dragging (in case mouse leaves canvas).
- Buttons: play/pause, step, randomize, clear.
- Resize event: maybe handle window resize to adjust canvas and grid.
Wait, let's code the grid as a 2D array for simplicity. For example, grid[y][x] is true if cell is alive.
Now, the rules: for each cell, count the number of alive neighbors (8 surrounding cells). Then apply the rules.
But for cells on the edge, what to do? Wrap around (toroidal) or treat as dead? The user didn't specify, but usually, for simplicity, treat as dead. So when checking neighbors, if x or y is out of bounds, skip (or count as 0).
Now, let's outline the functions:
- init(): sets up canvas, grid, event listeners.
- resizeCanvas(): adjusts canvas size to window, recalculates grid, resets grid?
- createGrid(): creates a new grid with all dead cells.
- randomizeGrid(): sets cells randomly to alive/dead (maybe 30% alive).
- clearGrid(): sets all cells to dead.
- getCell(x, y): checks if x,y is within bounds, returns grid[y][x] or false.
- countNeighbors(x, y): counts alive neighbors.
- nextGeneration(): calculates next grid based on current.
- drawGrid(): renders the grid on canvas.
- toggleCell(x, y): toggles the cell at (x,y) (canvas coordinates) to grid coordinates.
- play(): starts the animation loop.
- pause(): stops the animation loop.
- step(): runs one generation.
- updatePlayButton(): changes the button text and color based on isPlaying.
Now, let's think about the animation loop. When playing, we can use requestAnimationFrame, but we need to control the speed. Because requestAnimationFrame runs at 60fps, which is too fast. So we can add a delay, like using a timestamp to check how much time has passed since the last generation. For example, run a generation every 100ms (10fps) or so. Maybe add a speed slider? Wait, the user didn't mention that, but let's include it as a nice feature. Because the default might be too fast or slow.
Wait, the user's requirements: play/pause, step, randomize, clear. Let's check the example given in the user's message? Wait, no, the user just gave the instruction. Wait, but let's see, the assistant's previous messages mention a sample, but let's focus on the requirements.
Wait, let's proceed. Let's code with a speed control. Let's say a range input from 1 to 60 generations per second, default 10.
Now, let's start writing the code.
First, the HTML structure:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Conway's Game of Life</title>
<style>
/* CSS here */
</style>
</head>
<body>
<div class="controls">
<button id="playBtn">Play</button>
<button id="stepBtn">Step</button>
<button id="randomBtn">Randomize</button>
<button id="clearBtn">Clear</button>
<label for="speed">Speed: <span id="speedValue">10</span> gen/s</label>
<input type="range" id="speed" min="1" max="60" value="10">
<span id="genCounter">Generation: 0</span>
</div>
<canvas id="canvas"></canvas>
<script>
// JS here
</script>
</body>
</html>
Now, CSS:
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Arial', sans-serif;
background-color: #1a1a1a;
color: white;
overflow: hidden;
display: flex;
flex-direction: column;
height: 100vh;
}
.controls {
display: flex;
align-items: center;
gap: 15px;
padding: 10px 20px;
background-color: #2d2d2d;
border-bottom: 1px solid #444;
height: 50px;
}
button {
padding: 6px 14px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
transition: background-color 0.2s;
}
#playBtn {
background-color: #4CAF50;
color: white;
}
#playBtn:hover {
background-color: #45a049;
}
#playBtn.playing {
background-color: #f44336;
}
#playBtn.playing:hover {
background-color: #da190b;
}
#stepBtn {
background-color: #2196F3;
color: white;
}
#stepBtn:hover {
background-color: #0b7dda;
}
#randomBtn {
background-color: #ff9800;
color: white;
}
#randomBtn:hover {
background-color: #e68a00;
}
#clearBtn {
background-color: #9e9e9e;
color: white;
}
#clearBtn:hover {
background-color: #808080;
}
label {
font-size: 14px;
}
input[type="range"] {
width: 100px;
cursor: pointer;
}
#genCounter {
margin-left: auto;
font-size: 14px;
color: #ccc;
}
#canvas {
flex: 1;
background-color: #000;
cursor: crosshair;
}
Now, JS:
First, get all elements:
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const playBtn = document.getElementById('playBtn');
const stepBtn = document.getElementById('stepBtn');
const randomBtn = document.getElementById('randomBtn');
const clearBtn = document.getElementById('clearBtn');
const speedInput = document.getElementById('speed');
const speedValue = document.getElementById('speedValue');
const genCounter = document.getElementById('genCounter');
Variables:
const CELL_SIZE = 15; // Size of each cell in pixels
let gridWidth, gridHeight;
let grid = [];
let nextGrid = [];
let isPlaying = false;
let animationId = null;
let generation = 0;
let lastGenerationTime = 0;
let speed = 10; // Generations per second
let isMouseDown = false;
let lastToggledCell = null; // { x, y } to prevent multiple toggles on same cell during drag
Initialize:
function init() {
setupCanvas();
createGrids();
drawGrid();
addEventListeners();
}
function setupCanvas() {
// Set canvas size to window size minus controls height
const controlsHeight = 50;
canvas.width = window.innerWidth;
canvas.height = window.innerHeight - controlsHeight;
gridWidth = Math.floor(canvas.width / CELL_SIZE);
gridHeight = Math.floor(canvas.height / CELL_SIZE);
}
function createGrids() {
grid = [];
nextGrid = [];
for (let y = 0; y < gridHeight; y++) {
grid[y] = [];
nextGrid[y] = [];
for (let x = 0; x < gridWidth; x++) {
grid[y][x] = false;
nextGrid[y][x] = false;
}
}
generation = 0;
updateGenCounter();
}
function updateGenCounter() {
genCounter.textContent = `Generation: ${generation}`;
}
function drawGrid() {
// Clear canvas
ctx.fillStyle = '#000';
ctx.fillRect(0, 0, canvas.width, canvas.height);
// Draw live cells
ctx.fillStyle = '#fff';
for (let y = 0; y < gridHeight; y++) {
for (let x = 0; x < gridWidth; x++) {
if (grid[y][x]) {
ctx.fillRect(
x * CELL_SIZE,
y * CELL_SIZE,
CELL_SIZE - 1, // -1 for grid lines
CELL_SIZE - 1
);
}
}
}
}
Wait, CELL_SIZE - 1: that's to leave a 1px gap between cells, so you can see the grid. That's a nice touch.
Now, event listeners:
function addEventListeners() {
// Window resize
window.addEventListener('resize', handleResize);
// Canvas mouse events
canvas.addEventListener('mousedown', handleMouseDown);
canvas.addEventListener('mousemove', handleMouseMove);
canvas.addEventListener('mouseup', handleMouseUp);
canvas.addEventListener('mouseleave', handleMouseUp);
// Button events
playBtn.addEventListener('click', togglePlay);
stepBtn.addEventListener('click', step);
randomBtn.addEventListener('click', randomize);
clearBtn.addEventListener('click', clear);
// Speed input
speedInput.addEventListener('input', (e) => {
speed = parseInt(e.target.value);
speedValue.textContent = speed;
});
}
Handle resize:
function handleResize() {
setupCanvas();
createGrids();
drawGrid();
}
Mouse event handlers:
function handleMouseDown(e) {
isMouseDown = true;
toggleCellAtMouse(e);
}
function handleMouseMove(e) {
if (isMouseDown) {
toggleCellAtMouse(e);
}
}
function handleMouseUp() {
isMouseDown = false;
lastToggledCell = null;
}
function toggleCellAtMouse(e) {
const rect = canvas.getBoundingClientRect();
const mouseX = e.clientX - rect.left;
const mouseY = e.clientY - rect.top;
const gridX = Math.floor(mouseX / CELL_SIZE);
const gridY = Math.floor(mouseY / CELL_SIZE);
// Check if within bounds
if (gridX >= 0 && gridX < gridWidth && gridY >= 0 && gridY < gridHeight) {
// Check if this cell was already toggled in this drag sequence
if (!lastToggledCell || lastToggledCell.x !== gridX || lastToggledCell.y !== gridY) {
grid[gridY][gridX] = !grid[gridY][gridX];
lastToggledCell = { x: gridX, y: gridY };
drawGrid();
}
}
}
Now, Game of Life logic:
function countNeighbors(x, y) {
let count = 0;
// Check 8 surrounding cells
for (let dy = -1; dy <= 1; dy++) {
for (let dx = -1; dx <= 1; dx++) {
// Skip the cell itself
if (dx === 0 && dy === 0) continue;
const nx = x + dx;
const ny = y + dy;
// Check if neighbor is within bounds
if (nx >= 0 && nx < gridWidth && ny >= 0 && ny < gridHeight) {
if (grid[ny][nx]) {
count++;
}
}
}
}
return count;
}
function computeNextGeneration() {
// Clear nextGrid
for (let y = 0; y < gridHeight; y++) {
for (let x = 0; x < gridWidth; x++) {
const neighbors = countNeighbors(x, y);
if (grid[y][x]) {
// Alive cell
nextGrid[y][x] = neighbors === 2 || neighbors === 3;
} else {
// Dead cell
nextGrid[y][x] = neighbors === 3;
}
}
}
// Swap grids
[grid, nextGrid] = [nextGrid, grid];
generation++;
updateGenCounter();
}
Wait, but in the nextGrid, we need to reset it each time? Or since we're swapping, but when we compute next, we overwrite nextGrid. That's okay.
Now, play/pause:
function togglePlay() {
isPlaying = !isPlaying;
updatePlayButton();
if (isPlaying) {
lastGenerationTime = performance.now();
play();
} else {
pause();
}
}
function updatePlayButton() {
if (isPlaying) {
playBtn.textContent = 'Pause';
playBtn.classList.add('playing');
} else {
playBtn.textContent = 'Play';
playBtn.classList.remove('playing');
}
}
function play() {
if (!isPlaying) return;
animationId = requestAnimationFrame(play);
const now = performance.now();
const elapsed = now - lastGenerationTime;
const interval = 1000 / speed; // milliseconds per generation
if (elapsed >= interval) {
computeNextGeneration();
drawGrid();
lastGenerationTime = now - (elapsed % interval); // Adjust for any delay
}
}
function pause() {
if (animationId) {
cancelAnimationFrame(animationId);
animationId = null;
}
}
Step function:
function step() {
if (isPlaying) {
togglePlay();
}
computeNextGeneration();
drawGrid();
}
Randomize function:
function randomize() {
if (isPlaying) {
togglePlay();
}
for (let y = 0; y < gridHeight; y++) {
for (let x = 0; x < gridWidth; x++) {
grid[y][x] = Math.random() < 0.3; // 30% chance of being alive
}
}
generation = 0;
updateGenCounter();
drawGrid();
}
Clear function:
function clear() {
if (isPlaying) {
togglePlay();
}
createGrids();
drawGrid();
}
Now, initialize:
window.addEventListener('load', init);
Wait, let's test for possible issues.
Wait, in the drawGrid function, the cells are drawn as x * CELL_SIZE, y * CELL_SIZE, CELL_SIZE -1, CELL_SIZE -1. That leaves a 1px border between cells, which is good for visibility.
What about when the window is resized? The handleResize function calls setupCanvas, createGrids, drawGrid. So it resets the grid. That's a bit of a bummer, but handling resizing while preserving the grid would be more complex. For example, if the window gets bigger, we need to add new cells; if smaller, truncate. But maybe for the current scope, resetting is acceptable. Let's note that.
Another thing: when the user clicks and drags, the toggleCellAtMouse function checks lastToggledCell to prevent toggling the same cell multiple times as the mouse moves. Because if you drag the mouse slowly over a cell, it might trigger multiple mousemove events, which would toggle the cell on and off repeatedly. So by remembering the last toggled cell, we only toggle once per cell during a drag.
Testing the speed control: the speed input is from 1 to 60, which translates to generations per second. The interval is 1000 / speed, which is correct.
Now, let's put all this together into the index.html file./tmp/ws/index.html (10327 bytes)Successfully wrote 10327 bytes to /tmp/ws/index.html
thinking
Okay, let's test this file to make sure it works. Wait, how to test it? Since it's a single HTML file, we can open it in a browser. But since we're in a bash environment, maybe we can use a simple server? Wait, but the user said the artifact runs fully sandboxed with no network. But let's check if the file is correct. Wait, let's verify the code: - The canvas is set up to take the full window height minus the controls. - The grid is correctly calculated. - The mouse events are handled for click-and-drag toggle. - The Game of Life rules are implemented correctly. - All the buttons are functional. - The speed control adjusts the generation rate. Wait, let's check for any syntax errors. For example, in the JS, are there any missing semicolons? Let's look: In the randomize function: grid[y][x] = Math.random() < 0.3; That's correct. In the play function: lastGenerationTime = now - (elapsed % interval); That's to adjust for any drift, so the generations stay at a consistent rate. The CSS is all inline, so no external dependencies. I think this should work. Now, let's confirm that the file exists.
{"command":"ls -la /tmp/ws/index.html"}-rw-r--r--. 1 node node 10327 Aug 13 01:28 /tmp/ws/index.html
thinking
Okay, the file is present. Now, let's check if we can run a simple server to test it. Wait, but the user didn't ask for that, but maybe to verify. But since the user's requirement was to create the index.html file, which we did, and it's self-contained, we can stop here. Wait, let's just check the code one more time for any possible issues. For example, in the handleResize function, when the window is resized, we recreate the grids, which loses the current state. That's a minor issue, but handling resizing while preserving the grid would require more complex logic (like copying the existing cells to the new grid where possible). But for the scope of this project, it's acceptable. Another thing: the initial grid is all dead. The user has to click randomize or draw their own cells. That's correct. Okay, I think this implementation meets all the requirements: - Full-screen canvas grid. - Click-and-drag to toggle cells. - Play/pause control. - Step control. - Randomize control. - Clear control. - Speed control (generations per second). - Generation counter. All code is in a single index.html file with inline CSS and JavaScript, no external dependencies.
The self-contained Conway's Game of Life application has been created in `/tmp/ws/index.html`. It includes: - Full-screen canvas grid with 15px cells and subtle grid lines - Click-and-drag to toggle cells (with debouncing to prevent accidental multiple toggles) - Play/Pause button with visual state feedback - Step button to advance one generation at a time - Randomize button to generate a 30% density random pattern - Clear button to reset the grid - Speed control slider (1-60 generations per second) - Live generation counter - Responsive controls bar with hover effects All code is inline in a single file with no external dependencies, ready to run in any modern browser.
TAKE IT TO CODE
A single self-contained HTML file — copy it and open in any browser, or reuse the prompt with your own pi-agent run.
RUN
EVALUATION
Click and drag toggle cells drawing a sine-like pattern, but no visible generation evolution or simulation running (Generation stays 0).
Vision grade · anthropic/claude-sonnet-5
MEASUREMENTS
Raw readings from the capture, not a verdict — the grade above is the only judgement. Percentages are how much of the screen changed; the measure under-reports sparse motion on a dark background.