From d9c0c88333234242c6a778bf37ca061531690c56 Mon Sep 17 00:00:00 2001 From: Neeldhara Misra Date: Tue, 22 Jul 2025 04:47:21 +0530 Subject: [PATCH] Add Plate Swap Puzzle with full functionality and attribution --- src/App.tsx | 2 + src/components/InteractiveIndex.tsx | 9 + src/components/PlateSwapPuzzle.tsx | 522 ++++++++++++++++++++++++++++ src/pages/PlateSwapPuzzlePage.tsx | 12 + src/pages/theme-pages/Puzzles.tsx | 10 + 5 files changed, 555 insertions(+) create mode 100644 src/components/PlateSwapPuzzle.tsx create mode 100644 src/pages/PlateSwapPuzzlePage.tsx diff --git a/src/App.tsx b/src/App.tsx index 0339c8c..f6533d3 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -32,6 +32,7 @@ import KnightsPuzzlePage from "./pages/KnightsPuzzlePage"; import NimGamePage from "./pages/NimGamePage"; import AssistedNimGamePage from "./pages/AssistedNimGamePage"; import GoldCoinGamePage from "./pages/GoldCoinGamePage"; +import PlateSwapPuzzlePage from "./pages/PlateSwapPuzzlePage"; import Foundations from "./pages/theme-pages/discrete-math/Foundations"; import Proofs from "./pages/theme-pages/discrete-math/Proofs"; import Counting from "./pages/theme-pages/discrete-math/Counting"; @@ -90,6 +91,7 @@ const App = () => ( } /> } /> } /> + } /> {/* ADD ALL CUSTOM ROUTES ABOVE THE CATCH-ALL "*" ROUTE */} } /> diff --git a/src/components/InteractiveIndex.tsx b/src/components/InteractiveIndex.tsx index 97325bb..5347090 100644 --- a/src/components/InteractiveIndex.tsx +++ b/src/components/InteractiveIndex.tsx @@ -82,6 +82,15 @@ const allInteractives: Interactive[] = [ theme: 'Games', dateAdded: '2024-03-15' }, + { + id: 'plate-swap', + title: 'The Plate Swap Puzzle', + description: 'Arrange plates around a circular table so each person has their own plate. A challenging permutation puzzle!', + tags: ['logic', 'permutation', 'strategy', 'puzzle', 'cycles'], + path: '/puzzles/plate-swap', + theme: 'Puzzles', + dateAdded: '2024-12-19' + }, { id: 'knights-puzzle', title: 'Knights Exchange Puzzle', diff --git a/src/components/PlateSwapPuzzle.tsx b/src/components/PlateSwapPuzzle.tsx new file mode 100644 index 0000000..29d75ea --- /dev/null +++ b/src/components/PlateSwapPuzzle.tsx @@ -0,0 +1,522 @@ +import React, { useState } from 'react'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { Button } from '@/components/ui/button'; +import { Badge } from '@/components/ui/badge'; +import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'; +import { RefreshCw, RotateCcw, BookOpen, Trophy, CheckCircle, ArrowLeft, ExternalLink } from 'lucide-react'; +import SocialShare from './SocialShare'; +import { useSearchParams, useNavigate } from 'react-router-dom'; + +interface Person { + id: number; + plate: number; + isSelected: boolean; + isPaired: boolean; + pairId: number | null; + pairColor: string | null; +} + +interface GameState { + people: Person[]; + moves: number; + isComplete: boolean; + gameStarted: boolean; +} + +interface PlateSwapPuzzleProps { + showSocialShare?: boolean; +} + +const PlateSwapPuzzle: React.FC = ({ showSocialShare = false }) => { + const [searchParams] = useSearchParams(); + const navigate = useNavigate(); + const isFromPuzzles = searchParams.get('from') === 'puzzles'; + + const [gameState, setGameState] = useState(() => { + const people: Person[] = []; + for (let i = 1; i <= 16; i++) { + people.push({ + id: i, + plate: i === 16 ? 1 : i + 1, // 1 has 2's plate, 2 has 3's plate, ..., 16 has 1's plate + isSelected: false, + isPaired: false, + pairId: null, + pairColor: null + }); + } + + return { + people, + moves: 0, + isComplete: false, + gameStarted: true + }; + }); + + const [showRules, setShowRules] = useState(false); + const [showVictory, setShowVictory] = useState(false); + + const resetGame = () => { + const people: Person[] = []; + for (let i = 1; i <= 16; i++) { + people.push({ + id: i, + plate: i === 16 ? 1 : i + 1, // 1 has 2's plate, 2 has 3's plate, ..., 16 has 1's plate + isSelected: false, + isPaired: false, + pairId: null, + pairColor: null + }); + } + + setGameState({ + people, + moves: 0, + isComplete: false, + gameStarted: true + }); + setShowVictory(false); + }; + + const pairColors = [ + '#ef4444', // red + '#3b82f6', // blue + '#f59e0b', // amber + '#8b5cf6', // violet + '#ec4899', // pink + '#06b6d4', // cyan + '#84cc16', // lime + '#f97316' // orange + ]; + + const handlePersonClick = (personId: number) => { + if (!gameState.gameStarted || gameState.isComplete) return; + + const person = gameState.people.find(p => p.id === personId); + if (!person || person.plate === person.id) return; // Can't select people who already have their plate + + setGameState(prev => { + const updatedPeople = [...prev.people]; + const clickedPerson = updatedPeople.find(p => p.id === personId)!; + + // If this person is already paired, unselect them + if (clickedPerson.isPaired) { + const pairId = clickedPerson.pairId!; + const pairedPerson = updatedPeople.find(p => p.id === pairId)!; + + clickedPerson.isSelected = false; + clickedPerson.isPaired = false; + clickedPerson.pairId = null; + clickedPerson.pairColor = null; + pairedPerson.isSelected = false; + pairedPerson.isPaired = false; + pairedPerson.pairId = null; + pairedPerson.pairColor = null; + + return { ...prev, people: updatedPeople }; + } + + // If this person is selected, unselect them + if (clickedPerson.isSelected) { + clickedPerson.isSelected = false; + return { ...prev, people: updatedPeople }; + } + + // Check if there's already a selected person + const selectedPerson = updatedPeople.find(p => p.isSelected && !p.isPaired); + + if (selectedPerson) { + // Find the next available color + const usedColors = updatedPeople.filter(p => p.pairColor !== null).map(p => p.pairColor); + const availableColor = pairColors.find(color => !usedColors.includes(color)) || pairColors[0]; + + // Pair them up + selectedPerson.isSelected = false; + selectedPerson.isPaired = true; + selectedPerson.pairId = personId; + selectedPerson.pairColor = availableColor; + clickedPerson.isSelected = false; + clickedPerson.isPaired = true; + clickedPerson.pairId = selectedPerson.id; + clickedPerson.pairColor = availableColor; + } else { + // Select this person + clickedPerson.isSelected = true; + } + + return { ...prev, people: updatedPeople }; + }); + }; + + const handleSwap = () => { + if (!gameState.gameStarted || gameState.isComplete) return; + + const pairedPeople = gameState.people.filter(p => p.isPaired); + if (pairedPeople.length === 0) return; + + setGameState(prev => { + const updatedPeople = [...prev.people]; + + // Process each paired person and swap with their pair + const processedPairs = new Set(); + + pairedPeople.forEach(person => { + if (processedPairs.has(person.id)) return; // Skip if already processed + + const person1 = updatedPeople.find(p => p.id === person.id)!; + const person2 = updatedPeople.find(p => p.id === person.pairId!)!; + + // Swap plates + const tempPlate = person1.plate; + person1.plate = person2.plate; + person2.plate = tempPlate; + + // Reset pairing state + person1.isSelected = false; + person1.isPaired = false; + person1.pairId = null; + person1.pairColor = null; + person2.isSelected = false; + person2.isPaired = false; + person2.pairId = null; + person2.pairColor = null; + + // Mark both as processed + processedPairs.add(person1.id); + processedPairs.add(person2.id); + }); + + // Check if puzzle is complete + const isComplete = updatedPeople.every(p => p.plate === p.id); + + if (isComplete) { + setShowVictory(true); + } + + return { + ...prev, + people: updatedPeople, + moves: prev.moves + 1, + isComplete + }; + }); + }; + + const canSwap = () => { + const pairedPeople = gameState.people.filter(p => p.isPaired); + const peopleWithoutCorrectPlate = gameState.people.filter(p => p.plate !== p.id); + + // Check if all people without correct plates are paired + const allIncorrectPeoplePaired = peopleWithoutCorrectPlate.every(p => p.isPaired); + + return pairedPeople.length >= 2 && pairedPeople.length % 2 === 0 && allIncorrectPeoplePaired; + }; + + const renderPerson = (person: Person) => { + const isCorrect = person.plate === person.id; + const isSelected = person.isSelected; + const isPaired = person.isPaired; + + return ( +
handlePersonClick(person.id)} + > + {/* Plate number */} +
+ {person.plate} +
+
+ ); + }; + + const renderTable = () => { + const people = gameState.people; + if (people.length === 0) return null; + + // Calculate positions for a circular table + const radius = 240; + const centerX = 300; + const centerY = 300; + const outerRadius = 310; // Outer radius for seat numbers + + return ( +
+
+ + {/* Seat numbers on outer boundary */} + {people.map((person, index) => { + const angle = (index * 360) / 16 - 90; // Start from top + const x = centerX + outerRadius * Math.cos((angle * Math.PI) / 180); + const y = centerY + outerRadius * Math.sin((angle * Math.PI) / 180); + + return ( +
+ {person.id} +
+ ); + })} + + {/* Plates */} + {people.map((person, index) => { + const angle = (index * 360) / 16 - 90; // Start from top + const x = centerX + radius * Math.cos((angle * Math.PI) / 180); + const y = centerY + radius * Math.sin((angle * Math.PI) / 180); + + return ( +
+ {renderPerson(person)} +
+ ); + })} +
+ ); + }; + + return ( +
+ {/* Back to Puzzles button - only show when coming from puzzles */} + {isFromPuzzles && ( +
+ +
+ )} + + + + + + The Plate Swap Puzzle + + + + Arrange the plates so each person has their own plate.
Pair people up and swap their plates! +
+
+ + {/* Game Controls */} +
+ + +
+ Moves: + + {gameState.moves} + +
+
+ + {/* Game Board */} +
+ {renderTable()} +
+ + {/* Instructions */} +
+
+

+ {gameState.isComplete ? ( + "Congratulations! All plates are in their correct positions!" + ) : ( + "Click on people to pair them up. All people without correct plates must be paired before swapping." + )} +

+
+
+ + {/* Swap Button */} + {!gameState.isComplete && ( +
+ +
+ )} + + {/* Legend */} +
+

Legend

+
+
+
+ Person with wrong plate +
+
+
+ Person with correct plate +
+
+
+ Selected for pairing +
+
+
+ Paired up (color-coded) +
+
+
+
+
+ + + + {/* Rules Dialog */} + + + + + + The Plate Swap Puzzle Rules + + +
+
+

Objective

+

Arrange the plates so that each person has their own plate (Person 1 has Plate 1, Person 2 has Plate 2, etc.).

+
+ +
+

Initial Setup

+

Each person starts with the next person's plate: Person 1 has Plate 2, Person 2 has Plate 3, ..., Person 16 has Plate 1.

+
+ +
+

How to Play

+
    +
  1. Pair people up: Click on two people to select them for pairing.
  2. +
  3. Swap plates: Click the SWAP button to exchange the plates of all paired people.
  4. +
  5. Repeat: Continue pairing and swapping until everyone has their correct plate.
  6. +
+
+ +
+

Rules

+
    +
  • People who already have their correct plate cannot be selected.
  • +
  • You must pair an even number of people to swap.
  • +
  • The SWAP button is only enabled when everyone is properly paired.
  • +
  • Try to solve the puzzle in as few moves as possible!
  • +
+
+ +
+

Strategy Tips

+
    +
  • Look for cycles in the plate arrangement
  • +
  • Try to fix multiple people at once with strategic swaps
  • +
  • Plan your moves ahead to minimize the total number of swaps
  • +
  • Remember that each swap affects all paired people simultaneously
  • +
+
+
+
+
+ + {/* Victory Dialog */} + + + + + + Puzzle Complete! + + +
+
+ + + Congratulations! + +
+

+ You solved the puzzle in {gameState.moves} moves! +

+ {gameState.moves > 2 && ( +

+ Can you solve it in fewer moves? +

+ )} + +
+
+
+ + {/* Attribution */} +
+

+ I learned about this puzzle from{" "} + + Akash Kumar + + + {" "}over some very fun discussions! He in turn learned of this puzzle from{" "} + + Jaikumar Radhakrishnan + + + . +

+
+ + {/* Social Share Section */} + {showSocialShare && ( + + )} +
+ ); +}; + +export default PlateSwapPuzzle; \ No newline at end of file diff --git a/src/pages/PlateSwapPuzzlePage.tsx b/src/pages/PlateSwapPuzzlePage.tsx new file mode 100644 index 0000000..0e8f2e2 --- /dev/null +++ b/src/pages/PlateSwapPuzzlePage.tsx @@ -0,0 +1,12 @@ +import React from 'react'; +import PlateSwapPuzzle from '@/components/PlateSwapPuzzle'; + +const PlateSwapPuzzlePage = () => { + return ( +
+ +
+ ); +}; + +export default PlateSwapPuzzlePage; \ No newline at end of file diff --git a/src/pages/theme-pages/Puzzles.tsx b/src/pages/theme-pages/Puzzles.tsx index b24a5f9..2365b79 100644 --- a/src/pages/theme-pages/Puzzles.tsx +++ b/src/pages/theme-pages/Puzzles.tsx @@ -6,6 +6,16 @@ const Puzzles = () => { const navigate = useNavigate(); const puzzles = [ + { + id: "plate-swap", + title: "The Plate Swap Puzzle", + description: "Arrange plates around a circular table so each person has their own plate. A challenging permutation puzzle!", + path: "/puzzles/plate-swap", + tags: ["Logic", "Permutation", "Strategy"], + difficulty: "Advanced" as const, + duration: "10-30 min", + participants: "1 player" + }, { id: "knights-puzzle", title: "Knights Exchange Puzzle",