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 thegameBoardarray 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()andrenderBoard()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
Shipinterface 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 callsplaceShip()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
isSunkproperty totrue.
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:
- Set up your project: Create a project directory, initialize npm, install TypeScript, create a
tsconfig.jsonfile, and createindex.html,style.css, andsrc/index.tsfiles. - Write HTML: Create the basic HTML structure in
index.html, including a game board div and links to your CSS and JavaScript files. - Write CSS: Add basic styling to
style.cssto create the game board’s appearance. - 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. - Compile TypeScript: Run
tscin your terminal to compile your TypeScript code into JavaScript. - Test and Debug: Open
index.htmlin your browser and test your game. Use the browser’s developer console to debug any issues. - 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
- How do I add more ships?
You can modify the
initializeShips()function to callplaceShip()multiple times with different ship lengths. You’ll also need to adjust the game-over condition to check if all ships have been sunk. - 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.
- 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. - 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.
- 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!
