Create Domino Retiling puzzle
Implement an interactive Domino Retiling puzzle: - New DominoRetilingPuzzle component to tile an augmented 8x8 board into all horizontal dominoes with an extra right-hand square - New DominoRetilingPuzzlePage and route integration - Puzzles list integration via App.tsx - Pattern follows existing puzzle UI (cards, instructions, board, interactions) - Includes generate/random tiling, valid placement, and reset hooks for replayable puzzles X-Lovable-Edit-ID: edt-14d7396d-c5c0-4f4b-bf64-4264937b9c28
This commit is contained in:
commit
1dfa8d6617
4 changed files with 468 additions and 0 deletions
|
|
@ -61,6 +61,7 @@ import BurnsidesLemmaPage from './pages/BurnsidesLemmaPage';
|
||||||
import CubeColoringPage from './pages/CubeColoringPage';
|
import CubeColoringPage from './pages/CubeColoringPage';
|
||||||
import PresentsPuzzlePage from './pages/PresentsPuzzlePage';
|
import PresentsPuzzlePage from './pages/PresentsPuzzlePage';
|
||||||
import FerrersRogersRamanujanPage from './pages/FerrersRogersRamanujanPage';
|
import FerrersRogersRamanujanPage from './pages/FerrersRogersRamanujanPage';
|
||||||
|
import DominoRetilingPuzzlePage from './pages/DominoRetilingPuzzlePage';
|
||||||
|
|
||||||
const queryClient = new QueryClient();
|
const queryClient = new QueryClient();
|
||||||
|
|
||||||
|
|
@ -132,6 +133,7 @@ const App = () => (
|
||||||
<Route path="/puzzles/plate-swap" element={<PlateSwapPuzzlePage />} />
|
<Route path="/puzzles/plate-swap" element={<PlateSwapPuzzlePage />} />
|
||||||
<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="/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 />} />
|
||||||
|
|
|
||||||
428
src/components/DominoRetilingPuzzle.tsx
Normal file
428
src/components/DominoRetilingPuzzle.tsx
Normal file
|
|
@ -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<DominoRetilingPuzzleProps> = ({ showSocialShare = true }) => {
|
||||||
|
const [dominoes, setDominoes] = useState<Domino[]>([]);
|
||||||
|
const [selectedDomino, setSelectedDomino] = useState<number | null>(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<string>();
|
||||||
|
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<string> => {
|
||||||
|
const occupied = new Set<string>();
|
||||||
|
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(
|
||||||
|
<div
|
||||||
|
key={`cell-${row}-${col}`}
|
||||||
|
className={`border border-border/50 ${
|
||||||
|
isExtraSquare ? 'bg-primary/20' : (row + col) % 2 === 0 ? 'bg-muted/30' : 'bg-muted/60'
|
||||||
|
}`}
|
||||||
|
style={cellStyle}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
// Render valid placement indicators
|
||||||
|
if (validHorizontal && !isOccupied) {
|
||||||
|
cells.push(
|
||||||
|
<div
|
||||||
|
key={`valid-h-${row}-${col}`}
|
||||||
|
className="absolute bg-emerald-300/60 border-2 border-emerald-500 border-dashed rounded cursor-pointer hover:bg-emerald-400/70 transition-colors z-10"
|
||||||
|
style={{
|
||||||
|
left: col * 50 + 2,
|
||||||
|
top: row * 50 + 2,
|
||||||
|
width: 96,
|
||||||
|
height: 46,
|
||||||
|
}}
|
||||||
|
onClick={() => handlePlacementClick(row, col, true)}
|
||||||
|
title="Place horizontally"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (validVertical && !isOccupied) {
|
||||||
|
cells.push(
|
||||||
|
<div
|
||||||
|
key={`valid-v-${row}-${col}`}
|
||||||
|
className="absolute bg-blue-300/60 border-2 border-blue-500 border-dashed rounded cursor-pointer hover:bg-blue-400/70 transition-colors z-10"
|
||||||
|
style={{
|
||||||
|
left: col * 50 + 2,
|
||||||
|
top: row * 50 + 2,
|
||||||
|
width: 46,
|
||||||
|
height: 96,
|
||||||
|
}}
|
||||||
|
onClick={() => 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(
|
||||||
|
<div
|
||||||
|
key={`domino-${d.id}`}
|
||||||
|
className={`absolute rounded border-2 cursor-pointer transition-all z-20 ${getDominoColor(d.isHorizontal, isSelected)} ${
|
||||||
|
isSelected ? 'ring-2 ring-amber-300 ring-offset-2 scale-105' : 'hover:brightness-110'
|
||||||
|
}`}
|
||||||
|
style={{
|
||||||
|
left: d.col * 50 + 2,
|
||||||
|
top: d.row * 50 + 2,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
}}
|
||||||
|
onClick={() => handleDominoClick(d.id)}
|
||||||
|
>
|
||||||
|
<div className="absolute inset-0 flex items-center justify-center">
|
||||||
|
<div className={`w-3 h-3 rounded-full ${isSelected ? 'bg-amber-700' : d.isHorizontal ? 'bg-emerald-700' : 'bg-rose-700'}`} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return cells;
|
||||||
|
};
|
||||||
|
|
||||||
|
const horizontalCount = dominoes.filter(d => d.isHorizontal).length;
|
||||||
|
const verticalCount = dominoes.filter(d => !d.isHorizontal).length;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="max-w-4xl mx-auto p-6 space-y-6">
|
||||||
|
<Card className="bg-gradient-to-r from-primary/10 to-secondary/10 border-primary/20">
|
||||||
|
<CardHeader className="text-center">
|
||||||
|
<CardTitle className="text-3xl font-bold bg-gradient-to-r from-primary to-primary/70 bg-clip-text text-transparent">
|
||||||
|
Domino Retiling Puzzle
|
||||||
|
</CardTitle>
|
||||||
|
<p className="text-muted-foreground mt-2">
|
||||||
|
Can you retile the augmented board so all dominoes are horizontal?
|
||||||
|
</p>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-center space-y-4">
|
||||||
|
<p className="text-lg text-muted-foreground">
|
||||||
|
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.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="flex flex-wrap justify-center gap-4 items-center">
|
||||||
|
<Button onClick={generateRandomTiling} variant="outline" size="sm">
|
||||||
|
<Shuffle className="w-4 h-4 mr-2" />
|
||||||
|
New Puzzle
|
||||||
|
</Button>
|
||||||
|
<Button onClick={() => setShowHint(!showHint)} variant="outline" size="sm">
|
||||||
|
<Lightbulb className="w-4 h-4 mr-2" />
|
||||||
|
{showHint ? 'Hide Hint' : 'Show Hint'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-center items-center gap-4 flex-wrap">
|
||||||
|
<Badge variant="outline" className="text-sm">
|
||||||
|
Moves: {moveCount}
|
||||||
|
</Badge>
|
||||||
|
<Badge variant="outline" className="text-sm bg-emerald-100 text-emerald-800 border-emerald-300">
|
||||||
|
Horizontal: {horizontalCount}
|
||||||
|
</Badge>
|
||||||
|
<Badge variant="outline" className="text-sm bg-rose-100 text-rose-800 border-rose-300">
|
||||||
|
Vertical: {verticalCount}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{showHint && (
|
||||||
|
<Card className="border-amber-200 bg-amber-50">
|
||||||
|
<CardContent className="p-4">
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<Lightbulb className="w-5 h-5 text-amber-600 mt-0.5" />
|
||||||
|
<div className="text-sm text-amber-800">
|
||||||
|
<p className="font-medium mb-1">Hint:</p>
|
||||||
|
<p>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!</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Instructions */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader
|
||||||
|
className="cursor-pointer"
|
||||||
|
onClick={() => setShowInstructions(!showInstructions)}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<CardTitle className="text-lg">Instructions</CardTitle>
|
||||||
|
{showInstructions ? (
|
||||||
|
<ChevronUp className="w-5 h-5 text-muted-foreground" />
|
||||||
|
) : (
|
||||||
|
<ChevronDown className="w-5 h-5 text-muted-foreground" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
{showInstructions && (
|
||||||
|
<CardContent className="pt-0">
|
||||||
|
<div className="space-y-4">
|
||||||
|
<ul className="list-disc list-inside space-y-2 text-sm">
|
||||||
|
<li>Click on a domino to select it (turns yellow)</li>
|
||||||
|
<li>Valid placement positions will appear as dashed outlines</li>
|
||||||
|
<li>Click on a valid position to move the domino there</li>
|
||||||
|
<li>You can only move a domino if there are two adjacent empty squares to receive it</li>
|
||||||
|
<li>Goal: Make all 32 dominoes horizontal</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-4 text-sm text-muted-foreground">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="w-4 h-4 rounded bg-emerald-500" />
|
||||||
|
<span>Horizontal</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="w-4 h-4 rounded bg-rose-400" />
|
||||||
|
<span>Vertical</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="w-4 h-4 rounded bg-amber-400" />
|
||||||
|
<span>Selected</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Board */}
|
||||||
|
<Card>
|
||||||
|
<CardContent className="p-6">
|
||||||
|
<div className="flex justify-center overflow-x-auto">
|
||||||
|
<div
|
||||||
|
className="relative border-2 border-foreground/30 bg-background"
|
||||||
|
style={{
|
||||||
|
width: BOARD_COLS * 50,
|
||||||
|
height: BOARD_ROWS * 50,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{renderBoard()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{selectedDomino !== null && (
|
||||||
|
<p className="text-center text-sm text-muted-foreground mt-4">
|
||||||
|
Domino selected. Click on a valid placement (dashed outline) to move it, or click the domino again to deselect.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Result */}
|
||||||
|
{isSolved && (
|
||||||
|
<Card className="border-green-200 bg-green-50">
|
||||||
|
<CardContent className="p-6">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<CheckCircle className="w-6 h-6 text-green-600" />
|
||||||
|
<div>
|
||||||
|
<p className="font-medium text-green-800">
|
||||||
|
Wait... You solved it? That shouldn't be possible!
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-green-700 mt-1">
|
||||||
|
Actually, this puzzle is impossible to solve! The extra square creates a parity imbalance that prevents an all-horizontal tiling.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Social Share */}
|
||||||
|
{showSocialShare && (
|
||||||
|
<SocialShare
|
||||||
|
title="Domino Retiling Puzzle"
|
||||||
|
description="Can you retile the augmented 8×8+1 board so that all 32 dominoes are horizontal? A classic impossibility puzzle!"
|
||||||
|
url="/puzzles/domino-retiling"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DominoRetilingPuzzle;
|
||||||
28
src/pages/DominoRetilingPuzzlePage.tsx
Normal file
28
src/pages/DominoRetilingPuzzlePage.tsx
Normal file
|
|
@ -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 (
|
||||||
|
<Layout>
|
||||||
|
{fromPuzzles && (
|
||||||
|
<div className="max-w-4xl mx-auto px-6 pt-4">
|
||||||
|
<Link
|
||||||
|
to="/themes/puzzles"
|
||||||
|
className="inline-flex items-center text-sm text-muted-foreground hover:text-foreground transition-colors"
|
||||||
|
>
|
||||||
|
<ArrowLeft className="w-4 h-4 mr-1" />
|
||||||
|
Back to Puzzles
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<DominoRetilingPuzzle />
|
||||||
|
</Layout>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DominoRetilingPuzzlePage;
|
||||||
|
|
@ -85,6 +85,16 @@ const Puzzles = () => {
|
||||||
difficulty: "Intermediate" as const,
|
difficulty: "Intermediate" as const,
|
||||||
duration: "10-20 min",
|
duration: "10-20 min",
|
||||||
participants: "1 player"
|
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"
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue