Add The Gold Coin Game - a strategic two-player game where players move coins to acquire the gold coin
This commit is contained in:
parent
6af3ec81ef
commit
4f27765251
5 changed files with 399 additions and 0 deletions
|
|
@ -31,6 +31,7 @@ import NorthcottsGamePage from "./pages/NorthcottsGamePage";
|
|||
import KnightsPuzzlePage from "./pages/KnightsPuzzlePage";
|
||||
import NimGamePage from "./pages/NimGamePage";
|
||||
import AssistedNimGamePage from "./pages/AssistedNimGamePage";
|
||||
import GoldCoinGamePage from "./pages/GoldCoinGamePage";
|
||||
import Foundations from "./pages/theme-pages/discrete-math/Foundations";
|
||||
import Proofs from "./pages/theme-pages/discrete-math/Proofs";
|
||||
import Counting from "./pages/theme-pages/discrete-math/Counting";
|
||||
|
|
@ -88,6 +89,7 @@ const App = () => (
|
|||
<Route path="/knights-puzzle" element={<KnightsPuzzlePage />} />
|
||||
<Route path="/games/nim" element={<NimGamePage />} />
|
||||
<Route path="/games/assisted-nim" element={<AssistedNimGamePage />} />
|
||||
<Route path="/games/gold-coin" element={<GoldCoinGamePage />} />
|
||||
|
||||
{/* ADD ALL CUSTOM ROUTES ABOVE THE CATCH-ALL "*" ROUTE */}
|
||||
<Route path="*" element={<NotFound />} />
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import GameOfSim from '@/components/GameOfSim';
|
|||
import NorthcottsGame from '@/components/NorthcottsGame';
|
||||
import NimGame from '@/components/NimGame';
|
||||
import AssistedNimGame from '@/components/AssistedNimGame';
|
||||
import GoldCoinGame from '@/components/GoldCoinGame';
|
||||
|
||||
interface Game {
|
||||
id: string;
|
||||
|
|
@ -18,6 +19,13 @@ interface Game {
|
|||
}
|
||||
|
||||
const games: Game[] = [
|
||||
{
|
||||
id: 'gold-coin',
|
||||
title: 'The Gold Coin Game',
|
||||
description: 'A strategic two-player game where you move coins to the left or take the leftmost coin. The player who takes the gold coin wins!',
|
||||
tags: ['strategy', 'two-player', 'logic', 'game-theory', 'movement', 'coins'],
|
||||
component: GoldCoinGame
|
||||
},
|
||||
{
|
||||
id: 'assisted-nim',
|
||||
title: 'Assisted Game of Nim',
|
||||
|
|
|
|||
368
src/components/GoldCoinGame.tsx
Normal file
368
src/components/GoldCoinGame.tsx
Normal file
|
|
@ -0,0 +1,368 @@
|
|||
import React, { useState } from 'react';
|
||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { ArrowLeft, ArrowRight, Crown, Circle } from 'lucide-react';
|
||||
import SocialShare from '@/components/SocialShare';
|
||||
|
||||
type Player = 0 | 1;
|
||||
type CellState = 'empty' | 'penny' | 'gold' | 'gold-prev';
|
||||
|
||||
interface GameState {
|
||||
track: CellState[];
|
||||
currentPlayer: Player;
|
||||
winner: Player | null;
|
||||
gameStarted: boolean;
|
||||
selectedCoin: number | null;
|
||||
moveHistory: string[];
|
||||
}
|
||||
|
||||
const GoldCoinGame: React.FC = () => {
|
||||
const [gameState, setGameState] = useState<GameState>({
|
||||
track: ['empty', 'penny', 'empty', 'penny', 'empty', 'penny', 'empty', 'gold', 'empty', 'empty', 'empty'],
|
||||
currentPlayer: 0,
|
||||
winner: null,
|
||||
gameStarted: false,
|
||||
selectedCoin: null,
|
||||
moveHistory: []
|
||||
});
|
||||
|
||||
const [showRules, setShowRules] = useState(false);
|
||||
const [showGameOver, setShowGameOver] = useState(false);
|
||||
|
||||
const playerNames = ['Player 1', 'Player 2'];
|
||||
const playerColors = ['#3b82f6', '#ef4444'];
|
||||
|
||||
const initializeGame = () => {
|
||||
setGameState({
|
||||
track: ['empty', 'penny', 'empty', 'penny', 'empty', 'penny', 'empty', 'gold', 'empty', 'empty', 'empty'],
|
||||
currentPlayer: 0,
|
||||
winner: null,
|
||||
gameStarted: true,
|
||||
selectedCoin: null,
|
||||
moveHistory: []
|
||||
});
|
||||
};
|
||||
|
||||
const resetGame = () => {
|
||||
setGameState({
|
||||
track: ['empty', 'penny', 'empty', 'penny', 'empty', 'penny', 'empty', 'gold', 'empty', 'empty', 'empty'],
|
||||
currentPlayer: 0,
|
||||
winner: null,
|
||||
gameStarted: false,
|
||||
selectedCoin: null,
|
||||
moveHistory: []
|
||||
});
|
||||
setShowGameOver(false);
|
||||
};
|
||||
|
||||
const canMoveCoin = (fromIndex: number, toIndex: number): boolean => {
|
||||
if (fromIndex <= toIndex) return false; // Can only move left
|
||||
if (toIndex < 0) return false; // Can't move off the track
|
||||
|
||||
// Check if there are any coins in the way
|
||||
for (let i = toIndex + 1; i < fromIndex; i++) {
|
||||
if (gameState.track[i] !== 'empty') {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const canTakeCoin = (index: number): boolean => {
|
||||
// Can only take the leftmost coin
|
||||
for (let i = 0; i < index; i++) {
|
||||
if (gameState.track[i] !== 'empty') {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return gameState.track[index] !== 'empty';
|
||||
};
|
||||
|
||||
const handleCellClick = (index: number) => {
|
||||
if (gameState.winner !== null) return;
|
||||
|
||||
const cellState = gameState.track[index];
|
||||
|
||||
// If a coin is selected, try to move it
|
||||
if (gameState.selectedCoin !== null) {
|
||||
if (canMoveCoin(gameState.selectedCoin, index)) {
|
||||
const newTrack = [...gameState.track];
|
||||
newTrack[index] = newTrack[gameState.selectedCoin];
|
||||
newTrack[gameState.selectedCoin] = 'empty';
|
||||
|
||||
const moveDescription = `Player ${gameState.currentPlayer + 1} moved ${newTrack[index] === 'gold' ? 'gold coin' : 'penny'} from position ${gameState.selectedCoin + 1} to position ${index + 1}`;
|
||||
|
||||
setGameState(prev => ({
|
||||
...prev,
|
||||
track: newTrack,
|
||||
currentPlayer: ((prev.currentPlayer + 1) % 2) as Player,
|
||||
selectedCoin: null,
|
||||
moveHistory: [...prev.moveHistory, moveDescription]
|
||||
}));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Select a coin if it's not empty
|
||||
if (cellState !== 'empty') {
|
||||
setGameState(prev => ({
|
||||
...prev,
|
||||
selectedCoin: index
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
const handleTakeCoin = (index: number) => {
|
||||
if (gameState.winner !== null) return;
|
||||
if (!canTakeCoin(index)) return;
|
||||
|
||||
const cellState = gameState.track[index];
|
||||
const newTrack = [...gameState.track];
|
||||
newTrack[index] = 'empty';
|
||||
|
||||
const moveDescription = `Player ${gameState.currentPlayer + 1} took ${cellState === 'gold' ? 'gold coin' : 'penny'} from position ${index + 1}`;
|
||||
|
||||
// Check if this is a winning move
|
||||
const isWinningMove = cellState === 'gold';
|
||||
|
||||
setGameState(prev => ({
|
||||
...prev,
|
||||
track: newTrack,
|
||||
currentPlayer: isWinningMove ? prev.currentPlayer : ((prev.currentPlayer + 1) % 2) as Player,
|
||||
winner: isWinningMove ? prev.currentPlayer : null,
|
||||
selectedCoin: null,
|
||||
moveHistory: [...prev.moveHistory, moveDescription]
|
||||
}));
|
||||
|
||||
if (isWinningMove) {
|
||||
setShowGameOver(true);
|
||||
}
|
||||
};
|
||||
|
||||
const renderCell = (cellState: CellState, index: number) => {
|
||||
const isSelected = gameState.selectedCoin === index;
|
||||
const canTake = canTakeCoin(index);
|
||||
const isClickable = cellState !== 'empty' || gameState.selectedCoin !== null;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className={`
|
||||
relative w-12 h-12 border-2 border-gray-300 rounded-lg flex items-center justify-center cursor-pointer
|
||||
transition-all duration-200 hover:scale-105
|
||||
${isSelected ? 'border-blue-500 bg-blue-50' : ''}
|
||||
${canTake && cellState !== 'empty' ? 'border-green-500 bg-green-50' : ''}
|
||||
${!isClickable ? 'cursor-default hover:scale-100' : ''}
|
||||
`}
|
||||
onClick={() => handleCellClick(index)}
|
||||
>
|
||||
{cellState === 'gold' && (
|
||||
<div className="flex items-center justify-center">
|
||||
<Crown className="w-6 h-6 text-yellow-500" />
|
||||
</div>
|
||||
)}
|
||||
{cellState === 'penny' && (
|
||||
<div className="flex items-center justify-center">
|
||||
<Circle className="w-6 h-6 text-orange-500 fill-orange-500" />
|
||||
</div>
|
||||
)}
|
||||
{cellState === 'gold-prev' && (
|
||||
<div className="flex items-center justify-center opacity-50">
|
||||
<Crown className="w-6 h-6 text-yellow-500" />
|
||||
</div>
|
||||
)}
|
||||
<div className="absolute -bottom-6 text-xs text-gray-500 font-mono">
|
||||
{index + 1}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderTakeButton = (index: number) => {
|
||||
const canTake = canTakeCoin(index);
|
||||
const cellState = gameState.track[index];
|
||||
|
||||
if (!canTake || cellState === 'empty') return null;
|
||||
|
||||
return (
|
||||
<Button
|
||||
key={`take-${index}`}
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="mt-2 text-xs"
|
||||
onClick={() => handleTakeCoin(index)}
|
||||
>
|
||||
Take {cellState === 'gold' ? 'Gold' : 'Penny'}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="max-w-4xl mx-auto p-6 space-y-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Crown className="w-6 h-6 text-yellow-500" />
|
||||
The Gold Coin Game
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
A two-player strategy game. Move coins to the left or take the leftmost coin.
|
||||
The player who takes the gold coin wins!
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
{/* Game Controls */}
|
||||
<div className="flex gap-4">
|
||||
{!gameState.gameStarted ? (
|
||||
<Button onClick={initializeGame} className="flex-1">
|
||||
Start Game
|
||||
</Button>
|
||||
) : (
|
||||
<Button onClick={resetGame} variant="outline">
|
||||
Reset Game
|
||||
</Button>
|
||||
)}
|
||||
<Button onClick={() => setShowRules(true)} variant="outline">
|
||||
Rules
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Game Status */}
|
||||
{gameState.gameStarted && (
|
||||
<div className="flex items-center justify-between p-4 bg-gray-50 rounded-lg">
|
||||
<div className="flex items-center gap-4">
|
||||
<span className="font-medium">Current Player:</span>
|
||||
<Badge
|
||||
style={{ backgroundColor: playerColors[gameState.currentPlayer] }}
|
||||
className="text-white"
|
||||
>
|
||||
{playerNames[gameState.currentPlayer]}
|
||||
</Badge>
|
||||
</div>
|
||||
{gameState.selectedCoin !== null && (
|
||||
<div className="text-sm text-blue-600">
|
||||
Selected coin at position {gameState.selectedCoin + 1}. Click a position to move it.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Game Board */}
|
||||
<div className="space-y-4">
|
||||
<div className="flex justify-center">
|
||||
<div className="flex gap-2 items-end">
|
||||
{gameState.track.map((cellState, index) => (
|
||||
<div key={index} className="flex flex-col items-center">
|
||||
{renderCell(cellState, index)}
|
||||
{renderTakeButton(index)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Instructions */}
|
||||
<div className="text-center text-sm text-gray-600">
|
||||
{gameState.selectedCoin !== null ? (
|
||||
<p>Click a position to move the selected coin, or click "Take" to collect a coin</p>
|
||||
) : (
|
||||
<p>Click on a coin to select it, then click a position to move it left, or click "Take" to collect the leftmost coin</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Move History */}
|
||||
{gameState.moveHistory.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
<h3 className="font-medium">Move History</h3>
|
||||
<div className="max-h-32 overflow-y-auto space-y-1">
|
||||
{gameState.moveHistory.map((move, index) => (
|
||||
<div key={index} className="text-sm text-gray-600 bg-gray-50 p-2 rounded">
|
||||
{move}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Rules Dialog */}
|
||||
<Dialog open={showRules} onOpenChange={setShowRules}>
|
||||
<DialogContent className="max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>The Gold Coin Game Rules</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4 text-sm">
|
||||
<div>
|
||||
<h4 className="font-medium mb-2">Objective</h4>
|
||||
<p>Be the first player to acquire the gold coin and win the game!</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="font-medium mb-2">Setup</h4>
|
||||
<p>The game starts with coins placed on a track. The gold coin is positioned farthest to the right.</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="font-medium mb-2">On Your Turn</h4>
|
||||
<p>You have two options:</p>
|
||||
<ol className="list-decimal list-inside space-y-1 ml-4">
|
||||
<li><strong>Move a coin:</strong> Move any coin to the left by one or more spaces. A coin cannot jump over another coin.</li>
|
||||
<li><strong>Take a coin:</strong> Take the leftmost coin on the track into your possession.</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="font-medium mb-2">Winning</h4>
|
||||
<p>The player who successfully takes the gold coin is declared the winner!</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="font-medium mb-2">Strategy Tips</h4>
|
||||
<ul className="list-disc list-inside space-y-1 ml-4">
|
||||
<li>Try to control the position of the gold coin</li>
|
||||
<li>Use pennies to block your opponent's moves</li>
|
||||
<li>Plan several moves ahead</li>
|
||||
<li>Don't always take the first available coin</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Game Over Dialog */}
|
||||
<Dialog open={showGameOver} onOpenChange={setShowGameOver}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Game Over!</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="text-center space-y-4">
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
<Crown className="w-8 h-8 text-yellow-500" />
|
||||
<span className="text-xl font-bold">
|
||||
{playerNames[gameState.winner!]} wins!
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-gray-600">
|
||||
Congratulations! {playerNames[gameState.winner!]} successfully acquired the gold coin.
|
||||
</p>
|
||||
<Button onClick={resetGame} className="w-full">
|
||||
Play Again
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<SocialShare
|
||||
title="The Gold Coin Game"
|
||||
description="A strategic two-player game where you move coins to acquire the gold coin!"
|
||||
url={window.location.href}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default GoldCoinGame;
|
||||
|
|
@ -91,6 +91,15 @@ const allInteractives: Interactive[] = [
|
|||
theme: 'Puzzles',
|
||||
dateAdded: '2024-07-21'
|
||||
},
|
||||
{
|
||||
id: 'gold-coin',
|
||||
title: 'The Gold Coin Game',
|
||||
description: 'A strategic two-player game where you move coins to the left or take the leftmost coin. The player who takes the gold coin wins!',
|
||||
tags: ['strategy', 'two-player', 'logic', 'game-theory', 'movement', 'coins'],
|
||||
path: '/games/gold-coin',
|
||||
theme: 'Games',
|
||||
dateAdded: '2024-12-19'
|
||||
},
|
||||
{
|
||||
id: 'assisted-nim',
|
||||
title: 'Assisted Game of Nim',
|
||||
|
|
|
|||
12
src/pages/GoldCoinGamePage.tsx
Normal file
12
src/pages/GoldCoinGamePage.tsx
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import React from 'react';
|
||||
import GoldCoinGame from '@/components/GoldCoinGame';
|
||||
|
||||
const GoldCoinGamePage = () => {
|
||||
return (
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
<GoldCoinGame />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default GoldCoinGamePage;
|
||||
Loading…
Add table
Add a link
Reference in a new issue