TypeScript Tutorial: Building a Simple Interactive Web-Based Game of Battleships

Ever wanted to build your own game? How about a classic like Battleships? This tutorial will guide you, step-by-step, through creating a simple, interactive, web-based version of Battleships using TypeScript. We’ll cover everything from setting up your project to handling user input and displaying the game board. By the end, you’ll have a playable game and a solid understanding of how to use TypeScript to build interactive web applications. This tutorial is designed for beginners to intermediate developers, assuming a basic familiarity with HTML, CSS, and JavaScript.

Why Battleships? Why TypeScript?

Battleships is a fantastic project for learning because it involves several core programming concepts: data structures (representing the game board), user interaction (handling clicks and input), logic (determining hits and misses), and UI updates (displaying the game state). TypeScript adds a layer of type safety and structure to your code, making it easier to maintain and debug, especially as your projects grow in complexity. It’s an excellent way to learn how to write more robust and scalable web applications.

Setting Up Your Project

Before we dive into the code, let’s set up our project. We’ll use a simple HTML file to display the game, a CSS file for styling, and a TypeScript file for the game logic. You’ll need Node.js and npm (Node Package Manager) installed on your system to manage dependencies and compile TypeScript.

1. Create Project Directory

First, create a new directory for your project. Open your terminal or command prompt and navigate to where you want to store your project. Then, run the following command:

mkdir battleships-ts-game
cd battleships-ts-game

2. Initialize npm

Initialize a new npm project inside the directory:

npm init -y

This creates a package.json file, which will manage our project dependencies.

3. Install TypeScript

Next, install TypeScript as a development dependency:

npm install --save-dev typescript

4. Create TypeScript Configuration File

Create a tsconfig.json file in your project’s root directory. This file tells the TypeScript compiler how to compile your code. You can generate a basic one with the following command:

npx tsc --init

This command creates a tsconfig.json file with default settings. You can customize this file to suit your needs, but for this tutorial, the default settings will work fine. You might want to change the outDir to a folder like “dist” to store the compiled JavaScript files.

5. Create Project Files

Create the following files in your project directory:

  • index.html: The main HTML file for your game.
  • style.css: The CSS file for styling.
  • src/index.ts: The main TypeScript file where we’ll write our game logic. Make sure to create a “src” directory as well.

HTML Structure (index.html)

Let’s start with the HTML structure. Open index.html and add the following code:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Battleships Game</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <h1>Battleships</h1>
    <div id="game-board"></div>
    <script src="dist/index.js"></script>
</body>
</html>

This is a basic HTML structure. We have a title, a link to our CSS file, and a div with the ID “game-board” where we will render our game board. The script tag at the end links to the compiled JavaScript file (dist/index.js). Remember to compile your TypeScript code before running the HTML file.

CSS Styling (style.css)

Now, let’s add some basic styling to style.css. This is a very simple example; feel free to customize it further:

body {
    font-family: sans-serif;
    text-align: center;
}

#game-board {
    display: grid;
    grid-template-columns: repeat(10, 30px);
    grid-template-rows: repeat(10, 30px);
    width: 300px;
    margin: 20px auto;
}

.cell {
    border: 1px solid black;
    width: 30px;
    height: 30px;
    box-sizing: border-box;
    text-align: center;
    font-size: 16px;
    cursor: pointer;
}

.hit {
    background-color: red;
}

.miss {
    background-color: lightblue;
}

This CSS sets up a grid for our game board and styles the cells. We’ll add classes “hit” and “miss” dynamically to indicate hits and misses.

TypeScript Logic (src/index.ts)

Now, let’s get into the heart of the game: the TypeScript code. Open src/index.ts and start with the following code:


// Define the size of the game board
const boardSize = 10;

// Create a 2D array to represent the game board
let gameBoard: string[][] = [];

// Function to initialize the game board
function initializeBoard(): void {
  for (let row = 0; row < boardSize; row++) {
    gameBoard[row] = [];
    for (let col = 0; col < boardSize; col++) {
      gameBoard[row][col] = ""; // Represents an empty cell
    }
  }
}

// Function to render the game board in the HTML
function renderBoard(): void {
  const gameBoardElement = document.getElementById("game-board") as HTMLElement;
  if (!gameBoardElement) return; // Exit if the element is not found

  gameBoardElement.innerHTML = ""; // Clear the board

  for (let row = 0; row < boardSize; row++) {
    for (let col = 0; col < boardSize; col++) {
      const cell = document.createElement("div");
      cell.classList.add("cell");
      cell.dataset.row = row.toString(); // Store row index as data attribute
      cell.dataset.col = col.toString(); // Store column index as data attribute
      cell.addEventListener("click", handleCellClick);
      gameBoardElement.appendChild(cell);
    }
  }
}

