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, ChevronDown, ChevronUp, 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 [boardSize, setBoardSize] = useState(8); 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 [hoverPlacement, setHoverPlacement] = useState<{ row: number; col: number; isHorizontal: boolean } | null>(null); const BOARD_ROWS = boardSize; const BOARD_COLS = boardSize + 1; // n + 1 extra square on top row // Generate a random valid tiling of the n×n board using backtracking const generateRandomTiling = useCallback(() => { const newDominoes: Domino[] = []; const occupied = new Set(); let dominoId = 0; // Helper to find first uncovered cell const findFirstUncovered = (): { row: number; col: number } | null => { for (let row = 0; row < boardSize; row++) { for (let col = 0; col < boardSize; col++) { if (!occupied.has(`${row},${col}`)) { return { row, col }; } } } return null; }; // Greedy fill with backtracking if needed while (true) { const cell = findFirstUncovered(); if (!cell) break; // All covered const { row, col } = cell; // Try to place a domino here const canHorizontal = col + 1 < boardSize && !occupied.has(`${row},${col + 1}`); const canVertical = row + 1 < boardSize && !occupied.has(`${row + 1},${col}`); if (!canHorizontal && !canVertical) { // Stuck - this shouldn't happen with even board sizes, but reset if it does newDominoes.length = 0; occupied.clear(); dominoId = 0; continue; } let isHorizontal: boolean; if (canHorizontal && canVertical) { isHorizontal = Math.random() > 0.5; } else { isHorizontal = canHorizontal; } newDominoes.push({ id: dominoId++, row, col, isHorizontal }); occupied.add(`${row},${col}`); if (isHorizontal) { occupied.add(`${row},${col + 1}`); } else { occupied.add(`${row + 1},${col}`); } } setDominoes(newDominoes); setSelectedDomino(null); setMoveCount(0); setHoverPlacement(null); }, [boardSize]); 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 = useCallback((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 < boardSize; }, [BOARD_ROWS, BOARD_COLS, boardSize]); // Check if we can place a domino at a position const canPlaceDomino = useCallback((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}`); }, [getOccupiedCells]); // Handle clicking on a domino const handleDominoClick = (dominoId: number) => { if (selectedDomino === dominoId) { setSelectedDomino(null); setHoverPlacement(null); } else { setSelectedDomino(dominoId); setHoverPlacement(null); } }; // 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); setHoverPlacement(null); setMoveCount(prev => prev + 1); }; // Handle cell hover const handleCellHover = (row: number, col: number) => { if (selectedDomino === null) return; // Check which placements are valid for this cell const canH = canPlaceDomino(row, col, true, selectedDomino); const canV = canPlaceDomino(row, col, false, selectedDomino); if (canH && canV) { // Prefer horizontal setHoverPlacement({ row, col, isHorizontal: true }); } else if (canH) { setHoverPlacement({ row, col, isHorizontal: true }); } else if (canV) { setHoverPlacement({ row, col, isHorizontal: false }); } else { setHoverPlacement(null); } }; // 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): string => { if (isHorizontal) return 'bg-emerald-500 border-emerald-700'; return 'bg-rose-400 border-rose-600'; }; const cellSize = Math.max(32, Math.min(44, 480 / boardSize)); // Render the board const renderBoard = () => { const cells: JSX.Element[] = []; // Render the inaccessible shaded region (last column except top cell) for (let row = 1; row < BOARD_ROWS; row++) { cells.push(
); } // Render cells for (let row = 0; row < BOARD_ROWS; row++) { const maxCol = row === 0 ? BOARD_COLS : boardSize; for (let col = 0; col < maxCol; col++) { const isExtraSquare = row === 0 && col === boardSize; const cellStyle: React.CSSProperties = { position: 'absolute', left: col * cellSize, top: row * cellSize, width: cellSize, height: cellSize, }; cells.push(
handleCellHover(row, col)} onMouseLeave={() => setHoverPlacement(null)} onClick={() => { if (hoverPlacement && hoverPlacement.row === row && hoverPlacement.col === col) { handlePlacementClick(hoverPlacement.row, hoverPlacement.col, hoverPlacement.isHorizontal); } }} /> ); } } // Render dominoes (except selected one) dominoes.forEach(d => { if (d.id === selectedDomino) return; // Hide selected domino const width = d.isHorizontal ? cellSize * 2 - 4 : cellSize - 4; const height = d.isHorizontal ? cellSize - 4 : cellSize * 2 - 4; cells.push(
handleDominoClick(d.id)} >
); }); // Render hover preview domino if (hoverPlacement && selectedDomino !== null) { const width = hoverPlacement.isHorizontal ? cellSize * 2 - 4 : cellSize - 4; const height = hoverPlacement.isHorizontal ? cellSize - 4 : cellSize * 2 - 4; cells.push(
); } return cells; }; const horizontalCount = dominoes.filter(d => d.isHorizontal).length; const verticalCount = dominoes.filter(d => !d.isHorizontal).length; const numDominoes = (boardSize * boardSize) / 2; return (
Domino Retiling Puzzle

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

A {boardSize}×{boardSize} board is tiled with {numDominoes} dominoes. An extra square is added to the top-right. Move dominoes one at a time to make all dominoes horizontal.

setBoardSize(parseInt(e.target.value))} className="w-24 h-2 bg-muted rounded-lg appearance-none cursor-pointer" />
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 pick it up (it disappears from the board)
  • Hover over empty cells to see where you can place it
  • Click to place the domino in the highlighted position
  • You can only move a domino if there are two adjacent empty squares to receive it
  • Goal: Make all {numDominoes} dominoes horizontal
Horizontal
Vertical
Selected
)} {/* Board */}
{renderBoard()}
{selectedDomino !== null && (

Domino picked up. Hover over empty cells to preview placement, then click to place.

)}
{/* 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;