Avatar
Home
Projects
Contact
Available For Work
HomeProjectsContact
Local Time (Africa/Juba)
Local Time (Africa/Juba)
HomeProjectsContact
©2025, All Rights ReservedBuilt with by Maged

Checkmate Your Coding Skills: The Statemates Chess Platform Saga

A full-stack chess platform using SvelteKit, WebSockets, and the Stockfish engine, featuring adjustable AI difficulty, and real-time multiplayer.

Sep, 2024Completedmagedfaiz.xyzSource
Carousel image (1)
Carousel image (2)

Table of Contents

  • From Pawns to Promises: A Developer's Journey Back to the Board
  • The Opening Moves: Choosing the Right Pieces
  • The Middlegame: Taming the Engine
  • Snippet: Stockfish.ts
  • Snippet: Stockfish.ts
  • The Great Refactor: Dividing and Conquering
  • Snippet: GameState.ts
  • Snippet: AIGameState.ts

From Pawns to Promises: A Developer's Journey Back to the Board

Those summer days in Khartoum were filled with the rhythm of lessons, sports, and quiet moments at the chessboard. Chess wasn't just an activity; it was a mental escape, a strategic battle sandwiched between English classes and the heat of the afternoon sun. I didn't know it then, but those early moves were laying the foundation for something much bigger.

Decades later, the game called me back, but this time with a new kind of challenge: could I build my own chess platform?

This project was more than just an exercise in nostalgia. It was a technical gauntlet I set for myself. I wanted to dive deep into the complexities of chess engines, figure out the nuances of real-time multiplayer with WebSockets, and build the intricate game logic that makes it all work. It was a chance to test my skills against a classic and well-defined problem.

The Opening Moves: Choosing the Right Pieces

As a developer who spends most of my time with React and Next.js, my first instinct was to stick with what I knew. But to really challenge myself, I decided to step outside my comfort zone and revisit SvelteKit. I was looking for a framework that felt lean and fast, and Svelte's compiler-based approach felt like the right fit for a project that would involve a lot of state management on the client.

Of course, a chess platform is more than just a framework. I identified several key components I would need to either build or find:

  • A performant and customizable chessboard component.
  • A robust chess library for move validation and game state management.
  • The Stockfish engine to power the AI opponent.
  • A scalable and affordable server solution for real-time multiplayer games.

After researching my options, I landed on this tech stack:

  • SvelteKit as the core framework.
  • shadcn-svelte for the UI library, to quickly build a clean interface without getting bogged down in custom CSS.
  • svelte-chessground, a fantastic board component ported from Lichess.
  • for all the underlying chess logic and move validation.

Share Project

Checkmate Your Coding Skills: The Statemates Chess Platform Saga

A full-stack chess platform using SvelteKit, WebSockets, and the Stockfish engine, featuring adjustable AI difficulty, and real-time multiplayer.

Sep, 2024Completedmagedfaiz.xyzSource
Carousel image (1)
Carousel image (2)

Table of Contents

  • From Pawns to Promises: A Developer's Journey Back to the Board
  • The Opening Moves: Choosing the Right Pieces
  • The Middlegame: Taming the Engine
  • Snippet: Stockfish.ts
  • Snippet: Stockfish.ts
  • The Great Refactor: Dividing and Conquering
  • Snippet: GameState.ts
  • Snippet: AIGameState.ts

From Pawns to Promises: A Developer's Journey Back to the Board

Those summer days in Khartoum were filled with the rhythm of lessons, sports, and quiet moments at the chessboard. Chess wasn't just an activity; it was a mental escape, a strategic battle sandwiched between English classes and the heat of the afternoon sun. I didn't know it then, but those early moves were laying the foundation for something much bigger.

Decades later, the game called me back, but this time with a new kind of challenge: could I build my own chess platform?

This project was more than just an exercise in nostalgia. It was a technical gauntlet I set for myself. I wanted to dive deep into the complexities of chess engines, figure out the nuances of real-time multiplayer with WebSockets, and build the intricate game logic that makes it all work. It was a chance to test my skills against a classic and well-defined problem.

The Opening Moves: Choosing the Right Pieces

As a developer who spends most of my time with React and Next.js, my first instinct was to stick with what I knew. But to really challenge myself, I decided to step outside my comfort zone and revisit SvelteKit. I was looking for a framework that felt lean and fast, and Svelte's compiler-based approach felt like the right fit for a project that would involve a lot of state management on the client.

Of course, a chess platform is more than just a framework. I identified several key components I would need to either build or find:

  • A performant and customizable chessboard component.
  • A robust chess library for move validation and game state management.
  • The Stockfish engine to power the AI opponent.
  • A scalable and affordable server solution for real-time multiplayer games.

After researching my options, I landed on this tech stack:

  • SvelteKit as the core framework.
  • shadcn-svelte for the UI library, to quickly build a clean interface without getting bogged down in custom CSS.
  • svelte-chessground, a fantastic board component ported from Lichess.
  • for all the underlying chess logic and move validation.

Share Project

Snippet: MultiplayerGameState.ts
  • The Multiplayer Gambit: A Server-Side Pivot
  • Snippet: websocket.ts
  • The Game Room: The Heart of Multiplayer
  • Snippet: GameRoom.ts
  • Bonus Moves: Hints and Take-Backs
  • Snippet: AIGameState.ts
  • Lessons from the Chessboard
  • The Road Ahead: More Pawns to Promote
  • chess.js
  • Stockfish.js, a JavaScript port of the legendary C++ engine.
  • Express and ws for the multiplayer server—a classic, reliable combination for handling WebSockets.
  • With the pieces selected, I was ready to start building.

    The Middlegame: Taming the Engine

    Integrating the svelte-chessground component with chess.js for move validation was fairly straightforward. The real challenge began when I introduced Stockfish.

    The official Stockfish engine is written in C++, which doesn't naturally run in a browser's JavaScript environment. Thankfully, the open-source community maintains JavaScript ports of the engine, which are compiled to run efficiently in a Web Worker. This prevents the engine's intense calculations from freezing the user's browser tab.

    To manage the communication with the engine, I created a Stockfish class. This class acts as a wrapper, sending commands like uci, isready, and go to the engine in the worker and handling the asynchronous messages that come back.

    Once the engine was running, I faced the next problem: it was brutally, inhumanly strong. Even at its lowest setting, it played with a precision that would crush most casual players, which wasn't the experience I wanted to create. The solution was to build a system that could dynamically adjust Stockfish’s parameters to create a more natural difficulty curve. I exposed several of the engine's internal settings, like skill level, calculation depth, and "contempt", and mapped them to a simple 1-20 difficulty scale.

    The most important tweak for user experience was adding a slight, artificial delay to the AI's moves at lower difficulties. Nothing makes an AI feel more human than pretending it needs a moment to think.

    The Great Refactor: Dividing and Conquering

    As the application grew, the code that managed the game's state became increasingly complex. I had a single, monolithic GameState class that was trying to handle everything: single-player logic, AI interactions, and real-time multiplayer events. It was quickly becoming difficult to manage and debug.

    It was time for a strategic refactor. I decided to apply a classic object-oriented programming pattern. I broke down the monolithic class into a more logical structure:

    • A base GameState class that contains all the shared logic: handling moves, managing the board's state with chess.js, and checking for game-over conditions.
    • An AIGameState class that extends the base class and adds only the logic needed for interacting with the Stockfish engine.
    • A MultiplayerGameState class that also extends the base class, but adds the functionality for managing WebSocket connections and player timers.

    This new structure made the codebase far more modular and maintainable. If I needed to tweak the AI's behavior, I could do it in AIGameState without any risk of breaking the multiplayer logic, and vice versa.

    The Multiplayer Gambit: A Server-Side Pivot

    With the client-side logic organized, I turned my attention to the multiplayer server. Initially, I was drawn to modern, serverless solutions like Cloudflare Workers. They support WebSockets and have a generous free tier, which seemed perfect for a personal project. I even started building with Hono, a framework designed for these edge environments.

    However, I soon ran into a significant roadblock. While Workers are excellent for client-to-server communication, orchestrating communication between two clients in a stateful game room proved complicated. The recommended solution, Durable Objects, was powerful but fell outside the scope and budget I had in mind for this project.

    After weighing my options, I pivoted to a more traditional but highly effective solution: a simple Express server using the battle-tested ws library for WebSocket management. It was a pragmatic decision that gave me direct control over the game rooms and player connections, which was exactly what the project needed.

    The Game Room: The Heart of Multiplayer

    On the server, the centerpiece of the multiplayer logic is the GameRoom class. This class is responsible for everything that happens within a single match, from the moment the first player joins until a winner is decided.

    Its key responsibilities include:

    • Managing player connections, disconnections, and reconnections.
    • Broadcasting moves to each player to keep the game state in sync.
    • Enforcing game rules and monitoring for checkmate or stalemate.
    • Handling time controls, incrementing time after moves, and declaring a winner on timeout.
    • Facilitating rematch offers between players.

    One of the trickier parts was handling player reconnections without a full authentication system. The solution was to use a session cookie stored on the client. When a player joins a room, the server sends them a unique playerId. If their connection drops, they can rejoin the same game room with that ID, allowing the server to restore their session.

    Time management was another critical feature. The GameRoom class precisely tracks each player's remaining time, subtracting the time taken for each move and adding the configured increment. This server-side authority ensures fair and accurate timing, even with potential network lag.

    Bonus Moves: Hints and Take-Backs

    To make the single-player mode more user-friendly, I added "hint" and "undo" features. The hint button simply asks the Stockfish engine for its best move and displays it on the board. The undo feature reverses the last two moves of the player and the AI, and updates the engine with the previous board state. These small additions make playing against the formidable AI a much more forgiving and enjoyable learning experience.

    Lessons from the Chessboard

    Building Statemates was a long and rewarding game. Each feature was a new puzzle, and every bug was an unexpected counter-move. The biggest takeaways for me were:

    • SvelteKit's Simplicity: The framework's straightforward state management with stores and its minimal boilerplate allowed me to focus on the complex game logic instead of fighting the tools.
    • Object-oriented programming isn't just for Java or C#. Using classes and inheritance to structure the game's logic in TypeScript brought much-needed order to a complex system.
    • Pragmatism Over Hype: While trying new technologies like Cloudflare Workers was a great learning experience, knowing when to pivot to a traditional, reliable solution like Express was key to actually getting the project finished.
    • WebSockets are beautifully simple for real-time communication, but managing state, connections, and edge cases on the server requires careful architectural planning.

    The Road Ahead: More Pawns to Promote

    While the core features are complete, a project like this is never truly finished. I have a long roadmap of features I'd like to add, including user authentication, saved game history, and post-game analysis with Stockfish.

    This project taught me that coding and chess have a lot in common. They both require patience, strategic thinking, and a willingness to learn from your mistakes. For now, I'm happy with the result—a testament to what can be built with a bit of nostalgia and a lot of code.

    I invite you to check out Statemates. Challenge the AI or invite a friend for a game. And if you find any bugs, please let me know. After all, the game is never truly over; there's always another move to make.

    Snippet: MultiplayerGameState.ts
  • The Multiplayer Gambit: A Server-Side Pivot
  • Snippet: websocket.ts
  • The Game Room: The Heart of Multiplayer
  • Snippet: GameRoom.ts
  • Bonus Moves: Hints and Take-Backs
  • Snippet: AIGameState.ts
  • Lessons from the Chessboard
  • The Road Ahead: More Pawns to Promote
  • chess.js
  • Stockfish.js, a JavaScript port of the legendary C++ engine.
  • Express and ws for the multiplayer server—a classic, reliable combination for handling WebSockets.
  • With the pieces selected, I was ready to start building.

    The Middlegame: Taming the Engine

    Integrating the svelte-chessground component with chess.js for move validation was fairly straightforward. The real challenge began when I introduced Stockfish.

    The official Stockfish engine is written in C++, which doesn't naturally run in a browser's JavaScript environment. Thankfully, the open-source community maintains JavaScript ports of the engine, which are compiled to run efficiently in a Web Worker. This prevents the engine's intense calculations from freezing the user's browser tab.

    To manage the communication with the engine, I created a Stockfish class. This class acts as a wrapper, sending commands like uci, isready, and go to the engine in the worker and handling the asynchronous messages that come back.

    Once the engine was running, I faced the next problem: it was brutally, inhumanly strong. Even at its lowest setting, it played with a precision that would crush most casual players, which wasn't the experience I wanted to create. The solution was to build a system that could dynamically adjust Stockfish’s parameters to create a more natural difficulty curve. I exposed several of the engine's internal settings, like skill level, calculation depth, and "contempt", and mapped them to a simple 1-20 difficulty scale.

    The most important tweak for user experience was adding a slight, artificial delay to the AI's moves at lower difficulties. Nothing makes an AI feel more human than pretending it needs a moment to think.

    The Great Refactor: Dividing and Conquering

    As the application grew, the code that managed the game's state became increasingly complex. I had a single, monolithic GameState class that was trying to handle everything: single-player logic, AI interactions, and real-time multiplayer events. It was quickly becoming difficult to manage and debug.

    It was time for a strategic refactor. I decided to apply a classic object-oriented programming pattern. I broke down the monolithic class into a more logical structure:

    • A base GameState class that contains all the shared logic: handling moves, managing the board's state with chess.js, and checking for game-over conditions.
    • An AIGameState class that extends the base class and adds only the logic needed for interacting with the Stockfish engine.
    • A MultiplayerGameState class that also extends the base class, but adds the functionality for managing WebSocket connections and player timers.

    This new structure made the codebase far more modular and maintainable. If I needed to tweak the AI's behavior, I could do it in AIGameState without any risk of breaking the multiplayer logic, and vice versa.

    The Multiplayer Gambit: A Server-Side Pivot

    With the client-side logic organized, I turned my attention to the multiplayer server. Initially, I was drawn to modern, serverless solutions like Cloudflare Workers. They support WebSockets and have a generous free tier, which seemed perfect for a personal project. I even started building with Hono, a framework designed for these edge environments.

    However, I soon ran into a significant roadblock. While Workers are excellent for client-to-server communication, orchestrating communication between two clients in a stateful game room proved complicated. The recommended solution, Durable Objects, was powerful but fell outside the scope and budget I had in mind for this project.

    After weighing my options, I pivoted to a more traditional but highly effective solution: a simple Express server using the battle-tested ws library for WebSocket management. It was a pragmatic decision that gave me direct control over the game rooms and player connections, which was exactly what the project needed.

    The Game Room: The Heart of Multiplayer

    On the server, the centerpiece of the multiplayer logic is the GameRoom class. This class is responsible for everything that happens within a single match, from the moment the first player joins until a winner is decided.

    Its key responsibilities include:

    • Managing player connections, disconnections, and reconnections.
    • Broadcasting moves to each player to keep the game state in sync.
    • Enforcing game rules and monitoring for checkmate or stalemate.
    • Handling time controls, incrementing time after moves, and declaring a winner on timeout.
    • Facilitating rematch offers between players.

    One of the trickier parts was handling player reconnections without a full authentication system. The solution was to use a session cookie stored on the client. When a player joins a room, the server sends them a unique playerId. If their connection drops, they can rejoin the same game room with that ID, allowing the server to restore their session.

    Time management was another critical feature. The GameRoom class precisely tracks each player's remaining time, subtracting the time taken for each move and adding the configured increment. This server-side authority ensures fair and accurate timing, even with potential network lag.

    Bonus Moves: Hints and Take-Backs

    To make the single-player mode more user-friendly, I added "hint" and "undo" features. The hint button simply asks the Stockfish engine for its best move and displays it on the board. The undo feature reverses the last two moves of the player and the AI, and updates the engine with the previous board state. These small additions make playing against the formidable AI a much more forgiving and enjoyable learning experience.

    Lessons from the Chessboard

    Building Statemates was a long and rewarding game. Each feature was a new puzzle, and every bug was an unexpected counter-move. The biggest takeaways for me were:

    • SvelteKit's Simplicity: The framework's straightforward state management with stores and its minimal boilerplate allowed me to focus on the complex game logic instead of fighting the tools.
    • Object-oriented programming isn't just for Java or C#. Using classes and inheritance to structure the game's logic in TypeScript brought much-needed order to a complex system.
    • Pragmatism Over Hype: While trying new technologies like Cloudflare Workers was a great learning experience, knowing when to pivot to a traditional, reliable solution like Express was key to actually getting the project finished.
    • WebSockets are beautifully simple for real-time communication, but managing state, connections, and edge cases on the server requires careful architectural planning.

    The Road Ahead: More Pawns to Promote

    While the core features are complete, a project like this is never truly finished. I have a long roadmap of features I'd like to add, including user authentication, saved game history, and post-game analysis with Stockfish.

    This project taught me that coding and chess have a lot in common. They both require patience, strategic thinking, and a willingness to learn from your mistakes. For now, I'm happy with the result—a testament to what can be built with a bit of nostalgia and a lot of code.

    I invite you to check out Statemates. Challenge the AI or invite a friend for a game. And if you find any bugs, please let me know. After all, the game is never truly over; there's always another move to make.

    Stockfish.ts

    import { Engine, EngineState } from './engine';
    import type { ChessMove } from '$lib/chess/types';
    import { STARTING_FEN } from '$lib/constants';
    
    interface SearchParams {
    	moveTime: number;
    	depth: number;
    }
    
    /**
     * Stockfish class that interacts with the Stockfish chess engine via a Web Worker.
     * It provides methods to control the engine, set difficulty, and retrieve best moves.
     */
    export class Stockfish extends Engine {
    	private state: EngineState;
    	private difficulty: number;
    	private bestMove: ChessMove;
    	private ponder: ChessMove;
    	private searchParams: SearchParams;
    	private messageCallback: ((message: string) => void) | null = null;
    	private currentFen: string = STARTING_FEN;
    	private debug: boolean;
    
    	/**
    	 * Creates a new Stockfish instance.
    	 * @param debug - If true, enables detailed logging.
    	 */
    	constructor({ debug = false, difficulty = 10 }) {
    		super('/stockfish.js');
    		this.state = EngineState.Uninitialized;
    		this.difficulty = difficulty; // Default difficulty level (range: 1-20)
    		this.bestMove = { from: '', to: '' };
    		this.ponder = { from: '', to: '' };
    		this.searchParams = { moveTime: 1000, depth: 5, moveDelay: 400 };
    		this.debug = debug;
    		this.initialize();
    	}
    
    	private initialize(): void {
    		this.setState(EngineState.Initializing);
    		this.worker.postMessage('uci');
    		this.worker.onmessage = this.handleInitialization.bind(this);
    	}
    
    	private handleInitialization(event: MessageEvent): void {
    		const message = event.data;
    		if (message.includes('uciok')) {
    			this.setState(EngineState.Waiting);
    			this.setDifficulty(this.difficulty);
    			this.worker.postMessage('isready');
    		} else if (message.includes('readyok')) {
    			this.log('Engine is fully initialized and ready', 'info');
    			this.state = EngineState.Waiting;
    			this.worker.onmessage = this.handleMessage.bind(this);
    		}
    	}
    
    	private handleMessage(event: MessageEvent): void {
    		const message = event.data;
    		this.log('Stockfish message: ', message);
    		this.handleBestMoveMessage(message);
    		if (this.messageCallback) {
    			this.messageCallback(message);
    		}
    	}
    
    	onMessage(callback: (message: string) => void): void {
    		this.messageCallback = callback;
    	}
    
    	private handleBestMoveMessage(message: string): void {
    		if (!message.includes('bestmove')) return;
    
    		this.log(message, 'info');
    		const moves = message.split(' ');
    		this.bestMove = this.parseMove(moves[1]);
    		this.ponder = moves[3] ? this.parseMove(moves[3]) : { from: '', to: '' };
    		this.setState(EngineState.Waiting);
    	}
    
    	private parseMove(move: string): ChessMove {
    		return {
    			from: move.slice(0, 2),
    			to: move.slice(2, 4)
    		};
    	}
    
    // other methods
    
    }

    Stockfish.ts

    {
    
      /**
    	 * Sets the difficulty level of the chess engine.
    	 * @param level - Difficulty level (1-20, where 1 is easiest and 20 is hardest)
    	 *
    	 * This method adjusts several Stockfish parameters based on the difficulty level:
    	 * 1. Skill Level (0-20): Mapped using a sigmoid function for a more gradual increase.
    	 *    Lower values make the engine play weaker, allowing for more mistakes.
    	 *    At 0, the engine plays randomly from a selection of good moves.
    	 *
    	 * 2. Contempt (-100 to 100): Mapped using a sigmoid function centered at 0.
    	 *    Positive values make the engine play more aggressively and take more risks to avoid draws.
    	 *    Negative values make the engine more accepting of draws.
    	 *    At 0, the engine plays objectively.
    	 *
    	 * 3. MultiPV (5-1): Decreases linearly as difficulty increases.
    	 *    Determines the number of alternative moves the engine considers.
    	 *    At lower difficulties, more alternatives are considered, making play more varied.
    	 *    At higher difficulties, fewer alternatives are considered, focusing on the best moves.
    	 *
    	 * 4. Move Time (100-1800 ms): Increases non-linearly with difficulty.
    	 *    Determines how long the engine thinks about each move.
    	 *    Longer times at higher difficulties allow for deeper, more accurate analysis.
    	 *
    	 * 5. Depth (1-15): Increases non-linearly with difficulty.
    	 *    Determines how many moves ahead the engine calculates.
    	 *    Greater depth at higher difficulties results in stronger, more strategic play.
    	 *
    	 * 6. Move Delay (400-0 ms): Decreases linearly with difficulty.
    	 *    Adds a delay before the engine makes its move, ensuring a more engaging user experience.
    	 *    Shorter delays at higher difficulties balance out the longer move times.
    	 *
    	 * The new mappings ensure a smoother progression of difficulty:
    	 * - Beginner and Casual levels have longer delays and shorter move times for quick, varied play.
    	 * - Intermediate to Expert levels balance move time and delay for a natural progression.
    	 * - Master and Grandmaster levels have longer move times but shorter delays for deep analysis and quicker responses.
    	 * This progression aims to provide a more natural increase in difficulty while maintaining engagement.
    	 */
    	setDifficulty(level: number): void {
    		this.difficulty = level;
    		const skillLevel = this.mapLevelToSkill(level);
    		const contempt = this.mapLevelToContempt(level);
    		const moveTime = this.mapLevelToMoveTime(level);
    		const depth = this.mapLevelToDepth(level);
    		const multiPV = this.mapLevelToMultiPV(level);
    		const moveDelay = this.mapLevelToMoveDelay(level);
    
    		this.log(
    			`Setting difficulty: Skill Level ${skillLevel}, Contempt ${contempt}, MultiPV ${multiPV}, Move Time ${moveTime}, Depth ${depth}, Move Delay ${moveDelay}`,
    			'info'
    		);
    		this.worker.postMessage(`setoption name Skill Level value ${skillLevel}`);
    		this.worker.postMessage(`setoption name Contempt value ${contempt}`);
    		this.worker.postMessage(`setoption name MultiPV value ${multiPV}`);
    
    		this.searchParams = { moveTime, depth, moveDelay };
    	}
    
    
        /**
    	 * Maps the difficulty level (1-20) to a Stockfish Skill Level (0-20).
    	 * Uses a sigmoid function for a more gradual increase in skill level.
    	 *
    	 * @param level - The input difficulty level (1-20)
    	 * @returns The corresponding Stockfish Skill Level (0-20)
    	 */
    	private mapLevelToSkill(level: number): number {
    		const x = (level - 10) / 5; // Center the sigmoid at level 10
    		const sigmoid = 1 / (1 + Math.exp(-x));
    		return Math.round(sigmoid * 20);
    	}
    
    	/**
    	 * Maps the difficulty level (1-20) to a Stockfish Contempt value (-100 to 100).
    	 * Uses a sigmoid function for a more balanced progression, centered at 0.
    	 *
    	 * @param level - The input difficulty level (1-20)
    	 * @returns The corresponding Stockfish Contempt value (-100 to 100)
    	 */
    	private mapLevelToContempt(level: number): number {
    		const x = (level - 10) / 3; // Center the sigmoid at level 10
    		const sigmoid = 1 / (1 + Math.exp(-x));
    		return Math.round((sigmoid * 2 - 1) * 100); // Map to range -100 to 100
    	}
    
    	/**
    	 * Maps the difficulty level (1-20) to a search depth (1-15).
    	 * Uses a power function with exponent 1.4 for a balanced depth increase.
    	 *
    	 * @param level - The input difficulty level (1-20)
    	 * @returns The corresponding search depth (1-15)
    	 */
    	private mapLevelToDepth(level: number): number {
    		return Math.round(1 + Math.pow((level - 1) / 19, 1.4) * 14);
    	}
    
    	/**
    	 * Maps the difficulty level (1-20) to a move time (100-1800 ms).
    	 * Uses a power function with exponent 1.5 for a more balanced time progression.
    	 *
    	 * @param level - The input difficulty level (1-20)
    	 * @returns The corresponding move time in milliseconds (100-1800)
    	 */
    	private mapLevelToMoveTime(level: number): number {
    		return Math.round(100 + Math.pow((level - 1) / 19, 1.5) * 1700);
    	}
    
    	/**
    	 * Maps the difficulty level (1-20) to a move delay (400-0 ms).
    	 * Uses a linear function to provide a smooth decrease in delay.
    	 *
    	 * @param level - The input difficulty level (1-20)
    	 * @returns The corresponding move delay in milliseconds (400-0)
    	 */
    	private mapLevelToMoveDelay(level: number): number {
    		return Math.round(400 - ((level - 1) / 19) * 400);
    	}
    
    	/**
    	 * Maps the difficulty level (1-20) to a MultiPV value (5-1).
    	 * MultiPV decreases as difficulty increases, making the engine consider fewer alternative moves at higher difficulties.
    	 *
    	 * @param level - The input difficulty level (1-20)
    	 * @returns The corresponding MultiPV value (5-1)
    	 */
    	private mapLevelToMultiPV(level: number): number {
    		return Math.max(1, Math.floor((21 - level) / 4));
    	}
    
        go(): void {
    		if (this.state !== EngineState.Waiting) {
    			this.log('Engine is not ready to start searching', 'warn');
    			return;
    		}
    		this.setState(EngineState.Searching);
    		const { moveTime, depth, moveDelay } = this.searchParams;
    		this.log(`Delaying move by ${moveDelay}ms`);
    		setTimeout(() => {
    			this.log(`Sending go command to Stockfish with depth: ${depth}, movetime: ${moveTime}`);
    			this.worker.postMessage(`go depth ${depth} movetime ${moveTime}`);
    		}, moveDelay);
    	}
    
      // other methods
    }

    websocket.ts

    import WebSocket from 'ws';
    import { IncomingMessage } from 'http';
    import { URL } from 'url';
    import {
    	removePlayerFromGame,
    	handlePlayerMessage,
    	checkGameStart,
    	addPlayerToGame,
    	reconnectPlayerToGame
    } from './game';
    
    interface ConnectionParams {
    	id: string;
    	color: 'white' | 'black';
    	playerId: string | null;
    }
    
    export function handleWebSocketConnection(ws: WebSocket, req: IncomingMessage) {
    	const params = parseConnectionParams(req);
    	if (!params) {
    		closeConnection(ws, 1008, 'Invalid game room');
    		return;
    	}
    
    	console.log(`New connection attempt for game ${params.id}`);
    	console.log(`Color: ${params.color}, PlayerId: ${params.playerId}`);
    
    	try {
    		const activePlayerId = handlePlayerConnection(ws, params);
    		if (!activePlayerId) {
    			closeConnection(ws, 1008, 'Unable to join game');
    			return;
    		}
    
    		setupEventListeners(ws, params.id, activePlayerId);
    		checkGameStartStatus(params.id);
    	} catch (error) {
    		console.error('Error handling WebSocket connection:', error);
    		closeConnection(ws, 1011, 'Internal server error');
    	}
    }
    
    function parseConnectionParams(req: IncomingMessage): ConnectionParams | null {
    	const url = new URL(req.url!, `http://${req.headers.host}`);
    	const id = url.searchParams.get('id');
    	const color = url.searchParams.get('color') as 'white' | 'black';
    	const playerId = url.searchParams.get('playerId');
    
    	if (!id) {
    		return null;
    	}
    
    	return { id, color, playerId };
    }
    
    function handlePlayerConnection(ws: WebSocket, params: ConnectionParams): string | null {
    	if (!params.playerId) {
    		console.log('Adding new player to game');
    		const activePlayerId = addPlayerToGame(params.id, params.color, ws);
    		if (activePlayerId) {
    			ws.send(JSON.stringify({ type: 'connected', playerId: activePlayerId }));
    		}
    		return activePlayerId;
    	} else {
    		console.log('Reconnecting existing player');
    		const reconnected = reconnectPlayerToGame(params.id, params.playerId, ws);
    		return reconnected ? params.playerId : null;
    	}
    }
    
    function setupEventListeners(ws: WebSocket, gameId: string, playerId: string) {
    	ws.on('message', (message: string) => handlePlayerMessage(gameId, playerId, message));
    	ws.on('close', () => removePlayerFromGame(gameId, playerId));
    }
    
    function checkGameStartStatus(gameId: string) {
    	const gameStarted = checkGameStart(gameId);
    	if (gameStarted) {
    		console.log(`Game ${gameId} started with both players`);
    	}
    }
    
    function closeConnection(ws: WebSocket, code: number, reason: string) {
    	ws.close(code, reason);
    }

    GameRoom.ts

    import WebSocket from 'ws';
    import { Chess } from 'chess.js';
    import { nanoid } from 'nanoid';
    import { Player, TimeControl, TimeOption, Color, GameMessage } from './types';
    
    export class GameRoom {
        id: string = nanoid();
        players: Player[] = [];
        gameStarted: boolean = false;
        private chess: Chess = new Chess();
        private currentFen: string = this.chess.fen();
        private currentTurn: Color = 'white';
        private timeControl: TimeControl;
        private lastMoveTime?: number;
    
        constructor({ time = 0 }: { time: TimeOption }) {
            this.timeControl = this.convertTimeOption(time);
        }
    
        addPlayer(color: Color, ws: WebSocket): string {
            if (this.players.length >= 2) {
                throw new Error('Game room is full');
            }
            const playerId = nanoid();
            const timeRemaining = this.timeControl.isUnlimited ? null : this.timeControl.initial;
            const player = { id: playerId, color, ws, timeRemaining, connected: true };
            this.players.push(player);
    
            this.notifyPlayersOfJoin(playerId);
    
            if (this.players.length === 2) {
                this.startGame();
            }
    
            return playerId;
        }
    
        removePlayer(playerId: string) {
    		const playerIndex = this.players.findIndex((p) => p.id === playerId);
    		if (playerIndex !== -1) {
    			this.players[playerIndex].connected = false;
    			this.players[playerIndex].ws = null;
    		}
    		this.broadcastGameState();
    	}
    
    	reconnectPlayer(playerId: string, ws: WebSocket): boolean {
    		const player = this.findPlayerById(playerId);
    		if (!player) {
    			return false;
    		}
    
    		player.ws = ws;
    		player.connected = true;
    
    		this.sendToPlayer(player, {
    			type: 'gameState',
    			...this.getCurrentGameState(),
    			timeControl: this.timeControl
    		});
    		this.notifyOpponentOfReconnection(playerId);
    
    		return true;
    	}
    
    	handleMessage(playerId: string, message: GameMessage) {
    		const player = this.findPlayerById(playerId);
    		if (!player) return;
    
    		switch (message.type) {
    			case 'move':
    				this.handleMove(player, message.move);
    				break;
    			case 'offerRematch':
    				this.handleRematchOffer(playerId);
    				break;
    			case 'acceptRematch':
    				this.handleRematchAccept(playerId);
    				break;
    			case 'gameOver':
    				if (message.reason === 'timeout') {
    					this.handleTimeOut(message.winner);
    				}
    				break;
    		}
    	}
    
        private updateGameStateAfterMove(
            playerId: string,
            move: { from: string; to: string; promotion?: string }
        ) {
            this.currentTurn = this.currentTurn === 'white' ? 'black' : 'white';
            this.currentFen = this.chess.fen();
            this.broadcastMove(playerId, move);
            if (!this.timeControl.isUnlimited) {
                this.updatePlayerTime(this.findPlayerById(playerId)!);
            }
            this.checkGameEnd();
        }
    
        private updatePlayerTime(player: Player) {
    		if (player.timeRemaining === null) return;
    
    		const now = Date.now();
    		if (this.lastMoveTime) {
    			const timeTaken = (now - this.lastMoveTime) / 1000;
    			player.timeRemaining -= timeTaken;
    
    			if (player.timeRemaining <= this.timeControl.lowTimeThreshold) {
    				player.timeRemaining += this.timeControl.increment;
    			}
    
    			if (player.timeRemaining <= 0) {
    				this.handleTimeOut(player.color === 'white' ? 'black' : 'white');
    			}
    		}
    		this.lastMoveTime = now;
    		this.broadcastGameState();
    	}
    
        private resetPlayerTimes() {
    		this.players.forEach((player) => {
    			if (player.timeRemaining !== null) {
    				player.timeRemaining = this.timeControl.initial;
    			}
    		});
    	}
    
        // More methods...
    }

    AIGameState.ts

    {
    
    // ... other methods
    
      async getHint(): Promise<ChessMove | null> {
    		if (!this.started || this.player !== get(this.turn)) return null;
    		const hintMove = await this.engine.getHint(this.player === 'white' ? 'w' : 'b');
    		this.hint.set(hintMove);
    		return hintMove;
    	}
    
      undoMove() {
    		this.chess.undo();
    		this.chess.undo();
    		this.moveHistory.update((history) => history.slice(0, -2));
    		this.updateGameState();
    		this.engine.setPosition(this.chess.fen());
    	}
    
    // ... other methods
    
    }

    Stockfish.ts

    import { Engine, EngineState } from './engine';
    import type { ChessMove } from '$lib/chess/types';
    import { STARTING_FEN } from '$lib/constants';
    
    interface SearchParams {
    	moveTime: number;
    	depth: number;
    }
    
    /**
     * Stockfish class that interacts with the Stockfish chess engine via a Web Worker.
     * It provides methods to control the engine, set difficulty, and retrieve best moves.
     */
    export class Stockfish extends Engine {
    	private state: EngineState;
    	private difficulty: number;
    	private bestMove: ChessMove;
    	private ponder: ChessMove;
    	private searchParams: SearchParams;
    	private messageCallback: ((message: string) => void) | null = null;
    	private currentFen: string = STARTING_FEN;
    	private debug: boolean;
    
    	/**
    	 * Creates a new Stockfish instance.
    	 * @param debug - If true, enables detailed logging.
    	 */
    	constructor({ debug = false, difficulty = 10 }) {
    		super('/stockfish.js');
    		this.state = EngineState.Uninitialized;
    		this.difficulty = difficulty; // Default difficulty level (range: 1-20)
    		this.bestMove = { from: '', to: '' };
    		this.ponder = { from: '', to: '' };
    		this.searchParams = { moveTime: 1000, depth: 5, moveDelay: 400 };
    		this.debug = debug;
    		this.initialize();
    	}
    
    	private initialize(): void {
    		this.setState(EngineState.Initializing);
    		this.worker.postMessage('uci');
    		this.worker.onmessage = this.handleInitialization.bind(this);
    	}
    
    	private handleInitialization(event: MessageEvent): void {
    		const message = event.data;
    		if (message.includes('uciok')) {
    			this.setState(EngineState.Waiting);
    			this.setDifficulty(this.difficulty);
    			this.worker.postMessage('isready');
    		} else if (message.includes('readyok')) {
    			this.log('Engine is fully initialized and ready', 'info');
    			this.state = EngineState.Waiting;
    			this.worker.onmessage = this.handleMessage.bind(this);
    		}
    	}
    
    	private handleMessage(event: MessageEvent): void {
    		const message = event.data;
    		this.log('Stockfish message: ', message);
    		this.handleBestMoveMessage(message);
    		if (this.messageCallback) {
    			this.messageCallback(message);
    		}
    	}
    
    	onMessage(callback: (message: string) => void): void {
    		this.messageCallback = callback;
    	}
    
    	private handleBestMoveMessage(message: string): void {
    		if (!message.includes('bestmove')) return;
    
    		this.log(message, 'info');
    		const moves = message.split(' ');
    		this.bestMove = this.parseMove(moves[1]);
    		this.ponder = moves[3] ? this.parseMove(moves[3]) : { from: '', to: '' };
    		this.setState(EngineState.Waiting);
    	}
    
    	private parseMove(move: string): ChessMove {
    		return {
    			from: move.slice(0, 2),
    			to: move.slice(2, 4)
    		};
    	}
    
    // other methods
    
    }

    Stockfish.ts

    {
    
      /**
    	 * Sets the difficulty level of the chess engine.
    	 * @param level - Difficulty level (1-20, where 1 is easiest and 20 is hardest)
    	 *
    	 * This method adjusts several Stockfish parameters based on the difficulty level:
    	 * 1. Skill Level (0-20): Mapped using a sigmoid function for a more gradual increase.
    	 *    Lower values make the engine play weaker, allowing for more mistakes.
    	 *    At 0, the engine plays randomly from a selection of good moves.
    	 *
    	 * 2. Contempt (-100 to 100): Mapped using a sigmoid function centered at 0.
    	 *    Positive values make the engine play more aggressively and take more risks to avoid draws.
    	 *    Negative values make the engine more accepting of draws.
    	 *    At 0, the engine plays objectively.
    	 *
    	 * 3. MultiPV (5-1): Decreases linearly as difficulty increases.
    	 *    Determines the number of alternative moves the engine considers.
    	 *    At lower difficulties, more alternatives are considered, making play more varied.
    	 *    At higher difficulties, fewer alternatives are considered, focusing on the best moves.
    	 *
    	 * 4. Move Time (100-1800 ms): Increases non-linearly with difficulty.
    	 *    Determines how long the engine thinks about each move.
    	 *    Longer times at higher difficulties allow for deeper, more accurate analysis.
    	 *
    	 * 5. Depth (1-15): Increases non-linearly with difficulty.
    	 *    Determines how many moves ahead the engine calculates.
    	 *    Greater depth at higher difficulties results in stronger, more strategic play.
    	 *
    	 * 6. Move Delay (400-0 ms): Decreases linearly with difficulty.
    	 *    Adds a delay before the engine makes its move, ensuring a more engaging user experience.
    	 *    Shorter delays at higher difficulties balance out the longer move times.
    	 *
    	 * The new mappings ensure a smoother progression of difficulty:
    	 * - Beginner and Casual levels have longer delays and shorter move times for quick, varied play.
    	 * - Intermediate to Expert levels balance move time and delay for a natural progression.
    	 * - Master and Grandmaster levels have longer move times but shorter delays for deep analysis and quicker responses.
    	 * This progression aims to provide a more natural increase in difficulty while maintaining engagement.
    	 */
    	setDifficulty(level: number): void {
    		this.difficulty = level;
    		const skillLevel = this.mapLevelToSkill(level);
    		const contempt = this.mapLevelToContempt(level);
    		const moveTime = this.mapLevelToMoveTime(level);
    		const depth = this.mapLevelToDepth(level);
    		const multiPV = this.mapLevelToMultiPV(level);
    		const moveDelay = this.mapLevelToMoveDelay(level);
    
    		this.log(
    			`Setting difficulty: Skill Level ${skillLevel}, Contempt ${contempt}, MultiPV ${multiPV}, Move Time ${moveTime}, Depth ${depth}, Move Delay ${moveDelay}`,
    			'info'
    		);
    		this.worker.postMessage(`setoption name Skill Level value ${skillLevel}`);
    		this.worker.postMessage(`setoption name Contempt value ${contempt}`);
    		this.worker.postMessage(`setoption name MultiPV value ${multiPV}`);
    
    		this.searchParams = { moveTime, depth, moveDelay };
    	}
    
    
        /**
    	 * Maps the difficulty level (1-20) to a Stockfish Skill Level (0-20).
    	 * Uses a sigmoid function for a more gradual increase in skill level.
    	 *
    	 * @param level - The input difficulty level (1-20)
    	 * @returns The corresponding Stockfish Skill Level (0-20)
    	 */
    	private mapLevelToSkill(level: number): number {
    		const x = (level - 10) / 5; // Center the sigmoid at level 10
    		const sigmoid = 1 / (1 + Math.exp(-x));
    		return Math.round(sigmoid * 20);
    	}
    
    	/**
    	 * Maps the difficulty level (1-20) to a Stockfish Contempt value (-100 to 100).
    	 * Uses a sigmoid function for a more balanced progression, centered at 0.
    	 *
    	 * @param level - The input difficulty level (1-20)
    	 * @returns The corresponding Stockfish Contempt value (-100 to 100)
    	 */
    	private mapLevelToContempt(level: number): number {
    		const x = (level - 10) / 3; // Center the sigmoid at level 10
    		const sigmoid = 1 / (1 + Math.exp(-x));
    		return Math.round((sigmoid * 2 - 1) * 100); // Map to range -100 to 100
    	}
    
    	/**
    	 * Maps the difficulty level (1-20) to a search depth (1-15).
    	 * Uses a power function with exponent 1.4 for a balanced depth increase.
    	 *
    	 * @param level - The input difficulty level (1-20)
    	 * @returns The corresponding search depth (1-15)
    	 */
    	private mapLevelToDepth(level: number): number {
    		return Math.round(1 + Math.pow((level - 1) / 19, 1.4) * 14);
    	}
    
    	/**
    	 * Maps the difficulty level (1-20) to a move time (100-1800 ms).
    	 * Uses a power function with exponent 1.5 for a more balanced time progression.
    	 *
    	 * @param level - The input difficulty level (1-20)
    	 * @returns The corresponding move time in milliseconds (100-1800)
    	 */
    	private mapLevelToMoveTime(level: number): number {
    		return Math.round(100 + Math.pow((level - 1) / 19, 1.5) * 1700);
    	}
    
    	/**
    	 * Maps the difficulty level (1-20) to a move delay (400-0 ms).
    	 * Uses a linear function to provide a smooth decrease in delay.
    	 *
    	 * @param level - The input difficulty level (1-20)
    	 * @returns The corresponding move delay in milliseconds (400-0)
    	 */
    	private mapLevelToMoveDelay(level: number): number {
    		return Math.round(400 - ((level - 1) / 19) * 400);
    	}
    
    	/**
    	 * Maps the difficulty level (1-20) to a MultiPV value (5-1).
    	 * MultiPV decreases as difficulty increases, making the engine consider fewer alternative moves at higher difficulties.
    	 *
    	 * @param level - The input difficulty level (1-20)
    	 * @returns The corresponding MultiPV value (5-1)
    	 */
    	private mapLevelToMultiPV(level: number): number {
    		return Math.max(1, Math.floor((21 - level) / 4));
    	}
    
        go(): void {
    		if (this.state !== EngineState.Waiting) {
    			this.log('Engine is not ready to start searching', 'warn');
    			return;
    		}
    		this.setState(EngineState.Searching);
    		const { moveTime, depth, moveDelay } = this.searchParams;
    		this.log(`Delaying move by ${moveDelay}ms`);
    		setTimeout(() => {
    			this.log(`Sending go command to Stockfish with depth: ${depth}, movetime: ${moveTime}`);
    			this.worker.postMessage(`go depth ${depth} movetime ${moveTime}`);
    		}, moveDelay);
    	}
    
      // other methods
    }

    websocket.ts

    import WebSocket from 'ws';
    import { IncomingMessage } from 'http';
    import { URL } from 'url';
    import {
    	removePlayerFromGame,
    	handlePlayerMessage,
    	checkGameStart,
    	addPlayerToGame,
    	reconnectPlayerToGame
    } from './game';
    
    interface ConnectionParams {
    	id: string;
    	color: 'white' | 'black';
    	playerId: string | null;
    }
    
    export function handleWebSocketConnection(ws: WebSocket, req: IncomingMessage) {
    	const params = parseConnectionParams(req);
    	if (!params) {
    		closeConnection(ws, 1008, 'Invalid game room');
    		return;
    	}
    
    	console.log(`New connection attempt for game ${params.id}`);
    	console.log(`Color: ${params.color}, PlayerId: ${params.playerId}`);
    
    	try {
    		const activePlayerId = handlePlayerConnection(ws, params);
    		if (!activePlayerId) {
    			closeConnection(ws, 1008, 'Unable to join game');
    			return;
    		}
    
    		setupEventListeners(ws, params.id, activePlayerId);
    		checkGameStartStatus(params.id);
    	} catch (error) {
    		console.error('Error handling WebSocket connection:', error);
    		closeConnection(ws, 1011, 'Internal server error');
    	}
    }
    
    function parseConnectionParams(req: IncomingMessage): ConnectionParams | null {
    	const url = new URL(req.url!, `http://${req.headers.host}`);
    	const id = url.searchParams.get('id');
    	const color = url.searchParams.get('color') as 'white' | 'black';
    	const playerId = url.searchParams.get('playerId');
    
    	if (!id) {
    		return null;
    	}
    
    	return { id, color, playerId };
    }
    
    function handlePlayerConnection(ws: WebSocket, params: ConnectionParams): string | null {
    	if (!params.playerId) {
    		console.log('Adding new player to game');
    		const activePlayerId = addPlayerToGame(params.id, params.color, ws);
    		if (activePlayerId) {
    			ws.send(JSON.stringify({ type: 'connected', playerId: activePlayerId }));
    		}
    		return activePlayerId;
    	} else {
    		console.log('Reconnecting existing player');
    		const reconnected = reconnectPlayerToGame(params.id, params.playerId, ws);
    		return reconnected ? params.playerId : null;
    	}
    }
    
    function setupEventListeners(ws: WebSocket, gameId: string, playerId: string) {
    	ws.on('message', (message: string) => handlePlayerMessage(gameId, playerId, message));
    	ws.on('close', () => removePlayerFromGame(gameId, playerId));
    }
    
    function checkGameStartStatus(gameId: string) {
    	const gameStarted = checkGameStart(gameId);
    	if (gameStarted) {
    		console.log(`Game ${gameId} started with both players`);
    	}
    }
    
    function closeConnection(ws: WebSocket, code: number, reason: string) {
    	ws.close(code, reason);
    }

    GameRoom.ts

    import WebSocket from 'ws';
    import { Chess } from 'chess.js';
    import { nanoid } from 'nanoid';
    import { Player, TimeControl, TimeOption, Color, GameMessage } from './types';
    
    export class GameRoom {
        id: string = nanoid();
        players: Player[] = [];
        gameStarted: boolean = false;
        private chess: Chess = new Chess();
        private currentFen: string = this.chess.fen();
        private currentTurn: Color = 'white';
        private timeControl: TimeControl;
        private lastMoveTime?: number;
    
        constructor({ time = 0 }: { time: TimeOption }) {
            this.timeControl = this.convertTimeOption(time);
        }
    
        addPlayer(color: Color, ws: WebSocket): string {
            if (this.players.length >= 2) {
                throw new Error('Game room is full');
            }
            const playerId = nanoid();
            const timeRemaining = this.timeControl.isUnlimited ? null : this.timeControl.initial;
            const player = { id: playerId, color, ws, timeRemaining, connected: true };
            this.players.push(player);
    
            this.notifyPlayersOfJoin(playerId);
    
            if (this.players.length === 2) {
                this.startGame();
            }
    
            return playerId;
        }
    
        removePlayer(playerId: string) {
    		const playerIndex = this.players.findIndex((p) => p.id === playerId);
    		if (playerIndex !== -1) {
    			this.players[playerIndex].connected = false;
    			this.players[playerIndex].ws = null;
    		}
    		this.broadcastGameState();
    	}
    
    	reconnectPlayer(playerId: string, ws: WebSocket): boolean {
    		const player = this.findPlayerById(playerId);
    		if (!player) {
    			return false;
    		}
    
    		player.ws = ws;
    		player.connected = true;
    
    		this.sendToPlayer(player, {
    			type: 'gameState',
    			...this.getCurrentGameState(),
    			timeControl: this.timeControl
    		});
    		this.notifyOpponentOfReconnection(playerId);
    
    		return true;
    	}
    
    	handleMessage(playerId: string, message: GameMessage) {
    		const player = this.findPlayerById(playerId);
    		if (!player) return;
    
    		switch (message.type) {
    			case 'move':
    				this.handleMove(player, message.move);
    				break;
    			case 'offerRematch':
    				this.handleRematchOffer(playerId);
    				break;
    			case 'acceptRematch':
    				this.handleRematchAccept(playerId);
    				break;
    			case 'gameOver':
    				if (message.reason === 'timeout') {
    					this.handleTimeOut(message.winner);
    				}
    				break;
    		}
    	}
    
        private updateGameStateAfterMove(
            playerId: string,
            move: { from: string; to: string; promotion?: string }
        ) {
            this.currentTurn = this.currentTurn === 'white' ? 'black' : 'white';
            this.currentFen = this.chess.fen();
            this.broadcastMove(playerId, move);
            if (!this.timeControl.isUnlimited) {
                this.updatePlayerTime(this.findPlayerById(playerId)!);
            }
            this.checkGameEnd();
        }
    
        private updatePlayerTime(player: Player) {
    		if (player.timeRemaining === null) return;
    
    		const now = Date.now();
    		if (this.lastMoveTime) {
    			const timeTaken = (now - this.lastMoveTime) / 1000;
    			player.timeRemaining -= timeTaken;
    
    			if (player.timeRemaining <= this.timeControl.lowTimeThreshold) {
    				player.timeRemaining += this.timeControl.increment;
    			}
    
    			if (player.timeRemaining <= 0) {
    				this.handleTimeOut(player.color === 'white' ? 'black' : 'white');
    			}
    		}
    		this.lastMoveTime = now;
    		this.broadcastGameState();
    	}
    
        private resetPlayerTimes() {
    		this.players.forEach((player) => {
    			if (player.timeRemaining !== null) {
    				player.timeRemaining = this.timeControl.initial;
    			}
    		});
    	}
    
        // More methods...
    }

    AIGameState.ts

    {
    
    // ... other methods
    
      async getHint(): Promise<ChessMove | null> {
    		if (!this.started || this.player !== get(this.turn)) return null;
    		const hintMove = await this.engine.getHint(this.player === 'white' ? 'w' : 'b');
    		this.hint.set(hintMove);
    		return hintMove;
    	}
    
      undoMove() {
    		this.chess.undo();
    		this.chess.undo();
    		this.moveHistory.update((history) => history.slice(0, -2));
    		this.updateGameState();
    		this.engine.setPosition(this.chess.fen());
    	}
    
    // ... other methods
    
    }
    import { writable, type Writable } from 'svelte/store';
    import { Chess, type Move, type Square } from 'chess.js';
    import { getCheckState, toDestinations } from './utils';
    import type { GameSettings } from '$lib/stores/gameSettings';
    import { STARTING_FEN, MOVE_AUDIOS_PATHS } from '../constants';
    import type { CheckState, ChessMove, GameMode, GameOver, PromotionMove, MoveType } from './types';
    import type { Color } from 'chessground/types';
    
    export abstract class GameState {
    	protected chess: Chess;
    	private audioFiles: Record<MoveType, HTMLAudioElement> = {} as Record<MoveType, HTMLAudioElement>;
    	mode: GameMode;
    	player: Color;
    	moveHistory: Writable<ChessMove[]> = writable([]);
    	audioCue: Writable<MoveType> = writable('normal');
    	started: Writable<boolean> = writable(false);
    	promotionMove: Writable<PromotionMove> = writable(null);
    	checkState: Writable<CheckState> = writable({ inCheck: false });
    	gameOver: Writable<GameOver> = writable({ isOver: false, winner: null });
    	fen: Writable<string>;
    	turn: Writable<Color>;
    	destinations: Writable<Map<Square, Square[]>> = writable(new Map());
    	hint: Writable<ChessMove | null> = writable(null);
    
    	constructor(mode: GameMode, player: Color, fen: string = STARTING_FEN) {
    		this.chess = new Chess(fen);
    		this.mode = mode;
    		this.player = player;
    		this.fen = writable(fen);
    		this.turn = writable(this.chess.turn() === 'w' ? 'white' : 'black');
    		this.updateDestinations();
    
    		// Initialize audio files
    		Object.entries(MOVE_AUDIOS_PATHS).forEach(([key, path]) => {
    			this.audioFiles[key as MoveType] = new Audio(path);
    			this.audioFiles[key as MoveType].load();
    			this.audioFiles[key as MoveType].volume = 0.9;
    		});
    	}
    
    	abstract setDifficulty(difficulty: number): void;
    	abstract updateSettings(settings: GameSettings): void;
    	abstract undoMove(): void;
    	abstract getHint(): Promise<ChessMove | null>;
    
    	newGame() {
    		this.chess.reset();
    		this.updateGameState();
    		this.started.set(true);
    		this.audioCue.set('game-start');
    	}
    
    
    	endGame() {
    		this.chess.reset();
    		this.updateGameState();
    		this.started.set(false);
    		this.gameOver.set({ isOver: false, winner: null });
    		this.audioCue.set('game-end');
    	}
    
    	handlePlayerMove({ from, to }: ChessMove) {
    		if (this.isPromotionMove(from, to)) {
    			this.promotionMove.set({ from, to });
    		} else {
    			this.makeMove({ from, to });
    		}
    	}
    
    	makeMove({ from, to, promotion }: ChessMove): boolean {
    		try {
    			const move = this.chess.move({ from, to, promotion });
    			if (move) {
    				this.moveHistory.update((history) => [...history, { from, to, promotion }]);
    				this.updateGameState();
    				this.determineMoveType(move);
    				return true;
    			}
    		} catch (error) {
    			console.error('Invalid move:', { from, to, promotion }, error);
    		}
    		return false;
    	}
    
        protected updateGameState() {
    		this.fen.set(this.chess.fen());
    		this.turn.set(this.chess.turn() === 'w' ? 'white' : 'black');
    		this.updateDestinations();
    		this.checkState.set(getCheckState(this.chess));
    		this.checkGameOver();
    	}
    
    
      //other methods
    
    }
    import { writable, type Writable } from 'svelte/store';
    import { Chess, type Move, type Square } from 'chess.js';
    import { getCheckState, toDestinations } from './utils';
    import type { GameSettings } from '$lib/stores/gameSettings';
    import { STARTING_FEN, MOVE_AUDIOS_PATHS } from '../constants';
    import type { CheckState, ChessMove, GameMode, GameOver, PromotionMove, MoveType } from './types';
    import type { Color } from 'chessground/types';
    
    export abstract class GameState {
    	protected chess: Chess;
    	private audioFiles: Record<MoveType, HTMLAudioElement> = {} as Record<MoveType, HTMLAudioElement>;
    	mode: GameMode;
    	player: Color;
    	moveHistory: Writable<ChessMove[]> = writable([]);
    	audioCue: Writable<MoveType> = writable('normal');
    	started: Writable<boolean> = writable(false);
    	promotionMove: Writable<PromotionMove> = writable(null);
    	checkState: Writable<CheckState> = writable({ inCheck: false });
    	gameOver: Writable<GameOver> = writable({ isOver: false, winner: null });
    	fen: Writable<string>;
    	turn: Writable<Color>;
    	destinations: Writable<Map<Square, Square[]>> = writable(new Map());
    	hint: Writable<ChessMove | null> = writable(null);
    
    	constructor(mode: GameMode, player: Color, fen: string = STARTING_FEN) {
    		this.chess = new Chess(fen);
    		this.mode = mode;
    		this.player = player;
    		this.fen = writable(fen);
    		this.turn = writable(this.chess.turn() === 'w' ? 'white' : 'black');
    		this.updateDestinations();
    
    		// Initialize audio files
    		Object.entries(MOVE_AUDIOS_PATHS).forEach(([key, path]) => {
    			this.audioFiles[key as MoveType] = new Audio(path);
    			this.audioFiles[key as MoveType].load();
    			this.audioFiles[key as MoveType].volume = 0.9;
    		});
    	}
    
    	abstract setDifficulty(difficulty: number): void;
    	abstract updateSettings(settings: GameSettings): void;
    	abstract undoMove(): void;
    	abstract getHint(): Promise<ChessMove | null>;
    
    	newGame() {
    		this.chess.reset();
    		this.updateGameState();
    		this.started.set(true);
    		this.audioCue.set('game-start');
    	}
    
    
    	endGame() {
    		this.chess.reset();
    		this.updateGameState();
    		this.started.set(false);
    		this.gameOver.set({ isOver: false, winner: null });
    		this.audioCue.set('game-end');
    	}
    
    	handlePlayerMove({ from, to }: ChessMove) {
    		if (this.isPromotionMove(from, to)) {
    			this.promotionMove.set({ from, to });
    		} else {
    			this.makeMove({ from, to });
    		}
    	}
    
    	makeMove({ from, to, promotion }: ChessMove): boolean {
    		try {
    			const move = this.chess.move({ from, to, promotion });
    			if (move) {
    				this.moveHistory.update((history) => [...history, { from, to, promotion }]);
    				this.updateGameState();
    				this.determineMoveType(move);
    				return true;
    			}
    		} catch (error) {
    			console.error('Invalid move:', { from, to, promotion }, error);
    		}
    		return false;
    	}
    
        protected updateGameState() {
    		this.fen.set(this.chess.fen());
    		this.turn.set(this.chess.turn() === 'w' ? 'white' : 'black');
    		this.updateDestinations();
    		this.checkState.set(getCheckState(this.chess));
    		this.checkGameOver();
    	}
    
    
      //other methods
    
    }