Skip to content

Commit 2498e80

Browse files
author
sharma.ankit2
committed
feat(games): add Evolution Simulator GA game + expand battles
- Add new Evolution Simulator game with deterministic genetic algorithm engine - Implement modular GA pipeline: fitness, selection, crossover, mutation, simulation - Add full controls: target, population, mutation/crossover, max generations, strategy, speed - Add live stats, top-10 population view, gene comparison, and fitness chart - Integrate route and Games/Landing page entries - Expand Pathfinding Battles with maze type options - Expand Recursion Battles with multiple problem types beyond Fibonacci
1 parent 888d673 commit 2498e80

28 files changed

Lines changed: 1286 additions & 46 deletions

docs/assets/index-1nLZOlYR.js

Lines changed: 125 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/assets/index-DJW7OjyY.css

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/assets/index-K0InbwRs.css

Lines changed: 0 additions & 1 deletion
This file was deleted.

docs/index.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55
<link rel="icon" type="image/svg+xml" href="/algorithm-visualizer/vite.svg" />
66
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
77
<title>Algorithm Visualizer</title>
8-
<script type="module" crossorigin src="/algorithm-visualizer/assets/index-CoN-dU1W.js"></script>
9-
<link rel="stylesheet" crossorigin href="/algorithm-visualizer/assets/index-K0InbwRs.css">
8+
<script type="module" crossorigin src="/algorithm-visualizer/assets/index-1nLZOlYR.js"></script>
9+
<link rel="stylesheet" crossorigin href="/algorithm-visualizer/assets/index-DJW7OjyY.css">
1010
</head>
1111
<body>
1212
<div id="root"></div>

src/App.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import FibonacciGamePage from '@/features/games/fibonacci/FibonacciGamePage';
1818
import DijkstraGamePage from '@/features/games/dijkstra/DijkstraGamePage';
1919
import BattlePage from '@/features/games/battles/BattlePage';
2020
import MahjongGamePage from '@/features/games/mahjong/MahjongGamePage';
21+
import EvolutionSimulatorPage from '@/features/games/evolution-simulator/EvolutionSimulatorPage';
2122

