This commit is contained in:
gpt-engineer-app[bot] 2025-12-08 09:29:01 +00:00
parent fd3e1ac8d7
commit b8f6f38e8c
4 changed files with 468 additions and 0 deletions

View file

@ -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 = () => (
<Route path="/puzzles/plate-swap" element={<PlateSwapPuzzlePage />} />
<Route path="/puzzles/chessboard-repaint" element={<ChessboardRepaintPuzzlePage />} />
<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-search" element={<ZeckendorfSearchTrickPage />} />
<Route path="/discrete-math/foundations/rules-of-inference" element={<RulesOfInferencePlaygroundPage />} />

View 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;

View 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;

View file

@ -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"
}
];