diff --git a/src/App.tsx b/src/App.tsx index 8c8f166..0c4261a 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -61,6 +61,7 @@ import BurnsidesLemmaPage from './pages/BurnsidesLemmaPage'; import CubeColoringPage from './pages/CubeColoringPage'; import PresentsPuzzlePage from './pages/PresentsPuzzlePage'; import FerrersRogersRamanujanPage from './pages/FerrersRogersRamanujanPage'; +import DominoRetilingPuzzlePage from './pages/DominoRetilingPuzzlePage'; const queryClient = new QueryClient(); @@ -132,6 +133,7 @@ const App = () => ( } /> } /> } /> + } /> } /> } /> } /> diff --git a/src/components/DominoRetilingPuzzle.tsx b/src/components/DominoRetilingPuzzle.tsx new file mode 100644 index 0000000..a261cd1 --- /dev/null +++ b/src/components/DominoRetilingPuzzle.tsx @@ -0,0 +1,428 @@ +import React, { useState, useCallback, useEffect } from 'react'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Button } from '@/components/ui/button'; +import { Badge } from '@/components/ui/badge'; +import { CheckCircle, XCircle, Info, ChevronDown, ChevronUp, RotateCcw, Shuffle, Lightbulb } from 'lucide-react'; +import SocialShare from '@/components/SocialShare'; + +interface Domino { + id: number; + row: number; + col: number; + isHorizontal: boolean; +} + +interface DominoRetilingPuzzleProps { + showSocialShare?: boolean; +} + +const DominoRetilingPuzzle: React.FC = ({ showSocialShare = true }) => { + const [dominoes, setDominoes] = useState([]); + const [selectedDomino, setSelectedDomino] = useState(null); + const [showInstructions, setShowInstructions] = useState(false); + const [moveCount, setMoveCount] = useState(0); + const [showHint, setShowHint] = useState(false); + + const BOARD_ROWS = 8; + const BOARD_COLS = 9; // 8 + 1 extra square on top row + + // Generate a random valid tiling of the 8x8 board + const generateRandomTiling = useCallback(() => { + const newDominoes: Domino[] = []; + const occupied = new Set(); + let dominoId = 0; + + // Fill the 8x8 board with random dominoes + for (let row = 0; row < 8; row++) { + for (let col = 0; col < 8; col++) { + const key = `${row},${col}`; + if (occupied.has(key)) continue; + + // Randomly choose horizontal or vertical + const canHorizontal = col + 1 < 8 && !occupied.has(`${row},${col + 1}`); + const canVertical = row + 1 < 8 && !occupied.has(`${row + 1},${col}`); + + if (!canHorizontal && !canVertical) continue; + + let isHorizontal: boolean; + if (canHorizontal && canVertical) { + isHorizontal = Math.random() > 0.5; + } else { + isHorizontal = canHorizontal; + } + + newDominoes.push({ + id: dominoId++, + row, + col, + isHorizontal + }); + + occupied.add(key); + if (isHorizontal) { + occupied.add(`${row},${col + 1}`); + } else { + occupied.add(`${row + 1},${col}`); + } + } + } + + setDominoes(newDominoes); + setSelectedDomino(null); + setMoveCount(0); + }, []); + + useEffect(() => { + generateRandomTiling(); + }, [generateRandomTiling]); + + // Get all cells occupied by dominoes + const getOccupiedCells = useCallback((excludeDominoId?: number): Set => { + const occupied = new Set(); + dominoes.forEach(d => { + if (d.id === excludeDominoId) return; + occupied.add(`${d.row},${d.col}`); + if (d.isHorizontal) { + occupied.add(`${d.row},${d.col + 1}`); + } else { + occupied.add(`${d.row + 1},${d.col}`); + } + }); + return occupied; + }, [dominoes]); + + // Check if a position is valid for the augmented board + const isValidCell = (row: number, col: number): boolean => { + if (row < 0 || row >= BOARD_ROWS) return false; + if (row === 0) return col >= 0 && col < BOARD_COLS; + return col >= 0 && col < 8; + }; + + // Check if we can place a domino at a position + const canPlaceDomino = (row: number, col: number, isHorizontal: boolean, excludeDominoId: number): boolean => { + const cell1Row = row; + const cell1Col = col; + const cell2Row = isHorizontal ? row : row + 1; + const cell2Col = isHorizontal ? col + 1 : col; + + if (!isValidCell(cell1Row, cell1Col) || !isValidCell(cell2Row, cell2Col)) return false; + + const occupied = getOccupiedCells(excludeDominoId); + return !occupied.has(`${cell1Row},${cell1Col}`) && !occupied.has(`${cell2Row},${cell2Col}`); + }; + + // Get valid placements for the selected domino + const getValidPlacements = useCallback((dominoId: number): { row: number; col: number; isHorizontal: boolean }[] => { + const placements: { row: number; col: number; isHorizontal: boolean }[] = []; + + for (let row = 0; row < BOARD_ROWS; row++) { + const maxCol = row === 0 ? BOARD_COLS : 8; + for (let col = 0; col < maxCol; col++) { + // Check horizontal placement + if (canPlaceDomino(row, col, true, dominoId)) { + placements.push({ row, col, isHorizontal: true }); + } + // Check vertical placement + if (canPlaceDomino(row, col, false, dominoId)) { + placements.push({ row, col, isHorizontal: false }); + } + } + } + + return placements; + }, [getOccupiedCells]); + + // Handle clicking on a domino + const handleDominoClick = (dominoId: number) => { + if (selectedDomino === dominoId) { + setSelectedDomino(null); + } else { + setSelectedDomino(dominoId); + } + }; + + // Handle clicking on a valid placement + const handlePlacementClick = (row: number, col: number, isHorizontal: boolean) => { + if (selectedDomino === null) return; + + setDominoes(prev => prev.map(d => + d.id === selectedDomino + ? { ...d, row, col, isHorizontal } + : d + )); + setSelectedDomino(null); + setMoveCount(prev => prev + 1); + }; + + // Check if puzzle is solved (all dominoes horizontal) + const isSolved = dominoes.length > 0 && dominoes.every(d => d.isHorizontal); + + // Get domino color based on orientation + const getDominoColor = (isHorizontal: boolean, isSelected: boolean): string => { + if (isSelected) return 'bg-amber-400 border-amber-600'; + if (isHorizontal) return 'bg-emerald-500 border-emerald-700'; + return 'bg-rose-400 border-rose-600'; + }; + + // Render the board + const renderBoard = () => { + const cells: JSX.Element[] = []; + const validPlacements = selectedDomino !== null ? getValidPlacements(selectedDomino) : []; + const occupied = getOccupiedCells(); + + // Render cells + for (let row = 0; row < BOARD_ROWS; row++) { + const maxCol = row === 0 ? BOARD_COLS : 8; + for (let col = 0; col < maxCol; col++) { + const isOccupied = occupied.has(`${row},${col}`); + const isExtraSquare = row === 0 && col === 8; + + // Check if this is a valid horizontal placement start + const validHorizontal = validPlacements.find(p => p.row === row && p.col === col && p.isHorizontal); + const validVertical = validPlacements.find(p => p.row === row && p.col === col && !p.isHorizontal); + + const cellStyle: React.CSSProperties = { + position: 'absolute', + left: col * 50, + top: row * 50, + width: 50, + height: 50, + }; + + cells.push( +
+ ); + + // Render valid placement indicators + if (validHorizontal && !isOccupied) { + cells.push( +
handlePlacementClick(row, col, true)} + title="Place horizontally" + /> + ); + } + if (validVertical && !isOccupied) { + cells.push( +
handlePlacementClick(row, col, false)} + title="Place vertically" + /> + ); + } + } + } + + // Render dominoes + dominoes.forEach(d => { + const isSelected = selectedDomino === d.id; + const width = d.isHorizontal ? 96 : 46; + const height = d.isHorizontal ? 46 : 96; + + cells.push( +
handleDominoClick(d.id)} + > +
+
+
+
+ ); + }); + + return cells; + }; + + const horizontalCount = dominoes.filter(d => d.isHorizontal).length; + const verticalCount = dominoes.filter(d => !d.isHorizontal).length; + + return ( +
+ + + + Domino Retiling Puzzle + +

+ Can you retile the augmented board so all dominoes are horizontal? +

+
+ +
+

+ An 8×8 board is tiled with 32 dominoes. An extra square is added to the top-right. + Move dominoes one at a time to make all dominoes horizontal. +

+ +
+ + +
+ +
+ + Moves: {moveCount} + + + Horizontal: {horizontalCount} + + + Vertical: {verticalCount} + +
+
+
+
+ + {showHint && ( + + +
+ +
+

Hint:

+

Consider the coloring argument: in a standard chessboard coloring, each horizontal domino covers one black and one white square. + But on the augmented board (8×8 + 1 extra square), the parity changes. Think about what this means for the possibility of an all-horizontal tiling!

+
+
+
+
+ )} + + {/* Instructions */} + + setShowInstructions(!showInstructions)} + > +
+ Instructions + {showInstructions ? ( + + ) : ( + + )} +
+
+ {showInstructions && ( + +
+
    +
  • Click on a domino to select it (turns yellow)
  • +
  • Valid placement positions will appear as dashed outlines
  • +
  • Click on a valid position to move the domino there
  • +
  • You can only move a domino if there are two adjacent empty squares to receive it
  • +
  • Goal: Make all 32 dominoes horizontal
  • +
+ +
+
+
+ Horizontal +
+
+
+ Vertical +
+
+
+ Selected +
+
+
+ + )} + + + {/* Board */} + + +
+
+ {renderBoard()} +
+
+ {selectedDomino !== null && ( +

+ Domino selected. Click on a valid placement (dashed outline) to move it, or click the domino again to deselect. +

+ )} +
+
+ + {/* Result */} + {isSolved && ( + + +
+ +
+

+ Wait... You solved it? That shouldn't be possible! +

+

+ Actually, this puzzle is impossible to solve! The extra square creates a parity imbalance that prevents an all-horizontal tiling. +

+
+
+
+
+ )} + + {/* Social Share */} + {showSocialShare && ( + + )} +
+ ); +}; + +export default DominoRetilingPuzzle; \ No newline at end of file diff --git a/src/pages/DominoRetilingPuzzlePage.tsx b/src/pages/DominoRetilingPuzzlePage.tsx new file mode 100644 index 0000000..e3f7af7 --- /dev/null +++ b/src/pages/DominoRetilingPuzzlePage.tsx @@ -0,0 +1,28 @@ +import Layout from '@/components/Layout'; +import DominoRetilingPuzzle from '@/components/DominoRetilingPuzzle'; +import { useLocation, Link } from 'react-router-dom'; +import { ArrowLeft } from 'lucide-react'; + +const DominoRetilingPuzzlePage = () => { + const location = useLocation(); + const fromPuzzles = location.search.includes('from=puzzles'); + + return ( + + {fromPuzzles && ( +
+ + + Back to Puzzles + +
+ )} + +
+ ); +}; + +export default DominoRetilingPuzzlePage; \ No newline at end of file diff --git a/src/pages/theme-pages/Puzzles.tsx b/src/pages/theme-pages/Puzzles.tsx index aec26e1..fe0b0ba 100644 --- a/src/pages/theme-pages/Puzzles.tsx +++ b/src/pages/theme-pages/Puzzles.tsx @@ -85,6 +85,16 @@ const Puzzles = () => { difficulty: "Intermediate" as const, duration: "10-20 min", participants: "1 player" + }, + { + id: "domino-retiling", + title: "Domino Retiling Puzzle", + description: "An 8×8 board tiled with dominoes gets an extra square. Can you retile so all dominoes are horizontal?", + path: "/puzzles/domino-retiling", + tags: ["Parity", "Impossibility", "Logic", "Tiling"], + difficulty: "Advanced" as const, + duration: "10-30 min", + participants: "1 player" } ];