Add Northcott's Game interactive

Implemented Northcott's Game as an interactive within the games theme.
- Added game logic, board size selection, and misere mode toggle.
- Implemented two-player mode with visual turn indicators.
This commit is contained in:
gpt-engineer-app[bot] 2025-07-21 06:16:41 +00:00
parent 8c9a8507c8
commit bddd1eaae9
2 changed files with 340 additions and 0 deletions

View file

@ -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
}
];

View file

@ -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<NorthcottsGameProps> = ({ showSocialShare = false }) => {
const [gameState, setGameState] = useState<GameState>(() =>
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 (
<div className={`w-8 h-8 rounded-full flex items-center justify-center text-white font-bold text-sm ${
piece === 'L' ? 'bg-blue-600' : 'bg-red-600'
}`}>
{piece}
</div>
);
};
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 (
<div className="space-y-6">
<Card className={`${bgClass} ${borderClass} border-2`}>
<CardHeader>
<CardTitle className="text-2xl font-bold text-center">Northcott's Game</CardTitle>
<div className="flex flex-wrap gap-4 items-center justify-center">
<div className="flex items-center space-x-2">
<Label htmlFor="misere-mode">Misère Mode:</Label>
<Switch
id="misere-mode"
checked={gameState.misereMode}
onCheckedChange={toggleMisereMode}
/>
</div>
<div className="flex items-center space-x-2">
<Label>Rows:</Label>
<Select value={gameState.rows.toString()} onValueChange={(v) => changeBoardSize(parseInt(v), gameState.cols)}>
<SelectTrigger className="w-16">
<SelectValue />
</SelectTrigger>
<SelectContent>
{Array.from({length: 8}, (_, i) => i + 3).map(n => (
<SelectItem key={n} value={n.toString()}>{n}</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="flex items-center space-x-2">
<Label>Cols:</Label>
<Select value={gameState.cols.toString()} onValueChange={(v) => changeBoardSize(gameState.rows, parseInt(v))}>
<SelectTrigger className="w-16">
<SelectValue />
</SelectTrigger>
<SelectContent>
{Array.from({length: 8}, (_, i) => i + 3).map(n => (
<SelectItem key={n} value={n.toString()}>{n}</SelectItem>
))}
</SelectContent>
</Select>
</div>
<Button onClick={resetGame} variant="outline" size="sm">
<RotateCcw className="w-4 h-4 mr-2" />
Reset
</Button>
<Button onClick={randomGame} variant="outline" size="sm">
<Shuffle className="w-4 h-4 mr-2" />
Random
</Button>
</div>
</CardHeader>
<CardContent className="space-y-4">
<div className="text-center space-y-2">
{gameState.gameOver ? (
<div className="space-y-2">
<Badge variant="destructive" className="text-lg py-1 px-4">
Game Over! {gameState.winner} Wins!
</Badge>
<p className="text-sm text-muted-foreground">
{gameState.misereMode ? 'Last to move loses (Misère)' : 'Last to move wins (Normal)'}
</p>
</div>
) : (
<div className="space-y-2">
<Badge variant="secondary" className="text-lg py-1 px-4">
Current Player: {gameState.currentPlayer} ({gameState.currentPlayer === 'L' ? 'Blue' : 'Red'})
</Badge>
<p className="text-sm text-muted-foreground">
{gameState.misereMode ? 'Misère Mode: Last to move loses' : 'Normal Mode: Last to move wins'}
</p>
</div>
)}
</div>
<div className="flex justify-center">
<div className="grid gap-1" style={{ gridTemplateColumns: `repeat(${gameState.cols}, 1fr)` }}>
{gameState.board.map((row, rowIndex) =>
row.map((cell, colIndex) => (
<div
key={`${rowIndex}-${colIndex}`}
className={getCellClass(rowIndex, colIndex)}
onClick={() => handleCellClick(rowIndex, colIndex)}
>
{getPieceDisplay(cell)}
</div>
))
)}
</div>
</div>
<div className="text-xs text-muted-foreground space-y-1">
<p><strong>Rules:</strong> Players alternate turns. L (blue) moves right, R (red) moves left.</p>
<p>Pieces cannot jump over the opponent and must stay in their row.</p>
<p>Click a piece to select it, then click a valid destination to move.</p>
</div>
</CardContent>
</Card>
{showSocialShare && (
<SocialShare
title="Northcott's Game - Strategic Two-Player Grid Game"
description="Play this classic strategy game where players move pieces horizontally trying to be the last one able to move!"
/>
)}
</div>
);
};
export default NorthcottsGame;