Add Subtraction Game interactive
Implement "The Subtraction Game" interactive with sliders for n and k, a play/reset button, turn-based gameplay between two players, and visual feedback including confetti for the winner.
This commit is contained in:
parent
4be4ab3ef5
commit
1ff24d145c
6 changed files with 237 additions and 0 deletions
|
|
@ -52,6 +52,7 @@ import NQueensPuzzlePage from './pages/NQueensPuzzlePage';
|
|||
import RulesOfInferencePlaygroundPage from './pages/RulesOfInferencePlaygroundPage';
|
||||
import PebblePlacementGamePage from './pages/PebblePlacementGamePage';
|
||||
import BulgarianSolitairePage from './pages/BulgarianSolitairePage';
|
||||
import SubtractionGamePage from './pages/SubtractionGamePage';
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
|
|
@ -107,6 +108,7 @@ const App = () => (
|
|||
<Route path="/games/gold-coin" element={<GoldCoinGamePage />} />
|
||||
<Route path="/puzzles/pebble-placement" element={<PebblePlacementGamePage />} />
|
||||
<Route path="/puzzles/bulgarian-solitaire" element={<BulgarianSolitairePage />} />
|
||||
<Route path="/subtraction-game" element={<SubtractionGamePage />} />
|
||||
<Route path="/puzzles/sikinia-parliament" element={<SikiniaParliamentPuzzlePage />} />
|
||||
<Route path="/puzzles/plate-swap" element={<PlateSwapPuzzlePage />} />
|
||||
<Route path="/puzzles/chessboard-repaint" element={<ChessboardRepaintPuzzlePage />} />
|
||||
|
|
|
|||
|
|
@ -127,6 +127,15 @@ const allInteractives: Interactive[] = [
|
|||
theme: 'Puzzles',
|
||||
dateAdded: '2024-12-22'
|
||||
},
|
||||
{
|
||||
id: 'subtraction-game',
|
||||
title: 'The Subtraction Game',
|
||||
description: 'A strategic two-player game where players take turns removing circles, with the last player to move winning.',
|
||||
tags: ['strategy', 'two-player', 'game-theory', 'mathematical-games', 'nim-variant'],
|
||||
path: '/subtraction-game',
|
||||
theme: 'Games',
|
||||
dateAdded: '2024-12-22'
|
||||
},
|
||||
{
|
||||
id: 'knights-puzzle',
|
||||
title: 'Knights Exchange Puzzle',
|
||||
|
|
|
|||
207
src/components/SubtractionGame.tsx
Normal file
207
src/components/SubtractionGame.tsx
Normal file
|
|
@ -0,0 +1,207 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Slider } from '@/components/ui/slider';
|
||||
import confetti from 'canvas-confetti';
|
||||
|
||||
const SubtractionGame = () => {
|
||||
const [n, setN] = useState([10]);
|
||||
const [k, setK] = useState([3]);
|
||||
const [gameStarted, setGameStarted] = useState(false);
|
||||
const [circles, setCircles] = useState<number[]>([]);
|
||||
const [currentPlayer, setCurrentPlayer] = useState<'Lata' | 'Raj'>('Lata');
|
||||
const [winner, setWinner] = useState<string | null>(null);
|
||||
const [highlightedIndices, setHighlightedIndices] = useState<number[]>([]);
|
||||
|
||||
// Update k slider max when n changes
|
||||
useEffect(() => {
|
||||
if (k[0] > n[0]) {
|
||||
setK([n[0]]);
|
||||
}
|
||||
}, [n, k]);
|
||||
|
||||
// Calculate highlighted indices
|
||||
useEffect(() => {
|
||||
if (circles.length > 0) {
|
||||
const lastKIndices = [];
|
||||
const start = Math.max(0, circles.length - k[0]);
|
||||
for (let i = start; i < circles.length; i++) {
|
||||
lastKIndices.push(i);
|
||||
}
|
||||
setHighlightedIndices(lastKIndices);
|
||||
}
|
||||
}, [circles, k]);
|
||||
|
||||
const startGame = () => {
|
||||
const initialCircles = Array.from({ length: n[0] }, (_, i) => i);
|
||||
setCircles(initialCircles);
|
||||
setGameStarted(true);
|
||||
setCurrentPlayer('Lata');
|
||||
setWinner(null);
|
||||
};
|
||||
|
||||
const resetGame = () => {
|
||||
setGameStarted(false);
|
||||
setCircles([]);
|
||||
setWinner(null);
|
||||
setCurrentPlayer('Lata');
|
||||
setHighlightedIndices([]);
|
||||
};
|
||||
|
||||
const handleCircleClick = (index: number) => {
|
||||
if (!gameStarted || winner || !highlightedIndices.includes(index)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove all circles to the right of clicked circle
|
||||
const newCircles = circles.slice(0, index);
|
||||
setCircles(newCircles);
|
||||
|
||||
// Check if game is over
|
||||
if (newCircles.length === 0) {
|
||||
setWinner(currentPlayer);
|
||||
setGameStarted(false);
|
||||
// Trigger confetti
|
||||
confetti({
|
||||
particleCount: 100,
|
||||
spread: 70,
|
||||
origin: { y: 0.6 }
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Switch player
|
||||
setCurrentPlayer(currentPlayer === 'Lata' ? 'Raj' : 'Lata');
|
||||
};
|
||||
|
||||
const getBackgroundColor = () => {
|
||||
if (!gameStarted || winner) return 'bg-background';
|
||||
return currentPlayer === 'Lata' ? 'bg-red-100 dark:bg-red-950' : 'bg-purple-100 dark:bg-purple-950';
|
||||
};
|
||||
|
||||
const getCircleClass = (index: number) => {
|
||||
const baseClass = 'w-8 h-8 rounded-full border-2 border-black cursor-pointer transition-colors';
|
||||
const isHighlighted = highlightedIndices.includes(index);
|
||||
const fillClass = isHighlighted ? 'bg-yellow-200 hover:bg-yellow-300' : 'bg-gray-300 hover:bg-gray-400';
|
||||
return `${baseClass} ${fillClass}`;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`min-h-screen transition-colors duration-300 ${getBackgroundColor()}`}>
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<h1 className="text-4xl font-bold text-center mb-8">The Subtraction Game</h1>
|
||||
|
||||
{/* Game Controls */}
|
||||
<div className="bg-card rounded-lg p-6 mb-8 shadow-lg">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
|
||||
{/* N Slider */}
|
||||
<div className="space-y-3">
|
||||
<label className="text-sm font-medium">
|
||||
Number of circles (n): {n[0]}
|
||||
</label>
|
||||
<Slider
|
||||
value={n}
|
||||
onValueChange={setN}
|
||||
min={3}
|
||||
max={20}
|
||||
step={1}
|
||||
disabled={gameStarted}
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* K Slider */}
|
||||
<div className="space-y-3">
|
||||
<label className="text-sm font-medium">
|
||||
Max selection (k): {k[0]}
|
||||
</label>
|
||||
<Slider
|
||||
value={k}
|
||||
onValueChange={setK}
|
||||
min={1}
|
||||
max={n[0]}
|
||||
step={1}
|
||||
disabled={gameStarted}
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-center">
|
||||
<Button
|
||||
onClick={gameStarted ? resetGame : startGame}
|
||||
size="lg"
|
||||
className="px-8"
|
||||
>
|
||||
{gameStarted ? 'Reset' : 'Play'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Game Status */}
|
||||
{gameStarted && !winner && (
|
||||
<div className="text-center mb-6">
|
||||
<h2 className="text-2xl font-semibold">
|
||||
{currentPlayer}'s Turn
|
||||
</h2>
|
||||
<p className="text-muted-foreground mt-2">
|
||||
Click on any highlighted circle to make your move
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Winner Display */}
|
||||
{winner && (
|
||||
<div className="text-center mb-6">
|
||||
<h2 className="text-3xl font-bold text-primary">
|
||||
🎉 {winner} Wins! 🎉
|
||||
</h2>
|
||||
<p className="text-muted-foreground mt-2">
|
||||
{winner} made the last move and won the game!
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Game Board */}
|
||||
{circles.length > 0 && (
|
||||
<div className="bg-card rounded-lg p-8 shadow-lg">
|
||||
<div className="flex flex-wrap justify-center gap-3">
|
||||
{circles.map((_, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={getCircleClass(index)}
|
||||
onClick={() => handleCircleClick(index)}
|
||||
title={highlightedIndices.includes(index) ? 'Click to select' : 'Not selectable'}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{circles.length > 0 && (
|
||||
<div className="text-center mt-6">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{highlightedIndices.length} circle(s) highlighted • Last {Math.min(k[0], circles.length)} positions
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Game Rules */}
|
||||
<div className="bg-card rounded-lg p-6 mt-8 shadow-lg">
|
||||
<h3 className="text-xl font-semibold mb-4">How to Play</h3>
|
||||
<ul className="space-y-2 text-muted-foreground">
|
||||
<li>• Set the number of circles (n) and maximum selection range (k)</li>
|
||||
<li>• Players take turns clicking on highlighted circles</li>
|
||||
<li>• When you click a circle, all circles to its right disappear</li>
|
||||
<li>• The last k circles (or fewer if less than k remain) are always highlighted</li>
|
||||
<li>• The player who makes the last move wins!</li>
|
||||
<li>• Background color indicates whose turn it is</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SubtractionGame;
|
||||
7
src/pages/SubtractionGamePage.tsx
Normal file
7
src/pages/SubtractionGamePage.tsx
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
import SubtractionGame from '@/components/SubtractionGame';
|
||||
|
||||
const SubtractionGamePage = () => {
|
||||
return <SubtractionGame />;
|
||||
};
|
||||
|
||||
export default SubtractionGamePage;
|
||||
Loading…
Add table
Add a link
Reference in a new issue