Add Plate Swap Puzzle with full functionality and attribution

This commit is contained in:
Neeldhara Misra 2025-07-22 04:47:21 +05:30
parent 99482e4314
commit d9c0c88333
5 changed files with 555 additions and 0 deletions

View file

@ -32,6 +32,7 @@ import KnightsPuzzlePage from "./pages/KnightsPuzzlePage";
import NimGamePage from "./pages/NimGamePage"; import NimGamePage from "./pages/NimGamePage";
import AssistedNimGamePage from "./pages/AssistedNimGamePage"; import AssistedNimGamePage from "./pages/AssistedNimGamePage";
import GoldCoinGamePage from "./pages/GoldCoinGamePage"; import GoldCoinGamePage from "./pages/GoldCoinGamePage";
import PlateSwapPuzzlePage from "./pages/PlateSwapPuzzlePage";
import Foundations from "./pages/theme-pages/discrete-math/Foundations"; import Foundations from "./pages/theme-pages/discrete-math/Foundations";
import Proofs from "./pages/theme-pages/discrete-math/Proofs"; import Proofs from "./pages/theme-pages/discrete-math/Proofs";
import Counting from "./pages/theme-pages/discrete-math/Counting"; import Counting from "./pages/theme-pages/discrete-math/Counting";
@ -90,6 +91,7 @@ const App = () => (
<Route path="/games/nim" element={<NimGamePage />} /> <Route path="/games/nim" element={<NimGamePage />} />
<Route path="/games/assisted-nim" element={<AssistedNimGamePage />} /> <Route path="/games/assisted-nim" element={<AssistedNimGamePage />} />
<Route path="/games/gold-coin" element={<GoldCoinGamePage />} /> <Route path="/games/gold-coin" element={<GoldCoinGamePage />} />
<Route path="/puzzles/plate-swap" element={<PlateSwapPuzzlePage />} />
{/* ADD ALL CUSTOM ROUTES ABOVE THE CATCH-ALL "*" ROUTE */} {/* ADD ALL CUSTOM ROUTES ABOVE THE CATCH-ALL "*" ROUTE */}
<Route path="*" element={<NotFound />} /> <Route path="*" element={<NotFound />} />

View file

@ -82,6 +82,15 @@ const allInteractives: Interactive[] = [
theme: 'Games', theme: 'Games',
dateAdded: '2024-03-15' 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', id: 'knights-puzzle',
title: 'Knights Exchange Puzzle', title: 'Knights Exchange Puzzle',

View file

@ -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<PlateSwapPuzzleProps> = ({ showSocialShare = false }) => {
const [searchParams] = useSearchParams();
const navigate = useNavigate();
const isFromPuzzles = searchParams.get('from') === 'puzzles';
const [gameState, setGameState] = useState<GameState>(() => {
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<number>();
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 (
<div
key={person.id}
className={`
relative w-20 h-20 rounded-full border-4 flex items-center justify-center cursor-pointer
transition-all duration-200 hover:scale-105
${isCorrect ? 'border-green-500 bg-green-50' : 'border-gray-300 bg-white'}
${isSelected ? 'border-dashed border-blue-500 bg-blue-50 shadow-lg' : ''}
${isPaired && person.pairColor ? `border-[${person.pairColor}] bg-[${person.pairColor}]/10 shadow-lg` : ''}
${person.plate === person.id ? 'cursor-default hover:scale-100' : ''}
`}
style={{
borderColor: isPaired && person.pairColor ? person.pairColor : undefined,
backgroundColor: isPaired && person.pairColor ? `${person.pairColor}10` : undefined
}}
onClick={() => handlePersonClick(person.id)}
>
{/* Plate number */}
<div className="text-base font-bold text-gray-700">
{person.plate}
</div>
</div>
);
};
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 (
<div className="relative w-[600px] h-[600px] mx-auto">
<div className="absolute inset-0 border-4 border-gray-300 rounded-full bg-gray-50"></div>
{/* 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 (
<div
key={`seat-${person.id}`}
className="absolute w-8 h-8 bg-gray-800 text-white text-sm rounded-full flex items-center justify-center font-bold transform -translate-x-4 -translate-y-4"
style={{ left: x, top: y }}
>
{person.id}
</div>
);
})}
{/* 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 (
<div
key={person.id}
className="absolute transform -translate-x-10 -translate-y-10"
style={{ left: x, top: y }}
>
{renderPerson(person)}
</div>
);
})}
</div>
);
};
return (
<div className="max-w-4xl mx-auto p-6 space-y-6">
{/* Back to Puzzles button - only show when coming from puzzles */}
{isFromPuzzles && (
<div className="flex items-center gap-4">
<button
onClick={() => navigate('/themes/puzzles')}
className="text-primary hover:text-primary/80 font-medium flex items-center gap-2"
>
<ArrowLeft className="w-4 h-4" />
Go back to Puzzles
</button>
</div>
)}
<Card className="shadow-lg">
<CardHeader className="text-center pb-6">
<CardTitle className="flex items-center justify-center gap-3 text-3xl">
<RefreshCw className="w-8 h-8 text-blue-500" />
The Plate Swap Puzzle
<RefreshCw className="w-8 h-8 text-blue-500" />
</CardTitle>
<CardDescription className="text-lg max-w-2xl mx-auto">
Arrange the plates so each person has their own plate. <br /> Pair people up and swap their plates!
</CardDescription>
</CardHeader>
<CardContent className="space-y-8">
{/* Game Controls */}
<div className="flex justify-center items-center gap-4">
<Button onClick={() => setShowRules(true)} variant="outline" size="lg" className="px-8">
<BookOpen className="w-4 h-4 mr-2" />
Rules
</Button>
<Button onClick={resetGame} variant="outline" size="lg" className="px-8">
<RotateCcw className="w-4 h-4 mr-2" />
Reset Puzzle
</Button>
<div className="flex items-center gap-2">
<span className="font-semibold text-lg">Moves:</span>
<Badge className="text-lg px-4 py-2 bg-blue-600 text-white">
{gameState.moves}
</Badge>
</div>
</div>
{/* Game Board */}
<div className="space-y-8">
{renderTable()}
</div>
{/* Instructions */}
<div className="text-center mt-8">
<div className="inline-block bg-gray-50 px-6 py-3 rounded-lg border">
<p className="text-sm text-gray-700 font-medium">
{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."
)}
</p>
</div>
</div>
{/* Swap Button */}
{!gameState.isComplete && (
<div className="flex justify-center mt-4">
<Button
onClick={handleSwap}
disabled={!canSwap()}
size="lg"
className={`px-8 ${canSwap() ? 'bg-green-600 hover:bg-green-700' : 'bg-gray-400 cursor-not-allowed'}`}
>
SWAP
</Button>
</div>
)}
{/* Legend */}
<div className="space-y-3 mt-8">
<h3 className="font-semibold text-lg text-center">Legend</h3>
<div className="flex flex-wrap justify-center gap-6 text-sm">
<div className="flex items-center gap-2">
<div className="w-5 h-5 border-2 border-gray-300 bg-white rounded-full"></div>
<span>Person with wrong plate</span>
</div>
<div className="flex items-center gap-2">
<div className="w-5 h-5 border-2 border-green-500 bg-green-50 rounded-full"></div>
<span>Person with correct plate</span>
</div>
<div className="flex items-center gap-2">
<div className="w-5 h-5 border-2 border-dashed border-blue-500 bg-blue-50 rounded-full"></div>
<span>Selected for pairing</span>
</div>
<div className="flex items-center gap-2">
<div className="w-5 h-5 border-2 border-gray-300 bg-gray-50 rounded-full"></div>
<span>Paired up (color-coded)</span>
</div>
</div>
</div>
</CardContent>
</Card>
{/* Rules Dialog */}
<Dialog open={showRules} onOpenChange={setShowRules}>
<DialogContent className="max-w-2xl max-h-[80vh] overflow-y-auto">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<BookOpen className="w-5 h-5" />
The Plate Swap Puzzle Rules
</DialogTitle>
</DialogHeader>
<div className="space-y-6 text-sm">
<div>
<h4 className="font-semibold mb-2 text-lg">Objective</h4>
<p className="text-gray-700">Arrange the plates so that each person has their own plate (Person 1 has Plate 1, Person 2 has Plate 2, etc.).</p>
</div>
<div>
<h4 className="font-semibold mb-2 text-lg">Initial Setup</h4>
<p className="text-gray-700">Each person starts with the next person's plate: Person 1 has Plate 2, Person 2 has Plate 3, ..., Person 16 has Plate 1.</p>
</div>
<div>
<h4 className="font-semibold mb-2 text-lg">How to Play</h4>
<ol className="list-decimal list-inside space-y-2 ml-4 text-gray-700">
<li><strong>Pair people up:</strong> Click on two people to select them for pairing.</li>
<li><strong>Swap plates:</strong> Click the SWAP button to exchange the plates of all paired people.</li>
<li><strong>Repeat:</strong> Continue pairing and swapping until everyone has their correct plate.</li>
</ol>
</div>
<div>
<h4 className="font-semibold mb-2 text-lg">Rules</h4>
<ul className="list-disc list-inside space-y-2 ml-4 text-gray-700">
<li>People who already have their correct plate cannot be selected.</li>
<li>You must pair an even number of people to swap.</li>
<li>The SWAP button is only enabled when everyone is properly paired.</li>
<li>Try to solve the puzzle in as few moves as possible!</li>
</ul>
</div>
<div>
<h4 className="font-semibold mb-2 text-lg">Strategy Tips</h4>
<ul className="list-disc list-inside space-y-2 ml-4 text-gray-700">
<li>Look for cycles in the plate arrangement</li>
<li>Try to fix multiple people at once with strategic swaps</li>
<li>Plan your moves ahead to minimize the total number of swaps</li>
<li>Remember that each swap affects all paired people simultaneously</li>
</ul>
</div>
</div>
</DialogContent>
</Dialog>
{/* Victory Dialog */}
<Dialog open={showVictory} onOpenChange={setShowVictory}>
<DialogContent className="max-w-md">
<DialogHeader>
<DialogTitle className="flex items-center justify-center gap-2">
<Trophy className="w-6 h-6 text-yellow-500" />
Puzzle Complete!
</DialogTitle>
</DialogHeader>
<div className="text-center space-y-6">
<div className="flex items-center justify-center gap-3">
<CheckCircle className="w-10 h-10 text-green-500" />
<span className="text-2xl font-bold text-green-600">
Congratulations!
</span>
</div>
<p className="text-gray-600 text-lg">
You solved the puzzle in {gameState.moves} moves!
</p>
{gameState.moves > 2 && (
<p className="text-blue-600 font-medium">
Can you solve it in fewer moves?
</p>
)}
<Button onClick={resetGame} className="w-full" size="lg">
Play Again
</Button>
</div>
</DialogContent>
</Dialog>
{/* Attribution */}
<div className="pt-4 border-t border-outline">
<p className="text-xs text-muted-foreground">
I learned about this puzzle from{" "}
<a
href="https://sites.google.com/view/kumarakash/home"
target="_blank"
rel="noopener noreferrer"
className="text-accent hover:underline inline-flex items-center gap-1"
>
Akash Kumar
<ExternalLink className="w-3 h-3" />
</a>
{" "}over some very fun discussions! He in turn learned of this puzzle from{" "}
<a
href="https://www.tcs.tifr.res.in/~jaikumar/"
target="_blank"
rel="noopener noreferrer"
className="text-accent hover:underline inline-flex items-center gap-1"
>
Jaikumar Radhakrishnan
<ExternalLink className="w-3 h-3" />
</a>
.
</p>
</div>
{/* Social Share Section */}
{showSocialShare && (
<SocialShare
title="The Plate Swap Puzzle"
description="A challenging puzzle where you need to arrange plates around a circular table!"
url={window.location.href}
/>
)}
</div>
);
};
export default PlateSwapPuzzle;

View file

@ -0,0 +1,12 @@
import React from 'react';
import PlateSwapPuzzle from '@/components/PlateSwapPuzzle';
const PlateSwapPuzzlePage = () => {
return (
<div className="min-h-screen bg-background">
<PlateSwapPuzzle showSocialShare={true} />
</div>
);
};
export default PlateSwapPuzzlePage;

View file

@ -6,6 +6,16 @@ const Puzzles = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const puzzles = [ 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", id: "knights-puzzle",
title: "Knights Exchange Puzzle", title: "Knights Exchange Puzzle",