Refactor Northcott's Game

- Added "clean start" mode.
- Modified game start and reset functionality.
- Implemented forward/backward movement option.
- Adjusted layout and UI elements.
This commit is contained in:
gpt-engineer-app[bot] 2025-07-21 06:46:14 +00:00
parent bddd1eaae9
commit 35ec94ec40

View file

@ -16,6 +16,9 @@ interface GameState {
rows: number; rows: number;
cols: number; cols: number;
misereMode: boolean; misereMode: boolean;
cleanStart: boolean;
forwardOnly: boolean;
gameStarted: boolean;
selectedPiece: { row: number; col: number } | null; selectedPiece: { row: number; col: number } | null;
} }
@ -24,21 +27,38 @@ interface NorthcottsGameProps {
} }
const NorthcottsGame: React.FC<NorthcottsGameProps> = ({ showSocialShare = false }) => { const NorthcottsGame: React.FC<NorthcottsGameProps> = ({ showSocialShare = false }) => {
const [gameState, setGameState] = useState<GameState>(() => const [gameState, setGameState] = useState<GameState>(() => ({
initializeGame(3, 6, false) board: Array(3).fill(null).map(() => Array(6).fill(null)),
); currentPlayer: 'L',
gameOver: false,
winner: null,
rows: 3,
cols: 6,
misereMode: false,
cleanStart: false,
forwardOnly: true,
gameStarted: false,
selectedPiece: null
}));
function initializeGame(rows: number, cols: number, misereMode: boolean): GameState { function initializeGame(rows: number, cols: number, misereMode: boolean, cleanStart: boolean, forwardOnly: boolean): GameState {
const board: ('L' | 'R' | null)[][] = Array(rows).fill(null).map(() => Array(cols).fill(null)); 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 if (cleanStart) {
for (let row = 0; row < rows; row++) { // Clean start: all L on left, all R on right
// Random positions ensuring L < R for (let row = 0; row < rows; row++) {
const lPos = Math.floor(Math.random() * (cols - 1)); board[row][0] = 'L';
const rPos = lPos + 1 + Math.floor(Math.random() * (cols - lPos - 1)); board[row][cols - 1] = 'R';
}
board[row][lPos] = 'L'; } else {
board[row][rPos] = 'R'; // Random positions ensuring L is to the left of R
for (let row = 0; row < rows; row++) {
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 { return {
@ -49,13 +69,16 @@ const NorthcottsGame: React.FC<NorthcottsGameProps> = ({ showSocialShare = false
rows, rows,
cols, cols,
misereMode, misereMode,
cleanStart,
forwardOnly,
gameStarted: true,
selectedPiece: null selectedPiece: null
}; };
} }
const getValidMoves = (row: number, col: number, player: 'L' | 'R'): number[] => { const getValidMoves = (row: number, col: number, player: 'L' | 'R'): number[] => {
const moves: number[] = []; const moves: number[] = [];
const { board } = gameState; const { board, forwardOnly } = gameState;
if (player === 'L') { if (player === 'L') {
// L can move right (forward towards R) // L can move right (forward towards R)
@ -63,12 +86,28 @@ const NorthcottsGame: React.FC<NorthcottsGameProps> = ({ showSocialShare = false
if (board[row][newCol] === 'R') break; // Can't jump over R if (board[row][newCol] === 'R') break; // Can't jump over R
if (board[row][newCol] === null) moves.push(newCol); if (board[row][newCol] === null) moves.push(newCol);
} }
// L can also move left (backward) if forwardOnly is false
if (!forwardOnly) {
for (let newCol = col - 1; newCol >= 0; newCol--) {
if (board[row][newCol] === 'R') break; // Can't jump over R
if (board[row][newCol] === null) moves.push(newCol);
}
}
} else { } else {
// R can move left (forward towards L) // R can move left (forward towards L)
for (let newCol = col - 1; newCol >= 0; newCol--) { for (let newCol = col - 1; newCol >= 0; newCol--) {
if (board[row][newCol] === 'L') break; // Can't jump over L if (board[row][newCol] === 'L') break; // Can't jump over L
if (board[row][newCol] === null) moves.push(newCol); if (board[row][newCol] === null) moves.push(newCol);
} }
// R can also move right (backward) if forwardOnly is false
if (!forwardOnly) {
for (let newCol = col + 1; newCol < gameState.cols; newCol++) {
if (board[row][newCol] === 'L') break; // Can't jump over L
if (board[row][newCol] === null) moves.push(newCol);
}
}
} }
return moves; return moves;
@ -89,7 +128,7 @@ const NorthcottsGame: React.FC<NorthcottsGameProps> = ({ showSocialShare = false
}; };
const handleCellClick = (row: number, col: number) => { const handleCellClick = (row: number, col: number) => {
if (gameState.gameOver) return; if (gameState.gameOver || !gameState.gameStarted) return;
const { board, currentPlayer, selectedPiece } = gameState; const { board, currentPlayer, selectedPiece } = gameState;
@ -144,30 +183,61 @@ const NorthcottsGame: React.FC<NorthcottsGameProps> = ({ showSocialShare = false
const getValidMovesForBoard = (board: ('L' | 'R' | null)[][], row: number, col: number, player: 'L' | 'R'): number[] => { const getValidMovesForBoard = (board: ('L' | 'R' | null)[][], row: number, col: number, player: 'L' | 'R'): number[] => {
const moves: number[] = []; const moves: number[] = [];
const { forwardOnly } = gameState;
if (player === 'L') { if (player === 'L') {
// L can move right (forward towards R)
for (let newCol = col + 1; newCol < gameState.cols; newCol++) { for (let newCol = col + 1; newCol < gameState.cols; newCol++) {
if (board[row][newCol] === 'R') break; if (board[row][newCol] === 'R') break;
if (board[row][newCol] === null) moves.push(newCol); if (board[row][newCol] === null) moves.push(newCol);
} }
// L can also move left (backward) if forwardOnly is false
if (!forwardOnly) {
for (let newCol = col - 1; newCol >= 0; newCol--) {
if (board[row][newCol] === 'R') break;
if (board[row][newCol] === null) moves.push(newCol);
}
}
} else { } else {
// R can move left (forward towards L)
for (let newCol = col - 1; newCol >= 0; newCol--) { for (let newCol = col - 1; newCol >= 0; newCol--) {
if (board[row][newCol] === 'L') break; if (board[row][newCol] === 'L') break;
if (board[row][newCol] === null) moves.push(newCol); if (board[row][newCol] === null) moves.push(newCol);
} }
// R can also move right (backward) if forwardOnly is false
if (!forwardOnly) {
for (let newCol = col + 1; newCol < gameState.cols; newCol++) {
if (board[row][newCol] === 'L') break;
if (board[row][newCol] === null) moves.push(newCol);
}
}
} }
return moves; return moves;
}; };
const resetGame = () => { const resetGame = () => {
setGameState(initializeGame(gameState.rows, gameState.cols, gameState.misereMode)); setGameState(prev => ({
...prev,
board: Array(prev.rows).fill(null).map(() => Array(prev.cols).fill(null)),
currentPlayer: 'L',
gameOver: false,
winner: null,
gameStarted: false,
selectedPiece: null
}));
};
const startGame = () => {
setGameState(initializeGame(gameState.rows, gameState.cols, gameState.misereMode, gameState.cleanStart, gameState.forwardOnly));
}; };
const randomGame = () => { const randomGame = () => {
const rows = 3 + Math.floor(Math.random() * 8); // 3-10 const rows = 3 + Math.floor(Math.random() * 8); // 3-10
const cols = 3 + Math.floor(Math.random() * 8); // 3-10 const cols = 3 + Math.floor(Math.random() * 8); // 3-10
setGameState(initializeGame(rows, cols, gameState.misereMode)); setGameState(initializeGame(rows, cols, gameState.misereMode, false, gameState.forwardOnly));
}; };
const toggleMisereMode = () => { const toggleMisereMode = () => {
@ -177,8 +247,31 @@ const NorthcottsGame: React.FC<NorthcottsGameProps> = ({ showSocialShare = false
})); }));
}; };
const toggleCleanStart = () => {
setGameState(prev => ({
...prev,
cleanStart: !prev.cleanStart
}));
};
const toggleForwardOnly = () => {
setGameState(prev => ({
...prev,
forwardOnly: !prev.forwardOnly
}));
};
const changeBoardSize = (rows: number, cols: number) => { const changeBoardSize = (rows: number, cols: number) => {
setGameState(initializeGame(rows, cols, gameState.misereMode)); setGameState(prev => ({
...prev,
rows,
cols,
board: Array(rows).fill(null).map(() => Array(cols).fill(null)),
gameStarted: false,
gameOver: false,
winner: null,
selectedPiece: null
}));
}; };
const getCellClass = (row: number, col: number) => { const getCellClass = (row: number, col: number) => {
@ -222,6 +315,8 @@ const NorthcottsGame: React.FC<NorthcottsGameProps> = ({ showSocialShare = false
<Card className={`${bgClass} ${borderClass} border-2`}> <Card className={`${bgClass} ${borderClass} border-2`}>
<CardHeader> <CardHeader>
<CardTitle className="text-2xl font-bold text-center">Northcott's Game</CardTitle> <CardTitle className="text-2xl font-bold text-center">Northcott's Game</CardTitle>
{/* Mode Options Row */}
<div className="flex flex-wrap gap-4 items-center justify-center"> <div className="flex flex-wrap gap-4 items-center justify-center">
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<Label htmlFor="misere-mode">Misère Mode:</Label> <Label htmlFor="misere-mode">Misère Mode:</Label>
@ -232,6 +327,27 @@ const NorthcottsGame: React.FC<NorthcottsGameProps> = ({ showSocialShare = false
/> />
</div> </div>
<div className="flex items-center space-x-2">
<Label htmlFor="clean-start">Clean Start:</Label>
<Switch
id="clean-start"
checked={gameState.cleanStart}
onCheckedChange={toggleCleanStart}
/>
</div>
<div className="flex items-center space-x-2">
<Label htmlFor="forward-only">Forward Only:</Label>
<Switch
id="forward-only"
checked={gameState.forwardOnly}
onCheckedChange={toggleForwardOnly}
/>
</div>
</div>
{/* Board Size and Game Control Row */}
<div className="flex flex-wrap gap-4 items-center justify-center">
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<Label>Rows:</Label> <Label>Rows:</Label>
<Select value={gameState.rows.toString()} onValueChange={(v) => changeBoardSize(parseInt(v), gameState.cols)}> <Select value={gameState.rows.toString()} onValueChange={(v) => changeBoardSize(parseInt(v), gameState.cols)}>
@ -260,14 +376,13 @@ const NorthcottsGame: React.FC<NorthcottsGameProps> = ({ showSocialShare = false
</Select> </Select>
</div> </div>
<Button onClick={resetGame} variant="outline" size="sm"> <Button onClick={startGame} variant="outline" size="sm" disabled={gameState.gameStarted}>
<RotateCcw className="w-4 h-4 mr-2" /> Start Game
Reset
</Button> </Button>
<Button onClick={randomGame} variant="outline" size="sm"> <Button onClick={randomGame} variant="outline" size="sm">
<Shuffle className="w-4 h-4 mr-2" /> <Shuffle className="w-4 h-4 mr-2" />
Random Start A Random Game
</Button> </Button>
</div> </div>
</CardHeader> </CardHeader>
@ -311,11 +426,20 @@ const NorthcottsGame: React.FC<NorthcottsGameProps> = ({ showSocialShare = false
</div> </div>
</div> </div>
<div className="text-xs text-muted-foreground space-y-1"> <div className="text-xs text-muted-foreground space-y-1 text-center">
<p><strong>Rules:</strong> Players alternate turns. L (blue) moves right, R (red) moves left.</p> <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>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> <p>Click a piece to select it, then click a valid destination to move.</p>
</div> </div>
{gameState.gameStarted && (
<div className="flex justify-center pt-4">
<Button onClick={resetGame} variant="outline" size="sm">
<RotateCcw className="w-4 h-4 mr-2" />
Reset
</Button>
</div>
)}
</CardContent> </CardContent>
</Card> </Card>