Skip to content

Commit 482a824

Browse files
committed
feat: Add Game of Life visualization components and styles
1 parent 05bb52a commit 482a824

File tree

9 files changed

+471
-0
lines changed

9 files changed

+471
-0
lines changed

public/images/game-of-life.png

1.71 KB
Loading

src/app/components/algorithm-cards.jsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,11 @@ const algorithms = [
5858
title: 'Binary Search',
5959
description: "Binary search is an efficient algorithm for finding an item from a sorted list of item",
6060
image: '/AlgorithmVisualizer/images/binary-search.png?height=200&width=300'
61+
},{
62+
id: 'game-of-life',
63+
title: 'Game of Life',
64+
description: "Visualize the Game of Life cellular automaton",
65+
image: '/AlgorithmVisualizer/images/game-of-life.png?height=200&width=300'
6166
},
6267
// {
6368
// id: '15-puzzle',

src/app/game-of-life/grid.css

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
2+
.Grid{
3+
font-size: 0;
4+
}
5+
div{
6+
padding: 0px;
7+
margin: 0px;
8+
margin-bottom: 0px;
9+
padding-bottom: 0px;
10+
}

src/app/game-of-life/grid.jsx

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
2+
import './grid.css';
3+
import Node from "./node";
4+
5+
6+
export default function Grid({ grid, onMouseDown, onMouseEnter, onMouseUp }) {
7+
return (
8+
<div className="Grid">
9+
{grid.map((row, rowidx) => {
10+
return (
11+
<div key={rowidx}>
12+
{row.map((node, nodeidx) => {
13+
const { row, col, isAlive } = node;
14+
return (
15+
<Node
16+
key={nodeidx}
17+
row={row}
18+
col={col}
19+
node={node}
20+
isAlive={isAlive}
21+
onMouseDown={onMouseDown}
22+
onMouseEnter={onMouseEnter}
23+
onMouseUp={onMouseUp}
24+
/>
25+
);
26+
})}
27+
</div>
28+
);
29+
})}
30+
</div>
31+
);
32+
}

src/app/game-of-life/menu.jsx

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { Button } from '@/components/ui/button';
2+
import PropTypes from 'prop-types';
3+
4+
export default function Menu({ onStart, onStop, onClear }) {
5+
return (
6+
<div className="w-64 bg-gray-100 p-4 space-y-6">
7+
<h2 className="text-lg font-semibold">Settings</h2>
8+
9+
{/* <Button className="m-1" >Clear Path</Button> */}
10+
<Button className="m-1" onClick={onClear}>Clear Board</Button>
11+
<Button className="w-full" onClick={onStop} >Stop Simulation</Button>
12+
<Button className="w-full" onClick={onStart} >Start Simulation</Button>
13+
</div>
14+
);
15+
}
16+
17+
Menu.propTypes = {
18+
onStart: PropTypes.func.isRequired,
19+
onStop: PropTypes.func.isRequired,
20+
onClear: PropTypes.func.isRequired,
21+
};

