TypeScript Tutorial: Creating a Simple Interactive Web-Based Game of Battleship

Ever wanted to build your own game? This tutorial will guide you through creating a simple, yet engaging, Battleship game using TypeScript. We’ll cover everything from setting up the project to implementing game logic, all while keeping it beginner-friendly. Battleship is a classic for a reason – it’s easy to understand, fun to play, and a great way to learn fundamental programming concepts. By the end of this tutorial, you’ll have a fully functional Battleship game that you can play in your web browser, and you’ll have a solid understanding of how to use TypeScript to build interactive web applications.

Why TypeScript for Battleship?

TypeScript brings several advantages to the table when building a game like Battleship. It’s a superset of JavaScript, meaning all valid JavaScript code is also valid TypeScript. However, TypeScript adds static typing, which helps you catch errors early in the development process. This can save you a lot of time and frustration down the line. Here’s why TypeScript is a great choice:

  • Early Error Detection: TypeScript helps you catch type-related errors during development, before you even run your code.
  • Improved Code Readability: Types make your code easier to understand and maintain.
  • Enhanced Code Completion: Modern IDEs provide better code completion and suggestions with TypeScript.
  • Scalability: TypeScript makes it easier to manage larger projects.

Setting Up Your Project

Let’s get started by setting up our project. You’ll need Node.js and npm (Node Package Manager) installed on your system. If you don’t have them, download and install them from the official Node.js website. Open your terminal or command prompt and follow these steps:

  1. Create a Project Directory: Create a new directory for your project and navigate into it:
mkdir battleship-game
cd battleship-game
  1. Initialize npm: Initialize a new npm project:
npm init -y
  1. Install TypeScript: Install TypeScript globally or locally (we’ll install it locally for this tutorial):
npm install typescript --save-dev
  1. Create a TypeScript Configuration File: Create a tsconfig.json file in your project root. You can generate a basic one using the TypeScript compiler:
npx tsc --init

This will create a tsconfig.json file with default settings. You can customize this file to configure how TypeScript compiles your code. For this tutorial, we’ll keep the defaults, but let’s make a few adjustments to make sure our code compiles in the correct directory. Open tsconfig.json and make sure the following settings are present:

{
  "compilerOptions": {
    "target": "ES5",
    "module": "commonjs",
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  },
  "include": ["src/**/*"]
}

Here’s what each of these options means:

  • target: Specifies the JavaScript version to compile to (ES5 is widely supported).
  • module: Specifies the module system (commonjs is used for Node.js).
  • outDir: Specifies the output directory for the compiled JavaScript files.
  • rootDir: Specifies the root directory of your TypeScript files.
  • strict: Enables strict type checking.
  • esModuleInterop: Enables interoperability between CommonJS and ES modules.
  • skipLibCheck: Skips type checking of declaration files (*.d.ts).
  • forceConsistentCasingInFileNames: Enforces consistent casing in file names.
  • include: Specifies the files or patterns to include in compilation.
  1. Create Project Structure: Create a src directory and inside it, create an index.ts file. This is where we’ll write our game logic:
mkdir src
touch src/index.ts

Designing the Game Board

The Battleship game is played on a grid, typically 10×10. Let’s represent this grid using TypeScript. We’ll use a 2D array to represent the game board. Each cell in the array will either be empty (water), or contain a ship. We’ll also need a way to track hits and misses.

Open src/index.ts and add the following code:


// Define the size of the board
const BOARD_SIZE = 10;

// Define the cell types
const CELL_EMPTY = 0;
const CELL_SHIP = 1;
const CELL_HIT = 2;
const CELL_MISS = 3;

// Create the game board
let board: number[][] = [];

// Initialize the board
function initializeBoard() {
  for (let i = 0; i < BOARD_SIZE; i++) {
    board[i] = [];
    for (let j = 0; j < BOARD_SIZE; j++) {
      board[i][j] = CELL_EMPTY;
    }
  }
}

// Call the initializeBoard function to create the board
initializeBoard();

Let’s break down this code:

  • BOARD_SIZE: Defines the size of the board (10×10).
  • CELL_EMPTY, CELL_SHIP, CELL_HIT, CELL_MISS: Constants to represent the different states of a cell.
  • board: A 2D array (array of arrays) that represents the game board.
  • initializeBoard(): A function that initializes the board with all cells set to CELL_EMPTY.

Placing the Ships

Now, let’s place some ships on the board. For simplicity, we’ll place a few ships of varying lengths. We’ll use a function to place each ship, ensuring that it doesn’t overlap with other ships or go out of bounds.

Add the following code to src/index.ts:


// Define ship types
interface Ship {
  length: number;
  name: string;
}

const ships: Ship[] = [
  { length: 5, name: "Carrier" },
  { length: 4, name: "Battleship" },
  { length: 3, name: "Cruiser" },
  { length: 3, name: "Submarine" },
  { length: 2, name: "Destroyer" },
];

