diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 2ffe52a..994f44f 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -11,7 +11,6 @@ import { SettingsPage } from './pages/SettingsPage'; import { SortingPage } from './pages/SortingPage'; import { DPPage } from './pages/DPPage'; import { TreesPage } from './pages/TreesPage'; -import { BattlePage } from './pages/BattlePage'; import type { CatalogResponse } from './models/types'; import { api } from './services/api'; import { AudioCtx } from './context/AudioContext'; @@ -20,7 +19,7 @@ import { useSound } from './hooks/useSound'; import { fallbackCatalog } from './data/fallbackCatalog'; -type Page = 'landing' | 'sorting' | 'searching' | 'pathfinding' | 'dp' | 'trees' | 'history' | 'settings' | 'battle'; +type Page = 'landing' | 'sorting' | 'searching' | 'pathfinding' | 'dp' | 'trees' | 'history' | 'settings'; const getPageFromHash = (): Page => { const searchParams = new URLSearchParams(window.location.search); @@ -182,7 +181,6 @@ export default function App() { />
- {active === 'battle' && } {active === 'sorting' && } {active === 'searching' && } {active === 'pathfinding' && } diff --git a/frontend/src/components/HeadToHeadBattle.tsx b/frontend/src/components/HeadToHeadBattle.tsx deleted file mode 100644 index 7174972..0000000 --- a/frontend/src/components/HeadToHeadBattle.tsx +++ /dev/null @@ -1,102 +0,0 @@ -import { SortingCanvas } from './SortingCanvas'; -import type { RaceResponse, SimulationFrame } from '../models/types'; -import { useMemo } from 'react'; - -interface HeadToHeadBattleProps { - response: RaceResponse; - frameA: SimulationFrame; - frameB: SimulationFrame; - algoA: string; - algoB: string; -} - -export function HeadToHeadBattle({ response, frameA, frameB, algoA, algoB }: HeadToHeadBattleProps) { - // Calculate ops for current frame - const opsA = (frameA?.comparisons ?? 0) + (frameA?.swaps ?? 0); - const opsB = (frameB?.comparisons ?? 0) + (frameB?.swaps ?? 0); - - // Delta logic (Ops A - Ops B). Positive means A did more ops than B. - const delta = opsA - opsB; - - // To render a simple differential bar chart, we can scale it to a max value. - // We'll calculate the maximum possible delta from the entire race history to scale our bar. - const maxAbsDelta = useMemo(() => { - let max = 1; - if (!response || response.lanes.length < 2) return max; - const laneA = response.lanes[0]; - const laneB = response.lanes[1]; - const maxLen = Math.max(laneA.frames.length, laneB.frames.length); - - for (let i = 0; i < maxLen; i++) { - const fa = laneA.frames[Math.min(i, laneA.frames.length - 1)]; - const fb = laneB.frames[Math.min(i, laneB.frames.length - 1)]; - const oa = (fa?.comparisons ?? 0) + (fa?.swaps ?? 0); - const ob = (fb?.comparisons ?? 0) + (fb?.swaps ?? 0); - const d = Math.abs(oa - ob); - if (d > max) max = d; - } - return max; - }, [response]); - - const percentage = Math.min(100, (Math.abs(delta) / maxAbsDelta) * 50); // Scale 0-50% for left/right - - return ( -
- - {/* Side-by-side Visualizers */} -
-
-

{algoA}

-
- -
-
- Ops: {opsA} -
-
- -
-

{algoB}

-
- -
-
- Ops: {opsB} -
-
-
- - {/* Differential Graph Panel */} -
-

Live Operation Lead/Lag Delta

-

- (Algorithm A Ops - Algorithm B Ops) -

- -
- {/* Center line */} -
- - {/* The moving bar */} - {delta !== 0 && ( -
0 ? 'var(--rose-500)' : 'var(--emerald-500)', - left: delta > 0 ? '50%' : `calc(50% - ${percentage}%)`, - width: `${percentage}%`, - transition: 'all 0.1s linear', - }}>
- )} -
-
- {algoA} Advantage - Delta: {Math.abs(delta)} ops {delta > 0 ? `(${algoB} leading)` : (delta < 0 ? `(${algoA} leading)` : '(Tied)')} - 0 ? 'var(--rose-400)' : 'inherit' }}>{algoB} Advantage -
-
- -
- ); -} diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx index 3b884b9..0e432e3 100644 --- a/frontend/src/components/Sidebar.tsx +++ b/frontend/src/components/Sidebar.tsx @@ -1,11 +1,10 @@ import { BarChart3, Binary, GitBranch, History, Settings, ChevronLeft, ChevronRight, Zap, LayoutGrid, X, Sun, Moon, Layers, FolderTree } from 'lucide-react'; import { useAudio } from '../context/AudioContext'; -type Page = 'landing' | 'sorting' | 'searching' | 'pathfinding' | 'dp' | 'trees' | 'history' | 'settings' | 'battle'; +type Page = 'landing' | 'sorting' | 'searching' | 'pathfinding' | 'dp' | 'trees' | 'history' | 'settings'; const items = [ { id: 'landing', label: 'Overview', icon: LayoutGrid }, - { id: 'battle', label: '1v1 Battle', icon: Zap }, { id: 'sorting', label: 'Sorting Arena', icon: BarChart3 }, { id: 'searching', label: 'Search Arena', icon: Binary }, { id: 'pathfinding', label: 'Pathfinding Arena', icon: GitBranch }, diff --git a/frontend/src/pages/BattlePage.tsx b/frontend/src/pages/BattlePage.tsx deleted file mode 100644 index c88d9a6..0000000 --- a/frontend/src/pages/BattlePage.tsx +++ /dev/null @@ -1,215 +0,0 @@ -import { useEffect, useState, useRef, useMemo, useCallback } from 'react'; -import type { CatalogResponse, RaceResponse, SimulationFrame } from '../models/types'; -import { api } from '../services/api'; -import { useAudio } from '../context/AudioContext'; -import { Controls } from '../components/Controls'; -import { SelectField } from '../components/SelectField'; -import { Share2 } from 'lucide-react'; -import { usePlayback } from '../hooks/usePlayback'; -import { HeadToHeadBattle } from '../components/HeadToHeadBattle'; - -export function BattlePage({ catalog }: { catalog: CatalogResponse }) { - const { play } = useAudio(); - const [algorithms, setAlgorithms] = useState<[string, string]>(['Quick Sort', 'Bubble Sort']); - const [size, setSize] = useState(50); - const [datasetType, setDatasetType] = useState('Random'); - const [loading, setLoading] = useState(false); - const [activeResponse, setActiveResponse] = useState(null); - const [speed, setSpeed] = useState(1); - const [toastMessage, setToastMessage] = useState(null); - - const playback = usePlayback(activeResponse, speed); - const hasStartedPlaybackRef = useRef(false); - const winnerAnnouncedRef = useRef(false); - const requestIdRef = useRef(0); - - const fetchSimulation = useCallback( - async ( - newDataset: boolean, - autoplay = false, - customParams?: { algos?: [string, string]; dType?: string; sz?: number } - ) => { - const requestId = ++requestIdRef.current; - setLoading(true); - winnerAnnouncedRef.current = false; - if (autoplay) { - hasStartedPlaybackRef.current = true; - } else { - hasStartedPlaybackRef.current = false; - } - - const useAlgos = customParams?.algos ?? algorithms; - const useType = customParams?.dType ?? datasetType; - const useSize = customParams?.sz ?? size; - - try { - const data = await api.sorting({ - algorithms: useAlgos, - datasetType: useType, - size: useSize, - newDataset, - }); - - if (requestId !== requestIdRef.current) return; - setActiveResponse(data); - const maxFrames = Math.max(...data.lanes.map((l) => l.frames.length), 0); - playback.reset(); - - if (autoplay) { - playback.setPlaying(true); - } - } catch (error: any) { - if (requestId === requestIdRef.current) { - console.error("Battle simulation error:", error); - } - } finally { - if (requestId === requestIdRef.current) setLoading(false); - } - }, - [algorithms, datasetType, size, playback] - ); - - useEffect(() => { - if (catalog.sortingAlgorithms.length >= 2) { - if (!algorithms[0] || !algorithms[1]) { - setAlgorithms([catalog.sortingAlgorithms[0], catalog.sortingAlgorithms[1]]); - } - fetchSimulation(true, false, { - algos: algorithms[0] && algorithms[1] ? algorithms : [catalog.sortingAlgorithms[0], catalog.sortingAlgorithms[1]] as [string, string] - }); - } - }, [catalog.sortingAlgorithms]); - - async function startRace() { - play('start'); - if (!activeResponse || (playback.frameIndex >= playback.maxFrames - 1 && playback.maxFrames > 0)) { - await fetchSimulation(true, true); - } else { - playback.setPlaying(true); - hasStartedPlaybackRef.current = true; - } - } - - async function handleReset() { - await fetchSimulation(true, false); - } - - function handleAlgorithmChange(index: 0 | 1, nextAlgo: string) { - const nextAlgos = [...algorithms] as [string, string]; - nextAlgos[index] = nextAlgo; - setAlgorithms(nextAlgos); - fetchSimulation(false, false, { algos: nextAlgos }); - } - - const activeFrames = useMemo( - () => - activeResponse?.lanes.map((lane) => { - if (!lane.frames || lane.frames.length === 0) return undefined; - const safeIdx = Math.max(0, Math.min(playback.frameIndex, lane.frames.length - 1)); - return lane.frames[safeIdx]; - }), - [activeResponse, playback.frameIndex] - ); - - const isCompleted = !!(activeResponse && playback.frameIndex === playback.maxFrames - 1 && playback.maxFrames > 0); - const winnerLane = activeResponse?.lanes.find((l) => l.name === activeResponse.winner); - - let speedRatioStr = ''; - if (isCompleted && activeResponse && activeResponse.lanes.length === 2 && winnerLane) { - const loserLane = activeResponse.lanes.find(l => l.name !== winnerLane.name); - if (loserLane && winnerLane.stats.timeMs > 0) { - const ratio = loserLane.stats.timeMs / winnerLane.stats.timeMs; - speedRatioStr = `${ratio.toFixed(1)}x faster`; - } - } - - useEffect(() => { - if (isCompleted && activeResponse && hasStartedPlaybackRef.current && !winnerAnnouncedRef.current) { - winnerAnnouncedRef.current = true; - hasStartedPlaybackRef.current = false; - - if (activeResponse.winner) { - setTimeout(() => play('winner'), 120); - } else { - setTimeout(() => play('raceComplete'), 120); - } - } - }, [isCompleted, activeResponse, play]); - - return ( -
-
-
-

Head-to-Head 1v1 Battle

-

Direct showdown with differential live graph

-
-
- - {isCompleted && activeResponse?.winner && ( -
-
🏆
-
-

{activeResponse.winner} Wins!

-

- Completed sorting in {winnerLane?.stats.timeMs ?? 0} ms. - {speedRatioStr && {speedRatioStr}} -

-
-
- )} - -
- handleAlgorithmChange(0, next)} - /> - handleAlgorithmChange(1, next)} - /> - -
- - playback.setPlaying(!playback.playing)} - onReset={handleReset} - onStepForward={playback.stepForward} - onStepBackward={playback.stepBackward} - frameIndex={playback.frameIndex} - maxFrames={playback.maxFrames} - onSeek={playback.seek} - speed={speed} - onSpeedChange={setSpeed} - /> - - {activeResponse && activeFrames && activeFrames.length === 2 && ( - - )} -
- ); -} diff --git a/frontend/src/pages/LandingPage.tsx b/frontend/src/pages/LandingPage.tsx index b8157e5..881f52c 100644 --- a/frontend/src/pages/LandingPage.tsx +++ b/frontend/src/pages/LandingPage.tsx @@ -24,7 +24,7 @@ const HeroMiniCanvas = lazy(() => import('../components/HeroMiniCanvas').then(m const AlgorithmMatrix = lazy(() => import('../components/AlgorithmMatrix').then(m => ({ default: m.AlgorithmMatrix }))); interface Props { - onNavigate: (page: 'sorting' | 'searching' | 'pathfinding' | 'dp' | 'trees' | 'history' | 'settings' | 'battle') => void; + onNavigate: (page: 'sorting' | 'searching' | 'pathfinding' | 'dp' | 'trees' | 'history' | 'settings') => void; darkMode?: boolean; setDarkMode?: (val: boolean) => void; } @@ -282,23 +282,6 @@ export function LandingPage({ onNavigate, darkMode, setDarkMode }: Props) {
-
onNavigate('battle')}> -
-
- -
- 1v1 BATTLE ARENA -
-

Head-to-Head Algorithm Showdown

-

- Directly compare exactly two sorting algorithms side-by-side with a real-time differential graph highlighting the exact operation lead and lag deltas. -

-
- Launch Battle Arena - -
-
- {/* Row 2: Card 3 (Pathfinding Arena) + Card 4 (Web Audio) */}
onNavigate('pathfinding')}>