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 SunnyLinesPuzzlePage from './pages/SunnyLinesPuzzlePage';
|
||||
import SikiniaParliamentPuzzlePage from './pages/SikiniaParliamentPuzzlePage';
|
||||
import NQueensPuzzlePage from './pages/NQueensPuzzlePage';
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
|
|
@ -102,6 +103,7 @@ const App = () => (
|
|||
<Route path="/puzzles/sikinia-parliament" element={<SikiniaParliamentPuzzlePage />} />
|
||||
<Route path="/puzzles/plate-swap" element={<PlateSwapPuzzlePage />} />
|
||||
<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-search" element={<ZeckendorfSearchTrickPage />} />
|
||||
|
||||
|
|
|
|||
|
|
@ -180,6 +180,15 @@ const allInteractives: Interactive[] = [
|
|||
path: '/puzzles/sikinia-parliament',
|
||||
theme: 'Puzzles',
|
||||
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,
|
||||
duration: "5-15 min",
|
||||
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