diff --git a/src/components/KnightsPuzzle.tsx b/src/components/KnightsPuzzle.tsx new file mode 100644 index 0000000..6edcd67 --- /dev/null +++ b/src/components/KnightsPuzzle.tsx @@ -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(initialBoard); + const [selectedSquare, setSelectedSquare] = useState(null); + const [moveCount, setMoveCount] = useState(0); + const [isComplete, setIsComplete] = useState(false); + const [moveHistory, setMoveHistory] = useState([]); + + // 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 ; + } else if (piece === 'black') { + return ; + } + return null; + }; + + return ( +
+ + + + Knights Exchange Puzzle + Classic + + + Can the black and white knights swap places using legal chess moves? Move knights by clicking on them, then clicking on a valid destination. + + + +
+ {/* Game Board */} +
+
+ {board.map((row, rowIndex) => + row.map((piece, colIndex) => ( +
handleSquareClick(rowIndex, colIndex)} + > + {renderPiece(piece)} +
+ )) + )} +
+ + {/* Controls */} +
+ + +
+
+ + {/* Info Panel */} +
+
+

Game Status

+
+

Moves: {moveCount}

+

Status: {isComplete ? "🎉 Solved!" : "🎯 In Progress"}

+
+
+ +
+

How to Play

+
    +
  • • Click on a knight to select it
  • +
  • • Valid moves will be highlighted
  • +
  • • Click on a highlighted square to move
  • +
  • • Goal: Swap white and black knights
  • +
  • • Knights move in an L-shape (like in chess)
  • +
+
+ +
+

Initial Position

+
+ {initialBoard.map((row, rowIndex) => + row.map((piece, colIndex) => ( +
+ {piece === 'white' ? '♘' : piece === 'black' ? '♞' : ''} +
+ )) + )} +
+
+ +
+

Target Position

+
+ {targetBoard.map((row, rowIndex) => + row.map((piece, colIndex) => ( +
+ {piece === 'white' ? '♘' : piece === 'black' ? '♞' : ''} +
+ )) + )} +
+
+ + {/* Attribution */} +
+

+ Puzzle source:{" "} + + @neuwirthe.bsky.social + + +

+
+
+
+
+
+
+ ); +}; + +export default KnightsPuzzle; \ No newline at end of file diff --git a/src/pages/theme-pages/Puzzles.tsx b/src/pages/theme-pages/Puzzles.tsx index 21acd4c..b9f6b7c 100644 --- a/src/pages/theme-pages/Puzzles.tsx +++ b/src/pages/theme-pages/Puzzles.tsx @@ -1,15 +1,22 @@ -import ComingSoon from "@/components/ComingSoon"; import Layout from "@/components/Layout"; +import KnightsPuzzle from "@/components/KnightsPuzzle"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; const Puzzles = () => { return (
- +
+

Interactive Puzzles

+

+ Challenge your problem-solving skills with these educational puzzles designed to reinforce key concepts across multiple disciplines. +

+
+ +
+ +