diff --git a/src/components/GamesGallery.tsx b/src/components/GamesGallery.tsx index feee23d..fce8d8b 100644 --- a/src/components/GamesGallery.tsx +++ b/src/components/GamesGallery.tsx @@ -5,6 +5,7 @@ import { Badge } from '@/components/ui/badge'; import { Search } from 'lucide-react'; import Layout from '@/components/Layout'; import GameOfSim from '@/components/GameOfSim'; +import NorthcottsGame from '@/components/NorthcottsGame'; interface Game { id: string; @@ -21,6 +22,13 @@ const games: Game[] = [ description: 'A strategic two-player game where you take turns coloring edges between vertices, trying to avoid creating a triangle of your own color.', tags: ['strategy', 'graph-theory', 'two-player', 'logic', 'game-theory'], component: GameOfSim + }, + { + id: 'northcotts-game', + title: "Northcott's Game", + description: 'A classic strategy game played on a grid where players move pieces horizontally toward each other, trying to be the last one able to move.', + tags: ['strategy', 'two-player', 'grid-game', 'logic', 'movement'], + component: NorthcottsGame } ]; diff --git a/src/components/NorthcottsGame.tsx b/src/components/NorthcottsGame.tsx new file mode 100644 index 0000000..a9c859d --- /dev/null +++ b/src/components/NorthcottsGame.tsx @@ -0,0 +1,332 @@ +import React, { useState, 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 { Switch } from '@/components/ui/switch'; +import { Label } from '@/components/ui/label'; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; +import { RotateCcw, Shuffle } from 'lucide-react'; +import SocialShare from '@/components/SocialShare'; + +interface GameState { + board: ('L' | 'R' | null)[][]; + currentPlayer: 'L' | 'R'; + gameOver: boolean; + winner: 'L' | 'R' | null; + rows: number; + cols: number; + misereMode: boolean; + selectedPiece: { row: number; col: number } | null; +} + +interface NorthcottsGameProps { + showSocialShare?: boolean; +} + +const NorthcottsGame: React.FC = ({ showSocialShare = false }) => { + const [gameState, setGameState] = useState(() => + initializeGame(3, 6, false) + ); + + function initializeGame(rows: number, cols: number, misereMode: boolean): GameState { + const board: ('L' | 'R' | null)[][] = Array(rows).fill(null).map(() => Array(cols).fill(null)); + + // Place L and R pieces in each row ensuring L is to the left of R + for (let row = 0; row < rows; row++) { + // Random positions ensuring L < R + const lPos = Math.floor(Math.random() * (cols - 1)); + const rPos = lPos + 1 + Math.floor(Math.random() * (cols - lPos - 1)); + + board[row][lPos] = 'L'; + board[row][rPos] = 'R'; + } + + return { + board, + currentPlayer: 'L', + gameOver: false, + winner: null, + rows, + cols, + misereMode, + selectedPiece: null + }; + } + + const getValidMoves = (row: number, col: number, player: 'L' | 'R'): number[] => { + const moves: number[] = []; + const { board } = gameState; + + if (player === 'L') { + // L can move right (forward towards R) + for (let newCol = col + 1; newCol < gameState.cols; newCol++) { + if (board[row][newCol] === 'R') break; // Can't jump over R + if (board[row][newCol] === null) moves.push(newCol); + } + } else { + // R can move left (forward towards L) + for (let newCol = col - 1; newCol >= 0; newCol--) { + if (board[row][newCol] === 'L') break; // Can't jump over L + if (board[row][newCol] === null) moves.push(newCol); + } + } + + return moves; + }; + + const hasAnyMoves = (player: 'L' | 'R'): boolean => { + const { board } = gameState; + for (let row = 0; row < gameState.rows; row++) { + for (let col = 0; col < gameState.cols; col++) { + if (board[row][col] === player) { + if (getValidMoves(row, col, player).length > 0) { + return true; + } + } + } + } + return false; + }; + + const handleCellClick = (row: number, col: number) => { + if (gameState.gameOver) return; + + const { board, currentPlayer, selectedPiece } = gameState; + + if (selectedPiece) { + // Try to move selected piece + const validMoves = getValidMoves(selectedPiece.row, selectedPiece.col, currentPlayer); + + if (validMoves.includes(col) && row === selectedPiece.row) { + // Valid move + const newBoard = board.map(r => [...r]); + newBoard[selectedPiece.row][selectedPiece.col] = null; + newBoard[row][col] = currentPlayer; + + const nextPlayer = currentPlayer === 'L' ? 'R' : 'L'; + const hasMovesNext = hasAnyMovesForBoard(newBoard, nextPlayer); + + setGameState({ + ...gameState, + board: newBoard, + currentPlayer: nextPlayer, + selectedPiece: null, + gameOver: !hasMovesNext, + winner: !hasMovesNext ? (gameState.misereMode ? nextPlayer : currentPlayer) : null + }); + } else { + // Invalid move or selecting different piece + if (board[row][col] === currentPlayer) { + setGameState({ ...gameState, selectedPiece: { row, col } }); + } else { + setGameState({ ...gameState, selectedPiece: null }); + } + } + } else { + // Select piece + if (board[row][col] === currentPlayer) { + setGameState({ ...gameState, selectedPiece: { row, col } }); + } + } + }; + + const hasAnyMovesForBoard = (board: ('L' | 'R' | null)[][], player: 'L' | 'R'): boolean => { + for (let row = 0; row < gameState.rows; row++) { + for (let col = 0; col < gameState.cols; col++) { + if (board[row][col] === player) { + const moves = getValidMovesForBoard(board, row, col, player); + if (moves.length > 0) return true; + } + } + } + return false; + }; + + const getValidMovesForBoard = (board: ('L' | 'R' | null)[][], row: number, col: number, player: 'L' | 'R'): number[] => { + const moves: number[] = []; + + if (player === 'L') { + for (let newCol = col + 1; newCol < gameState.cols; newCol++) { + if (board[row][newCol] === 'R') break; + if (board[row][newCol] === null) moves.push(newCol); + } + } else { + for (let newCol = col - 1; newCol >= 0; newCol--) { + if (board[row][newCol] === 'L') break; + if (board[row][newCol] === null) moves.push(newCol); + } + } + + return moves; + }; + + const resetGame = () => { + setGameState(initializeGame(gameState.rows, gameState.cols, gameState.misereMode)); + }; + + const randomGame = () => { + const rows = 3 + Math.floor(Math.random() * 8); // 3-10 + const cols = 3 + Math.floor(Math.random() * 8); // 3-10 + setGameState(initializeGame(rows, cols, gameState.misereMode)); + }; + + const toggleMisereMode = () => { + setGameState(prev => ({ + ...prev, + misereMode: !prev.misereMode + })); + }; + + const changeBoardSize = (rows: number, cols: number) => { + setGameState(initializeGame(rows, cols, gameState.misereMode)); + }; + + const getCellClass = (row: number, col: number) => { + const { board, selectedPiece, currentPlayer } = gameState; + const piece = board[row][col]; + let classes = 'w-12 h-12 border-2 flex items-center justify-center cursor-pointer transition-all duration-200 '; + + if (selectedPiece && selectedPiece.row === row && selectedPiece.col === col) { + classes += 'border-primary bg-primary/20 '; + } else if (selectedPiece && selectedPiece.row === row) { + const validMoves = getValidMoves(selectedPiece.row, selectedPiece.col, currentPlayer); + if (validMoves.includes(col)) { + classes += 'border-green-500 bg-green-100 '; + } else { + classes += 'border-border bg-background '; + } + } else { + classes += 'border-border bg-background hover:bg-muted/50 '; + } + + return classes; + }; + + const getPieceDisplay = (piece: 'L' | 'R' | null) => { + if (!piece) return ''; + return ( +
+ {piece} +
+ ); + }; + + const currentPlayerColor = gameState.currentPlayer === 'L' ? 'blue' : 'red'; + const bgClass = gameState.currentPlayer === 'L' ? 'bg-blue-50' : 'bg-red-50'; + const borderClass = gameState.currentPlayer === 'L' ? 'border-blue-600' : 'border-red-900'; + + return ( +
+ + + Northcott's Game +
+
+ + +
+ +
+ + +
+ +
+ + +
+ + + + +
+
+ + +
+ {gameState.gameOver ? ( +
+ + Game Over! {gameState.winner} Wins! + +

+ {gameState.misereMode ? 'Last to move loses (Misère)' : 'Last to move wins (Normal)'} +

+
+ ) : ( +
+ + Current Player: {gameState.currentPlayer} ({gameState.currentPlayer === 'L' ? 'Blue' : 'Red'}) + +

+ {gameState.misereMode ? 'Misère Mode: Last to move loses' : 'Normal Mode: Last to move wins'} +

+
+ )} +
+ +
+
+ {gameState.board.map((row, rowIndex) => + row.map((cell, colIndex) => ( +
handleCellClick(rowIndex, colIndex)} + > + {getPieceDisplay(cell)} +
+ )) + )} +
+
+ +
+

Rules: Players alternate turns. L (blue) moves right, R (red) moves left.

+

Pieces cannot jump over the opponent and must stay in their row.

+

Click a piece to select it, then click a valid destination to move.

+
+
+
+ + {showSocialShare && ( + + )} +
+ ); +}; + +export default NorthcottsGame; \ No newline at end of file