From cb484c418ea457f04592574eda4e5846b0424f0f Mon Sep 17 00:00:00 2001
From: "gpt-engineer-app[bot]"
<159125892+gpt-engineer-app[bot]@users.noreply.github.com>
Date: Thu, 21 Aug 2025 11:16:49 +0000
Subject: [PATCH] Add Bulgarian Solitaire interactive
Create a new interactive component for Bulgarian Solitaire. This component will allow users to select a number `n` (2-6), visualize a container with `1+2+...+n` boxes, and arrange these boxes into `1+2+...+n` columns. Users can drag and drop boxes between columns and return them to the container by double-clicking. A "Play" button will initiate the simulation of the Bulgarian Solitaire process until a steady state is reached. This interactive will be added to the Puzzles section.
---
src/App.tsx | 2 +
src/components/BulgarianSolitaire.tsx | 307 ++++++++++++++++++++++++++
src/pages/BulgarianSolitairePage.tsx | 29 +++
src/pages/theme-pages/Puzzles.tsx | 10 +
4 files changed, 348 insertions(+)
create mode 100644 src/components/BulgarianSolitaire.tsx
create mode 100644 src/pages/BulgarianSolitairePage.tsx
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).
+