Add board size slider

Introduce an interactive feature to adjust the domino puzzle board size (2–12, even-only) and shade the blue extra square. The update includes:
- A new slider for selecting board dimensions (even numbers only, 2–12)
- Blue extra square shading to be visually distinguished
- Maintains existing puzzle mechanics across any chosen dimension
- Ensures the description text and UI reflect the selected board size dynamically

X-Lovable-Edit-ID: edt-d3f37ef8-791f-4f55-a0a9-9c25178e7cec
This commit is contained in:
gpt-engineer-app[bot] 2025-12-08 09:37:58 +00:00
commit bfff2b5651

View file

@ -17,6 +17,7 @@ interface DominoRetilingPuzzleProps {
}
const DominoRetilingPuzzle: React.FC<DominoRetilingPuzzleProps> = ({ showSocialShare = true }) => {
const [boardSize, setBoardSize] = useState(8);
const [dominoes, setDominoes] = useState<Domino[]>([]);
const [selectedDomino, setSelectedDomino] = useState<number | null>(null);
const [showInstructions, setShowInstructions] = useState(false);
@ -24,24 +25,24 @@ const DominoRetilingPuzzle: React.FC<DominoRetilingPuzzleProps> = ({ showSocialS
const [showHint, setShowHint] = useState(false);
const [hoverPlacement, setHoverPlacement] = useState<{ row: number; col: number; isHorizontal: boolean } | null>(null);
const BOARD_ROWS = 8;
const BOARD_COLS = 9; // 8 + 1 extra square on top row
const BOARD_ROWS = boardSize;
const BOARD_COLS = boardSize + 1; // n + 1 extra square on top row
// Generate a random valid tiling of the 8x8 board
// Generate a random valid tiling of the n×n 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++) {
// Fill the n×n board with random dominoes
for (let row = 0; row < boardSize; row++) {
for (let col = 0; col < boardSize; 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}`);
const canHorizontal = col + 1 < boardSize && !occupied.has(`${row},${col + 1}`);
const canVertical = row + 1 < boardSize && !occupied.has(`${row + 1},${col}`);
if (!canHorizontal && !canVertical) continue;
@ -72,7 +73,7 @@ const DominoRetilingPuzzle: React.FC<DominoRetilingPuzzleProps> = ({ showSocialS
setSelectedDomino(null);
setMoveCount(0);
setHoverPlacement(null);
}, []);
}, [boardSize]);
useEffect(() => {
generateRandomTiling();
@ -94,11 +95,11 @@ const DominoRetilingPuzzle: React.FC<DominoRetilingPuzzleProps> = ({ showSocialS
}, [dominoes]);
// Check if a position is valid for the augmented board
const isValidCell = (row: number, col: number): boolean => {
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 < 8;
};
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 => {
@ -167,32 +168,40 @@ const DominoRetilingPuzzle: React.FC<DominoRetilingPuzzleProps> = ({ showSocialS
return 'bg-rose-400 border-rose-600';
};
const cellSize = Math.min(50, 400 / boardSize);
// Render the board
const renderBoard = () => {
const cells: JSX.Element[] = [];
const occupied = getOccupiedCells(selectedDomino ?? undefined);
// Render cells
for (let row = 0; row < BOARD_ROWS; row++) {
const maxCol = row === 0 ? BOARD_COLS : 8;
const maxCol = row === 0 ? BOARD_COLS : boardSize;
for (let col = 0; col < maxCol; col++) {
const isExtraSquare = row === 0 && col === 8;
const isExtraSquare = row === 0 && col === boardSize;
const cellStyle: React.CSSProperties = {
position: 'absolute',
left: col * 50,
top: row * 50,
width: 50,
height: 50,
left: col * cellSize,
top: row * cellSize,
width: cellSize,
height: cellSize,
};
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'
isExtraSquare
? 'bg-blue-500/40'
: (row + col) % 2 === 0 ? 'bg-muted/30' : 'bg-muted/60'
} ${selectedDomino !== null ? 'cursor-pointer' : ''}`}
style={cellStyle}
style={{
...cellStyle,
...(isExtraSquare ? {
backgroundImage: 'repeating-linear-gradient(45deg, transparent, transparent 4px, rgba(59, 130, 246, 0.3) 4px, rgba(59, 130, 246, 0.3) 8px)',
} : {})
}}
onMouseEnter={() => handleCellHover(row, col)}
onMouseLeave={() => setHoverPlacement(null)}
onClick={() => {
@ -209,23 +218,23 @@ const DominoRetilingPuzzle: React.FC<DominoRetilingPuzzleProps> = ({ showSocialS
dominoes.forEach(d => {
if (d.id === selectedDomino) return; // Hide selected domino
const width = d.isHorizontal ? 96 : 46;
const height = d.isHorizontal ? 46 : 96;
const width = d.isHorizontal ? cellSize * 2 - 4 : cellSize - 4;
const height = d.isHorizontal ? cellSize - 4 : cellSize * 2 - 4;
cells.push(
<div
key={`domino-${d.id}`}
className={`absolute rounded border-2 cursor-pointer transition-all z-20 ${getDominoColor(d.isHorizontal)} hover:brightness-110`}
style={{
left: d.col * 50 + 2,
top: d.row * 50 + 2,
left: d.col * cellSize + 2,
top: d.row * cellSize + 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 ${d.isHorizontal ? 'bg-emerald-700' : 'bg-rose-700'}`} />
<div className={`w-2 h-2 rounded-full ${d.isHorizontal ? 'bg-emerald-700' : 'bg-rose-700'}`} />
</div>
</div>
);
@ -233,23 +242,23 @@ const DominoRetilingPuzzle: React.FC<DominoRetilingPuzzleProps> = ({ showSocialS
// Render hover preview domino
if (hoverPlacement && selectedDomino !== null) {
const width = hoverPlacement.isHorizontal ? 96 : 46;
const height = hoverPlacement.isHorizontal ? 46 : 96;
const width = hoverPlacement.isHorizontal ? cellSize * 2 - 4 : cellSize - 4;
const height = hoverPlacement.isHorizontal ? cellSize - 4 : cellSize * 2 - 4;
cells.push(
<div
key="hover-preview"
className="absolute rounded border-2 cursor-pointer z-30 bg-amber-400 border-amber-600 opacity-80 hover:opacity-100 transition-opacity"
style={{
left: hoverPlacement.col * 50 + 2,
top: hoverPlacement.row * 50 + 2,
left: hoverPlacement.col * cellSize + 2,
top: hoverPlacement.row * cellSize + 2,
width,
height,
pointerEvents: 'none',
}}
>
<div className="absolute inset-0 flex items-center justify-center">
<div className="w-3 h-3 rounded-full bg-amber-700" />
<div className="w-2 h-2 rounded-full bg-amber-700" />
</div>
</div>
);
@ -261,6 +270,8 @@ const DominoRetilingPuzzle: React.FC<DominoRetilingPuzzleProps> = ({ showSocialS
const horizontalCount = dominoes.filter(d => d.isHorizontal).length;
const verticalCount = dominoes.filter(d => !d.isHorizontal).length;
const numDominoes = (boardSize * boardSize) / 2;
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">
@ -275,11 +286,23 @@ const DominoRetilingPuzzle: React.FC<DominoRetilingPuzzleProps> = ({ showSocialS
<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.
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.
</p>
<div className="flex flex-wrap justify-center gap-4 items-center">
<div className="flex items-center gap-3">
<label className="text-sm font-medium">Board Size: {boardSize}×{boardSize}</label>
<input
type="range"
min="2"
max="12"
step="2"
value={boardSize}
onChange={(e) => setBoardSize(parseInt(e.target.value))}
className="w-24 h-2 bg-muted rounded-lg appearance-none cursor-pointer"
/>
</div>
<Button onClick={generateRandomTiling} variant="outline" size="sm">
<Shuffle className="w-4 h-4 mr-2" />
New Puzzle
@ -339,11 +362,11 @@ const DominoRetilingPuzzle: React.FC<DominoRetilingPuzzleProps> = ({ showSocialS
<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>Click on a domino to pick it up (it disappears from the board)</li>
<li>Hover over empty cells to see where you can place it</li>
<li>Click to place the domino in the highlighted position</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>
<li>Goal: Make all {numDominoes} dominoes horizontal</li>
</ul>
<div className="flex items-center gap-4 text-sm text-muted-foreground">
@ -372,8 +395,8 @@ const DominoRetilingPuzzle: React.FC<DominoRetilingPuzzleProps> = ({ showSocialS
<div
className="relative border-2 border-foreground/30 bg-background"
style={{
width: BOARD_COLS * 50,
height: BOARD_ROWS * 50,
width: BOARD_COLS * cellSize,
height: BOARD_ROWS * cellSize,
}}
>
{renderBoard()}
@ -381,7 +404,7 @@ const DominoRetilingPuzzle: React.FC<DominoRetilingPuzzleProps> = ({ showSocialS
</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.
Domino picked up. Hover over empty cells to preview placement, then click to place.
</p>
)}
</CardContent>