diff --git a/public/images/game-of-life.png b/public/images/game-of-life.png new file mode 100644 index 0000000..0e44910 Binary files /dev/null and b/public/images/game-of-life.png differ 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