Chess, a game of strategy, tactics, and foresight, has captivated minds for centuries. But have you ever considered building your own chess game? This tutorial will guide you through creating a simple, interactive chess game using TypeScript, a superset of JavaScript that adds static typing. We’ll explore fundamental concepts, implement core game logic, and learn how to make it playable in your web browser. This project is perfect for beginners and intermediate developers looking to deepen their understanding of TypeScript and game development principles.
Why Build a Chess Game with TypeScript?
TypeScript offers several advantages that make it an excellent choice for this project:
- Type Safety: TypeScript’s static typing helps catch errors during development, reducing runtime bugs and making your code more reliable.
- Code Readability: Type annotations and interfaces improve code clarity and maintainability.
- Refactoring Support: TypeScript’s strong typing makes refactoring easier and less error-prone.
- Object-Oriented Programming: TypeScript supports OOP principles like classes, inheritance, and polymorphism, making it ideal for structuring a complex game like chess.
By building this game, you’ll gain practical experience with these TypeScript features while learning the basics of game development.
Setting Up Your Development Environment
Before we dive into the code, you’ll need to set up your development environment. Here’s what you’ll need:
- Node.js and npm: Install Node.js, which comes with npm (Node Package Manager). npm will be used to manage project dependencies. You can download it from nodejs.org.
- TypeScript Compiler: Install the TypeScript compiler globally using npm:
npm install -g typescript. - Code Editor: Choose a code editor like Visual Studio Code (VS Code), which has excellent TypeScript support.
Project Structure
Let’s define the project structure. Create a new directory for your project, navigate into it in your terminal, and initialize a new npm project:
mkdir chess-game
cd chess-game
npm init -y
This will create a package.json file. Next, create the following directory structure:
chess-game/
├── src/
│ ├── index.ts
│ ├── chessboard.ts
│ ├── piece.ts
│ ├── pawn.ts
│ ├── rook.ts
│ ├── knight.ts
│ ├── bishop.ts
│ ├── queen.ts
│ ├── king.ts
│ └── utils.ts
├── index.html
├── tsconfig.json
└── package.json
Here’s what each file will contain:
index.ts: The main entry point for the game.chessboard.ts: Defines the chessboard class and its methods.piece.ts: Defines the base Piece class.pawn.ts,rook.ts,knight.ts,bishop.ts,queen.ts,king.ts: Define the classes for each chess piece, extending the Piece class.utils.ts: Contains utility functions.index.html: The HTML file that will render the game in the browser.tsconfig.json: TypeScript compiler configuration file.
Configuring TypeScript
Create a tsconfig.json file in your project’s root directory with the following content:
{
"compilerOptions": {
"target": "es5",
"module": "commonjs",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*"]
}
This configuration tells the TypeScript compiler to:
- Compile to ES5 JavaScript.
- Use CommonJS module format.
- Output compiled files to a
distdirectory. - Enable strict type checking.
- Include all files in the
srcdirectory.
Creating the Chessboard
Let’s start by creating the chessboard.ts file. This file will define the Chessboard class, which will represent the chess board and its state.
// src/chessboard.ts
import { Piece } from './piece';
import { Pawn } from './pawn';
import { Rook } from './rook';
import { Knight } from './knight';
import { Bishop } from './bishop';
import { Queen } from './queen';
import { King } from './king';
export class Chessboard {
board: (Piece | null)[][];
constructor() {
this.board = this.initializeBoard();
}
private initializeBoard(): (Piece | null)[][] {
const board: (Piece | null)[][] = Array(8).fill(null).map(() => Array(8).fill(null));
// White pieces
board[7][0] = new Rook('white');
board[7][1] = new Knight('white');
board[7][2] = new Bishop('white');
board[7][3] = new Queen('white');
board[7][4] = new King('white');
board[7][5] = new Bishop('white');
board[7][6] = new Knight('white');
board[7][7] = new Rook('white');
for (let i = 0; i < 8; i++) {
board[6][i] = new Pawn('white');
}
// Black pieces
board[0][0] = new Rook('black');
board[0][1] = new Knight('black');
board[0][2] = new Bishop('black');
board[0][3] = new Queen('black');
board[0][4] = new King('black');
board[0][5] = new Bishop('black');
board[0][6] = new Knight('black');
board[0][7] = new Rook('black');
for (let i = 0; i < 8; i++) {
board[1][i] = new Pawn('black');
}
return board;
}
// Method to get a piece at a specific position
getPiece(row: number, col: number): Piece | null {
return this.board[row][col];
}
// Method to set a piece at a specific position
setPiece(row: number, col: number, piece: Piece | null): void {
this.board[row][col] = piece;
}
// Method to move a piece
movePiece(fromRow: number, fromCol: number, toRow: number, toCol: number): boolean {
const piece = this.getPiece(fromRow, fromCol);
if (!piece) {
console.log("No piece at the starting position.");
return false;
}
if (!piece.isValidMove(this, fromRow, fromCol, toRow, toCol)) {
console.log("Invalid move for this piece.");
return false;
}
// Check for a piece at the destination
const targetPiece = this.getPiece(toRow, toCol);
if (targetPiece) {
console.log(`${targetPiece.color} ${targetPiece.constructor.name} captured!`);
}
// Move the piece
this.setPiece(toRow, toCol, piece);
this.setPiece(fromRow, fromCol, null);
return true;
}
// Method to print the board to the console (for debugging)
printBoard(): void {
for (let row = 0; row < 8; row++) {
let rowString = `${row} `;
for (let col = 0; col < 8; col++) {
const piece = this.getPiece(row, col);
rowString += piece ? `${piece.color.charAt(0).toUpperCase()}${piece.constructor.name.charAt(0).toUpperCase()} ` : '. '; // Display piece abbreviation
}
console.log(rowString);
}
console.log(' 0 1 2 3 4 5 6 7');
}
}
This class does the following:
- Imports Piece classes: Imports the necessary piece classes (Pawn, Rook, etc.).
- board property: A 2D array representing the chessboard. Each element can be a
Pieceobject ornullif the square is empty. - constructor: Initializes the board by calling
initializeBoard(). - initializeBoard(): Creates the initial board setup with pieces in their starting positions.
- getPiece(row, col): Returns the piece at a given row and column.
- setPiece(row, col, piece): Sets a piece at a given row and column.
- movePiece(fromRow, fromCol, toRow, toCol): Moves a piece from one square to another if the move is valid. It also handles capturing pieces.
- printBoard(): A utility function to print the current state of the board in the console for debugging purposes.
Creating the Piece Class
Next, let’s create the base Piece class in piece.ts. This class will serve as the parent class for all chess pieces.
// src/piece.ts
import { Chessboard } from './chessboard';
export abstract class Piece {
color: 'white' | 'black';
constructor(color: 'white' | 'black') {
this.color = color;
}
abstract isValidMove(board: Chessboard, fromRow: number, fromCol: number, toRow: number, toCol: number): boolean;
}
Key aspects of the Piece class:
- color property: Stores the color of the piece (‘white’ or ‘black’).
- constructor: Initializes the color of the piece.
- abstract isValidMove(): An abstract method that each subclass (Pawn, Rook, etc.) will implement to define the valid moves for that piece.
Implementing Individual Piece Classes
Now, let’s create the classes for each chess piece, extending the Piece class. We’ll start with the Pawn in pawn.ts:
// src/pawn.ts
import { Piece } from './piece';
import { Chessboard } from './chessboard';
export class Pawn extends Piece {
constructor(color: 'white' | 'black') {
super(color);
}
isValidMove(board: Chessboard, fromRow: number, fromCol: number, toRow: number, toCol: number): boolean {
const direction = this.color === 'white' ? -1 : 1; // White pawns move up, black pawns move down
const startRow = this.color === 'white' ? 6 : 1; // White pawns start on row 6, black on row 1
const pieceAtDestination = board.getPiece(toRow, toCol);
// Moving straight
if (fromCol === toCol) {
if (toRow === fromRow + direction) {
return !pieceAtDestination; // Can move one square forward if empty
} else if (fromRow === startRow && toRow === fromRow + 2 * direction) {
// Can move two squares forward from starting position if both squares are empty
return !pieceAtDestination && !board.getPiece(fromRow + direction, fromCol);
}
}
// Capturing diagonally
if (Math.abs(toCol - fromCol) === 1 && toRow === fromRow + direction) {
return pieceAtDestination && pieceAtDestination.color !== this.color; // Can capture diagonally if opponent's piece is there
}
return false;
}
}
The Pawn class:
- Extends Piece: Inherits from the
Piececlass. - constructor: Calls the parent constructor to set the color.
- isValidMove(): Implements the move logic for pawns. It checks for valid moves, including moving one or two squares forward (initial move), and capturing diagonally.
Now, let’s implement the other pieces. The following code blocks show the implementation of the remaining chess pieces. Each class extends `Piece` and implements the `isValidMove` method with the appropriate rules for each piece.
Rook (rook.ts):
// src/rook.ts
import { Piece } from './piece';
import { Chessboard } from './chessboard';
export class Rook extends Piece {
constructor(color: 'white' | 'black') {
super(color);
}
isValidMove(board: Chessboard, fromRow: number, fromCol: number, toRow: number, toCol: number): boolean {
// Check if the move is horizontal or vertical
if (fromRow !== toRow && fromCol !== toCol) {
return false; // Not horizontal or vertical
}
// Check for obstructions
if (fromRow === toRow) {
// Horizontal move
const start = Math.min(fromCol, toCol) + 1;
const end = Math.max(fromCol, toCol);
for (let i = start; i < end; i++) {
if (board.getPiece(fromRow, i)) {
return false; // Obstruction
}
}
}
if (fromCol === toCol) {
// Vertical move
const start = Math.min(fromRow, toRow) + 1;
const end = Math.max(fromRow, toRow);
for (let i = start; i < end; i++) {
if (board.getPiece(i, fromCol)) {
return false; // Obstruction
}
}
}
// Check if the destination square is occupied by a piece of the same color
const targetPiece = board.getPiece(toRow, toCol);
if (targetPiece && targetPiece.color === this.color) {
return false;
}
return true;
}
}
Knight (knight.ts):
// src/knight.ts
import { Piece } from './piece';
import { Chessboard } from './chessboard';
export class Knight extends Piece {
constructor(color: 'white' | 'black') {
super(color);
}
isValidMove(board: Chessboard, fromRow: number, fromCol: number, toRow: number, toCol: number): boolean {
const rowDiff = Math.abs(toRow - fromRow);
const colDiff = Math.abs(toCol - fromCol);
// Check for L-shaped move
if ((rowDiff === 2 && colDiff === 1) || (rowDiff === 1 && colDiff === 2)) {
// Check if the destination square is occupied by a piece of the same color
const targetPiece = board.getPiece(toRow, toCol);
if (targetPiece && targetPiece.color === this.color) {
return false;
}
return true;
}
return false;
}
}
Bishop (bishop.ts):
// src/bishop.ts
import { Piece } from './piece';
import { Chessboard } from './chessboard';
export class Bishop extends Piece {
constructor(color: 'white' | 'black') {
super(color);
}
isValidMove(board: Chessboard, fromRow: number, fromCol: number, toRow: number, toCol: number): boolean {
const rowDiff = Math.abs(toRow - fromRow);
const colDiff = Math.abs(toCol - fromCol);
// Check if the move is diagonal
if (rowDiff !== colDiff) {
return false;
}
// Check for obstructions
const rowDirection = toRow > fromRow ? 1 : -1;
const colDirection = toCol > fromCol ? 1 : -1;
for (let i = 1; i < rowDiff; i++) {
if (board.getPiece(fromRow + i * rowDirection, fromCol + i * colDirection)) {
return false; // Obstruction
}
}
// Check if the destination square is occupied by a piece of the same color
const targetPiece = board.getPiece(toRow, toCol);
if (targetPiece && targetPiece.color === this.color) {
return false;
}
return true;
}
}
Queen (queen.ts):
// src/queen.ts
import { Piece } from './piece';
import { Chessboard } from './chessboard';
export class Queen extends Piece {
constructor(color: 'white' | 'black') {
super(color);
}
isValidMove(board: Chessboard, fromRow: number, fromCol: number, toRow: number, toCol: number): boolean {
const rowDiff = Math.abs(toRow - fromRow);
const colDiff = Math.abs(toCol - fromCol);
// Check if the move is horizontal, vertical, or diagonal
if (fromRow !== toRow && fromCol !== toCol && rowDiff !== colDiff) {
return false;
}
// Check for obstructions (horizontal and vertical)
if (fromRow === toRow) {
const start = Math.min(fromCol, toCol) + 1;
const end = Math.max(fromCol, toCol);
for (let i = start; i < end; i++) {
if (board.getPiece(fromRow, i)) {
return false; // Obstruction
}
}
}
if (fromCol === toCol) {
const start = Math.min(fromRow, toRow) + 1;
const end = Math.max(fromRow, toRow);
for (let i = start; i fromRow ? 1 : -1;
const colDirection = toCol > fromCol ? 1 : -1;
for (let i = 1; i < rowDiff; i++) {
if (board.getPiece(fromRow + i * rowDirection, fromCol + i * colDirection)) {
return false; // Obstruction
}
}
}
// Check if the destination square is occupied by a piece of the same color
const targetPiece = board.getPiece(toRow, toCol);
if (targetPiece && targetPiece.color === this.color) {
return false;
}
return true;
}
}
King (king.ts):
// src/king.ts
import { Piece } from './piece';
import { Chessboard } from './chessboard';
export class King extends Piece {
constructor(color: 'white' | 'black') {
super(color);
}
isValidMove(board: Chessboard, fromRow: number, fromCol: number, toRow: number, toCol: number): boolean {
const rowDiff = Math.abs(toRow - fromRow);
const colDiff = Math.abs(toCol - fromCol);
// Check if the move is one square in any direction
if (rowDiff > 1 || colDiff > 1) {
return false;
}
// Check if the destination square is occupied by a piece of the same color
const targetPiece = board.getPiece(toRow, toCol);
if (targetPiece && targetPiece.color === this.color) {
return false;
}
return true;
}
}
Each of these piece classes follows a similar structure:
- Extends Piece: Inherits from the
Piececlass. - constructor: Calls the parent constructor to set the color.
- isValidMove(): Implements the move logic for the specific piece. This is the most complex part, as it enforces the rules of each piece.
Creating the User Interface (index.html)
Now, let’s create a simple HTML file (index.html) to render the chessboard and allow the user to interact with it.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Simple Chess Game</title>
<style>
.chessboard {
display: grid;
grid-template-columns: repeat(8, 50px);
grid-template-rows: repeat(8, 50px);
width: 400px;
height: 400px;
border: 2px solid black;
}
.square {
width: 50px;
height: 50px;
display: flex;
justify-content: center;
align-items: center;
font-size: 30px;
}
.white {
background-color: #f0d9b5;
}
.black {
background-color: #b58863;
}
.selected {
background-color: yellow;
}
</style>
</head>
<body>
<div class="chessboard" id="chessboard">
</div>
<script src="dist/index.js"></script>
</body>
</html>
This HTML file:
- Defines the basic structure of the page.
- Includes a
<div>with the classchessboard, which will hold the chess board squares. - Includes a basic CSS to style the board and squares.
- Links to the compiled JavaScript file (
dist/index.js).
Implementing the Main Game Logic (index.ts)
Now, let’s write the main game logic in index.ts. This file will handle the game initialization, rendering the board, and user interaction.
// src/index.ts
import { Chessboard } from './chessboard';
import { Piece } from './piece';
const chessboardElement = document.getElementById('chessboard') as HTMLElement;
let chessboard: Chessboard;
let selectedPiece: { row: number; col: number } | null = null;
// Function to render the chessboard
function renderBoard(): void {
if (!chessboardElement) return;
chessboardElement.innerHTML = ''; // Clear the board
for (let row = 0; row < 8; row++) {
for (let col = 0; col handleSquareClick(row, col));
chessboardElement.appendChild(square);
}
}
}
// Function to get the piece symbol
function getPieceSymbol(piece: Piece): string {
switch (piece.constructor.name) {
case 'Pawn': return piece.color === 'white' ? '♙' : '♟';
case 'Rook': return piece.color === 'white' ? '♖' : '♜';
case 'Knight': return piece.color === 'white' ? '♘' : '♞';
case 'Bishop': return piece.color === 'white' ? '♗' : '♝';
case 'Queen': return piece.color === 'white' ? '♕' : '♛';
case 'King': return piece.color === 'white' ? '♔' : '♚';
default: return '';
}
}
// Function to handle square clicks
function handleSquareClick(row: number, col: number): void {
if (selectedPiece) {
// Move the piece
const fromRow = selectedPiece.row;
const fromCol = selectedPiece.col;
if (chessboard.movePiece(fromRow, fromCol, row, col)) {
selectedPiece = null;
} else {
console.log('Invalid move.');
}
} else {
// Select a piece
const piece = chessboard.getPiece(row, col);
if (piece) {
selectedPiece = { row, col };
}
}
renderBoard();
}
// Initialize the game
function initializeGame(): void {
chessboard = new Chessboard();
renderBoard();
}
// Start the game when the page loads
document.addEventListener('DOMContentLoaded', initializeGame);
Key parts of index.ts:
- Imports: Imports the
ChessboardandPiececlasses. - chessboardElement: Gets a reference to the chessboard element in the HTML.
- chessboard: A variable to hold the
Chessboardinstance. - selectedPiece: Keeps track of the currently selected piece.
- renderBoard(): Renders the chessboard in the HTML. It iterates through the board array, creates
<div>elements for each square, sets the background color, and displays the piece symbol if a piece is present. - getPieceSymbol(): Returns the Unicode symbol for each chess piece based on its type and color.
- handleSquareClick(): Handles clicks on the chessboard squares. It either selects a piece or attempts to move a selected piece to the clicked square.
- initializeGame(): Initializes the game by creating a new
Chessboardinstance and rendering the board. - Event Listener: Attaches an event listener to the
DOMContentLoadedevent to start the game when the page loads.
Compiling and Running the Game
Now, let’s compile and run the game. In your terminal, navigate to your project directory and run the following command:
tsc
This command will compile your TypeScript code and generate the JavaScript files in the dist directory.
To run the game, open index.html in your web browser. You should see the chessboard with the pieces in their starting positions. Click on a piece to select it, then click on a valid destination square to move it.
Common Mistakes and How to Fix Them
Here are some common mistakes and how to avoid them:
- Incorrect File Paths: Double-check your file paths in the
importstatements. TypeScript will throw an error if it can’t find a module. - Type Errors: TypeScript’s type checking can be strict. Make sure your variables and function parameters have the correct types. Use type annotations to help catch errors early.
- Invalid Move Logic: Carefully review the
isValidMovemethods for each piece. Ensure that the move rules are correctly implemented. Test each piece’s movement thoroughly. - Missing Event Listeners: Make sure you’ve added event listeners to your HTML elements (e.g., the squares) to handle user interaction.
- Incorrect CSS Styling: Ensure your CSS is correctly applied to the HTML elements to display the board and pieces properly.
Key Takeaways
- TypeScript Fundamentals: This tutorial provides hands-on practice with TypeScript’s core features, including classes, interfaces, inheritance, and static typing.
- Object-Oriented Programming (OOP): You’ve learned how to structure your code using OOP principles, creating classes for each chess piece and organizing the game logic.
- Game Development Basics: You’ve built a simple interactive game, learning about game state management, user input handling, and rendering.
- Code Organization: You’ve learned how to structure a project with multiple files and modules for better organization and maintainability.
FAQ
- Can I add more features to this game? Absolutely! You can add features like:
- Check and checkmate detection.
- Castling.
- En passant capture.
- Promotion of pawns.
- A user interface for displaying whose turn it is.
- An AI opponent.
- How can I improve the visual appearance of the game? You can use CSS to style the board, pieces, and overall game layout. Consider using images for the pieces instead of text symbols.
- How can I add sound effects? You can use the HTML
<audio>element or a JavaScript audio library to play sound effects when pieces are moved or captured. - Where can I learn more about TypeScript? You can refer to the official TypeScript documentation: https://www.typescriptlang.org/docs/
Building a chess game in TypeScript is a great way to learn and apply your programming skills. You can expand upon this foundation to create a more feature-rich and engaging game. By understanding the core concepts and following the steps outlined in this tutorial, you’ve created a solid base to build upon. From here, the possibilities for enhancement are endless.