// Function to handle cell clicks
function handleCellClick(event: MouseEvent): void {
  const cell = event.target as HTMLElement;
  const row = parseInt(cell.dataset.row || "0");
  const col = parseInt(cell.dataset.col || "0");

  // Basic click handling (for now)
  console.log(`Clicked cell: Row ${row}, Column ${col}`);
}

// Initialize the game
initializeBoard();
renderBoard();

Let’s break down this code:

  • boardSize: Defines the size of the game board (10×10).
  • gameBoard: A 2D array (array of arrays) that represents the game board. Each element in the array represents a cell on the board. We initialize this with empty strings to signify that nothing is in the cell.
  • initializeBoard(): This function initializes the gameBoard array with empty strings, representing empty cells.
  • renderBoard(): This function creates the HTML elements for the game board and adds them to the “game-board” div in our HTML. It also adds event listeners to each cell to handle clicks.
  • handleCellClick(): This function is called when a cell is clicked. It retrieves the row and column of the clicked cell and logs the coordinates to the console.
  • The last two lines call initializeBoard() and renderBoard() to set up the game when the page loads.

Compiling Your TypeScript Code

Before you can run the game in your browser, you need to compile your TypeScript code into JavaScript. Open your terminal in the project directory and run:

tsc

This command will use the tsconfig.json file to compile your src/index.ts file into dist/index.js.

Running the Game

Now, open index.html in your web browser. You should see an empty game board. When you click on a cell, the row and column coordinates should be logged in your browser’s developer console.

Adding Game Logic

Now that we have the basic board and click handling, let’s add the core game logic: placing ships, handling hits and misses, and determining when the game is over.

1. Place Ships

First, we need to place the ships on the board. For simplicity, let’s place a single ship of length 3. We’ll choose a random starting point and orientation (horizontal or vertical).

Add the following code to your src/index.ts file:


// Define ship properties
interface Ship {
    length: number;
    coordinates: { row: number; col: number }[];
    isSunk: boolean;
}

// Array to hold the ships
let ships: Ship[] = [];

// Function to randomly place a ship on the board
function placeShip(shipLength: number): Ship | null {
    let ship: Ship = {
        length: shipLength,
        coordinates: [],
        isSunk: false,
    };

    let placed = false;
    while (!placed) {
        const orientation = Math.random() < 0.5 ? "horizontal" : "vertical";
        const row = Math.floor(Math.random() * boardSize);
        const col = Math.floor(Math.random() * boardSize);

        let canPlace = true;
        let coordinates: { row: number; col: number }[] = [];

        if (orientation === "horizontal") {
            if (col + shipLength > boardSize) continue; // Out of bounds
            for (let i = 0; i < shipLength; i++) {
                if (gameBoard[row][col + i] !== "") {
                    canPlace = false; // Overlaps with existing ship
                    break;
                }
                coordinates.push({ row, col: col + i });
            }
        } else {
            if (row + shipLength > boardSize) continue; // Out of bounds
            for (let i = 0; i < shipLength; i++) {
                if (gameBoard[row + i][col] !== "") {
                    canPlace = false; // Overlaps with existing ship
                    break;
                }
                coordinates.push({ row: row + i, col });
            }
        }

        if (canPlace) {
            for (const coord of coordinates) {
                gameBoard[coord.row][coord.col] = "S"; // 'S' represents a ship
            }
            ship.coordinates = coordinates;
            placed = true;
        }
    }
    return ship;
}


// Initialize ships
function initializeShips(): void {
    // Example: Place one ship of length 3
    const newShip = placeShip(3);
    if (newShip) {
        ships.push(newShip);
    }
}

Here’s what the ship placement code does:

  • Defines a Ship interface to represent a ship’s properties (length, coordinates, and whether it’s sunk).
  • Creates an array of ships called ships.
  • The placeShip() function attempts to place a ship of a given length randomly on the board. It checks for out-of-bounds conditions and overlaps with existing ships. If it finds a valid spot, it marks the board with “S” to represent the ship.
  • The initializeShips() function calls placeShip() to place the ships at the beginning of the game.

Modify the initializeBoard() function to call initializeShips() after the board is initialized, like so:


// Initialize the game
initializeBoard();
initializeShips(); // Place the ships
renderBoard();

2. Handle Hits and Misses

Now, let’s modify the handleCellClick() function to handle hits and misses. We will check if a cell contains a ship (“S”) and update the board accordingly.


