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:
parent
bddd1eaae9
commit
35ec94ec40
1 changed files with 146 additions and 22 deletions
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue