Add Knight Puzzle interactive

Implement a knight puzzle interactive game, based on the provided image and description. Add the interactive to the puzzles theme and attribute it to the specified source.
This commit is contained in:
gpt-engineer-app[bot] 2025-07-21 09:00:45 +00:00
parent df4c563417
commit 3c1a209d89
2 changed files with 313 additions and 5 deletions

View file

@ -0,0 +1,301 @@
import { useState, useCallback } from "react";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { RefreshCw, RotateCcw, ExternalLink } from "lucide-react";
import { toast } from "sonner";
type Piece = 'white' | 'black' | null;
type Position = { row: number; col: number };
interface KnightsPuzzleProps {
showSocialShare?: boolean;
}
const KnightsPuzzle = ({ showSocialShare = false }: KnightsPuzzleProps) => {
// Initial state: white knights at top corners, black knights at bottom corners
const initialBoard: Piece[][] = [
['white', null, 'white'],
[null, null, null],
['black', null, 'black']
];
// Target state: black knights at top corners, white knights at bottom corners
const targetBoard: Piece[][] = [
['black', null, 'black'],
[null, null, null],
['white', null, 'white']
];
const [board, setBoard] = useState<Piece[][]>(initialBoard);
const [selectedSquare, setSelectedSquare] = useState<Position | null>(null);
const [moveCount, setMoveCount] = useState(0);
const [isComplete, setIsComplete] = useState(false);
const [moveHistory, setMoveHistory] = useState<Position[][]>([]);
// Knight move offsets
const knightMoves = [
[-2, -1], [-2, 1], [-1, -2], [-1, 2],
[1, -2], [1, 2], [2, -1], [2, 1]
];
const isValidPosition = (row: number, col: number): boolean => {
return row >= 0 && row < 3 && col >= 0 && col < 3;
};
const getValidMoves = (fromRow: number, fromCol: number): Position[] => {
const validMoves: Position[] = [];
knightMoves.forEach(([dRow, dCol]) => {
const newRow = fromRow + dRow;
const newCol = fromCol + dCol;
if (isValidPosition(newRow, newCol) && board[newRow][newCol] === null) {
validMoves.push({ row: newRow, col: newCol });
}
});
return validMoves;
};
const isValidMove = (from: Position, to: Position): boolean => {
const rowDiff = Math.abs(from.row - to.row);
const colDiff = Math.abs(from.col - to.col);
return (rowDiff === 2 && colDiff === 1) || (rowDiff === 1 && colDiff === 2);
};
const checkWinCondition = (currentBoard: Piece[][]): boolean => {
for (let row = 0; row < 3; row++) {
for (let col = 0; col < 3; col++) {
if (currentBoard[row][col] !== targetBoard[row][col]) {
return false;
}
}
}
return true;
};
const handleSquareClick = (row: number, col: number) => {
const piece = board[row][col];
if (selectedSquare) {
const from = selectedSquare;
const to = { row, col };
// If clicking on the same square, deselect
if (from.row === to.row && from.col === to.col) {
setSelectedSquare(null);
return;
}
// If clicking on an empty square and it's a valid knight move
if (piece === null && isValidMove(from, to)) {
const newBoard = board.map(r => [...r]);
const movingPiece = newBoard[from.row][from.col];
newBoard[from.row][from.col] = null;
newBoard[to.row][to.col] = movingPiece;
setBoard(newBoard);
setMoveCount(moveCount + 1);
setMoveHistory([...moveHistory, [from, to]]);
setSelectedSquare(null);
// Check win condition
if (checkWinCondition(newBoard)) {
setIsComplete(true);
toast.success(`Puzzle solved in ${moveCount + 1} moves!`);
}
} else if (piece !== null) {
// Select a different piece
setSelectedSquare({ row, col });
} else {
setSelectedSquare(null);
}
} else if (piece !== null) {
// Select a piece
setSelectedSquare({ row, col });
}
};
const resetPuzzle = () => {
setBoard(initialBoard);
setSelectedSquare(null);
setMoveCount(0);
setIsComplete(false);
setMoveHistory([]);
};
const undoLastMove = () => {
if (moveHistory.length === 0) return;
const lastMove = moveHistory[moveHistory.length - 1];
const [from, to] = lastMove;
const newBoard = board.map(r => [...r]);
const movingPiece = newBoard[to.row][to.col];
newBoard[to.row][to.col] = null;
newBoard[from.row][from.col] = movingPiece;
setBoard(newBoard);
setMoveCount(moveCount - 1);
setMoveHistory(moveHistory.slice(0, -1));
setSelectedSquare(null);
setIsComplete(false);
};
const getSquareClass = (row: number, col: number): string => {
const baseClass = "w-16 h-16 border-2 border-outline flex items-center justify-center cursor-pointer transition-all duration-200";
const isSelected = selectedSquare?.row === row && selectedSquare?.col === col;
const validMoves = selectedSquare ? getValidMoves(selectedSquare.row, selectedSquare.col) : [];
const isValidMoveTarget = validMoves.some(move => move.row === row && move.col === col);
let bgClass = (row + col) % 2 === 0 ? "bg-surface" : "bg-surface-variant";
if (isSelected) {
bgClass = "bg-accent";
} else if (isValidMoveTarget) {
bgClass = "bg-accent/30 ring-2 ring-accent";
}
return `${baseClass} ${bgClass}`;
};
const renderPiece = (piece: Piece) => {
if (piece === 'white') {
return <span className="text-4xl"></span>;
} else if (piece === 'black') {
return <span className="text-4xl"></span>;
}
return null;
};
return (
<div className="max-w-4xl mx-auto p-6">
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
Knights Exchange Puzzle
<Badge variant="outline">Classic</Badge>
</CardTitle>
<CardDescription>
Can the black and white knights swap places using legal chess moves? Move knights by clicking on them, then clicking on a valid destination.
</CardDescription>
</CardHeader>
<CardContent>
<div className="flex flex-col lg:flex-row gap-8">
{/* Game Board */}
<div className="flex flex-col items-center gap-4">
<div className="grid grid-cols-3 gap-1 border-2 border-outline rounded-lg p-4 bg-background">
{board.map((row, rowIndex) =>
row.map((piece, colIndex) => (
<div
key={`${rowIndex}-${colIndex}`}
className={getSquareClass(rowIndex, colIndex)}
onClick={() => handleSquareClick(rowIndex, colIndex)}
>
{renderPiece(piece)}
</div>
))
)}
</div>
{/* Controls */}
<div className="flex gap-2">
<Button onClick={resetPuzzle} variant="outline" size="sm">
<RefreshCw className="w-4 h-4 mr-2" />
Reset
</Button>
<Button
onClick={undoLastMove}
variant="outline"
size="sm"
disabled={moveHistory.length === 0}
>
<RotateCcw className="w-4 h-4 mr-2" />
Undo
</Button>
</div>
</div>
{/* Info Panel */}
<div className="flex-1 space-y-4">
<div className="space-y-2">
<h3 className="text-lg font-semibold">Game Status</h3>
<div className="space-y-1 text-sm">
<p>Moves: <span className="font-mono">{moveCount}</span></p>
<p>Status: {isComplete ? "🎉 Solved!" : "🎯 In Progress"}</p>
</div>
</div>
<div className="space-y-2">
<h3 className="text-lg font-semibold">How to Play</h3>
<ul className="text-sm space-y-1 text-muted-foreground">
<li> Click on a knight to select it</li>
<li> Valid moves will be highlighted</li>
<li> Click on a highlighted square to move</li>
<li> Goal: Swap white and black knights</li>
<li> Knights move in an L-shape (like in chess)</li>
</ul>
</div>
<div className="space-y-2">
<h3 className="text-lg font-semibold">Initial Position</h3>
<div className="grid grid-cols-3 gap-1 w-24 h-24 border border-outline rounded">
{initialBoard.map((row, rowIndex) =>
row.map((piece, colIndex) => (
<div
key={`init-${rowIndex}-${colIndex}`}
className={`flex items-center justify-center text-xs ${
(rowIndex + colIndex) % 2 === 0 ? "bg-surface" : "bg-surface-variant"
}`}
>
{piece === 'white' ? '♘' : piece === 'black' ? '♞' : ''}
</div>
))
)}
</div>
</div>
<div className="space-y-2">
<h3 className="text-lg font-semibold">Target Position</h3>
<div className="grid grid-cols-3 gap-1 w-24 h-24 border border-outline rounded">
{targetBoard.map((row, rowIndex) =>
row.map((piece, colIndex) => (
<div
key={`target-${rowIndex}-${colIndex}`}
className={`flex items-center justify-center text-xs ${
(rowIndex + colIndex) % 2 === 0 ? "bg-surface" : "bg-surface-variant"
}`}
>
{piece === 'white' ? '♘' : piece === 'black' ? '♞' : ''}
</div>
))
)}
</div>
</div>
{/* Attribution */}
<div className="pt-4 border-t border-outline">
<p className="text-xs text-muted-foreground">
Puzzle source:{" "}
<a
href="https://bsky.app/profile/neuwirthe.bsky.social/post/3luhmc7gilc2x"
target="_blank"
rel="noopener noreferrer"
className="text-accent hover:underline inline-flex items-center gap-1"
>
@neuwirthe.bsky.social
<ExternalLink className="w-3 h-3" />
</a>
</p>
</div>
</div>
</div>
</CardContent>
</Card>
</div>
);
};
export default KnightsPuzzle;

View file

@ -1,15 +1,22 @@
import ComingSoon from "@/components/ComingSoon";
import Layout from "@/components/Layout"; import Layout from "@/components/Layout";
import KnightsPuzzle from "@/components/KnightsPuzzle";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
const Puzzles = () => { const Puzzles = () => {
return ( return (
<Layout> <Layout>
<div className="py-12 px-4"> <div className="py-12 px-4">
<div className="max-w-6xl mx-auto"> <div className="max-w-6xl mx-auto">
<ComingSoon <div className="mb-8">
title="Interactive Puzzles" <h1 className="text-4xl font-bold text-foreground mb-4">Interactive Puzzles</h1>
description="Get ready for an engaging collection of educational puzzles designed to challenge your problem-solving skills and reinforce key concepts across multiple disciplines." <p className="text-muted-foreground text-lg">
/> Challenge your problem-solving skills with these educational puzzles designed to reinforce key concepts across multiple disciplines.
</p>
</div>
<div className="space-y-8">
<KnightsPuzzle showSocialShare={true} />
</div>
</div> </div>
</div> </div>
</Layout> </Layout>