// Function to handle cell clicks
function handleCellClick(event: MouseEvent): void {
    const cell = event.target as HTMLElement;
    const row = parseInt(cell.dataset.row || "0");
    const col = parseInt(cell.dataset.col || "0");

    if (gameBoard[row][col] === "S") {
        // Hit!
        gameBoard[row][col] = "X"; // 'X' represents a hit
        cell.classList.add("hit");
        console.log("Hit!");
    } else if (gameBoard[row][col] === "") {
        // Miss!
        gameBoard[row][col] = "-"; // '-' represents a miss
        cell.classList.add("miss");
        console.log("Miss!");
    } else {
        // Already clicked
        console.log("Already clicked this cell.");
        return;
    }

    // Check if the game is over after each click
    checkGameOver();
}

Here’s what the updated function does:

  • Checks if the clicked cell contains a ship (“S”). If it does, it marks the cell as a hit (“X”) and adds the “hit” class to the cell’s HTML element.
  • If the cell is empty, it marks it as a miss (“-“) and adds the “miss” class.
  • Adds a check to ensure the cell has not already been clicked.
  • Calls checkGameOver() after each click to see if the game is over. We will create this next.

3. Check Game Over

Let’s create the checkGameOver() function to determine if all the ships have been sunk.


// Function to check if all ships are sunk
function checkGameOver(): void {
    let allShipsSunk = true;
    for (const ship of ships) {
        if (!ship.isSunk) {
            allShipsSunk = false;
            break;
        }
    }

    if (allShipsSunk) {
        alert("Game Over! You sunk all the ships!");
        // You can add logic to reset the game here.
    }
}

This function iterates through the ships array and checks if all ships have been sunk. We also need to update the ship’s isSunk property when a ship is sunk. Let’s modify the handleCellClick function to do this:


function handleCellClick(event: MouseEvent): void {
    const cell = event.target as HTMLElement;
    const row = parseInt(cell.dataset.row || "0");
    const col = parseInt(cell.dataset.col || "0");

    if (gameBoard[row][col] === "S") {
        // Hit!
        gameBoard[row][col] = "X"; // 'X' represents a hit
        cell.classList.add("hit");
        console.log("Hit!");

        //Check if the ship is sunk
        for (const ship of ships) {
            let sunk = true;
            for (const coord of ship.coordinates) {
                if (gameBoard[coord.row][coord.col] !== "X") {
                    sunk = false;
                    break;
                }
            }
            if (sunk) {
                ship.isSunk = true;
                console.log("Ship sunk!");
            }
        }

    } else if (gameBoard[row][col] === "") {
        // Miss!
        gameBoard[row][col] = "-"; // '-' represents a miss
        cell.classList.add("miss");
        console.log("Miss!");
    } else {
        // Already clicked
        console.log("Already clicked this cell.");
        return;
    }

    // Check if the game is over after each click
    checkGameOver();
}

In this updated version:

  • After a hit, we iterate through all the ships.
  • For each ship, we check if all its coordinates have been hit (“X”).
  • If all coordinates of a ship are hit, we set the isSunk property to true.

4. Update the Board Display

Finally, we need to update the renderBoard() function to reflect the hits, misses, and sunk ships. Modify the function to check the values of the cells and add appropriate classes to the cells.


// Function to render the game board in the HTML
function renderBoard(): void {
    const gameBoardElement = document.getElementById("game-board") as HTMLElement;
    if (!gameBoardElement) return; // Exit if the element is not found

    gameBoardElement.innerHTML = ""; // Clear the board

    for (let row = 0; row < boardSize; row++) {
        for (let col = 0; col < boardSize; col++) {
            const cell = document.createElement("div");
            cell.classList.add("cell");
            cell.dataset.row = row.toString(); // Store row index as data attribute
            cell.dataset.col = col.toString(); // Store column index as data attribute

            // Add classes based on the cell's value
            if (gameBoard[row][col] === "X") {
                cell.classList.add("hit");
            } else if (gameBoard[row][col] === "-") {
                cell.classList.add("miss");
            }

            cell.addEventListener("click", handleCellClick);
            gameBoardElement.appendChild(cell);
        }
    }
}

Now, when you click on a cell, the board will update to show hits, misses, and sunk ships.

Common Mistakes and How to Fix Them

Here are some common mistakes and how to fix them when building a Battleships game in TypeScript:

1. Incorrect Board Dimensions

Mistake: Using the wrong board size or not using a consistent board size throughout the code. This can lead to out-of-bounds errors and incorrect game play.

