From 482a824c202c5f5c951198679b88aa1d02ed32c5 Mon Sep 17 00:00:00 2001 From: Tamim Ehsan Date: Fri, 25 Jul 2025 11:04:46 +0600 Subject: [PATCH] feat: Add Game of Life visualization components and styles --- public/images/game-of-life.png | Bin 0 -> 1749 bytes src/app/components/algorithm-cards.jsx | 5 + src/app/game-of-life/grid.css | 10 ++ src/app/game-of-life/grid.jsx | 32 +++++ src/app/game-of-life/menu.jsx | 21 +++ src/app/game-of-life/node.css | 100 ++++++++++++++ src/app/game-of-life/node.jsx | 22 +++ src/app/game-of-life/page.jsx | 181 +++++++++++++++++++++++++ src/app/game-of-life/styles.css | 100 ++++++++++++++ 9 files changed, 471 insertions(+) create mode 100644 public/images/game-of-life.png create mode 100644 src/app/game-of-life/grid.css create mode 100644 src/app/game-of-life/grid.jsx create mode 100644 src/app/game-of-life/menu.jsx create mode 100644 src/app/game-of-life/node.css create mode 100644 src/app/game-of-life/node.jsx create mode 100644 src/app/game-of-life/page.jsx create mode 100644 src/app/game-of-life/styles.css diff --git a/public/images/game-of-life.png b/public/images/game-of-life.png new file mode 100644 index 0000000000000000000000000000000000000000..0e44910d377bbf048efad77b509c14c4c1bc2786 GIT binary patch literal 1749 zcmZuvc{tR082_QkedJb5&SG*-hFGKnYi+yZC|B4ShDMITG{`y36dId`))BE2Hbg^d zu!Bg995KdyPq}BQv?WLOH|?|k?fX98_jA1O^Ss~Z`+NZaPz}V#;!p%^AOHp*L$QG- zp;#1OuZ3dpD83Jd#+87O$bTsS;IRJq|M-6>g~Q@QzX<`bLa`y?{(N;F00h9~S3p1- z03e+p*TrJPzAgOLISdMnAb%K~K@p5i&V`4Bf7=y|#YN&k-wBKJ4+l{m8->I02f@*1 z@Q8y%28WHo@*N=&5fKn1Py~~6L|Z-u__5*8PV;`hJ+FLppc zF!caR5i7Xy`t$I$dmYbGBS_|gyr~)Wgz`JO2e{dRn9+;~4$9CbwfbQdyYSG`uPZ?a z)Zf9e4BX-2#>nT#A<(~V@`{$P8SyS-w60Nt$A)-iy>GKe*G?gw7Vh2bCXF06Bm2GX zLs2hU>6u>@P}{x@}O556QXy?8}43oG22--TXokR zCPfo^??sN0FWsSRUoGy`NQyS-gAQ+FW_lFi*_fUdhC>=&EkoB1o%0eM#8QuIm=B)% zcu6kSU*Ze(u0(#}0O=28rT=Zik}Q~S@SLJ#A8%262%YWH6r8pUsn(+gkKOOpjG|1O zx!F}_@j>>z9M5L3eg0UJXS2yMs@93m2+HGLg22GN$2FgBM8}%uq&1DJ43=p*ylvC< zler(8d;1k6N7RyfP2YI&DB)!i#hPUNqZLKpVBWX0o?AX?8FI>PF^TRe@PGg2fuZHqZS)WBIrHL8{jR!riO3M)Q7zO|Zxy zey)6kC00vxdq>0_4}i+ocUC5CK|6TG^mj2Unp-V>@aiL5>jmdL72U&$$jZr^26vp? z4t}s%6MW%4;sUcg6x4Y-%wP4VqU6N2qmL7+?403e4O~(Tmv+mLA0k-&Xj40>JROD( zi9QKi)l)io*()aAESEq`gtxk@9MZk&fdugH3NQ-S*LOOoLtYx+yH25$F1cBZuXk>= z!Qf(Z7u|Bs=%~SmT&6jsMfeKOt`EU9?i{ftevZOy+i*Cm%o-AcF7AHXN)bIJoJO2x z9mKUcW+Qpi>ZuDa<2j2o^bZJqWt^{E2}4qALc4`>%tKS!^J#TKX1zMw)h`ig>_89g zPUvLjQ$%9VYEA7LnlPhiX0P*MwVVqX`MDGE&~AcCffX~k;ydK-0V7Un7g2xR!Ppbh OeK@hVUip?hbLJnQiWa8; literal 0 HcmV?d00001 diff --git a/src/app/components/algorithm-cards.jsx b/src/app/components/algorithm-cards.jsx index 9ac12ac..87647eb 100644 --- a/src/app/components/algorithm-cards.jsx +++ b/src/app/components/algorithm-cards.jsx @@ -58,6 +58,11 @@ const algorithms = [ title: 'Binary Search', description: "Binary search is an efficient algorithm for finding an item from a sorted list of item", image: '/AlgorithmVisualizer/images/binary-search.png?height=200&width=300' + },{ + id: 'game-of-life', + title: 'Game of Life', + description: "Visualize the Game of Life cellular automaton", + image: '/AlgorithmVisualizer/images/game-of-life.png?height=200&width=300' }, // { // id: '15-puzzle', diff --git a/src/app/game-of-life/grid.css b/src/app/game-of-life/grid.css new file mode 100644 index 0000000..cdd1145 --- /dev/null +++ b/src/app/game-of-life/grid.css @@ -0,0 +1,10 @@ + +.Grid{ + font-size: 0; +} +div{ + padding: 0px; + margin: 0px; + margin-bottom: 0px; + padding-bottom: 0px; +} \ No newline at end of file diff --git a/src/app/game-of-life/grid.jsx b/src/app/game-of-life/grid.jsx new file mode 100644 index 0000000..e058a37 --- /dev/null +++ b/src/app/game-of-life/grid.jsx @@ -0,0 +1,32 @@ + +import './grid.css'; +import Node from "./node"; + + +export default function Grid({ grid, onMouseDown, onMouseEnter, onMouseUp }) { + return ( +
+ {grid.map((row, rowidx) => { + return ( +
+ {row.map((node, nodeidx) => { + const { row, col, isAlive } = node; + return ( + + ); + })} +
+ ); + })} +
+ ); +} \ No newline at end of file diff --git a/src/app/game-of-life/menu.jsx b/src/app/game-of-life/menu.jsx new file mode 100644 index 0000000..348a04d --- /dev/null +++ b/src/app/game-of-life/menu.jsx @@ -0,0 +1,21 @@ +import { Button } from '@/components/ui/button'; +import PropTypes from 'prop-types'; + +export default function Menu({ onStart, onStop, onClear }) { + return ( +
+

Settings

+ + {/* */} + + + +
+ ); +} + +Menu.propTypes = { + onStart: PropTypes.func.isRequired, + onStop: PropTypes.func.isRequired, + onClear: PropTypes.func.isRequired, +}; \ No newline at end of file diff --git a/src/app/game-of-life/node.css b/src/app/game-of-life/node.css new file mode 100644 index 0000000..f915cfe --- /dev/null +++ b/src/app/game-of-life/node.css @@ -0,0 +1,100 @@ +.node{ + height:25px; + width:25px; + background-color: white; + outline:1px solid rgb(175, 216, 248); + display: inline-block; +} + +.node-start{ + background-color: chartreuse; +} +.node-end{ + background-color: brown; +} +.node-visited{ + animation-name: visitedAnimation; + animation-iteration-count: 1; + animation-duration: 1.5s; + animation-delay: 0; + background-color: rgba(0, 190, 218, 0.75); +} + +@keyframes visitedAnimation { + 0% { + transform: scale(0.3); + background-color: rgba(0, 0, 66, 0.75); + border-radius: 100%; + } + + 50% { + background-color: rgba(17, 104, 217, 0.75); + } + + 75% { + transform: scale(1.2); + background-color: rgba(0, 217, 159, 0.75); + } + + 100% { + transform: scale(1); + background-color: rgba(0, 190, 218, 0.75); + } +} + +.node-wall { + background-color: black; + outline: 1px solid black; + /* animation-name: wallAnimation; + animation-duration: 0.3s; + animation-timing-function: ease-out; + animation-delay: 0; + animation-direction: alternate; + animation-iteration-count: 1; + animation-fill-mode: forwards; + animation-play-state: running; */ +} +@keyframes wallAnimation { + 0% { + transform: scale(.3); + background-color: rgb(12, 53, 71); + } + + 50% { + transform: scale(1.2); + background-color: rgb(12, 53, 71); + } + + 100% { + transform: scale(1.0); + background-color: rgb(12, 53, 71); + } +} + +.node-shortest-path { + animation-name: shortestPath; + animation-duration: 1.5s; + animation-timing-function: ease-out; + animation-delay: 0; + animation-direction: alternate; + animation-iteration-count: 1; + animation-fill-mode: forwards; + animation-play-state: running; +} + +@keyframes shortestPath { + 0% { + transform: scale(0.6); + background-color: rgb(255, 254, 106); + } + + 50% { + transform: scale(1.2); + background-color: rgb(255, 254, 106); + } + + 100% { + transform: scale(1); + background-color: rgb(255, 254, 106); + } +} \ No newline at end of file diff --git a/src/app/game-of-life/node.jsx b/src/app/game-of-life/node.jsx new file mode 100644 index 0000000..7548011 --- /dev/null +++ b/src/app/game-of-life/node.jsx @@ -0,0 +1,22 @@ + +import "./node.css"; + +export default function Node({ node, onMouseDown, onMouseEnter, onMouseUp }) { + return ( +
onMouseDown(node.row, node.col)} + onMouseEnter={() => onMouseEnter(node.row, node.col)} + onMouseUp={() => onMouseUp(node.row, node.col)} + /> + ); + + function getClassName() { + if (node.isAlive === true) { + return "node node-wall"; + }else { + return "node"; + } + } +} \ No newline at end of file diff --git a/src/app/game-of-life/page.jsx b/src/app/game-of-life/page.jsx new file mode 100644 index 0000000..e42c22c --- /dev/null +++ b/src/app/game-of-life/page.jsx @@ -0,0 +1,181 @@ +"use client"; +import Navbar from '@/components/navbar'; +import { createRef, useRef, useState, useEffect } from 'react'; +import Menu from "./menu"; +import Grid from "./grid"; + + +export default function GameOfLifePage() { + + let gridRef = createRef(); + + const [grid, setGrid] = useState([]); + const [running, setRunning] = useState(false); + const runningRef = useRef(false); // Add this ref + + + useEffect(() => { + const width = gridRef.current.offsetWidth; + const height = gridRef.current.offsetHeight; + const row = Math.max(Math.floor(height / 25) - 2, 10); + const col = Math.floor(width / 25); + setGrid(getInitialGrid(row, col)); + }, []); + + const handleMouseDown = (row, col) => { + + const newGrid = getNewGridWithWallToggled(grid, row, col); + setGrid(newGrid); + + // this.setState({ mouseIsPressed: true }); + } + + const handleMouseEnter = (row, col) => { + // if (this.state.mouseIsPressed === false) return; + // if ((this.state.startNode.row !== row || this.state.startNode.col !== col) && (this.state.endNode.row !== row || this.state.endNode.col !== col)) { + // const newGrid = getNewGridWithWallToggled(this.state.grid, row, col); + // this.setState({ grid: newGrid }); + // } + } + + const handleMouseUp = (row, col) => { + // this.setState({ mouseIsPressed: false }); + } + + const handleStart = () => { + setRunning(true); + runningRef.current = true; // Update ref + + gameOfLife(); + } + + const handleStop = () => { + setRunning(false); + runningRef.current = false; + console.log("Simulation stopped"); + } + + const handleClearBoard = () => { + setRunning(false); + runningRef.current = false; + const width = gridRef.current.offsetWidth; + const height = gridRef.current.offsetHeight; + const row = Math.max(Math.floor(height / 25) - 2, 10); + const col = Math.floor(width / 25); + setGrid(getInitialGrid(row, col)); + } + + const gameOfLife = async () => { + let newGrid = getNextGeneration(grid); + while (runningRef.current) { + setGrid(newGrid); + newGrid = getNextGeneration(newGrid); + await sleep(200); + } + } + + return ( +
+ + + +
+ + +
+
+ +
+
+
+
+ ); +} + +const getInitialGrid = (row, col) => { + let grid = []; + for (let i = 0; i < row; i++) { + let row = []; + for (let j = 0; j < col; j++) { + row.push(createNode(i, j)); + } + grid.push(row); + } + return grid; +} + +const createNode = (row, col) => { + return { + row, + col, + isAlive: false + } +} + +const getNewGridWithWallToggled = (grid, row, col) => { + const newGrid = grid.slice(); + const node = newGrid[row][col]; + + const newNode = { + ...node, + isAlive: !node.isAlive, + }; + + newGrid[row][col] = newNode; + return newGrid; +}; + +const getNextGeneration = (grid) => { + const newGrid = grid.slice(); + for (let i = 0; i < grid.length; i++) { + newGrid[i] = grid[i].slice(); + for (let j = 0; j < grid[i].length; j++) { + const node = grid[i][j]; + const aliveNeighbors = getAliveNeighbors(grid, node); + + if (node.isAlive && (aliveNeighbors < 2 || aliveNeighbors > 3)) { + newGrid[i][j] = { + ...node, + isAlive: false + } + } + if (!node.isAlive && aliveNeighbors === 3) { + newGrid[i][j] = { + ...node, + isAlive: true + } + } + } + } + return newGrid; +} + +const getAliveNeighbors = (grid, node) => { + + const { row, col } = node; + const dirx = [-1, 1, 0, 0, -1, -1, 1, 1]; + const diry = [0, 0, -1, 1, -1, 1, -1, 1]; + let count = 0; + for (let i = 0; i < 8; i++) { + const newRow = row + dirx[i]; + const newCol = col + diry[i]; + if (newRow >= 0 && newRow < grid.length && newCol >= 0 && newCol < grid[0].length && grid[newRow][newCol].isAlive) { + count++; + } + } + + return count; +} + +function sleep(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); +} \ No newline at end of file diff --git a/src/app/game-of-life/styles.css b/src/app/game-of-life/styles.css new file mode 100644 index 0000000..080607a --- /dev/null +++ b/src/app/game-of-life/styles.css @@ -0,0 +1,100 @@ +.node{ + height:25px; + width:25px; + background-color: white; + outline:1px solid rgb(175, 216, 248); + display: inline-block; +} + +.node-start{ + background-color: chartreuse; +} +.node-end{ + background-color: brown; +} +.node-visited{ + animation-name: visitedAnimation; + animation-iteration-count: 1; + animation-duration: 1.5s; + animation-delay: 0; + background-color: rgba(0, 190, 218, 0.75); +} + +@keyframes visitedAnimation { + 0% { + transform: scale(0.3); + background-color: rgba(0, 0, 66, 0.75); + border-radius: 100%; + } + + 50% { + background-color: rgba(17, 104, 217, 0.75); + } + + 75% { + transform: scale(1.2); + background-color: rgba(0, 217, 159, 0.75); + } + + 100% { + transform: scale(1); + background-color: rgba(0, 190, 218, 0.75); + } +} + +.node-alive { + background-color: black; + outline: 1px solid black; + animation-name: wallAnimation; + animation-duration: 0.3s; + animation-timing-function: ease-out; + animation-delay: 0; + animation-direction: alternate; + animation-iteration-count: 1; + animation-fill-mode: forwards; + animation-play-state: running; +} +@keyframes wallAnimation { + 0% { + transform: scale(.3); + background-color: rgb(12, 53, 71); + } + + 50% { + transform: scale(1.2); + background-color: rgb(12, 53, 71); + } + + 100% { + transform: scale(1.0); + background-color: rgb(12, 53, 71); + } +} + +.node-shortest-path { + animation-name: shortestPath; + animation-duration: 1.5s; + animation-timing-function: ease-out; + animation-delay: 0; + animation-direction: alternate; + animation-iteration-count: 1; + animation-fill-mode: forwards; + animation-play-state: running; +} + +@keyframes shortestPath { + 0% { + transform: scale(0.6); + background-color: rgb(255, 254, 106); + } + + 50% { + transform: scale(1.2); + background-color: rgb(255, 254, 106); + } + + 100% { + transform: scale(1); + background-color: rgb(255, 254, 106); + } +} \ No newline at end of file