neighbor-sum-avoidance: render arena pre-start with interactions disabled
Show the circle arena before the user clicks Start (greyed via opacity-60 pointer-events-none), and guard drag/drop/click handlers on isActive. Clicking Start enables interactions and kicks off the timer as before. Also resync slot count with the Numbers slider while idle. Marks T1 done. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
fcbdcfccdf
commit
1c754de4f7
2 changed files with 85 additions and 63 deletions
|
|
@ -41,6 +41,14 @@ const NeighborSumAvoidance = ({ shareUrl }: NeighborSumAvoidanceProps) => {
|
||||||
};
|
};
|
||||||
}, [isActive, isComplete]);
|
}, [isActive, isComplete]);
|
||||||
|
|
||||||
|
// Keep the pre-start arena in sync with the Numbers slider.
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isActive) {
|
||||||
|
setSlots(Array(numberCount).fill(null));
|
||||||
|
setAvailableNumbers(Array.from({ length: numberCount }, (_, i) => i + 1));
|
||||||
|
}
|
||||||
|
}, [numberCount, isActive]);
|
||||||
|
|
||||||
const isDivisible = (a: number, b: number) => {
|
const isDivisible = (a: number, b: number) => {
|
||||||
const sum = a + b;
|
const sum = a + b;
|
||||||
return sum % 3 === 0 || sum % 5 === 0 || sum % 7 === 0;
|
return sum % 3 === 0 || sum % 5 === 0 || sum % 7 === 0;
|
||||||
|
|
@ -110,12 +118,13 @@ const NeighborSumAvoidance = ({ shareUrl }: NeighborSumAvoidanceProps) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDragStart = (num: number, fromSlot: number | null) => {
|
const handleDragStart = (num: number, fromSlot: number | null) => {
|
||||||
|
if (!isActive) return;
|
||||||
setDraggedNumber(num);
|
setDraggedNumber(num);
|
||||||
setDraggedFrom(fromSlot);
|
setDraggedFrom(fromSlot);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDrop = (toSlot: number) => {
|
const handleDrop = (toSlot: number) => {
|
||||||
if (draggedNumber === null) return;
|
if (!isActive || draggedNumber === null) return;
|
||||||
|
|
||||||
if (mode === 'default') {
|
if (mode === 'default') {
|
||||||
// Default mode: place number in slot
|
// Default mode: place number in slot
|
||||||
|
|
@ -140,6 +149,7 @@ const NeighborSumAvoidance = ({ shareUrl }: NeighborSumAvoidanceProps) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSlotClick = (slotIndex: number) => {
|
const handleSlotClick = (slotIndex: number) => {
|
||||||
|
if (!isActive) return;
|
||||||
if (mode === 'default') {
|
if (mode === 'default') {
|
||||||
// Remove from slot in default mode
|
// Remove from slot in default mode
|
||||||
if (slots[slotIndex] !== null) {
|
if (slots[slotIndex] !== null) {
|
||||||
|
|
@ -284,8 +294,9 @@ const NeighborSumAvoidance = ({ shareUrl }: NeighborSumAvoidanceProps) => {
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-6">
|
<CardContent className="space-y-6">
|
||||||
{!isActive ? (
|
{/* Config controls — shown before start */}
|
||||||
<div className="space-y-6">
|
{!isActive && (
|
||||||
|
<div className="space-y-4">
|
||||||
<div className="flex items-center justify-center gap-6">
|
<div className="flex items-center justify-center gap-6">
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<Label htmlFor="mode-switch">Default Mode</Label>
|
<Label htmlFor="mode-switch">Default Mode</Label>
|
||||||
|
|
@ -296,7 +307,7 @@ const NeighborSumAvoidance = ({ shareUrl }: NeighborSumAvoidanceProps) => {
|
||||||
/>
|
/>
|
||||||
<Label htmlFor="mode-switch">Guided Mode</Label>
|
<Label htmlFor="mode-switch">Guided Mode</Label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-4 min-w-[200px]">
|
<div className="flex items-center gap-4 min-w-[200px]">
|
||||||
<Label htmlFor="number-count">Numbers: {numberCount}</Label>
|
<Label htmlFor="number-count">Numbers: {numberCount}</Label>
|
||||||
<Slider
|
<Slider
|
||||||
|
|
@ -311,70 +322,79 @@ const NeighborSumAvoidance = ({ shareUrl }: NeighborSumAvoidanceProps) => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-4 text-center">
|
<div className="text-sm text-muted-foreground space-y-2 text-center">
|
||||||
<div className="text-sm text-muted-foreground space-y-2">
|
{mode === 'default' ? (
|
||||||
{mode === 'default' ? (
|
<>
|
||||||
<>
|
<p>Drag numbers from below into the circle slots.</p>
|
||||||
<p>Drag numbers from below into the circle slots.</p>
|
<p>Adjacent numbers that sum to a multiple of 3, 5, or 7 will show a red arc.</p>
|
||||||
<p>Adjacent numbers that sum to a multiple of 3, 5, or 7 will show a red arc.</p>
|
<p>Click a filled slot to remove the number.</p>
|
||||||
<p>Click a filled slot to remove the number.</p>
|
</>
|
||||||
</>
|
) : (
|
||||||
) : (
|
<>
|
||||||
<>
|
<p>Numbers are connected by edges if their sum is NOT divisible by 3, 5, or 7.</p>
|
||||||
<p>Numbers are connected by edges if their sum is NOT divisible by 3, 5, or 7.</p>
|
<p>Click two slots to swap their numbers and form a cycle on the circle's edge.</p>
|
||||||
<p>Click two slots to swap their numbers and form a cycle on the circle's edge.</p>
|
</>
|
||||||
</>
|
)}
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<Button onClick={() => startGame(mode)} size="lg">
|
|
||||||
Start {mode === 'default' ? 'Default' : 'Guided'} Mode
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
)}
|
||||||
<div className="space-y-6">
|
|
||||||
<div className="flex items-center justify-between">
|
{/* Timer + controls — shown after start */}
|
||||||
<div className="flex items-center gap-2">
|
{isActive && (
|
||||||
<Timer className="w-4 h-4" />
|
<div className="flex items-center justify-between">
|
||||||
<span className="font-mono">{formatTime(time)}</span>
|
<div className="flex items-center gap-2">
|
||||||
</div>
|
<Timer className="w-4 h-4" />
|
||||||
{mode === 'guided' && (
|
<span className="font-mono">{formatTime(time)}</span>
|
||||||
<div className="text-sm text-muted-foreground">
|
|
||||||
Swaps: {swaps}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<Button variant="outline" size="sm" onClick={restart}>
|
|
||||||
<RotateCcw className="w-4 h-4 mr-2" />
|
|
||||||
Restart
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
|
{mode === 'guided' && (
|
||||||
{renderCircle()}
|
<div className="text-sm text-muted-foreground">
|
||||||
|
Swaps: {swaps}
|
||||||
{mode === 'default' && availableNumbers.length > 0 && (
|
|
||||||
<div className="flex flex-wrap justify-center gap-4">
|
|
||||||
{availableNumbers.map(num => (
|
|
||||||
<div
|
|
||||||
key={num}
|
|
||||||
draggable
|
|
||||||
onDragStart={() => handleDragStart(num, null)}
|
|
||||||
className="w-12 h-12 rounded-full border-2 border-primary bg-background flex items-center justify-center text-lg font-bold cursor-move hover:scale-110 transition-transform"
|
|
||||||
>
|
|
||||||
{num}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
<Button variant="outline" size="sm" onClick={restart}>
|
||||||
|
<RotateCcw className="w-4 h-4 mr-2" />
|
||||||
|
Restart
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{isComplete && (
|
{/* Arena — always visible */}
|
||||||
<div className="text-center space-y-4">
|
<div className={!isActive ? 'opacity-60 pointer-events-none' : ''}>
|
||||||
<p className="text-lg font-semibold text-primary">
|
{renderCircle()}
|
||||||
🎉 Congratulations! You solved it in {formatTime(time)}
|
</div>
|
||||||
{mode === 'guided' && ` with ${swaps} swaps`}!
|
|
||||||
</p>
|
{/* Start button — shown before start */}
|
||||||
<Button onClick={restart}>Play Again</Button>
|
{!isActive && (
|
||||||
|
<div className="text-center">
|
||||||
|
<Button onClick={() => startGame(mode)} size="lg">
|
||||||
|
Start {mode === 'default' ? 'Default' : 'Guided'} Mode
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Available numbers for default mode */}
|
||||||
|
{isActive && mode === 'default' && availableNumbers.length > 0 && (
|
||||||
|
<div className="flex flex-wrap justify-center gap-4">
|
||||||
|
{availableNumbers.map(num => (
|
||||||
|
<div
|
||||||
|
key={num}
|
||||||
|
draggable
|
||||||
|
onDragStart={() => handleDragStart(num, null)}
|
||||||
|
className="w-12 h-12 rounded-full border-2 border-primary bg-background flex items-center justify-center text-lg font-bold cursor-move hover:scale-110 transition-transform"
|
||||||
|
>
|
||||||
|
{num}
|
||||||
</div>
|
</div>
|
||||||
)}
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isComplete && (
|
||||||
|
<div className="text-center space-y-4">
|
||||||
|
<p className="text-lg font-semibold text-primary">
|
||||||
|
🎉 Congratulations! You solved it in {formatTime(time)}
|
||||||
|
{mode === 'guided' && ` with ${swaps} swaps`}!
|
||||||
|
</p>
|
||||||
|
<Button onClick={restart}>Play Again</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|
|
||||||
|
|
@ -333,8 +333,10 @@
|
||||||
{
|
{
|
||||||
"id": "474e8061-c7a2-49a2-ba85-a8c57814ea28",
|
"id": "474e8061-c7a2-49a2-ba85-a8c57814ea28",
|
||||||
"text": "in both default and guided mode, show the arena. to begin with all interactions should be disabled, and enabled as soon as the user clicks the start button (this should set off th etimer too, as it does now).",
|
"text": "in both default and guided mode, show the arena. to begin with all interactions should be disabled, and enabled as soon as the user clicks the start button (this should set off th etimer too, as it does now).",
|
||||||
"done": false,
|
"done": true,
|
||||||
"refId": "T1"
|
"refId": "T1",
|
||||||
|
"remark": "Arena now renders pre-start (greyed with opacity-60 pointer-events-none). Drag/drop/click handlers short-circuit on !isActive as defense-in-depth. Start button still triggers startGame (kicks off timer + populates slots). Added a useEffect to resync slots/availableNumbers when the Numbers slider changes before start, so the pre-start arena always matches the chosen count.",
|
||||||
|
"doneDate": "2026-04-17"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue