diff --git a/src/components/coordinates-trainer/Board.tsx b/src/components/coordinates-trainer/Board.tsx
new file mode 100644
index 00000000..58c5dd8a
--- /dev/null
+++ b/src/components/coordinates-trainer/Board.tsx
@@ -0,0 +1,199 @@
+import { Box, Typography, useTheme } from "@mui/material";
+import type { GameSettings, GameState } from "@/types/coordinatesTrainer";
+import {
+ getSquareColor,
+ getDisplayFiles,
+ getDisplayRanks,
+} from "@/hooks/useCoordinateTrainer";
+
+interface ChessBoardProps {
+ gameState: GameState;
+ settings: GameSettings;
+ onSquareClick: (square: string) => void;
+}
+
+export default function ChessBoard({
+ gameState,
+ settings,
+ onSquareClick,
+}: ChessBoardProps) {
+ const theme = useTheme();
+
+ const displayFiles = getDisplayFiles(gameState.currentBoardOrientation);
+ const displayRanks = getDisplayRanks(gameState.currentBoardOrientation);
+
+ return (
+
+
+ {/* Rank Numbers (Left Side) */}
+
+ {displayRanks.map((rank) => (
+
+ {rank}
+
+ ))}
+
+
+ {/* File Letters (Bottom) */}
+
+ {displayFiles.map((file) => (
+
+ {file}
+
+ ))}
+
+
+ {/* Chess Board */}
+ e.preventDefault()}
+ >
+ {displayRanks.map((rank) =>
+ displayFiles.map((file) => {
+ const square = `${file}${rank}`;
+
+ return (
+ onSquareClick(square)}
+ sx={{
+ bgcolor: getSquareColor(file, rank),
+ display: "flex",
+ alignItems: settings.showCoordinates
+ ? "flex-end"
+ : "center",
+ justifyContent: settings.showCoordinates
+ ? "flex-start"
+ : "center",
+ cursor:
+ gameState.status === "playing" ? "pointer" : "default",
+ position: "relative",
+ transition: "all 0.2s ease",
+ "&:hover":
+ gameState.status === "playing"
+ ? {
+ opacity: 0.8,
+ transform: "scale(0.95)",
+ }
+ : {},
+ fontSize: { xs: "0.6rem", sm: "0.8rem", md: "1rem" },
+ fontWeight: "bold",
+ color: settings.showCoordinates
+ ? "rgba(0,0,0,0.4)"
+ : "transparent",
+ p: settings.showCoordinates ? { xs: 0.3, sm: 0.5 } : 0,
+ }}
+ >
+ {settings.showCoordinates && square}
+
+ );
+ })
+ )}
+
+
+ {/* Floating Target Square */}
+ {gameState.status === "playing" && (
+
+
+ {gameState.targetSquare}
+
+
+ )}
+
+
+ );
+}
diff --git a/src/components/coordinates-trainer/GameInfo.tsx b/src/components/coordinates-trainer/GameInfo.tsx
new file mode 100644
index 00000000..9abb3789
--- /dev/null
+++ b/src/components/coordinates-trainer/GameInfo.tsx
@@ -0,0 +1,208 @@
+import { Icon } from "@iconify/react";
+import {
+ Box,
+ Button,
+ Chip,
+ IconButton,
+ LinearProgress,
+ Typography,
+ useTheme,
+} from "@mui/material";
+import type { GameSettings, GameState } from "@/types/coordinatesTrainer";
+
+interface GameInfoProps {
+ gameState: GameState;
+ settings: GameSettings;
+ onPauseGame: () => void;
+ onStopGame: () => void;
+ onStartGame: () => void;
+ onShowSettings: () => void;
+ formatTime: (seconds: number) => string;
+}
+
+export default function GameInfo({
+ gameState,
+ settings,
+ onPauseGame,
+ onStopGame,
+ onStartGame,
+ onShowSettings,
+ formatTime,
+}: GameInfoProps) {
+ const theme = useTheme();
+
+ return (
+
+ {/* Header */}
+
+
+
+ Coordinate Trainer
+
+
+
+
+
+
+
+
+
+
+
+ {/* Timer and Score */}
+
+
+ {settings.timeControl > 0 && (
+
+ )}
+
+
+ {/* Progress Bar */}
+ {settings.timeControl > 0 && (
+
+ )}
+
+ {/* Game Controls */}
+
+
+ ) : (
+
+ )
+ }
+ onClick={onPauseGame}
+ disabled={gameState.status === "finished"}
+ size="small"
+ >
+ {gameState.status === "paused" ? "Resume" : "Pause"}
+
+ }
+ onClick={onStopGame}
+ size="small"
+ >
+ Stop
+
+
+
+
+ {/* Game Status Display */}
+
+ {gameState.status === "playing" && !gameState.feedback && (
+ <>
+
+ Find Square
+
+
+ Click on the highlighted square on the board
+
+ >
+ )}
+
+ {gameState.status === "paused" && (
+
+ Game Paused
+
+ )}
+
+ {gameState.status === "finished" && (
+
+
+ Time's Up!
+
+
+ {gameState.score}
+
+
+ squares found
+
+
+
+ )}
+
+ {/* Feedback Display */}
+ {gameState.feedback && (
+
+
+ {gameState.feedback.message}
+
+
+ {gameState.feedback.type === "error"
+ ? `Keep trying to find ${gameState.targetSquare}!`
+ : "Next target coming up..."}
+
+
+ )}
+
+
+ );
+}
diff --git a/src/components/coordinates-trainer/GameSetup.tsx b/src/components/coordinates-trainer/GameSetup.tsx
new file mode 100644
index 00000000..2aaef160
--- /dev/null
+++ b/src/components/coordinates-trainer/GameSetup.tsx
@@ -0,0 +1,106 @@
+import { Icon } from "@iconify/react";
+import {
+ Box,
+ Button,
+ Card,
+ CardContent,
+ FormControl,
+ FormControlLabel,
+ InputLabel,
+ MenuItem,
+ Select,
+ Switch,
+ Typography,
+} from "@mui/material";
+import type { GameSettings } from "@/types/coordinatesTrainer";
+import { timeControls } from "@/hooks/useCoordinateTrainer";
+
+interface GameSetupProps {
+ settings: GameSettings;
+ onSettingsChange: (settings: Partial) => void;
+ onStartGame: () => void;
+}
+
+export default function GameSetup({
+ settings,
+ onSettingsChange,
+ onStartGame,
+}: GameSetupProps) {
+ return (
+
+
+
+ Coordinate Trainer
+
+
+ Train your board vision by finding squares quickly
+
+
+
+
+ Time Control
+
+
+
+
+ Board Orientation
+
+
+
+
+ onSettingsChange({ showCoordinates: e.target.checked })
+ }
+ />
+ }
+ label="Show coordinates in squares"
+ sx={{ mb: 2 }}
+ />
+
+
+ }
+ onClick={onStartGame}
+ sx={{ py: 1.5 }}
+ >
+ Start Training
+
+
+
+ );
+}
diff --git a/src/components/coordinates-trainer/SettingsDialog.tsx b/src/components/coordinates-trainer/SettingsDialog.tsx
new file mode 100644
index 00000000..2d1d2fd9
--- /dev/null
+++ b/src/components/coordinates-trainer/SettingsDialog.tsx
@@ -0,0 +1,88 @@
+import {
+ Dialog,
+ DialogTitle,
+ DialogContent,
+ DialogActions,
+ Button,
+ FormControl,
+ FormControlLabel,
+ InputLabel,
+ MenuItem,
+ Select,
+ Switch,
+ Box,
+} from "@mui/material";
+import { timeControls } from "@/hooks/useCoordinateTrainer";
+import { GameSettings } from "@/types/coordinatesTrainer";
+
+interface SettingsDialogProps {
+ open: boolean;
+ settings: GameSettings;
+ onClose: () => void;
+ onSettingsChange: (settings: Partial) => void;
+}
+
+export default function SettingsDialog({
+ open,
+ settings,
+ onClose,
+ onSettingsChange,
+}: SettingsDialogProps) {
+ return (
+
+ );
+}
diff --git a/src/hooks/useCoordinateTrainer.ts b/src/hooks/useCoordinateTrainer.ts
new file mode 100644
index 00000000..afb2a9ba
--- /dev/null
+++ b/src/hooks/useCoordinateTrainer.ts
@@ -0,0 +1,177 @@
+import { GameSettings, GameState } from "@/types/coordinatesTrainer";
+import { useState, useEffect, useCallback } from "react";
+
+export const files = ["a", "b", "c", "d", "e", "f", "g", "h"];
+export const ranks = [8, 7, 6, 5, 4, 3, 2, 1];
+
+// This should be made compattible witht the board theme that the user has selected
+export const getSquareColor = (file: string, rank: number): string => {
+ const isLight = (files.indexOf(file) + ranks.indexOf(rank)) % 2 === 0;
+ return isLight ? "#f0d9b5" : "#b58863";
+};
+
+export const getDisplayFiles = (orientation: "white" | "black"): string[] => {
+ return orientation === "black" ? [...files].reverse() : files;
+};
+
+export const getDisplayRanks = (orientation: "white" | "black"): number[] => {
+ return orientation === "black" ? [...ranks].reverse() : ranks;
+};
+
+const getRandomSquare = (): string => {
+ const file = files[Math.floor(Math.random() * 8)];
+ const rank = ranks[Math.floor(Math.random() * 8)];
+ return `${file}${rank}`;
+};
+
+/* 1.This hook serves as the core to the co-ordinates trainer. Following is an overview
+ 2.Setting and removing feedback text using timeouts.
+ 3.Generating and verfying target squares.
+ 4.Handling training start and end.
+*/
+export const useCoordinateGame = (settings: GameSettings) => {
+ const [gameState, setGameState] = useState({
+ status: "setup",
+ score: 0,
+ timeLeft: 0,
+ targetSquare: getRandomSquare(),
+ currentBoardOrientation: "white",
+ feedback: null,
+ wrongAnswer: false,
+ });
+
+ // Timer effect
+ useEffect(() => {
+ let interval: NodeJS.Timeout;
+ if (gameState.status === "playing" && gameState.timeLeft > 0) {
+ interval = setInterval(() => {
+ setGameState((prev) => {
+ if (prev.timeLeft <= 1) {
+ return { ...prev, status: "finished", timeLeft: 0 };
+ }
+ return { ...prev, timeLeft: prev.timeLeft - 1 };
+ });
+ }, 1000);
+ }
+ return () => clearInterval(interval);
+ }, [gameState.status, gameState.timeLeft]);
+
+ // Clear feedback after 2 seconds
+ useEffect(() => {
+ let timer: NodeJS.Timeout;
+ if (gameState.feedback) {
+ timer = setTimeout(() => {
+ setGameState((prev) => ({ ...prev, feedback: null }));
+ }, 2000);
+ }
+ return () => clearTimeout(timer);
+ }, [gameState.feedback]);
+
+ const startGame = useCallback(() => {
+ const orientation =
+ settings.boardColor === "random"
+ ? Math.random() > 0.5
+ ? "white"
+ : "black"
+ : (settings.boardColor as "white" | "black");
+
+ setGameState({
+ status: "playing",
+ score: 0,
+ timeLeft: settings.timeControl,
+ targetSquare: getRandomSquare(),
+ currentBoardOrientation: orientation,
+ feedback: null,
+ wrongAnswer: false,
+ });
+ }, [settings]);
+
+ const pauseGame = useCallback(() => {
+ setGameState((prev) => ({
+ ...prev,
+ status: prev.status === "paused" ? "playing" : "paused",
+ }));
+ }, []);
+
+ const stopGame = useCallback(() => {
+ setGameState((prev) => ({
+ ...prev,
+ status: "setup",
+ score: 0,
+ timeLeft: 0,
+ feedback: null,
+ }));
+ }, []);
+
+ const handleSquareClick = useCallback(
+ (square: string) => {
+ if (gameState.status !== "playing") return;
+
+ if (square === gameState.targetSquare) {
+ setGameState((prev) => ({
+ ...prev,
+ score: prev.score + 1,
+ feedback: { message: "✅ Correct!", type: "success" },
+ wrongAnswer: false,
+ targetSquare: getRandomSquare(),
+ }));
+ } else {
+ setGameState((prev) => ({
+ ...prev,
+ feedback: { message: "❌ Wrong! Try again", type: "error" },
+ wrongAnswer: true,
+ }));
+ }
+ },
+ [gameState.status, gameState.targetSquare]
+ );
+
+ const formatTime = useCallback((seconds: number): string => {
+ const mins = Math.floor(seconds / 60);
+ const secs = seconds % 60;
+ return `${mins}:${secs.toString().padStart(2, "0")}`;
+ }, []);
+
+ return {
+ gameState,
+ startGame,
+ pauseGame,
+ stopGame,
+ handleSquareClick,
+ formatTime,
+ };
+};
+
+export const timeControls = [
+ { label: "30 seconds", value: 30 },
+ { label: "1 minute", value: 60 },
+ { label: "2 minutes", value: 120 },
+ { label: "5 minutes", value: 300 },
+ { label: "Unlimited", value: 0 },
+];
+
+/*
+This hook get the user prefferd settings and passes it to the game state handling hook.
+This one hook servers to both the game settings Dialog and the game Setup Form that appears in the start.
+Any Change in the settings triggers a respective change in the game state
+*/
+export const useGameSettings = () => {
+ const [settings, setSettings] = useState({
+ timeControl: 60,
+ boardColor: "white",
+ showCoordinates: false,
+ });
+
+ const [showSettings, setShowSettings] = useState(false);
+
+ const updateSettings = (newSettings: Partial) => {
+ setSettings((prev) => ({ ...prev, ...newSettings }));
+ };
+
+ return {
+ settings,
+ showSettings,
+ setShowSettings,
+ updateSettings,
+ };
+};
diff --git a/src/pages/coordinate-trainer.tsx b/src/pages/coordinate-trainer.tsx
new file mode 100644
index 00000000..e11acc8c
--- /dev/null
+++ b/src/pages/coordinate-trainer.tsx
@@ -0,0 +1,74 @@
+import { Box } from "@mui/material";
+import {
+ useCoordinateGame,
+ useGameSettings,
+} from "@/hooks/useCoordinateTrainer";
+import ChessBoard from "@/components/coordinates-trainer/Board";
+import GameInfo from "@/components/coordinates-trainer/GameInfo";
+import GameSetup from "@/components/coordinates-trainer/GameSetup";
+import SettingsDialog from "@/components/coordinates-trainer/SettingsDialog";
+
+export default function CoordinateTrainer() {
+ const { settings, showSettings, setShowSettings, updateSettings } =
+ useGameSettings();
+ const {
+ gameState,
+ startGame,
+ pauseGame,
+ stopGame,
+ handleSquareClick,
+ formatTime,
+ } = useCoordinateGame(settings);
+
+ if (gameState.status === "setup") {
+ return (
+
+
+
+ );
+ }
+
+ return (
+ <>
+
+
+ setShowSettings(true)}
+ formatTime={formatTime}
+ />
+
+
+ setShowSettings(false)}
+ onSettingsChange={updateSettings}
+ />
+ >
+ );
+}
diff --git a/src/sections/layout/NavMenu.tsx b/src/sections/layout/NavMenu.tsx
index 337b2299..cb512e01 100644
--- a/src/sections/layout/NavMenu.tsx
+++ b/src/sections/layout/NavMenu.tsx
@@ -14,6 +14,7 @@ import {
const MenuOptions = [
{ text: "Play", icon: "streamline:chess-pawn", href: "/play" },
{ text: "Analysis", icon: "streamline:magnifying-glass-solid", href: "/" },
+ { text: "Train", icon: "streamline:target", href: "/coordinate-trainer" },
{
text: "Database",
icon: "streamline:database",
diff --git a/src/types/coordinatesTrainer.ts b/src/types/coordinatesTrainer.ts
new file mode 100644
index 00000000..b74dac18
--- /dev/null
+++ b/src/types/coordinatesTrainer.ts
@@ -0,0 +1,18 @@
+export interface GameSettings {
+ timeControl: number;
+ boardColor: "random" | "white" | "black";
+ showCoordinates: boolean;
+}
+
+export interface GameState {
+ status: "setup" | "playing" | "paused" | "finished";
+ score: number;
+ timeLeft: number;
+ targetSquare: string;
+ currentBoardOrientation: "white" | "black";
+ feedback: {
+ message: string;
+ type: "success" | "error";
+ } | null;
+ wrongAnswer: boolean;
+}