2223
function App() {
2324
return (
@@ -43,6 +44,7 @@ function App() {
4344
<Route path="/games/dijkstra" element={<DijkstraGamePage />} />
4445
<Route path="/games/battles" element={<BattlePage />} />
4546
<Route path="/games/mahjong" element={<MahjongGamePage />} />
47+
<Route path="/games/evolution-simulator" element={<EvolutionSimulatorPage />} />
4648
</Routes>
4749
</div>
4850
</BrowserRouter>

src/features/games/GamesPage.tsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Link, useLocation } from 'react-router-dom';
2-
import { Puzzle, ArrowRight, Swords, Grid3X3, Layers } from 'lucide-react';
2+
import { Puzzle, ArrowRight, Swords, Grid3X3, Layers, Dna } from 'lucide-react';
33

44
const games = [
55
{
@@ -52,6 +52,16 @@ const games = [
5252
shadowColor: 'shadow-red-500/20',
5353
icon: Layers,
5454
},
55+
{
56+
path: '/games/evolution-simulator',
57+
title: 'Evolution Simulator',
58+
subtitle: 'Genetic Algorithm playground',
59+
description: 'Evolve random strings toward a target phrase with deterministic genetic operations. Tune mutation, crossover, and selection strategy to study convergence behavior generation by generation.',
60+
skills: ['Genetic Algorithms', 'Selection', 'Crossover', 'Mutation'],
61+
gradient: 'from-lime-500 to-emerald-500',
62+
shadowColor: 'shadow-lime-500/20',
63+
icon: Dna,
64+
},
5565
];
5666

5767
export default function GamesPage() {

src/features/games/battles/BattlePage.tsx

Lines changed: 48 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,15 @@ import { useBattle } from './hooks/useBattle';
88
import { usePathfindingBattle } from './hooks/usePathfindingBattle';
99
import { useRecursionBattle } from './hooks/useRecursionBattle';
1010
import type { BattleCategory } from './types/battle';
11-
import { PF_ALGORITHM_OPTIONS } from './engine/pathfindingEngine';
12-
import { REC_ALGORITHM_OPTIONS } from './engine/recursionEngine';
11+
import { PF_ALGORITHM_OPTIONS, MAZE_OPTIONS } from './engine/pathfindingEngine';
12+
import { REC_ALGORITHM_OPTIONS, PROBLEM_OPTIONS } from './engine/recursionEngine';
1313
import type { GameMode } from './types/battle';
1414
import { SPEED_PRESETS } from './types/battle';
1515

1616
const categories: { value: BattleCategory; label: string; desc: string; icon: typeof BarChart3 }[] = [
1717
{ value: 'sorting', label: 'Sorting', desc: 'Bubble, Quick, Merge & more', icon: BarChart3 },
1818
{ value: 'pathfinding', label: 'Pathfinding', desc: 'BFS, DFS, Dijkstra, A*', icon: Map },
19-
{ value: 'recursion', label: 'Recursion', desc: 'Naive vs Memoized Fibonacci', icon: GitBranch },
19+
{ value: 'recursion', label: 'Recursion', desc: 'Fibonacci, Factorial, Staircase & more', icon: GitBranch },
2020
];
2121

2222
function CategoryPicker({ onSelect }: { onSelect: (c: BattleCategory) => void }) {
@@ -90,6 +90,21 @@ function PathfindingSetup({ battle }: { battle: ReturnType<typeof usePathfinding
9090
</div>
9191
))}
9292
</div>
93+
{/* Maze Type */}
94+
<div className="rounded-xl border border-slate-700/50 bg-slate-900/60 p-4 backdrop-blur-sm">
95+
<h3 className="text-xs font-semibold uppercase tracking-wider text-slate-400 mb-3">Maze Type</h3>
96+
<div className="grid grid-cols-3 gap-1.5">
97+
{MAZE_OPTIONS.map((opt) => (
98+
<button key={opt.value}
99+
onClick={() => battle.setMazeType(opt.value)}
100+
className={`rounded-lg px-2 py-2 text-[11px] font-medium transition-all ${
101+
battle.mazeType === opt.value
102+
? 'bg-emerald-500/20 text-emerald-300 ring-1 ring-emerald-500/40'
103+
: 'bg-slate-800/50 text-slate-400 hover:bg-slate-700/50'
104+
}`}>{opt.label}</button>
105+
))}
106+
</div>
107+
</div>
93108
<GameModeAndSpeed gameMode={battle.gameMode} setGameMode={battle.setGameMode} speed={battle.speed} setSpeed={battle.setSpeed}
94109
soundEnabled={battle.soundEnabled} toggleSound={battle.toggleSound} prediction={battle.prediction} setPrediction={battle.setPrediction}
95110
nameA={nameA} nameB={nameB} />
@@ -108,6 +123,7 @@ function PathfindingSetup({ battle }: { battle: ReturnType<typeof usePathfinding
108123
function RecursionSetup({ battle }: { battle: ReturnType<typeof useRecursionBattle> }) {
109124
const nameA = REC_ALGORITHM_OPTIONS.find((o) => o.value === battle.algorithmA)!.label;
110125
const nameB = REC_ALGORITHM_OPTIONS.find((o) => o.value === battle.algorithmB)!.label;
126+
const problemOpt = PROBLEM_OPTIONS.find((o) => o.value === battle.problem)!;
111127

112128
return (
113129
<div className="flex-1 overflow-y-auto">
@@ -117,10 +133,28 @@ function RecursionSetup({ battle }: { battle: ReturnType<typeof useRecursionBatt
117133
{' '}vs{' '}
118134
<span className="bg-gradient-to-r from-cyan-400 to-blue-400 bg-clip-text text-transparent">{nameB}</span>
119135
</h2>
120-
<p className="text-xs text-slate-500 mt-1">Compare Fibonacci implementations</p>
136+
<p className="text-xs text-slate-500 mt-1">{problemOpt.desc}</p>
121137
</section>
122138
<section className="px-4 pb-8">
123139
<div className="mx-auto max-w-2xl space-y-4">
140+
{/* Problem Type */}
141+
<div className="rounded-xl border border-slate-700/50 bg-slate-900/60 p-4 backdrop-blur-sm">
142+
<h3 className="text-xs font-semibold uppercase tracking-wider text-slate-400 mb-3">Problem</h3>
143+
<div className="grid grid-cols-2 sm:grid-cols-4 gap-1.5">
144+
{PROBLEM_OPTIONS.map((opt) => (
145+
<button key={opt.value}
146+
onClick={() => battle.changeProblem(opt.value)}
147+
className={`rounded-lg px-2 py-2 text-[11px] font-medium transition-all ${
148+
battle.problem === opt.value
149+
? 'bg-amber-500/20 text-amber-300 ring-1 ring-amber-500/40'
150+
: 'bg-slate-800/50 text-slate-400 hover:bg-slate-700/50'
151+
}`}>
152+
<span className="block font-semibold">{opt.label}</span>
153+
<span className="block text-[9px] text-slate-500 mt-0.5">{opt.desc}</span>
154+
</button>
155+
))}
156+
</div>
157+
</div>
124158
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
125159
{(['A', 'B'] as const).map((side) => (
126160
<div key={side} className="rounded-xl border border-slate-700/50 bg-slate-900/60 p-4 backdrop-blur-sm">
@@ -142,13 +176,19 @@ function RecursionSetup({ battle }: { battle: ReturnType<typeof useRecursionBatt
142176
<div className="rounded-xl border border-slate-700/50 bg-slate-900/60 p-4 backdrop-blur-sm">
143177
<h3 className="text-xs font-semibold uppercase tracking-wider text-slate-400 mb-3">Input</h3>
144178
<div className="flex items-center justify-between mb-1">
145-
<span className="text-xs text-slate-500">Fibonacci N</span>
179+
<span className="text-xs text-slate-500">{problemOpt.inputLabel}</span>
146180
<span className="text-sm font-mono font-bold text-indigo-300">{battle.inputN}</span>
147181
</div>
148-
<input type="range" min={5} max={25} value={battle.inputN}
182+
<input type="range" min={problemOpt.inputMin} max={problemOpt.inputMax} value={battle.inputN}
149183
onChange={(e) => battle.setInputN(Number(e.target.value))}
150184
className="w-full accent-indigo-500" />
151-
<p className="text-[10px] text-slate-500 mt-1">⚠️ Naive recursive is exponential — N &gt; 20 generates many steps</p>
185+
<p className="text-[10px] text-slate-500 mt-1">
186+
{battle.problem === 'fibonacci' || battle.problem === 'staircase'
187+
? '⚠️ Naive recursive is exponential — large N generates many steps'
188+
: battle.problem === 'coin-change'
189+
? '⚠️ Naive coin change branches 3 ways per call — grows very fast'
190+
: '⚠️ Factorial is linear — naive and memoized are similar here'}
191+
</p>
152192
</div>
153193
<GameModeAndSpeed gameMode={battle.gameMode} setGameMode={battle.setGameMode} speed={battle.speed} setSpeed={battle.setSpeed}
154194
soundEnabled={battle.soundEnabled} toggleSound={battle.toggleSound} prediction={battle.prediction} setPrediction={battle.setPrediction}
@@ -306,6 +346,7 @@ export default function BattlePage() {
306346
winner={recursion.winner} prediction={recursion.prediction}
307347
predictionCorrect={recursion.predictionCorrect}
308348
speed={recursion.speed} soundEnabled={recursion.soundEnabled}
349+
problem={recursion.problem}
309350
onPause={recursion.pause} onResume={recursion.resume} onReset={recursion.reset}
310351
onSetSpeed={recursion.setSpeed} onToggleSound={recursion.toggleSound}
311352
/></>;

src/features/games/battles/components/RecursionArena.tsx

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,16 @@
11
import { Pause, Play, RotateCcw, Volume2, VolumeX, Trophy, Equal } from 'lucide-react';
22
import type { RecursionAlgoState } from '../hooks/useRecursionBattle';
33
import { REC_ALGORITHM_OPTIONS, REC_COMPLEXITY } from '../engine/recursionEngine';
4+
import type { RecursionProblem } from '../engine/recursionEngine';
45
import { SPEED_PRESETS } from '../types/battle';
56

7+
const FUNC_NAMES: Record<RecursionProblem, string> = {
8+
fibonacci: 'fib',
9+
factorial: 'fact',
10+
staircase: 'climb',
11+
'coin-change': 'coins',
12+
};
13+
614
interface RecursionArenaProps {
715
stateA: RecursionAlgoState;
816
stateB: RecursionAlgoState;
@@ -12,14 +20,15 @@ interface RecursionArenaProps {
1220
predictionCorrect: boolean | null;
1321
speed: number;
1422
soundEnabled: boolean;
23+
problem?: RecursionProblem;
1524
onPause: () => void;
1625
onResume: () => void;
1726
onReset: () => void;
1827
onSetSpeed: (s: number) => void;
1928
onToggleSound: (v: boolean) => void;
2029
}
2130

22-
function RecursionPanel({ state, color }: { state: RecursionAlgoState; color: 'rose' | 'cyan' }) {
31+
function RecursionPanel({ state, color, funcName }: { state: RecursionAlgoState; color: 'rose' | 'cyan'; funcName: string }) {
2332
const name = REC_ALGORITHM_OPTIONS.find((o) => o.value === state.algorithm)!.label;
2433
const step = state.currentIndex < state.steps.length ? state.steps[state.currentIndex] : null;
2534
const maxSteps = state.steps.length;
@@ -70,7 +79,7 @@ function RecursionPanel({ state, color }: { state: RecursionAlgoState; color: 'r
7079
{/* Current state */}
7180
{step && (
7281
<div className="flex items-center gap-2 text-[10px] text-slate-400 font-mono">
73-
<span>fib({step.currentN})</span>
82+
<span>{funcName}({step.currentN})</span>
7483
{step.result !== null && <span className="text-emerald-400">= {step.result}</span>}
7584
</div>
7685
)}
@@ -104,6 +113,7 @@ export default function RecursionArena({
104113
predictionCorrect,
105114
speed,
106115
soundEnabled,
116+
problem = 'fibonacci',
107117
onPause,
108118
onResume,
109119
onReset,
@@ -113,6 +123,7 @@ export default function RecursionArena({
113123
const isRunning = status === 'running';
114124
const nameA = REC_ALGORITHM_OPTIONS.find((o) => o.value === stateA.algorithm)!.label;
115125
const nameB = REC_ALGORITHM_OPTIONS.find((o) => o.value === stateB.algorithm)!.label;
126+
const funcName = FUNC_NAMES[problem];
116127

117128
return (
118129
<div className="flex-1 overflow-y-auto p-4 md:p-6">
@@ -151,8 +162,8 @@ export default function RecursionArena({
151162

152163
{/* Dual panels */}
153164
<div className="flex flex-col sm:flex-row gap-4">
154-
<RecursionPanel state={stateA} color="rose" />
155-
<RecursionPanel state={stateB} color="cyan" />
165+
<RecursionPanel state={stateA} color="rose" funcName={funcName} />
166+
<RecursionPanel state={stateB} color="cyan" funcName={funcName} />
156167
</div>
157168

158169
{/* Result */}
@@ -229,7 +240,11 @@ export default function RecursionArena({
229240
: stateB.totalCalls > stateA.totalCalls
230241
? `${nameB} made ${stateB.totalCalls} function calls compared to ${nameA}'s ${stateA.totalCalls}. `
231242
: `Both made ${stateA.totalCalls} calls. `}
232-
Naive recursion recalculates the same subproblems exponentially (O(2ⁿ)), while memoization caches results reducing to O(n). The iterative approach avoids recursion overhead entirely with O(1) space.
243+
{problem === 'factorial'
244+
? 'Factorial is inherently linear — naive and memoized perform similarly. The iterative version avoids stack overhead.'
245+
: problem === 'coin-change'
246+
? `Naive coin change explores all combinations (branching factor 3), while memoization caches sub-amounts to avoid redundant work. Iterative DP builds the table bottom-up.`
247+
: `Naive recursion recalculates the same subproblems exponentially (O(2ⁿ)), while memoization caches results reducing to O(n). The iterative approach avoids recursion overhead entirely with O(1) space.`}
233248
</p>
234249
</div>
235250

src/features/games/battles/engine/pathfindingEngine.ts

Lines changed: 99 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,13 @@ const PF_COLS = 25;
77
const WALL_DENSITY = 0.25;
88

99
export type PathAlgorithm = AlgorithmType;
10+
export type MazeType = 'random' | 'recursive-division' | 'dfs-maze';
11+
12+
export const MAZE_OPTIONS: { value: MazeType; label: string }[] = [
13+
{ value: 'random', label: 'Random Scatter' },
14+
{ value: 'recursive-division', label: 'Recursive Division' },
15+
{ value: 'dfs-maze', label: 'DFS Maze' },
16+
];
1017

1118
export interface PathfindingStep {
1219
visitedSoFar: Set<string>;
@@ -24,39 +31,115 @@ export interface PathfindingResult {
2431

2532
function key(r: number, c: number) { return `${r},${c}`; }
2633

27-
export function createBattleGrid(): { grid: GridMatrix; start: { row: number; col: number }; end: { row: number; col: number } } {
34+
function makeEmptyGrid(): GridMatrix {
2835
const grid: GridMatrix = [];
2936
for (let r = 0; r < PF_ROWS; r++) {
3037
const row: GridNode[] = [];
3138
for (let c = 0; c < PF_COLS; c++) {
3239
row.push({
33-
row: r,
34-
col: c,
35-
type: NodeType.EMPTY,
36-
distance: Infinity,
37-
heuristic: 0,
38-
totalCost: Infinity,
39-
parent: null,
40-
isVisited: false,
40+
row: r, col: c, type: NodeType.EMPTY,
41+
distance: Infinity, heuristic: 0, totalCost: Infinity,
42+
parent: null, isVisited: false,
4143
});
4244
}
4345
grid.push(row);
4446
}
47+
return grid;
48+
}
4549

46-
const start = { row: Math.floor(PF_ROWS / 2), col: 1 };
47-
const end = { row: Math.floor(PF_ROWS / 2), col: PF_COLS - 2 };
48-
grid[start.row][start.col].type = NodeType.START;
49-
grid[end.row][end.col].type = NodeType.END;
50-
50+
function randomScatter(grid: GridMatrix, start: { row: number; col: number }, end: { row: number; col: number }) {
5151
for (let r = 0; r < PF_ROWS; r++) {
5252
for (let c = 0; c < PF_COLS; c++) {
5353
if ((r === start.row && c === start.col) || (r === end.row && c === end.col)) continue;
54-
if (Math.random() < WALL_DENSITY) {
55-
grid[r][c].type = NodeType.WALL;
54+
if (Math.random() < WALL_DENSITY) grid[r][c].type = NodeType.WALL;
55+
}
56+
}
57+
}
58+
59+
function recursiveDivision(grid: GridMatrix, start: { row: number; col: number }, end: { row: number; col: number }) {
60+
const isReserved = (r: number, c: number) =>
61+
(r === start.row && c === start.col) || (r === end.row && c === end.col);
62+
63+
function divide(rStart: number, rEnd: number, cStart: number, cEnd: number, horizontal: boolean) {
64+
if (horizontal) {
65+
if (rEnd - rStart < 2) return;
66+
const possibleRows: number[] = [];
67+
for (let r = rStart + 1; r < rEnd; r += 1) possibleRows.push(r);
68+
if (possibleRows.length === 0) return;
69+
const wallRow = possibleRows[Math.floor(Math.random() * possibleRows.length)];
70+
const passCol = cStart + Math.floor(Math.random() * (cEnd - cStart + 1));
71+
for (let c = cStart; c <= cEnd; c++) {
72+
if (c === passCol || isReserved(wallRow, c)) continue;
73+
grid[wallRow][c].type = NodeType.WALL;
5674
}
75+
divide(rStart, wallRow - 1, cStart, cEnd, !horizontal);
76+
divide(wallRow + 1, rEnd, cStart, cEnd, !horizontal);
77+
} else {
78+
if (cEnd - cStart < 2) return;
79+
const possibleCols: number[] = [];
80+
for (let c = cStart + 1; c < cEnd; c += 1) possibleCols.push(c);
81+
if (possibleCols.length === 0) return;
82+
const wallCol = possibleCols[Math.floor(Math.random() * possibleCols.length)];
83+
const passRow = rStart + Math.floor(Math.random() * (rEnd - rStart + 1));
84+
for (let r = rStart; r <= rEnd; r++) {
85+
if (r === passRow || isReserved(r, wallCol)) continue;
86+
grid[r][wallCol].type = NodeType.WALL;
87+
}
88+
divide(rStart, rEnd, cStart, wallCol - 1, !horizontal);
89+
divide(rStart, rEnd, wallCol + 1, cEnd, !horizontal);
5790
}
5891
}
5992

93+
divide(0, PF_ROWS - 1, 0, PF_COLS - 1, Math.random() > 0.5);
94+
}
95+
96+
function dfsMaze(grid: GridMatrix, start: { row: number; col: number }, end: { row: number; col: number }) {
97+
// Fill everything with walls, then carve passages with DFS
98+
for (let r = 0; r < PF_ROWS; r++)
99+
for (let c = 0; c < PF_COLS; c++)
100+
grid[r][c].type = NodeType.WALL;
101+
102+
const visited = new Set<string>();
103+
function carve(r: number, c: number) {
104+
visited.add(`${r},${c}`);
105+
grid[r][c].type = NodeType.EMPTY;
106+
const dirs = [[0, 2], [0, -2], [2, 0], [-2, 0]].sort(() => Math.random() - 0.5);
107+
for (const [dr, dc] of dirs) {
108+
const nr = r + dr;
109+
const nc = c + dc;
110+
if (nr >= 0 && nr < PF_ROWS && nc >= 0 && nc < PF_COLS && !visited.has(`${nr},${nc}`)) {
111+
// Carve the wall between current and next
112+
grid[r + dr / 2][c + dc / 2].type = NodeType.EMPTY;
113+
carve(nr, nc);
114+
}
115+
}
116+
}
117+
118+
// Start carving from an odd cell
119+
const sr = start.row % 2 === 0 ? start.row + 1 : start.row;
120+
const sc = start.col % 2 === 0 ? start.col + 1 : start.col;
121+
carve(Math.min(sr, PF_ROWS - 1), Math.min(sc, PF_COLS - 1));
122+
123+
// Ensure start and end cells + their neighbors are open
124+
grid[start.row][start.col].type = NodeType.EMPTY;
125+
grid[end.row][end.col].type = NodeType.EMPTY;
126+
if (start.col + 1 < PF_COLS) grid[start.row][start.col + 1].type = NodeType.EMPTY;
127+
if (end.col - 1 >= 0) grid[end.row][end.col - 1].type = NodeType.EMPTY;
128+
}
129+
130+
export function createBattleGrid(mazeType: MazeType = 'random'): { grid: GridMatrix; start: { row: number; col: number }; end: { row: number; col: number } } {
131+
const grid = makeEmptyGrid();
132+
const start = { row: Math.floor(PF_ROWS / 2), col: 1 };
133+
const end = { row: Math.floor(PF_ROWS / 2), col: PF_COLS - 2 };
134+
135+
switch (mazeType) {
136+
case 'recursive-division': recursiveDivision(grid, start, end); break;
137+
case 'dfs-maze': dfsMaze(grid, start, end); break;
138+
default: randomScatter(grid, start, end); break;
139+
}
140+
141+
grid[start.row][start.col].type = NodeType.START;
142+
grid[end.row][end.col].type = NodeType.END;
60143
return { grid, start, end };
61144
}
62145

0 commit comments

Comments
 (0)