diff --git a/src/App.tsx b/src/App.tsx index 5453ec7..9227a21 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -64,6 +64,7 @@ import FerrersRogersRamanujanPage from './pages/FerrersRogersRamanujanPage'; import DominoRetilingPuzzlePage from './pages/DominoRetilingPuzzlePage'; import RentDivisionPuzzlePage from './pages/RentDivisionPuzzlePage'; import BagchalGamePage from './pages/BagchalGamePage'; +import AscDescGridPuzzlePage from './pages/AscDescGridPuzzlePage'; const queryClient = new QueryClient(); @@ -138,6 +139,7 @@ const App = () => ( } /> } /> } /> + } /> } /> } /> } /> diff --git a/src/components/AscDescGridPuzzle.tsx b/src/components/AscDescGridPuzzle.tsx new file mode 100644 index 0000000..acb3e8b --- /dev/null +++ b/src/components/AscDescGridPuzzle.tsx @@ -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(['A', 'D', 'A']); + const [colLabels, setColLabels] = useState(['A', 'D', 'A']); + const [grid, setGrid] = useState<(number | null)[][]>( + Array(3).fill(null).map(() => Array(3).fill(null)) + ); + const [selectedNumber, setSelectedNumber] = useState(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 => { + const placed = new Set(); + 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 ( +
+ + + + Ascending-Descending Grid Puzzle + + + +

+ Fill the grid with numbers 1 to n² such that rows labeled A are + in ascending order (left→right) and rows labeled D are + in descending order. The same applies to columns (top→bottom). +

+

+ Click on row/column labels to toggle between A and D. Click a number, then click a cell to place it. +

+
+
+ + {/* Controls */} + + +
+
+ + +
+
+ + + +
+
+ + {showHint && ( +
+ Hint: 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. +
+ )} + + {!solvabilityCheck.solvable && ( +
+ ⚠️ {solvabilityCheck.reason} +
+ )} +
+
+ + {/* Number Palette */} + + +
+ Select a number to place ({placedNumbers.size}/{totalNumbers} placed): +
+
+ {Array.from({ length: totalNumbers }, (_, i) => i + 1).map(num => { + const isPlaced = placedNumbers.has(num); + const isSelected = selectedNumber === num; + return ( + + ); + })} +
+
+
+ + {/* Grid */} +
+
+ {/* Column labels */} +
+
{/* Empty corner */} + {colLabels.map((label, c) => ( + + ))} +
+ + {/* Grid rows */} + {Array.from({ length: gridSize }, (_, r) => ( +
+ {/* Row label */} + + + {/* Grid cells */} + {Array.from({ length: gridSize }, (_, c) => { + const value = grid[r]?.[c]; + const hasError = hasRowError(r) || hasColError(c); + + return ( + + ); + })} +
+ ))} +
+
+ + {/* Validate Button */} +
+ +
+ + {/* Validation Result */} + {validationResult && ( + + +

+ {validationResult.message} +

+
+
+ )} + + {/* Instructions */} + + + How to Play + + +
    +
  1. The grid has row labels on the left and column labels on top
  2. +
  3. A means ascending order (numbers increase left→right or top→bottom)
  4. +
  5. D means descending order (numbers decrease left→right or top→bottom)
  6. +
  7. Click on labels to toggle between A and D
  8. +
  9. Click a number from the palette, then click a cell to place it
  10. +
  11. Click a placed number in the grid to remove it
  12. +
  13. Fill all cells with numbers 1 to n² satisfying all constraints
  14. +
  15. Some label configurations are unsolvable — the puzzle will warn you!
  16. +
+
+
+ + {showSocialShare && ( + + )} +
+ ); +}; + +export default AscDescGridPuzzle; diff --git a/src/pages/AscDescGridPuzzlePage.tsx b/src/pages/AscDescGridPuzzlePage.tsx new file mode 100644 index 0000000..7963768 --- /dev/null +++ b/src/pages/AscDescGridPuzzlePage.tsx @@ -0,0 +1,14 @@ +import Layout from "@/components/Layout"; +import AscDescGridPuzzle from "@/components/AscDescGridPuzzle"; + +const AscDescGridPuzzlePage = () => { + return ( + +
+ +
+
+ ); +}; + +export default AscDescGridPuzzlePage; diff --git a/src/pages/theme-pages/Puzzles.tsx b/src/pages/theme-pages/Puzzles.tsx index fe0b0ba..14a38a9 100644 --- a/src/pages/theme-pages/Puzzles.tsx +++ b/src/pages/theme-pages/Puzzles.tsx @@ -95,6 +95,16 @@ const Puzzles = () => { difficulty: "Advanced" as const, duration: "10-30 min", 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" } ];