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:
gpt-engineer-app[bot] 2025-07-24 10:36:29 +00:00
parent a52dceb089
commit d050cbe520
5 changed files with 340 additions and 0 deletions

View file

@ -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 />} />

View file

@ -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'
}
];

View 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;

View 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;

View file

@ -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"
}
];