src/app/game-of-life/node.css

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
.node{
2+
height:25px;
3+
width:25px;
4+
background-color: white;
5+
outline:1px solid rgb(175, 216, 248);
6+
display: inline-block;
7+
}
8+
9+
.node-start{
10+
background-color: chartreuse;
11+
}
12+
.node-end{
13+
background-color: brown;
14+
}
15+
.node-visited{
16+
animation-name: visitedAnimation;
17+
animation-iteration-count: 1;
18+
animation-duration: 1.5s;
19+
animation-delay: 0;
20+
background-color: rgba(0, 190, 218, 0.75);
21+
}
22+
23+
@keyframes visitedAnimation {
24+
0% {
25+
transform: scale(0.3);
26+
background-color: rgba(0, 0, 66, 0.75);
27+
border-radius: 100%;
28+
}
29+
30+
50% {
31+
background-color: rgba(17, 104, 217, 0.75);
32+
}
33+
34+
75% {
35+
transform: scale(1.2);
36+
background-color: rgba(0, 217, 159, 0.75);
37+
}
38+
39+
100% {
40+
transform: scale(1);
41+
background-color: rgba(0, 190, 218, 0.75);
42+
}
43+
}
44+
45+
.node-wall {
46+
background-color: black;
47+
outline: 1px solid black;
48+
/* animation-name: wallAnimation;
49+
animation-duration: 0.3s;
50+
animation-timing-function: ease-out;
51+
animation-delay: 0;
52+
animation-direction: alternate;
53+
animation-iteration-count: 1;
54+
animation-fill-mode: forwards;
55+
animation-play-state: running; */
56+
}
57+
@keyframes wallAnimation {
58+
0% {
59+
transform: scale(.3);
60+
background-color: rgb(12, 53, 71);
61+
}
62+
63+
50% {
64+
transform: scale(1.2);
65+
background-color: rgb(12, 53, 71);
66+
}
67+
68+
100% {
69+
transform: scale(1.0);
70+
background-color: rgb(12, 53, 71);
71+
}
72+
}
73+
74+
.node-shortest-path {
75+
animation-name: shortestPath;
76+
animation-duration: 1.5s;
77+
animation-timing-function: ease-out;
78+
animation-delay: 0;
79+
animation-direction: alternate;
80+
animation-iteration-count: 1;
81+
animation-fill-mode: forwards;
82+
animation-play-state: running;
83+
}
84+
85+
@keyframes shortestPath {
86+
0% {
87+
transform: scale(0.6);
88+
background-color: rgb(255, 254, 106);
89+
}
90+
91+
50% {
92+
transform: scale(1.2);
93+
background-color: rgb(255, 254, 106);
94+
}
95+
96+
100% {
97+
transform: scale(1);
98+
background-color: rgb(255, 254, 106);
99+
}
100+
}

src/app/game-of-life/node.jsx

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
2+
import "./node.css";
3+
4+
export default function Node({ node, onMouseDown, onMouseEnter, onMouseUp }) {
5+
return (
6+
<div
7+
id={`node-${node.row}-${node.col}`}
8+
className={getClassName()}
9+
onMouseDown={() => onMouseDown(node.row, node.col)}
10+
onMouseEnter={() => onMouseEnter(node.row, node.col)}
11+
onMouseUp={() => onMouseUp(node.row, node.col)}
12+
/>
13+
);
14+
15+
function getClassName() {
16+
if (node.isAlive === true) {
17+
return "node node-wall";
18+
}else {
19+
return "node";
20+
}
21+
}
22+
}

src/app/game-of-life/page.jsx

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
"use client";
2+
import Navbar from '@/components/navbar';
3+
import { createRef, useRef, useState, useEffect } from 'react';
4+
import Menu from "./menu";
5+
import Grid from "./grid";
6+
7+
8+
export default function GameOfLifePage() {
9+
10+
let gridRef = createRef();
11+
12+
const [grid, setGrid] = useState([]);
13+
const [running, setRunning] = useState(false);
14+
const runningRef = useRef(false); // Add this ref
15+
16+
17+
useEffect(() => {
18+
const width = gridRef.current.offsetWidth;
19+
const height = gridRef.current.offsetHeight;
20+
const row = Math.max(Math.floor(height / 25) - 2, 10);
21+
const col = Math.floor(width / 25);
22+
setGrid(getInitialGrid(row, col));
23+
}, []);
24+
25+
const handleMouseDown = (row, col) => {
26+
27+
const newGrid = getNewGridWithWallToggled(grid, row, col);
28+
setGrid(newGrid);
29+
30+
// this.setState({ mouseIsPressed: true });
31+
}
32+
33+
const handleMouseEnter = (row, col) => {
34+
// if (this.state.mouseIsPressed === false) return;
35+
// if ((this.state.startNode.row !== row || this.state.startNode.col !== col) && (this.state.endNode.row !== row || this.state.endNode.col !== col)) {
36+
// const newGrid = getNewGridWithWallToggled(this.state.grid, row, col);
37+
// this.setState({ grid: newGrid });
38+
// }
39+
}
40+
41+
const handleMouseUp = (row, col) => {
42+
// this.setState({ mouseIsPressed: false });
43+
}
44+
45+
const handleStart = () => {
46+
setRunning(true);
47+
runningRef.current = true; // Update ref
48+
49+
gameOfLife();
50+
}
51+
52+
const handleStop = () => {
53+
setRunning(false);
54+
runningRef.current = false;
55+
console.log("Simulation stopped");
56+
}
57+
58+
const handleClearBoard = () => {
59+
setRunning(false);
60+
runningRef.current = false;
61+
const width = gridRef.current.offsetWidth;
62+
const height = gridRef.current.offsetHeight;
63+
const row = Math.max(Math.floor(height / 25) - 2, 10);
64+
const col = Math.floor(width / 25);
65+
setGrid(getInitialGrid(row, col));
66+
}
67+
68+
const gameOfLife = async () => {
69+
let newGrid = getNextGeneration(grid);
70+
while (runningRef.current) {
71+
setGrid(newGrid);
72+
newGrid = getNextGeneration(newGrid);
73+
await sleep(200);
74+
}
75+
}
76+
77+
return (
78+
<div className="flex flex-col h-screen">
79+
80+
<Navbar title="Game of Life" />
81+
82+
<div className="flex flex-1 overflow-hidden">
83+
<Menu
84+
onStart={handleStart}
85+
onStop={handleStop}
86+
onClear={handleClearBoard}
87+
/>
88+
89+
<div className="flex flex-1 flex-col items-center justify-center overflow-auto">
90+
<div className="w-full h-full flex items-center justify-center" ref={gridRef}>
91+
<Grid
92+
grid={grid}
93+
onMouseDown={handleMouseDown}
94+
onMouseEnter={handleMouseEnter}
95+
onMouseUp={handleMouseUp}
96+
/>
97+
</div>
98+
</div>
99+
</div>
100+
</div>
101+
);
102+
}
103+
104+
const getInitialGrid = (row, col) => {
105+
let grid = [];
106+
for (let i = 0; i < row; i++) {
107+
let row = [];
108+
for (let j = 0; j < col; j++) {
109+
row.push(createNode(i, j));
110+
}
111+
grid.push(row);
112+
}
113+
return grid;
114+
}
115+
116+
const createNode = (row, col) => {
117+
return {
118+
row,
119+
col,
120+
isAlive: false
121+
}
122+
}
123+
124+
const getNewGridWithWallToggled = (grid, row, col) => {
125+
const newGrid = grid.slice();
126+
const node = newGrid[row][col];
127+
128+
const newNode = {
129+
...node,
130+
isAlive: !node.isAlive,
131+
};
132+
133+
newGrid[row][col] = newNode;
134+
return newGrid;
135+
};
136+
137+
const getNextGeneration = (grid) => {
138+
const newGrid = grid.slice();
139+
for (let i = 0; i < grid.length; i++) {
140+
newGrid[i] = grid[i].slice();
141+
for (let j = 0; j < grid[i].length; j++) {
142+
const node = grid[i][j];
143+
const aliveNeighbors = getAliveNeighbors(grid, node);
144+
145+
if (node.isAlive && (aliveNeighbors < 2 || aliveNeighbors > 3)) {
146+
newGrid[i][j] = {
147+
...node,
148+
isAlive: false
149+
}
150+
}
151+
if (!node.isAlive && aliveNeighbors === 3) {
152+
newGrid[i][j] = {
153+
...node,
154+
isAlive: true
155+
}
156+
}
157+
}
158+
}
159+
return newGrid;
160+
}
161+
162+
const getAliveNeighbors = (grid, node) => {
163+
164+
const { row, col } = node;
165+
const dirx = [-1, 1, 0, 0, -1, -1, 1, 1];
166+
const diry = [0, 0, -1, 1, -1, 1, -1, 1];
167+
let count = 0;
168+
for (let i = 0; i < 8; i++) {
169+
const newRow = row + dirx[i];
170+
const newCol = col + diry[i];
171+
if (newRow >= 0 && newRow < grid.length && newCol >= 0 && newCol < grid[0].length && grid[newRow][newCol].isAlive) {
172+
count++;
173+
}
174+
}
175+
176+
return count;
177+
}
178+
179+
function sleep(ms) {
180+
return new Promise(resolve => setTimeout(resolve, ms));
181+
}

0 commit comments

Comments
 (0)