Add AscDesc grid puzzle
Implement interactive Ascending-Descending Grid Puzzle (Puzzles theme) - New AscDescGridPuzzle component with grid, A/D labels, number palette, validation, and hints - New AscDescGridPuzzlePage and route integration - Hook into App routing to include new puzzle in puzzles list X-Lovable-Edit-ID: edt-5f6c4886-712d-47ac-ae85-3364cbbeb22e
This commit is contained in:
commit
c2d2cfa990
4 changed files with 463 additions and 0 deletions
|
|
@ -64,6 +64,7 @@ import FerrersRogersRamanujanPage from './pages/FerrersRogersRamanujanPage';
|
||||||
import DominoRetilingPuzzlePage from './pages/DominoRetilingPuzzlePage';
|
import DominoRetilingPuzzlePage from './pages/DominoRetilingPuzzlePage';
|
||||||
import RentDivisionPuzzlePage from './pages/RentDivisionPuzzlePage';
|
import RentDivisionPuzzlePage from './pages/RentDivisionPuzzlePage';
|
||||||
import BagchalGamePage from './pages/BagchalGamePage';
|
import BagchalGamePage from './pages/BagchalGamePage';
|
||||||
|
import AscDescGridPuzzlePage from './pages/AscDescGridPuzzlePage';
|
||||||
|
|
||||||
const queryClient = new QueryClient();
|
const queryClient = new QueryClient();
|
||||||
|
|
||||||
|
|
@ -138,6 +139,7 @@ const App = () => (
|
||||||
<Route path="/puzzles/chessboard-repaint" element={<ChessboardRepaintPuzzlePage />} />
|
<Route path="/puzzles/chessboard-repaint" element={<ChessboardRepaintPuzzlePage />} />
|
||||||
<Route path="/puzzles/n-queens" element={<NQueensPuzzlePage />} />
|
<Route path="/puzzles/n-queens" element={<NQueensPuzzlePage />} />
|
||||||
<Route path="/puzzles/domino-retiling" element={<DominoRetilingPuzzlePage />} />
|
<Route path="/puzzles/domino-retiling" element={<DominoRetilingPuzzlePage />} />
|
||||||
|
<Route path="/puzzles/asc-desc-grid" element={<AscDescGridPuzzlePage />} />
|
||||||
<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 />} />
|
||||||
<Route path="/discrete-math/foundations/rules-of-inference" element={<RulesOfInferencePlaygroundPage />} />
|
<Route path="/discrete-math/foundations/rules-of-inference" element={<RulesOfInferencePlaygroundPage />} />
|
||||||
|
|
|
||||||
437
src/components/AscDescGridPuzzle.tsx
Normal file
437
src/components/AscDescGridPuzzle.tsx
Normal file
|
|
@ -0,0 +1,437 @@
|
||||||
|
import { useState, useCallback } from "react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Slider } from "@/components/ui/slider";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import { RotateCcw, Check, Shuffle, Lightbulb } from "lucide-react";
|
||||||
|
import SocialShare from "./SocialShare";
|
||||||
|
|
||||||
|
type Label = 'A' | 'D';
|
||||||
|
|
||||||
|
interface AscDescGridPuzzleProps {
|
||||||
|
showSocialShare?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const AscDescGridPuzzle = ({ showSocialShare = true }: AscDescGridPuzzleProps) => {
|
||||||
|
const [gridSize, setGridSize] = useState(3);
|
||||||
|
const [rowLabels, setRowLabels] = useState<Label[]>(['A', 'D', 'A']);
|
||||||
|
const [colLabels, setColLabels] = useState<Label[]>(['A', 'D', 'A']);
|
||||||
|
const [grid, setGrid] = useState<(number | null)[][]>(
|
||||||
|
Array(3).fill(null).map(() => Array(3).fill(null))
|
||||||
|
);
|
||||||
|
const [selectedNumber, setSelectedNumber] = useState<number | null>(null);
|
||||||
|
const [validationResult, setValidationResult] = useState<{
|
||||||
|
isValid: boolean;
|
||||||
|
message: string;
|
||||||
|
errors: { row?: number; col?: number; type: string }[];
|
||||||
|
} | null>(null);
|
||||||
|
const [showHint, setShowHint] = useState(false);
|
||||||
|
|
||||||
|
// Get all numbers that have been placed
|
||||||
|
const getPlacedNumbers = useCallback((): Set<number> => {
|
||||||
|
const placed = new Set<number>();
|
||||||
|
for (let r = 0; r < gridSize; r++) {
|
||||||
|
for (let c = 0; c < gridSize; c++) {
|
||||||
|
if (grid[r]?.[c] !== null && grid[r]?.[c] !== undefined) {
|
||||||
|
placed.add(grid[r][c] as number);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return placed;
|
||||||
|
}, [grid, gridSize]);
|
||||||
|
|
||||||
|
// Reset the puzzle
|
||||||
|
const resetPuzzle = useCallback(() => {
|
||||||
|
setGrid(Array(gridSize).fill(null).map(() => Array(gridSize).fill(null)));
|
||||||
|
setSelectedNumber(null);
|
||||||
|
setValidationResult(null);
|
||||||
|
setShowHint(false);
|
||||||
|
}, [gridSize]);
|
||||||
|
|
||||||
|
// Change grid size
|
||||||
|
const handleGridSizeChange = (value: number[]) => {
|
||||||
|
const newSize = value[0];
|
||||||
|
setGridSize(newSize);
|
||||||
|
setRowLabels(Array(newSize).fill('A').map((_, i) => (i % 2 === 0 ? 'A' : 'D') as Label));
|
||||||
|
setColLabels(Array(newSize).fill('A').map((_, i) => (i % 2 === 0 ? 'A' : 'D') as Label));
|
||||||
|
setGrid(Array(newSize).fill(null).map(() => Array(newSize).fill(null)));
|
||||||
|
setSelectedNumber(null);
|
||||||
|
setValidationResult(null);
|
||||||
|
setShowHint(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Toggle row label
|
||||||
|
const toggleRowLabel = (index: number) => {
|
||||||
|
setRowLabels(prev => {
|
||||||
|
const newLabels = [...prev];
|
||||||
|
newLabels[index] = newLabels[index] === 'A' ? 'D' : 'A';
|
||||||
|
return newLabels;
|
||||||
|
});
|
||||||
|
setValidationResult(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Toggle column label
|
||||||
|
const toggleColLabel = (index: number) => {
|
||||||
|
setColLabels(prev => {
|
||||||
|
const newLabels = [...prev];
|
||||||
|
newLabels[index] = newLabels[index] === 'A' ? 'D' : 'A';
|
||||||
|
return newLabels;
|
||||||
|
});
|
||||||
|
setValidationResult(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Randomize labels
|
||||||
|
const randomizeLabels = () => {
|
||||||
|
setRowLabels(Array(gridSize).fill(null).map(() => (Math.random() > 0.5 ? 'A' : 'D') as Label));
|
||||||
|
setColLabels(Array(gridSize).fill(null).map(() => (Math.random() > 0.5 ? 'A' : 'D') as Label));
|
||||||
|
resetPuzzle();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle cell click
|
||||||
|
const handleCellClick = (row: number, col: number) => {
|
||||||
|
if (selectedNumber === null) {
|
||||||
|
// If clicking on a cell with a number, remove it
|
||||||
|
if (grid[row][col] !== null) {
|
||||||
|
setGrid(prev => {
|
||||||
|
const newGrid = prev.map(r => [...r]);
|
||||||
|
newGrid[row][col] = null;
|
||||||
|
return newGrid;
|
||||||
|
});
|
||||||
|
setValidationResult(null);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Place the selected number
|
||||||
|
setGrid(prev => {
|
||||||
|
const newGrid = prev.map(r => [...r]);
|
||||||
|
// Remove the number from any previous position
|
||||||
|
for (let r = 0; r < gridSize; r++) {
|
||||||
|
for (let c = 0; c < gridSize; c++) {
|
||||||
|
if (newGrid[r][c] === selectedNumber) {
|
||||||
|
newGrid[r][c] = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
newGrid[row][col] = selectedNumber;
|
||||||
|
return newGrid;
|
||||||
|
});
|
||||||
|
setSelectedNumber(null);
|
||||||
|
setValidationResult(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check if puzzle is complete and valid
|
||||||
|
const validatePuzzle = () => {
|
||||||
|
const errors: { row?: number; col?: number; type: string }[] = [];
|
||||||
|
const totalNumbers = gridSize * gridSize;
|
||||||
|
const placed = getPlacedNumbers();
|
||||||
|
|
||||||
|
// Check if all numbers are placed
|
||||||
|
if (placed.size !== totalNumbers) {
|
||||||
|
setValidationResult({
|
||||||
|
isValid: false,
|
||||||
|
message: `Place all numbers from 1 to ${totalNumbers} first.`,
|
||||||
|
errors: []
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check rows
|
||||||
|
for (let r = 0; r < gridSize; r++) {
|
||||||
|
const row = grid[r];
|
||||||
|
const label = rowLabels[r];
|
||||||
|
|
||||||
|
for (let c = 0; c < gridSize - 1; c++) {
|
||||||
|
const current = row[c] as number;
|
||||||
|
const next = row[c + 1] as number;
|
||||||
|
|
||||||
|
if (label === 'A' && current >= next) {
|
||||||
|
errors.push({ row: r, type: 'row' });
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (label === 'D' && current <= next) {
|
||||||
|
errors.push({ row: r, type: 'row' });
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check columns
|
||||||
|
for (let c = 0; c < gridSize; c++) {
|
||||||
|
const label = colLabels[c];
|
||||||
|
|
||||||
|
for (let r = 0; r < gridSize - 1; r++) {
|
||||||
|
const current = grid[r][c] as number;
|
||||||
|
const next = grid[r + 1][c] as number;
|
||||||
|
|
||||||
|
if (label === 'A' && current >= next) {
|
||||||
|
errors.push({ col: c, type: 'col' });
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (label === 'D' && current <= next) {
|
||||||
|
errors.push({ col: c, type: 'col' });
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errors.length === 0) {
|
||||||
|
setValidationResult({
|
||||||
|
isValid: true,
|
||||||
|
message: "🎉 Congratulations! The puzzle is solved correctly!",
|
||||||
|
errors: []
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setValidationResult({
|
||||||
|
isValid: false,
|
||||||
|
message: `Found ${errors.length} constraint violation(s). Check highlighted rows/columns.`,
|
||||||
|
errors
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check for circular constraint (unsolvable pattern)
|
||||||
|
const checkSolvability = (): { solvable: boolean; reason?: string } => {
|
||||||
|
// Check for the circular constraint pattern described in the paper
|
||||||
|
// If rows i < j have labels D,A (or A,D) and columns p < q have labels A,D (or D,A),
|
||||||
|
// then we have a circular constraint making it unsolvable
|
||||||
|
|
||||||
|
for (let i = 0; i < gridSize; i++) {
|
||||||
|
for (let j = i + 1; j < gridSize; j++) {
|
||||||
|
// Check if rows i and j have opposite labels
|
||||||
|
if ((rowLabels[i] === 'D' && rowLabels[j] === 'A') ||
|
||||||
|
(rowLabels[i] === 'A' && rowLabels[j] === 'D')) {
|
||||||
|
|
||||||
|
for (let p = 0; p < gridSize; p++) {
|
||||||
|
for (let q = p + 1; q < gridSize; q++) {
|
||||||
|
// Check if columns p and q have opposite labels in the "wrong" way
|
||||||
|
const rowPattern = rowLabels[i] === 'D' ? 'DA' : 'AD';
|
||||||
|
const colPattern = colLabels[p] === 'A' ? 'AD' : 'DA';
|
||||||
|
|
||||||
|
// The circular constraint occurs when:
|
||||||
|
// rows i,j are D,A and columns p,q are A,D (or vice versa pattern)
|
||||||
|
if ((rowLabels[i] === 'D' && rowLabels[j] === 'A' &&
|
||||||
|
colLabels[p] === 'A' && colLabels[q] === 'D') ||
|
||||||
|
(rowLabels[i] === 'A' && rowLabels[j] === 'D' &&
|
||||||
|
colLabels[p] === 'D' && colLabels[q] === 'A')) {
|
||||||
|
return {
|
||||||
|
solvable: false,
|
||||||
|
reason: `Circular constraint detected: rows ${i+1},${j+1} (${rowLabels[i]},${rowLabels[j]}) and columns ${p+1},${q+1} (${colLabels[p]},${colLabels[q]}) create an impossible configuration.`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { solvable: true };
|
||||||
|
};
|
||||||
|
|
||||||
|
const solvabilityCheck = checkSolvability();
|
||||||
|
const placedNumbers = getPlacedNumbers();
|
||||||
|
const totalNumbers = gridSize * gridSize;
|
||||||
|
|
||||||
|
const hasRowError = (row: number) =>
|
||||||
|
validationResult?.errors.some(e => e.row === row && e.type === 'row');
|
||||||
|
|
||||||
|
const hasColError = (col: number) =>
|
||||||
|
validationResult?.errors.some(e => e.col === col && e.type === 'col');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full max-w-4xl mx-auto p-4 space-y-6">
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
Ascending-Descending Grid Puzzle
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<p className="text-muted-foreground">
|
||||||
|
Fill the grid with numbers 1 to n² such that rows labeled <Badge variant="secondary">A</Badge> are
|
||||||
|
in <strong>ascending</strong> order (left→right) and rows labeled <Badge variant="outline">D</Badge> are
|
||||||
|
in <strong>descending</strong> order. The same applies to columns (top→bottom).
|
||||||
|
</p>
|
||||||
|
<p className="text-muted-foreground text-sm">
|
||||||
|
Click on row/column labels to toggle between A and D. Click a number, then click a cell to place it.
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Controls */}
|
||||||
|
<Card>
|
||||||
|
<CardContent className="pt-6 space-y-4">
|
||||||
|
<div className="flex flex-wrap gap-4 items-center">
|
||||||
|
<div className="flex-1 min-w-[200px]">
|
||||||
|
<label className="text-sm font-medium mb-2 block">
|
||||||
|
Grid Size: {gridSize} × {gridSize}
|
||||||
|
</label>
|
||||||
|
<Slider
|
||||||
|
value={[gridSize]}
|
||||||
|
onValueChange={handleGridSizeChange}
|
||||||
|
min={2}
|
||||||
|
max={5}
|
||||||
|
step={1}
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button onClick={randomizeLabels} variant="outline" size="sm">
|
||||||
|
<Shuffle className="w-4 h-4 mr-1" /> Randomize
|
||||||
|
</Button>
|
||||||
|
<Button onClick={resetPuzzle} variant="outline" size="sm">
|
||||||
|
<RotateCcw className="w-4 h-4 mr-1" /> Reset
|
||||||
|
</Button>
|
||||||
|
<Button onClick={() => setShowHint(!showHint)} variant="outline" size="sm">
|
||||||
|
<Lightbulb className="w-4 h-4 mr-1" /> {showHint ? 'Hide' : 'Show'} Hint
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{showHint && (
|
||||||
|
<div className="p-3 bg-muted rounded-lg text-sm">
|
||||||
|
<strong>Hint:</strong> Some configurations are unsolvable! A circular constraint occurs when
|
||||||
|
rows i {"<"} j have labels D,A (in that order) while columns p {"<"} q have labels A,D (in that order),
|
||||||
|
or vice versa. Try to avoid these patterns.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!solvabilityCheck.solvable && (
|
||||||
|
<div className="p-3 bg-destructive/10 border border-destructive/20 rounded-lg text-sm text-destructive">
|
||||||
|
⚠️ {solvabilityCheck.reason}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Number Palette */}
|
||||||
|
<Card>
|
||||||
|
<CardContent className="pt-6">
|
||||||
|
<div className="mb-2 text-sm font-medium">
|
||||||
|
Select a number to place ({placedNumbers.size}/{totalNumbers} placed):
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{Array.from({ length: totalNumbers }, (_, i) => i + 1).map(num => {
|
||||||
|
const isPlaced = placedNumbers.has(num);
|
||||||
|
const isSelected = selectedNumber === num;
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
key={num}
|
||||||
|
variant={isSelected ? "default" : isPlaced ? "ghost" : "outline"}
|
||||||
|
size="sm"
|
||||||
|
className={`w-10 h-10 ${isPlaced && !isSelected ? 'opacity-40' : ''}`}
|
||||||
|
onClick={() => setSelectedNumber(isSelected ? null : num)}
|
||||||
|
>
|
||||||
|
{num}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Grid */}
|
||||||
|
<div className="flex justify-center">
|
||||||
|
<div className="inline-block">
|
||||||
|
{/* Column labels */}
|
||||||
|
<div className="flex">
|
||||||
|
<div className="w-10 h-10" /> {/* Empty corner */}
|
||||||
|
{colLabels.map((label, c) => (
|
||||||
|
<button
|
||||||
|
key={`col-${c}`}
|
||||||
|
onClick={() => toggleColLabel(c)}
|
||||||
|
className={`w-12 h-10 flex items-center justify-center font-bold text-lg cursor-pointer transition-colors rounded-t
|
||||||
|
${hasColError(c) ? 'bg-destructive/20 text-destructive' : 'hover:bg-muted'}
|
||||||
|
${label === 'A' ? 'text-green-600 dark:text-green-400' : 'text-blue-600 dark:text-blue-400'}`}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Grid rows */}
|
||||||
|
{Array.from({ length: gridSize }, (_, r) => (
|
||||||
|
<div key={`row-${r}`} className="flex">
|
||||||
|
{/* Row label */}
|
||||||
|
<button
|
||||||
|
onClick={() => toggleRowLabel(r)}
|
||||||
|
className={`w-10 h-12 flex items-center justify-center font-bold text-lg cursor-pointer transition-colors rounded-l
|
||||||
|
${hasRowError(r) ? 'bg-destructive/20 text-destructive' : 'hover:bg-muted'}
|
||||||
|
${rowLabels[r] === 'A' ? 'text-green-600 dark:text-green-400' : 'text-blue-600 dark:text-blue-400'}`}
|
||||||
|
>
|
||||||
|
{rowLabels[r]}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Grid cells */}
|
||||||
|
{Array.from({ length: gridSize }, (_, c) => {
|
||||||
|
const value = grid[r]?.[c];
|
||||||
|
const hasError = hasRowError(r) || hasColError(c);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={`cell-${r}-${c}`}
|
||||||
|
onClick={() => handleCellClick(r, c)}
|
||||||
|
className={`w-12 h-12 border border-border flex items-center justify-center text-lg font-semibold
|
||||||
|
transition-all cursor-pointer
|
||||||
|
${value !== null ? 'bg-primary/10' : 'bg-background hover:bg-muted'}
|
||||||
|
${hasError && validationResult ? 'bg-destructive/10' : ''}
|
||||||
|
${selectedNumber !== null ? 'hover:bg-primary/20' : ''}
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
{value !== null ? value : ''}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Validate Button */}
|
||||||
|
<div className="flex justify-center">
|
||||||
|
<Button onClick={validatePuzzle} size="lg" disabled={placedNumbers.size !== totalNumbers}>
|
||||||
|
<Check className="w-5 h-5 mr-2" /> Check Solution
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Validation Result */}
|
||||||
|
{validationResult && (
|
||||||
|
<Card className={validationResult.isValid ? 'border-green-500' : 'border-destructive'}>
|
||||||
|
<CardContent className="pt-6">
|
||||||
|
<p className={`text-center font-medium ${validationResult.isValid ? 'text-green-600 dark:text-green-400' : 'text-destructive'}`}>
|
||||||
|
{validationResult.message}
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Instructions */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-lg">How to Play</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-2 text-sm text-muted-foreground">
|
||||||
|
<ol className="list-decimal list-inside space-y-1">
|
||||||
|
<li>The grid has row labels on the left and column labels on top</li>
|
||||||
|
<li><Badge variant="secondary" className="mx-1">A</Badge> means ascending order (numbers increase left→right or top→bottom)</li>
|
||||||
|
<li><Badge variant="outline" className="mx-1">D</Badge> means descending order (numbers decrease left→right or top→bottom)</li>
|
||||||
|
<li>Click on labels to toggle between A and D</li>
|
||||||
|
<li>Click a number from the palette, then click a cell to place it</li>
|
||||||
|
<li>Click a placed number in the grid to remove it</li>
|
||||||
|
<li>Fill all cells with numbers 1 to n² satisfying all constraints</li>
|
||||||
|
<li>Some label configurations are unsolvable — the puzzle will warn you!</li>
|
||||||
|
</ol>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{showSocialShare && (
|
||||||
|
<SocialShare
|
||||||
|
title="Ascending-Descending Grid Puzzle"
|
||||||
|
description="Fill the grid so each row and column follows its ascending or descending constraint!"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AscDescGridPuzzle;
|
||||||
14
src/pages/AscDescGridPuzzlePage.tsx
Normal file
14
src/pages/AscDescGridPuzzlePage.tsx
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
import Layout from "@/components/Layout";
|
||||||
|
import AscDescGridPuzzle from "@/components/AscDescGridPuzzle";
|
||||||
|
|
||||||
|
const AscDescGridPuzzlePage = () => {
|
||||||
|
return (
|
||||||
|
<Layout>
|
||||||
|
<div className="py-8">
|
||||||
|
<AscDescGridPuzzle />
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AscDescGridPuzzlePage;
|
||||||
|
|
@ -95,6 +95,16 @@ const Puzzles = () => {
|
||||||
difficulty: "Advanced" as const,
|
difficulty: "Advanced" as const,
|
||||||
duration: "10-30 min",
|
duration: "10-30 min",
|
||||||
participants: "1 player"
|
participants: "1 player"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "asc-desc-grid",
|
||||||
|
title: "Ascending-Descending Grid Puzzle",
|
||||||
|
description: "Fill an n×n grid with numbers 1 to n² such that each row and column follows its ascending or descending constraint.",
|
||||||
|
path: "/puzzles/asc-desc-grid",
|
||||||
|
tags: ["Logic", "Constraints", "Numbers", "Grid"],
|
||||||
|
difficulty: "Intermediate" as const,
|
||||||
|
duration: "5-15 min",
|
||||||
|
participants: "1 player"
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue