diff --git a/src/App.tsx b/src/App.tsx index c8c7298..9bc55c7 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -51,6 +51,7 @@ import SikiniaParliamentPuzzlePage from './pages/SikiniaParliamentPuzzlePage'; import NQueensPuzzlePage from './pages/NQueensPuzzlePage'; import RulesOfInferencePlaygroundPage from './pages/RulesOfInferencePlaygroundPage'; import PebblePlacementGamePage from './pages/PebblePlacementGamePage'; +import BulgarianSolitairePage from './pages/BulgarianSolitairePage'; const queryClient = new QueryClient(); @@ -104,6 +105,7 @@ const App = () => ( } /> } /> } /> + } /> } /> } /> } /> diff --git a/src/components/BulgarianSolitaire.tsx b/src/components/BulgarianSolitaire.tsx new file mode 100644 index 0000000..cff1632 --- /dev/null +++ b/src/components/BulgarianSolitaire.tsx @@ -0,0 +1,307 @@ +import React, { useState, useEffect } from 'react'; +import { Button } from '@/components/ui/button'; +import { Slider } from '@/components/ui/slider'; +import SocialShare from '@/components/SocialShare'; +import { toast } from 'sonner'; + +interface BulgarianSolitaireProps { + showSocialShare?: boolean; +} + +interface Box { + id: number; + columnIndex: number | null; // null means it's in the container +} + +const BulgarianSolitaire: React.FC = ({ showSocialShare }) => { + const [n, setN] = useState(3); + const [boxes, setBoxes] = useState([]); + const [columns, setColumns] = useState([]); + const [isPlaying, setIsPlaying] = useState(false); + const [draggedBox, setDraggedBox] = useState(null); + const [step, setStep] = useState(0); + const [isComplete, setIsComplete] = useState(false); + + const totalBoxes = (n * (n + 1)) / 2; + + // Initialize boxes and columns when n changes + useEffect(() => { + const newBoxes: Box[] = []; + for (let i = 0; i < totalBoxes; i++) { + newBoxes.push({ id: i, columnIndex: null }); + } + setBoxes(newBoxes); + setColumns(Array.from({ length: totalBoxes }, () => [])); + setStep(0); + setIsComplete(false); + }, [n, totalBoxes]); + + const handleDragStart = (e: React.DragEvent, boxId: number) => { + setDraggedBox(boxId); + e.dataTransfer.effectAllowed = 'move'; + }; + + const handleDragOver = (e: React.DragEvent) => { + e.preventDefault(); + e.dataTransfer.dropEffect = 'move'; + }; + + const handleDrop = (e: React.DragEvent, columnIndex: number) => { + e.preventDefault(); + if (draggedBox === null) return; + + const box = boxes.find(b => b.id === draggedBox); + if (!box || box.columnIndex === columnIndex) return; + + // Remove from previous location + if (box.columnIndex !== null) { + setColumns(prev => prev.map((col, idx) => + idx === box.columnIndex ? col.filter(id => id !== draggedBox) : col + )); + } + + // Add to new column + setColumns(prev => prev.map((col, idx) => + idx === columnIndex ? [...col, draggedBox] : col + )); + + // Update box location + setBoxes(prev => prev.map(b => + b.id === draggedBox ? { ...b, columnIndex } : b + )); + + setDraggedBox(null); + }; + + const handleDoubleClick = (boxId: number) => { + const box = boxes.find(b => b.id === boxId); + if (!box || box.columnIndex === null) return; + + // Remove from column + setColumns(prev => prev.map((col, idx) => + idx === box.columnIndex ? col.filter(id => id !== boxId) : col + )); + + // Return to container + setBoxes(prev => prev.map(b => + b.id === boxId ? { ...b, columnIndex: null } : b + )); + }; + + const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); + + const isTriangularConfiguration = (cols: number[][]) => { + const nonEmptyCols = cols.filter(col => col.length > 0).sort((a, b) => a.length - b.length); + if (nonEmptyCols.length !== n) return false; + + for (let i = 0; i < n; i++) { + if (nonEmptyCols[i].length !== i + 1) return false; + } + return true; + }; + + const simulateBulgarianSolitaire = async () => { + if (boxes.filter(b => b.columnIndex === null).length > 0) { + toast.error("Please place all boxes in columns before playing!"); + return; + } + + setIsPlaying(true); + setStep(0); + let currentColumns = [...columns]; + let stepCount = 0; + + while (!isTriangularConfiguration(currentColumns) && stepCount < 50) { + stepCount++; + setStep(stepCount); + + // Create new pile from non-empty columns + const newPile: number[] = []; + const updatedColumns = currentColumns.map(col => { + if (col.length > 0) { + const boxToMove = col[col.length - 1]; // Take from top + newPile.push(boxToMove); + return col.slice(0, -1); // Remove from column + } + return col; + }); + + // Add the new pile + const emptyColumnIndex = updatedColumns.findIndex(col => col.length === 0); + if (emptyColumnIndex !== -1) { + updatedColumns[emptyColumnIndex] = newPile; + } else { + // This shouldn't happen in a properly configured game + console.error("No empty column found for new pile"); + break; + } + + currentColumns = updatedColumns; + setColumns(currentColumns); + + // Update box positions + setBoxes(prev => prev.map(box => { + for (let i = 0; i < currentColumns.length; i++) { + if (currentColumns[i].includes(box.id)) { + return { ...box, columnIndex: i }; + } + } + return box; + })); + + await sleep(1000); // Wait 1 second between steps + } + + setIsComplete(isTriangularConfiguration(currentColumns)); + setIsPlaying(false); + + if (isTriangularConfiguration(currentColumns)) { + toast.success(`Reached triangular configuration in ${stepCount} steps!`); + } else { + toast.error("Maximum steps reached without convergence"); + } + }; + + const reset = () => { + const newBoxes: Box[] = []; + for (let i = 0; i < totalBoxes; i++) { + newBoxes.push({ id: i, columnIndex: null }); + } + setBoxes(newBoxes); + setColumns(Array.from({ length: totalBoxes }, () => [])); + setStep(0); + setIsComplete(false); + setIsPlaying(false); + }; + + const containerBoxes = boxes.filter(box => box.columnIndex === null); + + return ( +
+
+

Bulgarian Solitaire

+

+ Choose a number n, then drag the {totalBoxes} boxes into columns. + Click "Play" to simulate the Bulgarian Solitaire process where one box is taken from each non-empty column to form a new column, continuing until reaching the triangular configuration (1, 2, 3, ..., n). +

+
+ + {/* Controls */} +
+
+ + setN(value[0])} + min={2} + max={6} + step={1} + className="w-32" + disabled={isPlaying} + /> +
+ +
+ + {/* Container */} +
+
+
Container
+
+ {containerBoxes.map((box) => ( +
handleDragStart(e, box.id)} + className="w-8 h-8 bg-primary rounded cursor-move hover:bg-primary/80 transition-colors flex items-center justify-center text-primary-foreground text-xs font-medium" + > + {box.id + 1} +
+ ))} +
+
+
+ + {/* Columns */} +
+
+

