Add N-Queens puzzle
Create an N-Queens puzzle where users can place queens on an n x n chessboard. The size of the board can be adjusted using a slider. A timer can be started before placing any queens. Clicking a square places a queen, and clicking a queen removes it. Attacked squares are visually indicated.
This commit is contained in:
parent
a52dceb089
commit
d050cbe520
5 changed files with 340 additions and 0 deletions
|
|
@ -47,6 +47,7 @@ import NotFound from "./pages/NotFound";
|
||||||
import GridTilingPuzzlePage from './pages/GridTilingPuzzlePage';
|
import GridTilingPuzzlePage from './pages/GridTilingPuzzlePage';
|
||||||
import SunnyLinesPuzzlePage from './pages/SunnyLinesPuzzlePage';
|
import SunnyLinesPuzzlePage from './pages/SunnyLinesPuzzlePage';
|
||||||
import SikiniaParliamentPuzzlePage from './pages/SikiniaParliamentPuzzlePage';
|
import SikiniaParliamentPuzzlePage from './pages/SikiniaParliamentPuzzlePage';
|
||||||
|
import NQueensPuzzlePage from './pages/NQueensPuzzlePage';
|
||||||
|
|
||||||
const queryClient = new QueryClient();
|
const queryClient = new QueryClient();
|
||||||
|
|
||||||
|
|
@ -102,6 +103,7 @@ const App = () => (
|
||||||
<Route path="/puzzles/sikinia-parliament" element={<SikiniaParliamentPuzzlePage />} />
|
<Route path="/puzzles/sikinia-parliament" element={<SikiniaParliamentPuzzlePage />} />
|
||||||
<Route path="/puzzles/plate-swap" element={<PlateSwapPuzzlePage />} />
|
<Route path="/puzzles/plate-swap" element={<PlateSwapPuzzlePage />} />
|
||||||
<Route path="/puzzles/chessboard-repaint" element={<ChessboardRepaintPuzzlePage />} />
|
<Route path="/puzzles/chessboard-repaint" element={<ChessboardRepaintPuzzlePage />} />
|
||||||
|
<Route path="/puzzles/n-queens" element={<NQueensPuzzlePage />} />
|
||||||
<Route path="/discrete-math/foundations/zeckendorf" element={<ZeckendorfGamePage />} />
|
<Route path="/discrete-math/foundations/zeckendorf" element={<ZeckendorfGamePage />} />
|
||||||
<Route path="/discrete-math/foundations/zeckendorf-search" element={<ZeckendorfSearchTrickPage />} />
|
<Route path="/discrete-math/foundations/zeckendorf-search" element={<ZeckendorfSearchTrickPage />} />
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -180,6 +180,15 @@ const allInteractives: Interactive[] = [
|
||||||
path: '/puzzles/sikinia-parliament',
|
path: '/puzzles/sikinia-parliament',
|
||||||
theme: 'Puzzles',
|
theme: 'Puzzles',
|
||||||
dateAdded: '2024-12-22'
|
dateAdded: '2024-12-22'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'n-queens',
|
||||||
|
title: 'N-Queens Puzzle',
|
||||||
|
description: 'Place as many queens as possible on an n×n chessboard so that no two queens attack each other.',
|
||||||
|
tags: ['logic', 'chess', 'placement', 'classic'],
|
||||||
|
path: '/puzzles/n-queens',
|
||||||
|
theme: 'Puzzles',
|
||||||
|
dateAdded: '2024-12-22'
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
||||||
307
src/components/NQueensPuzzle.tsx
Normal file
307
src/components/NQueensPuzzle.tsx
Normal file
|
|
@ -0,0 +1,307 @@
|
||||||
|
import React, { useState, useEffect, 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 { Badge } from '@/components/ui/badge';
|
||||||
|
import { Separator } from '@/components/ui/separator';
|
||||||
|
import { Play, Pause, RotateCcw, Crown, Timer, Trophy } from 'lucide-react';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import SocialShare from './SocialShare';
|
||||||
|
|
||||||
|
interface NQueensPuzzleProps {
|
||||||
|
showSocialShare?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const NQueensPuzzle: React.FC<NQueensPuzzleProps> = ({ showSocialShare = false }) => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [boardSize, setBoardSize] = useState([8]);
|
||||||
|
const [queens, setQueens] = useState<Set<string>>(new Set());
|
||||||
|
const [timerStarted, setTimerStarted] = useState(false);
|
||||||
|
const [timeElapsed, setTimeElapsed] = useState(0);
|
||||||
|
const [isRunning, setIsRunning] = useState(false);
|
||||||
|
|
||||||
|
// Timer effect
|
||||||
|
useEffect(() => {
|
||||||
|
let interval: NodeJS.Timeout;
|
||||||
|
if (isRunning) {
|
||||||
|
interval = setInterval(() => {
|
||||||
|
setTimeElapsed(prev => prev + 1);
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, [isRunning]);
|
||||||
|
|
||||||
|
const formatTime = (seconds: number) => {
|
||||||
|
const minutes = Math.floor(seconds / 60);
|
||||||
|
const remainingSeconds = seconds % 60;
|
||||||
|
return `${minutes}:${remainingSeconds.toString().padStart(2, '0')}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const startTimer = () => {
|
||||||
|
setTimerStarted(true);
|
||||||
|
setIsRunning(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const pauseTimer = () => {
|
||||||
|
setIsRunning(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetPuzzle = () => {
|
||||||
|
setQueens(new Set());
|
||||||
|
setTimerStarted(false);
|
||||||
|
setIsRunning(false);
|
||||||
|
setTimeElapsed(0);
|
||||||
|
};
|
||||||
|
|
||||||
|
const isQueenAt = (row: number, col: number) => {
|
||||||
|
return queens.has(`${row}-${col}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const isSquareAttacked = useCallback((row: number, col: number) => {
|
||||||
|
for (const queenPos of queens) {
|
||||||
|
const [queenRow, queenCol] = queenPos.split('-').map(Number);
|
||||||
|
|
||||||
|
// Same row or column
|
||||||
|
if (queenRow === row || queenCol === col) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Diagonal
|
||||||
|
if (Math.abs(queenRow - row) === Math.abs(queenCol - col)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}, [queens]);
|
||||||
|
|
||||||
|
const handleSquareClick = (row: number, col: number) => {
|
||||||
|
if (!timerStarted) return;
|
||||||
|
|
||||||
|
const posKey = `${row}-${col}`;
|
||||||
|
|
||||||
|
if (queens.has(posKey)) {
|
||||||
|
// Remove queen
|
||||||
|
const newQueens = new Set(queens);
|
||||||
|
newQueens.delete(posKey);
|
||||||
|
setQueens(newQueens);
|
||||||
|
} else {
|
||||||
|
// Add queen
|
||||||
|
const newQueens = new Set(queens);
|
||||||
|
newQueens.add(posKey);
|
||||||
|
setQueens(newQueens);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSquareStyle = (row: number, col: number) => {
|
||||||
|
const isLight = (row + col) % 2 === 0;
|
||||||
|
const hasQueen = isQueenAt(row, col);
|
||||||
|
const isAttacked = !hasQueen && isSquareAttacked(row, col);
|
||||||
|
|
||||||
|
let baseClasses = "aspect-square border border-border flex items-center justify-center text-2xl font-bold transition-all cursor-pointer relative";
|
||||||
|
|
||||||
|
if (isLight) {
|
||||||
|
baseClasses += " bg-amber-100 dark:bg-amber-200";
|
||||||
|
} else {
|
||||||
|
baseClasses += " bg-amber-600 dark:bg-amber-700";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasQueen) {
|
||||||
|
baseClasses += " bg-primary/20 ring-2 ring-primary";
|
||||||
|
} else if (isAttacked) {
|
||||||
|
baseClasses += " bg-destructive/10";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!timerStarted) {
|
||||||
|
baseClasses += " cursor-not-allowed opacity-50";
|
||||||
|
} else {
|
||||||
|
baseClasses += " hover:bg-accent/20";
|
||||||
|
}
|
||||||
|
|
||||||
|
return baseClasses;
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderSquare = (row: number, col: number) => {
|
||||||
|
const hasQueen = isQueenAt(row, col);
|
||||||
|
const isAttacked = !hasQueen && isSquareAttacked(row, col);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={`${row}-${col}`}
|
||||||
|
className={getSquareStyle(row, col)}
|
||||||
|
onClick={() => handleSquareClick(row, col)}
|
||||||
|
>
|
||||||
|
{hasQueen && <Crown className="w-6 h-6 text-foreground" />}
|
||||||
|
{isAttacked && (
|
||||||
|
<div className="absolute inset-0 bg-destructive/20 flex items-center justify-center">
|
||||||
|
<div className="w-2 h-2 bg-destructive rounded-full" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const currentBoardSize = boardSize[0];
|
||||||
|
const maxPossibleQueens = currentBoardSize;
|
||||||
|
const currentQueenCount = queens.size;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-background p-4">
|
||||||
|
<div className="max-w-6xl mx-auto space-y-6">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="text-center space-y-2">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() => navigate('/puzzles')}
|
||||||
|
className="mb-4"
|
||||||
|
>
|
||||||
|
← Back to Puzzles
|
||||||
|
</Button>
|
||||||
|
<h1 className="text-4xl font-bold text-foreground">N-Queens Puzzle</h1>
|
||||||
|
<p className="text-muted-foreground max-w-2xl mx-auto">
|
||||||
|
Place as many queens as possible on the chessboard so that no two queens attack each other.
|
||||||
|
Queens attack horizontally, vertically, and diagonally.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid lg:grid-cols-3 gap-6">
|
||||||
|
{/* Controls */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<Timer className="w-5 h-5" />
|
||||||
|
Game Controls
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium text-foreground mb-2 block">
|
||||||
|
Board Size: {currentBoardSize}×{currentBoardSize}
|
||||||
|
</label>
|
||||||
|
<Slider
|
||||||
|
value={boardSize}
|
||||||
|
onValueChange={setBoardSize}
|
||||||
|
min={4}
|
||||||
|
max={12}
|
||||||
|
step={1}
|
||||||
|
className="w-full"
|
||||||
|
disabled={timerStarted}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Separator />
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-sm text-muted-foreground">Time:</span>
|
||||||
|
<Badge variant="outline" className="font-mono">
|
||||||
|
{formatTime(timeElapsed)}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-2">
|
||||||
|
{!timerStarted ? (
|
||||||
|
<Button onClick={startTimer} className="flex-1">
|
||||||
|
<Play className="w-4 h-4 mr-2" />
|
||||||
|
Start
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
onClick={isRunning ? pauseTimer : () => setIsRunning(true)}
|
||||||
|
variant="secondary"
|
||||||
|
className="flex-1"
|
||||||
|
>
|
||||||
|
{isRunning ? (
|
||||||
|
<>
|
||||||
|
<Pause className="w-4 h-4 mr-2" />
|
||||||
|
Pause
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Play className="w-4 h-4 mr-2" />
|
||||||
|
Resume
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Button onClick={resetPuzzle} variant="outline">
|
||||||
|
<RotateCcw className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Separator />
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-sm text-muted-foreground">Queens Placed:</span>
|
||||||
|
<Badge variant={currentQueenCount === maxPossibleQueens ? "default" : "secondary"}>
|
||||||
|
{currentQueenCount}/{maxPossibleQueens}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{currentQueenCount === maxPossibleQueens && (
|
||||||
|
<div className="flex items-center gap-2 text-sm text-green-600 dark:text-green-400">
|
||||||
|
<Trophy className="w-4 h-4" />
|
||||||
|
Perfect solution found!
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Instructions</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-2 text-sm text-muted-foreground">
|
||||||
|
<p>• Start the timer to begin placing queens</p>
|
||||||
|
<p>• Click empty squares to place queens</p>
|
||||||
|
<p>• Click placed queens to remove them</p>
|
||||||
|
<p>• Red dots show attacked squares</p>
|
||||||
|
<p>• Try to place {currentBoardSize} queens without conflicts!</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Chessboard */}
|
||||||
|
<div className="lg:col-span-2 flex justify-center">
|
||||||
|
<div className="inline-block">
|
||||||
|
<div
|
||||||
|
className="grid border-2 border-border"
|
||||||
|
style={{
|
||||||
|
gridTemplateColumns: `repeat(${currentBoardSize}, 1fr)`,
|
||||||
|
aspectRatio: '1'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{Array.from({ length: currentBoardSize }, (_, row) =>
|
||||||
|
Array.from({ length: currentBoardSize }, (_, col) =>
|
||||||
|
renderSquare(row, col)
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{!timerStarted && (
|
||||||
|
<div className="text-center mt-4">
|
||||||
|
<p className="text-muted-foreground text-sm">
|
||||||
|
Start the timer to begin placing queens
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{showSocialShare && (
|
||||||
|
<SocialShare
|
||||||
|
title="N-Queens Puzzle"
|
||||||
|
description="Test your logic with the classic N-Queens puzzle!"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NQueensPuzzle;
|
||||||
12
src/pages/NQueensPuzzlePage.tsx
Normal file
12
src/pages/NQueensPuzzlePage.tsx
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
import React from 'react';
|
||||||
|
import NQueensPuzzle from '@/components/NQueensPuzzle';
|
||||||
|
|
||||||
|
const NQueensPuzzlePage = () => {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-background">
|
||||||
|
<NQueensPuzzle showSocialShare={true} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NQueensPuzzlePage;
|
||||||
|
|
@ -45,6 +45,16 @@ const Puzzles = () => {
|
||||||
difficulty: "Intermediate" as const,
|
difficulty: "Intermediate" as const,
|
||||||
duration: "5-15 min",
|
duration: "5-15 min",
|
||||||
participants: "1 player"
|
participants: "1 player"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "n-queens",
|
||||||
|
title: "N-Queens Puzzle",
|
||||||
|
description: "Place as many queens as possible on an n×n chessboard so that no two queens attack each other.",
|
||||||
|
path: "/puzzles/n-queens",
|
||||||
|
tags: ["Logic", "Chess", "Placement", "Classic"],
|
||||||
|
difficulty: "Intermediate" as const,
|
||||||
|
duration: "5-15 min",
|
||||||
|
participants: "1 player"
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue