diff --git a/src/App.tsx b/src/App.tsx index 7702395..a4240c1 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -46,6 +46,7 @@ import Graphs from "./pages/theme-pages/discrete-math/Graphs"; import NotFound from "./pages/NotFound"; import GridTilingPuzzlePage from './pages/GridTilingPuzzlePage'; import SunnyLinesPuzzlePage from './pages/SunnyLinesPuzzlePage'; +import SikiniaParliamentPuzzlePage from './pages/SikiniaParliamentPuzzlePage'; const queryClient = new QueryClient(); @@ -98,6 +99,7 @@ const App = () => ( } /> } /> } /> + } /> } /> } /> } /> diff --git a/src/components/SikiniaParliamentPuzzle.tsx b/src/components/SikiniaParliamentPuzzle.tsx new file mode 100644 index 0000000..40a2d37 --- /dev/null +++ b/src/components/SikiniaParliamentPuzzle.tsx @@ -0,0 +1,356 @@ +import React, { useState, useEffect, useCallback } from 'react'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Button } from '@/components/ui/button'; +import { Badge } from '@/components/ui/badge'; +import { Alert, AlertDescription } from '@/components/ui/alert'; +import { Play, RotateCcw, Trophy, Clock, Info, ChevronDown, ChevronUp } from 'lucide-react'; +import SocialShare from '@/components/SocialShare'; + +interface Person { + id: number; + x: number; + y: number; + color: 'grey' | 'blue' | 'pink'; +} + +interface Edge { + from: number; + to: number; + isViolation: boolean; +} + +interface SikiniaParliamentPuzzleProps { + showSocialShare?: boolean; +} + +const SikiniaParliamentPuzzle: React.FC = ({ showSocialShare = false }) => { + const [people, setPeople] = useState([]); + const [edges, setEdges] = useState([]); + const [gameStarted, setGameStarted] = useState(false); + const [timer, setTimer] = useState(0); + const [isRunning, setIsRunning] = useState(false); + const [bestTime, setBestTime] = useState(null); + const [isComplete, setIsComplete] = useState(false); + const [showExplanation, setShowExplanation] = useState(false); + + // Initialize people in circular layout + useEffect(() => { + const centerX = 200; + const centerY = 200; + const radius = 150; + const newPeople: Person[] = []; + + for (let i = 0; i < 10; i++) { + const angle = (i * 2 * Math.PI) / 10 - Math.PI / 2; // Start from top + const x = centerX + radius * Math.cos(angle); + const y = centerY + radius * Math.sin(angle); + newPeople.push({ + id: i, + x, + y, + color: 'grey' + }); + } + setPeople(newPeople); + }, []); + + // Timer effect + useEffect(() => { + let interval: NodeJS.Timeout; + if (isRunning) { + interval = setInterval(() => { + setTimer(timer => timer + 1); + }, 1000); + } + return () => clearInterval(interval); + }, [isRunning]); + + // Generate random graph with max degree 3 + const generateRandomGraph = useCallback(() => { + const newEdges: Edge[] = []; + const degrees = new Array(10).fill(0); + + // Try to create edges while respecting max degree constraint + for (let attempts = 0; attempts < 100 && newEdges.length < 15; attempts++) { + const from = Math.floor(Math.random() * 10); + const to = Math.floor(Math.random() * 10); + + if (from !== to && + degrees[from] < 3 && + degrees[to] < 3 && + !newEdges.some(e => (e.from === from && e.to === to) || (e.from === to && e.to === from))) { + newEdges.push({ from, to, isViolation: false }); + degrees[from]++; + degrees[to]++; + } + } + + setEdges(newEdges); + }, []); + + // Check for violations and update edge colors + const updateViolations = useCallback(() => { + setEdges(currentEdges => { + return currentEdges.map(edge => { + const person1 = people.find(p => p.id === edge.from); + const person2 = people.find(p => p.id === edge.to); + + if (!person1 || !person2 || person1.color === 'grey' || person2.color === 'grey') { + return { ...edge, isViolation: false }; + } + + if (person1.color === person2.color) { + // Check if either person has more than 1 enemy of their color + const person1Enemies = currentEdges.filter(e => + (e.from === person1.id || e.to === person1.id) && + e !== edge + ).map(e => e.from === person1.id ? e.to : e.from) + .map(id => people.find(p => p.id === id)) + .filter(p => p && p.color === person1.color); + + const person2Enemies = currentEdges.filter(e => + (e.from === person2.id || e.to === person2.id) && + e !== edge + ).map(e => e.from === person2.id ? e.to : e.from) + .map(id => people.find(p => p.id === id)) + .filter(p => p && p.color === person2.color); + + return { ...edge, isViolation: person1Enemies.length >= 1 || person2Enemies.length >= 1 }; + } + + return { ...edge, isViolation: false }; + }); + }); + }, [people]); + + // Check if puzzle is solved + const checkCompletion = useCallback(() => { + if (!gameStarted) return; + + const allColored = people.every(p => p.color !== 'grey'); + const noViolations = edges.every(e => !e.isViolation); + + if (allColored && noViolations && !isComplete) { + setIsComplete(true); + setIsRunning(false); + + if (!bestTime || timer < bestTime) { + setBestTime(timer); + localStorage.setItem('sikinia-best-time', timer.toString()); + } + } + }, [people, edges, gameStarted, isComplete, timer, bestTime]); + + useEffect(() => { + updateViolations(); + checkCompletion(); + }, [people, updateViolations, checkCompletion]); + + // Load best time from localStorage + useEffect(() => { + const saved = localStorage.getItem('sikinia-best-time'); + if (saved) { + setBestTime(parseInt(saved)); + } + }, []); + + const startGame = () => { + generateRandomGraph(); + setGameStarted(true); + setIsRunning(true); + setTimer(0); + setIsComplete(false); + setPeople(people.map(p => ({ ...p, color: 'grey' }))); + }; + + const resetGame = () => { + setGameStarted(false); + setIsRunning(false); + setTimer(0); + setIsComplete(false); + setEdges([]); + setPeople(people.map(p => ({ ...p, color: 'grey' }))); + }; + + const colorPerson = (id: number) => { + if (!gameStarted) return; + + setPeople(people.map(person => { + if (person.id === id) { + const nextColor = person.color === 'grey' ? 'blue' : + person.color === 'blue' ? 'pink' : 'grey'; + return { ...person, color: nextColor }; + } + return person; + })); + }; + + const formatTime = (seconds: number) => { + const mins = Math.floor(seconds / 60); + const secs = seconds % 60; + return `${mins}:${secs.toString().padStart(2, '0')}`; + }; + + const getPersonColor = (color: string) => { + switch (color) { + case 'blue': return '#3b82f6'; + case 'pink': return '#ec4899'; + default: return '#6b7280'; + } + }; + + return ( +
+ + +
+
+ + Parliament of Sikinia Puzzle + Graph Theory + +

+ Separate parliament members into two houses so each has at most one enemy in their house +

+
+ +
+
+ + {showExplanation && ( + + + Goal: Color all 10 parliament members blue or pink such that each member has at most one enemy of their own color in their house.
+ How to play: Click grey icons to make them blue, blue to make them pink, pink to make them grey. Red edges indicate violations (someone has more than one enemy of their color).
+ Mathematical insight: This demonstrates that any graph with maximum degree 3 is 2-colorable in a way that each vertex has at most one neighbor of its color. +
+
+ )} + +
+
+ {!gameStarted ? ( + + ) : ( + + )} +
+ +
+
+ + {formatTime(timer)} +
+ {bestTime && ( +
+ + {formatTime(bestTime)} +
+ )} +
+
+ + {isComplete && ( + + + 🎉 Congratulations! You've successfully separated the parliament in {formatTime(timer)}! + {timer === bestTime && " New best time!"} + + + )} + +
+
+ + {/* Draw edges */} + {edges.map((edge, index) => { + const person1 = people.find(p => p.id === edge.from); + const person2 = people.find(p => p.id === edge.to); + if (!person1 || !person2) return null; + + return ( + + ); + })} + + {/* Draw people */} + {people.map((person) => ( + + colorPerson(person.id)} + /> + + {person.id + 1} + + + ))} + +
+
+ +
+
+
+ Unassigned +
+
+
+ House 1 +
+
+
+ House 2 +
+
+
+
+ + {/* Social Share */} + {showSocialShare && ( + + )} +
+ ); +}; + +export default SikiniaParliamentPuzzle; \ No newline at end of file diff --git a/src/pages/SikiniaParliamentPuzzlePage.tsx b/src/pages/SikiniaParliamentPuzzlePage.tsx new file mode 100644 index 0000000..f18e7d1 --- /dev/null +++ b/src/pages/SikiniaParliamentPuzzlePage.tsx @@ -0,0 +1,12 @@ +import React from 'react'; +import SikiniaParliamentPuzzle from '@/components/SikiniaParliamentPuzzle'; + +const SikiniaParliamentPuzzlePage = () => { + return ( +
+ +
+ ); +}; + +export default SikiniaParliamentPuzzlePage; \ No newline at end of file diff --git a/src/pages/theme-pages/Puzzles.tsx b/src/pages/theme-pages/Puzzles.tsx index 93513c9..98d6df7 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: "sikinia-parliament", + title: "Parliament of Sikinia Puzzle", + description: "Separate parliament members into two houses so each has at most one enemy in their house. A graph theory puzzle!", + path: "/puzzles/sikinia-parliament", + tags: ["Graph Theory", "Logic", "Coloring"], + difficulty: "Intermediate" as const, + duration: "5-20 min", + participants: "1 player" + }, { id: "plate-swap", title: "The Plate Swap Puzzle",