// Function to check if a ship can be placed
function canPlaceShip(board: number[][], row: number, col: number, direction: 'horizontal' | 'vertical', shipLength: number): boolean {
  if (direction === 'horizontal') {
    if (col + shipLength > BOARD_SIZE) return false;
    for (let i = 0; i < shipLength; i++) {
      if (board[row][col + i] !== CELL_EMPTY) return false;
    }
  } else {
    if (row + shipLength > BOARD_SIZE) return false;
    for (let i = 0; i < shipLength; i++) {
      if (board[row + i][col] !== CELL_EMPTY) return false;
    }
  }
  return true;
}

// Function to place a ship on the board
function placeShip(board: number[][], ship: Ship) {
    let placed = false;
    while (!placed) {
        const row = Math.floor(Math.random() * BOARD_SIZE);
        const col = Math.floor(Math.random() * BOARD_SIZE);
        const direction = Math.random() < 0.5 ? 'horizontal' : 'vertical';

        if (canPlaceShip(board, row, col, direction, ship.length)) {
            if (direction === 'horizontal') {
                for (let i = 0; i < ship.length; i++) {
                    board[row][col + i] = CELL_SHIP;
                }
            } else {
                for (let i = 0; i < ship.length; i++) {
                    board[row + i][col] = CELL_SHIP;
                }
            }
            placed = true;
        }
    }
}

// Place ships on the board
function placeShipsOnBoard() {
  ships.forEach(ship => {
    placeShip(board, ship);
  });
}

placeShipsOnBoard();

Here’s what the new code does:

  • Ship interface: Defines the structure of a ship (length and name).
  • ships array: An array of Ship objects representing the ships in the game.
  • canPlaceShip(): Checks if a ship can be placed at a given position without overlapping or going out of bounds.
  • placeShip(): Randomly places a ship on the board, ensuring that it doesn’t overlap with other ships.
  • placeShipsOnBoard(): Iterates through the ships array and calls placeShip() for each ship.

Implementing the Game Logic

Now, let’s implement the core game logic: taking turns, making guesses, and checking for hits, misses, and sunk ships.

Add the following code to src/index.ts:


// Function to handle a player's guess
function handleGuess(row: number, col: number): boolean {
  if (row < 0 || row >= BOARD_SIZE || col < 0 || col >= BOARD_SIZE) {
    console.log("Invalid guess. Coordinates out of bounds.");
    return false;
  }

  if (board[row][col] === CELL_EMPTY) {
    board[row][col] = CELL_MISS;
    console.log("Miss!");
    return false;
  } else if (board[row][col] === CELL_SHIP) {
    board[row][col] = CELL_HIT;
    console.log("Hit!");
    return true;
  } else if (board[row][col] === CELL_HIT || board[row][col] === CELL_MISS) {
    console.log("You already guessed this cell.");
    return false;
  }
  return false;
}

// Function to check if all ships are sunk
function areAllShipsSunk(): boolean {
  for (let i = 0; i < BOARD_SIZE; i++) {
    for (let j = 0; j < BOARD_SIZE; j++) {
      if (board[i][j] === CELL_SHIP) {
        return false;
      }
    }
  }
  return true;
}

// Function to start the game
function startGame() {
  let gameWon = false;
  let turns = 0;

  while (!gameWon) {
      turns++;
      // Get player's guess (replace with actual input from the user)
      const row = parseInt(prompt("Enter row (0-9):") || "-1");
      const col = parseInt(prompt("Enter column (0-9):") || "-1");

      if (row < 0 || col < 0 || row >= BOARD_SIZE || col >= BOARD_SIZE) {
          console.log("Invalid input. Try again.");
          continue;
      }

      const isHit = handleGuess(row, col);

      if (isHit) {
          if (areAllShipsSunk()) {
              gameWon = true;
              console.log(`Congratulations! You sunk all the ships in ${turns} turns.`);
          }
      }
  }
}

startGame();

Let’s break down this code:

  • handleGuess(): Takes row and column coordinates as input, checks if the guess is valid, and updates the board accordingly. It returns true if a ship was hit, and false otherwise.
  • areAllShipsSunk(): Checks if all ships have been sunk by iterating through the board and checking for any remaining CELL_SHIP values.
  • startGame(): This is the main game loop. It prompts the user for row and column inputs, calls handleGuess(), and checks if the game is won.

Displaying the Board (Basic Output)

For now, we’ll display the board in the console. In a real-world application, you would use HTML and CSS to render the board visually in the browser. However, for this tutorial, console output will suffice to keep things simple.

Add the following function to src/index.ts:


// Function to display the board in the console
function displayBoard() {
    console.log("  0 1 2 3 4 5 6 7 8 9");
    for (let i = 0; i < BOARD_SIZE; i++) {
        let rowString = i + " ";
        for (let j = 0; j < BOARD_SIZE; j++) {
            switch (board[i][j]) {
                case CELL_EMPTY:
                    rowString += ". ";
                    break;
                case CELL_SHIP:
                    rowString += ". "; // Hide ships for now
                    break;
                case CELL_HIT:
                    rowString += "X ";
                    break;
                case CELL_MISS:
                    rowString += "O ";
                    break;
                default:
                    rowString += "? ";
            }
        }
        console.log(rowString);
    }
}