Fix: Define a constant variable for the board size (e.g., const boardSize = 10;) and use this variable consistently throughout your code when creating the board, placing ships, and handling user input. Always double-check your loops and array accesses to ensure you’re not going outside the bounds of the board.

2. Ship Placement Overlap

Mistake: Ships overlapping each other during placement. This can lead to unexpected behavior and make the game unfair.

Fix: Before placing a ship, check if the cells where the ship will be placed are already occupied by another ship. Modify the placeShip() function to check for existing ships before placing a new one. If a ship overlaps, try a different location or orientation.

3. Incorrect Hit/Miss Detection

Mistake: Failing to correctly identify hits and misses. This might be due to incorrect comparisons or not updating the game board correctly.

Fix: Ensure your handleCellClick() function correctly checks if a cell contains a ship (e.g., if (gameBoard[row][col] === "S")). Also, make sure you update the board with “X” for hits and “-” for misses. Double-check your logic and the values you’re using to represent hits, misses, and ships.

4. Game Not Ending Correctly

Mistake: The game doesn’t end when all ships are sunk. This could be caused by incorrect logic in the checkGameOver() function or not correctly marking ships as sunk.

Fix: Ensure your checkGameOver() function iterates through all the ships and checks if each ship’s isSunk property is true. In handleCellClick(), you must update the isSunk property of a ship when all its coordinates have been hit. Also, make sure to call checkGameOver() after each click.

5. Not Compiling TypeScript

Mistake: Forgetting to compile your TypeScript code into JavaScript before running your game in the browser.

Fix: Always run the command tsc in your project directory after making changes to your TypeScript files. This will generate the .js files that your HTML uses.

Step-by-Step Instructions

Here’s a recap of the steps to build your Battleships game:

  1. Set up your project: Create a project directory, initialize npm, install TypeScript, create a tsconfig.json file, and create index.html, style.css, and src/index.ts files.
  2. Write HTML: Create the basic HTML structure in index.html, including a game board div and links to your CSS and JavaScript files.
  3. Write CSS: Add basic styling to style.css to create the game board’s appearance.
  4. Write TypeScript:
    • Define the game board size.
    • Create a 2D array to represent the game board.
    • Write an initializeBoard() function to initialize the game board.
    • Write a renderBoard() function to render the game board in the HTML.
    • Write a placeShip() function to randomly place ships on the board.
    • Write an initializeShips() function to place the ships.
    • Write a handleCellClick() function to handle cell clicks, detect hits and misses, and update the game board.
    • Write a checkGameOver() function to determine if the game is over.
  5. Compile TypeScript: Run tsc in your terminal to compile your TypeScript code into JavaScript.
  6. Test and Debug: Open index.html in your browser and test your game. Use the browser’s developer console to debug any issues.
  7. Iterate and Improve: Add features like multiple ships, different ship sizes, and a more sophisticated UI.

Key Takeaways

  • TypeScript enhances code quality and maintainability through static typing.
  • Understanding data structures (like 2D arrays) is essential for game development.
  • Event listeners are fundamental for handling user interaction in web applications.
  • Breaking down a complex problem into smaller, manageable functions makes development easier.
  • Testing and debugging are crucial steps in the development process.

FAQ

  1. How do I add more ships?

    You can modify the initializeShips() function to call placeShip() multiple times with different ship lengths. You’ll also need to adjust the game-over condition to check if all ships have been sunk.

  2. How can I make the game more visually appealing?

    Improve the CSS to add more styling, such as different colors for hits and misses, and add images to represent ships. You can also use JavaScript to add animations.

  3. How do I prevent the user from clicking the same cell multiple times?

    In the handleCellClick() function, add a check to see if the cell has already been clicked. If it has, prevent further action on that cell. You can also disable the click event on a cell after it has been clicked.

  4. How can I add a computer opponent?

    Implement an AI opponent by writing functions that randomly or strategically choose cells to attack. The AI would need its own game board to track its guesses and the player’s ship placements. You would also need to update the game logic to handle the AI’s turns and responses.

  5. What are some good resources for learning more about TypeScript?

    The official TypeScript documentation is an excellent resource. You can also find many tutorials and courses on platforms like Udemy, Coursera, and freeCodeCamp.

Building a Battleships game in TypeScript is a great way to learn about web development fundamentals. This tutorial provides a solid foundation for your project. From here, you can expand on this basic game, adding features, improving the UI, and implementing more advanced game mechanics. Consider adding sound effects, improving the game’s intelligence, or creating a two-player mode. You can also deploy your game online to share it with friends. The possibilities are endless, and the journey of creating your own game is a rewarding experience. Keep practicing, experimenting, and exploring the power of TypeScript!