diff --git a/src/components/NeighborSumAvoidance.tsx b/src/components/NeighborSumAvoidance.tsx index fbcd5b4..b1652b5 100644 --- a/src/components/NeighborSumAvoidance.tsx +++ b/src/components/NeighborSumAvoidance.tsx @@ -3,6 +3,7 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/com import { Button } from '@/components/ui/button'; import { Switch } from '@/components/ui/switch'; import { Label } from '@/components/ui/label'; +import { Slider } from '@/components/ui/slider'; import { Timer, RotateCcw } from 'lucide-react'; import confetti from 'canvas-confetti'; import { toast } from 'sonner'; @@ -15,15 +16,18 @@ interface NeighborSumAvoidanceProps { const NeighborSumAvoidance = ({ showSocialShare = false }: NeighborSumAvoidanceProps) => { const [mode, setMode] = useState<'default' | 'guided'>('default'); const [isActive, setIsActive] = useState(false); + const [numberCount, setNumberCount] = useState(9); const [slots, setSlots] = useState<(number | null)[]>(Array(9).fill(null)); const [availableNumbers, setAvailableNumbers] = useState([1, 2, 3, 4, 5, 6, 7, 8, 9]); const [draggedNumber, setDraggedNumber] = useState(null); const [draggedFrom, setDraggedFrom] = useState(null); const [draggedSlot, setDraggedSlot] = useState(null); + const [dragPosition, setDragPosition] = useState<{ x: number; y: number } | null>(null); const [time, setTime] = useState(0); const [swaps, setSwaps] = useState(0); const [isComplete, setIsComplete] = useState(false); const timerRef = useRef(null); + const svgRef = useRef(null); useEffect(() => { if (isActive && !isComplete) { @@ -87,11 +91,11 @@ const NeighborSumAvoidance = ({ showSocialShare = false }: NeighborSumAvoidanceP setIsComplete(false); if (selectedMode === 'default') { - setSlots(Array(9).fill(null)); - setAvailableNumbers([1, 2, 3, 4, 5, 6, 7, 8, 9]); + setSlots(Array(numberCount).fill(null)); + setAvailableNumbers(Array.from({ length: numberCount }, (_, i) => i + 1)); } else { // Guided mode: random arrangement - const shuffled = [...Array(9)].map((_, i) => i + 1).sort(() => Math.random() - 0.5); + const shuffled = Array.from({ length: numberCount }, (_, i) => i + 1).sort(() => Math.random() - 0.5); setSlots(shuffled); setAvailableNumbers([]); } @@ -100,15 +104,37 @@ const NeighborSumAvoidance = ({ showSocialShare = false }: NeighborSumAvoidanceP const restart = () => { setIsActive(false); setIsComplete(false); - setSlots(Array(9).fill(null)); - setAvailableNumbers([1, 2, 3, 4, 5, 6, 7, 8, 9]); + setSlots(Array(numberCount).fill(null)); + setAvailableNumbers(Array.from({ length: numberCount }, (_, i) => i + 1)); setTime(0); setSwaps(0); + setDragPosition(null); }; - const handleDragStart = (num: number, fromSlot: number | null) => { + const handleDragStart = (num: number, fromSlot: number | null, e?: React.MouseEvent) => { setDraggedNumber(num); setDraggedFrom(fromSlot); + if (e && mode === 'guided') { + e.preventDefault(); + } + }; + + const handleMouseMove = (e: React.MouseEvent) => { + if (mode === 'guided' && draggedNumber !== null && svgRef.current) { + const rect = svgRef.current.getBoundingClientRect(); + setDragPosition({ + x: e.clientX - rect.left, + y: e.clientY - rect.top + }); + } + }; + + const handleMouseUp = () => { + if (mode === 'guided') { + setDraggedNumber(null); + setDraggedFrom(null); + setDragPosition(null); + } }; const handleDrop = (toSlot: number) => { @@ -134,6 +160,7 @@ const NeighborSumAvoidance = ({ showSocialShare = false }: NeighborSumAvoidanceP setDraggedNumber(null); setDraggedFrom(null); + setDragPosition(null); }; const handleSlotClick = (slotIndex: number) => { @@ -145,24 +172,17 @@ const NeighborSumAvoidance = ({ showSocialShare = false }: NeighborSumAvoidanceP newSlots[slotIndex] = null; setSlots(newSlots); } - } else if (mode === 'guided') { - // Swap in guided mode - if (draggedSlot === null) { - setDraggedSlot(slotIndex); - } else if (draggedSlot !== slotIndex) { - const newSlots = [...slots]; - const temp = newSlots[slotIndex]; - newSlots[slotIndex] = newSlots[draggedSlot]; - newSlots[draggedSlot] = temp; - setSlots(newSlots); - setSwaps(s => s + 1); - setDraggedSlot(null); - } else { - setDraggedSlot(null); - } } }; + const getSlotPosition = (index: number, radius: number, centerX: number, centerY: number) => { + const angle = (index * 360 / slots.length - 90) * Math.PI / 180; + return { + x: centerX + radius * Math.cos(angle), + y: centerY + radius * Math.sin(angle) + }; + }; + const renderCircle = () => { const radius = 150; const centerX = 200; @@ -170,14 +190,21 @@ const NeighborSumAvoidance = ({ showSocialShare = false }: NeighborSumAvoidanceP const violations = getViolations(); return ( - + {/* Draw circle */} {/* Draw violation arcs */} {violations.map(i => { - const angle1 = (i * 360 / 9 - 90) * Math.PI / 180; - const angle2 = ((i + 1) * 360 / 9 - 90) * Math.PI / 180; + const angle1 = (i * 360 / slots.length - 90) * Math.PI / 180; + const angle2 = ((i + 1) * 360 / slots.length - 90) * Math.PI / 180; const x1 = centerX + radius * Math.cos(angle1); const y1 = centerY + radius * Math.sin(angle1); const x2 = centerX + radius * Math.cos(angle2); @@ -196,26 +223,28 @@ const NeighborSumAvoidance = ({ showSocialShare = false }: NeighborSumAvoidanceP {/* Draw edges in guided mode */} {mode === 'guided' && slots.every(s => s !== null) && slots.map((num1, i) => { + // Skip if this slot is being dragged + if (draggedFrom === i) return null; + return slots.slice(i + 1).map((num2, j) => { const idx2 = i + j + 1; + // Skip if the other slot is being dragged + if (draggedFrom === idx2) return null; + if (num1 !== null && num2 !== null && !isDivisible(num1, num2)) { - const angle1 = (i * 360 / 9 - 90) * Math.PI / 180; - const angle2 = (idx2 * 360 / 9 - 90) * Math.PI / 180; - const x1 = centerX + radius * Math.cos(angle1); - const y1 = centerY + radius * Math.sin(angle1); - const x2 = centerX + radius * Math.cos(angle2); - const y2 = centerY + radius * Math.sin(angle2); + const pos1 = getSlotPosition(i, radius, centerX, centerY); + const pos2 = getSlotPosition(idx2, radius, centerX, centerY); // Check if this forms part of the cycle (adjacent positions on circle) - const isAdjacentOnCircle = Math.abs(i - idx2) === 1 || Math.abs(i - idx2) === 8; + const isAdjacentOnCircle = Math.abs(i - idx2) === 1 || Math.abs(i - idx2) === slots.length - 1; return ( { + if (i === draggedFrom) return null; + const draggedNum = slots[draggedFrom]; + if (draggedNum !== null && num !== null && !isDivisible(draggedNum, num)) { + const pos = getSlotPosition(i, radius, centerX, centerY); + const isAdjacentOnCircle = Math.abs(draggedFrom - i) === 1 || Math.abs(draggedFrom - i) === slots.length - 1; + + return ( + + ); + } + return null; + })} - {/* Draw slots */} + {/* Draw slots (non-dragged) */} {slots.map((num, i) => { - const angle = (i * 360 / 9 - 90) * Math.PI / 180; - const x = centerX + radius * Math.cos(angle); - const y = centerY + radius * Math.sin(angle); + // Skip rendering if this is being dragged + if (mode === 'guided' && draggedFrom === i) return null; - const isSelected = mode === 'guided' && draggedSlot === i; + const pos = getSlotPosition(i, radius, centerX, centerY); return ( e.preventDefault()} onDrop={() => handleDrop(i)} + onMouseDown={(e) => mode === 'guided' && num !== null && handleDragStart(num, i, e as any)} onClick={() => handleSlotClick(i)} className="cursor-pointer" /> {num !== null && ( {num} @@ -263,6 +316,31 @@ const NeighborSumAvoidance = ({ showSocialShare = false }: NeighborSumAvoidanceP ); })} + + {/* Draw dragged slot */} + {mode === 'guided' && draggedFrom !== null && dragPosition && ( + + + + {slots[draggedFrom]} + + + )} ); }; @@ -273,7 +351,7 @@ const NeighborSumAvoidance = ({ showSocialShare = false }: NeighborSumAvoidanceP Neighbor Sum Avoidance - Can you arrange the numbers 1-9 in a circle so that the sum of two neighbors is never divisible by 3, 5, or 7? + Can you arrange the numbers 1-{numberCount} in a circle so that the sum of two neighbors is never divisible by 3, 5, or 7? @@ -289,6 +367,21 @@ const NeighborSumAvoidance = ({ showSocialShare = false }: NeighborSumAvoidanceP +
+
+ + setNumberCount(value[0])} + min={5} + max={15} + step={1} + className="flex-1" + /> + {numberCount} +
+
+
{mode === 'default' ? ( @@ -300,7 +393,7 @@ const NeighborSumAvoidance = ({ showSocialShare = false }: NeighborSumAvoidanceP ) : ( <>

Numbers are connected by edges if their sum is NOT divisible by 3, 5, or 7.

-

Click two slots to swap their numbers and form a cycle on the circle's edge.

+

Drag circles to move numbers and form a cycle on the circle's edge.

)}