interactives/src/components/ParityBitsGame.tsx
gpt-engineer-app[bot] de640b5029 Fix parity bits highlighting and labels
Updated parity bits game to make labels persistent after animation. Highlights now use yellow for white cells and orange for black cells, including the last row and column. Also fixed spacing issues with column labels.
2025-08-22 17:39:03 +00:00

438 lines
No EOL
16 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useState, useCallback } from 'react';
import { Button } from '@/components/ui/button';
import { Slider } from '@/components/ui/slider';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Alert, AlertDescription } from '@/components/ui/alert';
import { RotateCcw, Info } from 'lucide-react';
interface ParityBitsGameProps {
showSocialShare?: boolean;
}
const ParityBitsGame = ({ showSocialShare = true }: ParityBitsGameProps) => {
const [gridSize, setGridSize] = useState(3);
const [grid, setGrid] = useState<boolean[][]>(() =>
Array(gridSize).fill(null).map(() => Array(gridSize).fill(false))
);
const [parityBitsAdded, setParityBitsAdded] = useState(false);
const [parityGrid, setParityGrid] = useState<boolean[][]>([]);
const [flippedCell, setFlippedCell] = useState<{row: number, col: number} | null>(null);
const [errorDetected, setErrorDetected] = useState<{row: number, col: number} | null>(null);
const [phase, setPhase] = useState<'setup' | 'parity' | 'flip' | 'detect'>('setup');
const [animationState, setAnimationState] = useState<{
isAnimating: boolean;
currentRow: number;
currentCol: number;
showRowParity: boolean;
showColParity: boolean;
rowParities: boolean[];
colParities: boolean[];
rowCounts: number[];
colCounts: number[];
}>({
isAnimating: false,
currentRow: -1,
currentCol: -1,
showRowParity: false,
showColParity: false,
rowParities: [],
colParities: [],
rowCounts: [],
colCounts: []
});
// Reset game when grid size changes
const handleGridSizeChange = useCallback((newSize: number[]) => {
const size = newSize[0];
setGridSize(size);
setGrid(Array(size).fill(null).map(() => Array(size).fill(false)));
setParityBitsAdded(false);
setParityGrid([]);
setFlippedCell(null);
setErrorDetected(null);
setPhase('setup');
}, []);
// Toggle a cell in the main grid
const toggleCell = (row: number, col: number) => {
if (phase !== 'setup') return;
const newGrid = [...grid];
newGrid[row][col] = !newGrid[row][col];
setGrid(newGrid);
};
// Calculate parity for a row or column
const calculateParity = (bits: boolean[]): boolean => {
return bits.filter(bit => bit).length % 2 === 1;
};
// Add parity bits to create the extended grid
const addParityBits = () => {
const extended = Array(gridSize + 1).fill(null).map(() => Array(gridSize + 1).fill(false));
// Copy original grid
for (let i = 0; i < gridSize; i++) {
for (let j = 0; j < gridSize; j++) {
extended[i][j] = grid[i][j];
}
}
// Calculate row parities
for (let i = 0; i < gridSize; i++) {
extended[i][gridSize] = calculateParity(grid[i]);
}
// Calculate column parities
for (let j = 0; j < gridSize; j++) {
const column = grid.map(row => row[j]);
extended[gridSize][j] = calculateParity(column);
}
// Calculate overall parity
const allBits = grid.flat();
const rowParities = Array(gridSize).fill(null).map((_, i) => calculateParity(grid[i]));
extended[gridSize][gridSize] = calculateParity([...allBits, ...rowParities]);
setParityGrid(extended);
setParityBitsAdded(true);
setPhase('parity');
};
// Flip a bit in the parity grid
const flipBit = (row: number, col: number) => {
if (phase !== 'parity') return;
const newGrid = [...parityGrid];
newGrid[row][col] = !newGrid[row][col];
setParityGrid(newGrid);
setFlippedCell({row, col});
setPhase('flip');
};
// Identify the flipped bit using parity check with animation
const identifyFlip = async () => {
if (!parityBitsAdded) return;
// Calculate all parities first
const rowParities: boolean[] = [];
const colParities: boolean[] = [];
const rowCounts: number[] = [];
const colCounts: number[] = [];
for (let i = 0; i < gridSize; i++) {
const rowBits = parityGrid[i].slice(0, gridSize);
const rowCount = rowBits.filter(bit => bit).length;
const expectedParity = calculateParity(rowBits);
rowParities[i] = expectedParity !== parityGrid[i][gridSize];
rowCounts[i] = rowCount;
}
for (let j = 0; j < gridSize; j++) {
const colBits = parityGrid.slice(0, gridSize).map(row => row[j]);
const colCount = colBits.filter(bit => bit).length;
const expectedParity = calculateParity(colBits);
colParities[j] = expectedParity !== parityGrid[gridSize][j];
colCounts[j] = colCount;
}
setAnimationState({
isAnimating: true,
currentRow: -1,
currentCol: -1,
showRowParity: false,
showColParity: false,
rowParities,
colParities,
rowCounts,
colCounts
});
// Animate through rows (including parity row)
for (let i = 0; i <= gridSize; i++) {
setAnimationState(prev => ({...prev, currentRow: i}));
await new Promise(resolve => setTimeout(resolve, 800));
}
// Show row parities
setAnimationState(prev => ({...prev, currentRow: -1, showRowParity: true}));
await new Promise(resolve => setTimeout(resolve, 1000));
// Animate through columns (including parity column)
for (let j = 0; j <= gridSize; j++) {
setAnimationState(prev => ({...prev, currentCol: j}));
await new Promise(resolve => setTimeout(resolve, 800));
}
// Show column parities and keep them persistent
setAnimationState(prev => ({...prev, currentCol: -1, showColParity: true}));
await new Promise(resolve => setTimeout(resolve, 1000));
// Find and highlight the error
const errorRow = rowParities.findIndex(parity => parity);
const errorCol = colParities.findIndex(parity => parity);
if (errorRow >= 0 && errorCol >= 0) {
setErrorDetected({row: errorRow, col: errorCol});
}
// Keep labels persistent - don't reset them
setAnimationState(prev => ({
...prev,
isAnimating: false,
currentRow: -1,
currentCol: -1
}));
setPhase('detect');
};
// Random start - fill random cells
const randomStart = () => {
if (phase !== 'setup') return;
const newGrid = Array(gridSize).fill(null).map(() => Array(gridSize).fill(false));
// Fill about 30-50% of cells randomly
const fillPercentage = 0.3 + Math.random() * 0.2; // 30-50%
const totalCells = gridSize * gridSize;
const cellsToFill = Math.floor(totalCells * fillPercentage);
const positions: {row: number, col: number}[] = [];
for (let i = 0; i < gridSize; i++) {
for (let j = 0; j < gridSize; j++) {
positions.push({row: i, col: j});
}
}
// Shuffle and take first cellsToFill positions
for (let i = positions.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[positions[i], positions[j]] = [positions[j], positions[i]];
}
for (let i = 0; i < cellsToFill; i++) {
const {row, col} = positions[i];
newGrid[row][col] = true;
}
setGrid(newGrid);
};
const resetGame = () => {
setGrid(Array(gridSize).fill(null).map(() => Array(gridSize).fill(false)));
setParityBitsAdded(false);
setParityGrid([]);
setFlippedCell(null);
setErrorDetected(null);
setPhase('setup');
setAnimationState({
isAnimating: false,
currentRow: -1,
currentCol: -1,
showRowParity: false,
showColParity: false,
rowParities: [],
colParities: [],
rowCounts: [],
colCounts: []
});
};
const renderGrid = () => {
const currentGrid = parityBitsAdded ? parityGrid : grid;
const size = parityBitsAdded ? gridSize + 1 : gridSize;
return (
<div className="flex flex-col items-center gap-2">
{/* Main grid with row labels */}
<div className="flex items-center gap-2">
<div className="grid gap-1" style={{gridTemplateColumns: `repeat(${size}, 1fr)`}}>
{Array(size).fill(null).map((_, row) =>
Array(size).fill(null).map((_, col) => {
const isParityCell = parityBitsAdded && (row === gridSize || col === gridSize);
const isFlipped = flippedCell?.row === row && flippedCell?.col === col;
const isError = errorDetected?.row === row && errorDetected?.col === col;
const isCurrentRowBeingChecked = animationState.currentRow === row;
const isCurrentColBeingChecked = animationState.currentCol === col;
const shouldHighlightForAnimation = isCurrentRowBeingChecked || isCurrentColBeingChecked;
const isBlackCell = currentGrid[row]?.[col];
const highlightColor = isBlackCell ? 'ring-2 ring-orange-400 bg-orange-400/20' : 'ring-2 ring-yellow-400 bg-yellow-400/15';
return (
<button
key={`${row}-${col}`}
className={`
w-12 h-12 border-2 transition-all duration-200
${currentGrid[row]?.[col] ? 'bg-foreground' : 'bg-background'}
${isParityCell ? 'border-primary' : 'border-foreground'}
${isError ? 'ring-4 ring-green-500 bg-green-500/20' : ''}
${shouldHighlightForAnimation ? highlightColor : ''}
${phase === 'setup' && !isParityCell ? 'hover:bg-accent cursor-pointer' : ''}
${phase === 'parity' ? 'hover:bg-accent cursor-pointer' : ''}
`}
onClick={() => {
if (phase === 'setup') toggleCell(row, col);
else if (phase === 'parity') flipBit(row, col);
}}
disabled={false}
/>
);
})
)}
</div>
{/* Row parity labels */}
{(animationState.currentRow >= 0 || animationState.showRowParity) && (
<div className="flex flex-col gap-1">
{Array(gridSize).fill(null).map((_, row) => {
const hasOddParity = animationState.rowParities[row];
const count = animationState.rowCounts[row];
const showLabel = (row === animationState.currentRow) || animationState.showRowParity;
return (
<div key={`row-${row}`} className="w-12 h-12 flex items-center justify-center">
{showLabel && (
<span className={`text-xs font-medium ${hasOddParity ? 'text-red-500' : 'text-foreground'}`}>
{count}
</span>
)}
</div>
);
})}
<div className="w-12 h-12"></div> {/* Empty space for parity cell */}
</div>
)}
</div>
{/* Column parity labels */}
{(animationState.currentCol >= 0 || animationState.showColParity) && (
<div className="flex gap-1 justify-center">
{Array(gridSize).fill(null).map((_, col) => {
const hasOddParity = animationState.colParities[col];
const count = animationState.colCounts[col];
const showLabel = (col === animationState.currentCol) || animationState.showColParity;
return (
<div key={`col-${col}`} className="w-12 h-6 flex items-center justify-center">
{showLabel && (
<span className={`text-xs font-medium ${hasOddParity ? 'text-red-500' : 'text-foreground'}`}>
{count}
</span>
)}
</div>
);
})}
<div className="w-12 h-6"></div> {/* Empty space for parity cell */}
</div>
)}
</div>
);
};
return (
<div className="container mx-auto p-6 max-w-4xl">
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Info className="h-5 w-5" />
Parity Bits Error Detection
</CardTitle>
</CardHeader>
<CardContent className="space-y-6">
{/* Instructions */}
<Alert>
<AlertDescription>
{phase === 'setup' && 'Set your grid by clicking squares to make them black. Then add parity bits.'}
{phase === 'parity' && 'Parity bits added! Now flip exactly one bit anywhere in the grid.'}
{phase === 'flip' && 'Bit flipped! Click "Identify Flip" to see error detection in action.'}
{phase === 'detect' && 'Animation complete! Rows and columns with odd parity were highlighted, revealing the error location in green.'}
</AlertDescription>
</Alert>
{/* Controls */}
<div className="flex flex-col sm:flex-row items-center gap-4">
<div className="flex items-center gap-2">
<label htmlFor="grid-size" className="text-sm font-medium whitespace-nowrap">
Grid Size: {gridSize}×{gridSize}
</label>
<Slider
id="grid-size"
min={2}
max={10}
step={1}
value={[gridSize]}
onValueChange={handleGridSizeChange}
className="w-24"
disabled={phase !== 'setup'}
/>
</div>
<div className="flex gap-2">
<Button
onClick={addParityBits}
disabled={phase !== 'setup'}
variant={phase === 'setup' ? 'default' : 'outline'}
>
Add Parity Bits
</Button>
<Button
onClick={randomStart}
disabled={phase !== 'setup'}
variant="outline"
>
Random Start
</Button>
<Button
onClick={identifyFlip}
disabled={phase !== 'flip' || animationState.isAnimating}
variant={phase === 'flip' ? 'default' : 'outline'}
>
{animationState.isAnimating ? 'Checking...' : 'Identify Flip'}
</Button>
<Button
onClick={resetGame}
variant="outline"
size="icon"
>
<RotateCcw className="h-4 w-4" />
</Button>
</div>
</div>
{/* Grid */}
<div className="flex justify-center">
{renderGrid()}
</div>
{/* Status Information */}
{parityBitsAdded && (
<div className="text-center space-y-2">
<p className="text-sm text-muted-foreground">
Parity cells have colored borders. Each row and column should have an even number of black squares.
</p>
{flippedCell && (
<p className="text-sm text-destructive">
Flipped bit at position ({flippedCell.row + 1}, {flippedCell.col + 1})
</p>
)}
{errorDetected && (
<p className="text-sm text-green-600 font-medium">
Error detected at position ({errorDetected.row + 1}, {errorDetected.col + 1}) - highlighted in green!
</p>
)}
</div>
)}
{/* Legend */}
<div className="text-xs text-muted-foreground text-center space-y-1">
<p><strong>How it works:</strong> Parity bits ensure even parity in each row/column.</p>
<p>When a bit flips, the affected row and column will have odd parity, pinpointing the error.</p>
</div>
</CardContent>
</Card>
</div>
);
};
export default ParityBitsGame;