Columns

+ {step > 0 && ( +

Step: {step}

+ )} +
+ +
+ {columns.map((column, index) => ( +
handleDrop(e, index)} + className="border border-muted-foreground rounded-lg min-h-[100px] p-2 bg-background/50 flex flex-col-reverse gap-1" + > +
+ {index + 1} +
+ {column.map((boxId, boxIndex) => ( +
handleDoubleClick(boxId)} + className="w-6 h-6 bg-primary rounded cursor-pointer hover:bg-primary/80 transition-colors flex items-center justify-center text-primary-foreground text-xs font-medium mx-auto" + title="Double-click to return to container" + > + {boxId + 1} +
+ ))} +
+ ))} +
+
+ + {/* Play Button */} +
+ +
+ + {/* Status */} + {isComplete && ( +
+

Triangular Configuration Reached! 🎉

+

+ The Bulgarian Solitaire process converged to the stable triangular configuration in {step} steps. +

+
+ )} + + {/* Instructions */} +
+

How to Play:

+
    +
  • Choose a value for n (2-6) using the slider
  • +
  • Drag the {totalBoxes} numbered boxes from the container into any of the columns
  • +
  • Double-click any box in a column to return it to the container
  • +
  • Once all boxes are placed in columns, click "Play" to start the simulation
  • +
  • Watch as the algorithm takes one box from each non-empty column to form a new column
  • +
  • The process continues until reaching the triangular configuration: columns of sizes 1, 2, 3, ..., n
  • +
+
+ + {showSocialShare && ( + + )} +
+ ); +}; + +export default BulgarianSolitaire; \ No newline at end of file diff --git a/src/pages/BulgarianSolitairePage.tsx b/src/pages/BulgarianSolitairePage.tsx new file mode 100644 index 0000000..2a9dfc6 --- /dev/null +++ b/src/pages/BulgarianSolitairePage.tsx @@ -0,0 +1,29 @@ +import BulgarianSolitaire from "@/components/BulgarianSolitaire"; +import { useSearchParams } from "react-router-dom"; + +const BulgarianSolitairePage = () => { + const [searchParams] = useSearchParams(); + const from = searchParams.get('from'); + + if (from === 'puzzles') { + return ( +
+
+
+ +
+ +
+
+ ); + } + + return ; +}; + +export default BulgarianSolitairePage; \ No newline at end of file diff --git a/src/pages/theme-pages/Puzzles.tsx b/src/pages/theme-pages/Puzzles.tsx index 15d3d7a..faf758a 100644 --- a/src/pages/theme-pages/Puzzles.tsx +++ b/src/pages/theme-pages/Puzzles.tsx @@ -16,6 +16,16 @@ const Puzzles = () => { duration: "5-15 min", participants: "1 player" }, + { + id: "bulgarian-solitaire", + title: "Bulgarian Solitaire", + description: "Explore this fascinating mathematical puzzle where boxes rearrange themselves into a triangular configuration through iterative processes.", + path: "/puzzles/bulgarian-solitaire", + tags: ["Mathematical", "Solitaire", "Algorithm", "Convergence"], + difficulty: "Intermediate" as const, + duration: "5-10 min", + participants: "1 player" + }, { id: "sikinia-parliament", title: "Parliament of Sikinia Puzzle",