Modify the startGame() function to display the board after each guess:


function startGame() {
  let gameWon = false;
  let turns = 0;

  while (!gameWon) {
      turns++;
      displayBoard(); // Display the board before each turn.
      // Get player's guess (replace with actual input from the user)
      const row = parseInt(prompt("Enter row (0-9):") || "-1");
      const col = parseInt(prompt("Enter column (0-9):") || "-1");

      if (row < 0 || col < 0 || row >= BOARD_SIZE || col >= BOARD_SIZE) {
          console.log("Invalid input. Try again.");
          continue;
      }

      const isHit = handleGuess(row, col);

      if (isHit) {
          if (areAllShipsSunk()) {
              gameWon = true;
              displayBoard(); // Display the final board
              console.log(`Congratulations! You sunk all the ships in ${turns} turns.`);
          }
      }
  }
}

Running the Game

Now that you’ve written the code, it’s time to compile and run your Battleship game! In your terminal, run the following commands:


tsc
node dist/index.js

This will first compile your TypeScript code into JavaScript and then execute the JavaScript code using Node.js. You’ll be prompted to enter row and column numbers for your guesses. The game will provide feedback on whether your guess was a hit or a miss, and display the board after each turn. Try playing the game and see if you can sink all the ships!

Improving the Game (Optional Enhancements)

Here are some ways you can enhance your Battleship game:

  • User Interface (HTML/CSS): Instead of console output, create a visual board using HTML and CSS. Use HTML elements to represent the cells, and update their appearance based on hits, misses, and ship placements.
  • Ship Placement Control: Allow the player to choose where to place their ships at the beginning of the game.
  • Difficulty Levels: Implement different difficulty levels by varying the number and size of the ships.
  • Computer Opponent: Create a simple AI opponent that makes random guesses.
  • Scorekeeping: Track the number of turns it takes the player to win.
  • Sound Effects: Add sound effects for hits, misses, and sinking ships to enhance the game experience.

Common Mistakes and How to Fix Them

Here are some common mistakes beginners make when building a game like Battleship, and how to avoid them:

  • Incorrect Board Initialization: Make sure your board is correctly initialized with the correct number of rows and columns, and that each cell is set to the appropriate initial value (CELL_EMPTY).
  • Out-of-Bounds Errors: Always check that the row and column indices are within the valid range (0 to 9 in our case) before accessing the board array. Use conditional statements to prevent these errors.
  • Ship Overlap: Ensure that ships don’t overlap when placing them on the board. Use the canPlaceShip() function to check for valid placement.
  • Infinite Loops: Be careful with your game loops (like the one in startGame()). Make sure there’s a way for the game to end (e.g., when all ships are sunk).
  • Type Errors: TypeScript helps prevent type errors, but you still need to pay attention to your types. Make sure you’re using the correct types for variables and function parameters. Use the type checker to help you identify and fix these issues.

Key Takeaways

  • TypeScript for Game Development: TypeScript provides static typing and other features that can significantly improve your game development experience.
  • Game Board Representation: Understanding how to represent a game board using a 2D array is fundamental to many games.
  • Game Logic Implementation: You learned how to implement core game logic, including taking turns, handling guesses, and checking for hits and misses.
  • Modular Code Design: Breaking down your code into functions (like handleGuess(), placeShip()) makes it easier to understand, maintain, and extend.
  • Iterative Development: Start with a basic version of the game and gradually add features and improvements.

FAQ

Here are some frequently asked questions about building a Battleship game with TypeScript:

  1. Why use TypeScript instead of JavaScript?

    TypeScript adds static typing, which helps you catch errors early, improves code readability, and makes your code more maintainable. While JavaScript is perfectly capable of building this game, TypeScript offers significant advantages in larger projects.

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

    Use HTML and CSS to create a visual board and use JavaScript (or TypeScript) to update the board’s appearance based on game events. You can also add animations and sound effects for a more immersive experience.

  3. How can I add a computer opponent?

    You can create a simple AI opponent by having it make random guesses. For a more challenging opponent, you can implement more sophisticated AI techniques, such as tracking previous guesses and avoiding cells near sunk ships.

  4. How do I handle user input in the browser?

    You can use HTML input elements (e.g., text fields, buttons) to get user input and use JavaScript to handle events (e.g., button clicks, form submissions) to process the input and update the game state.

  5. Where can I learn more about TypeScript?

    The official TypeScript documentation ([https://www.typescriptlang.org/](https://www.typescriptlang.org/)) is an excellent resource. You can also find many tutorials and courses online on platforms like Udemy, Coursera, and freeCodeCamp.

This tutorial has provided a solid foundation for building a Battleship game with TypeScript. While this is a basic console-based version, the principles you’ve learned can be applied to create more complex and visually appealing games. The ability to structure data, handle user input, and implement game logic are essential skills for any game developer, and this project provides an excellent starting point for those wanting to venture into the world of game development. The combination of TypeScript’s type safety and the classic gameplay of Battleship makes for an enjoyable and educational project, and this experience is a great first step toward building more complex and creative games.