-
Notifications
You must be signed in to change notification settings - Fork 47
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #141 from codedex-io/tic-tac-toe
Add Build Tic-Tac-Toe with JavaScript Project Tutorial by Daniel Li
- Loading branch information
Showing
3 changed files
with
350 additions
and
0 deletions.
There are no files selected for viewing
280 changes: 280 additions & 0 deletions
280
projects/build-tic-tac-toe-with-javascript/build-tic-tac-toe-with-javascript.mdx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,280 @@ | ||
--- | ||
title: Build Tic-Tac-Toe With JavaScript | ||
author: Daniel Li | ||
uid: 5k5WC31oPvcUQmaDCgK38v6Et133 | ||
datePublished: 2024-08-02 | ||
published: false | ||
description: Use JavaScript and Node.js to build a simple Tic-Tac-Toe game. | ||
header: https://raw.githubusercontent.com/codedex-io/projects/main/projects/build-tic-tac-toe-with-javascript/header.png | ||
tags: | ||
- beginner | ||
- javascript | ||
--- | ||
|
||
<BannerImage | ||
link="https://raw.githubusercontent.com/codedex-io/projects/main/projects/build-tic-tac-toe-with-javascript/header.png" | ||
description="Title Image" | ||
uid={true} | ||
cl="for-sidebar" | ||
/> | ||
|
||
# Build Tic-Tac-Toe With JavaScript | ||
|
||
<AuthorAvatar | ||
author_name="Daniel Li" | ||
author_avatar="/images/projects/authors/daniel-li.png" | ||
username="Tenny" | ||
uid={true} | ||
/> | ||
|
||
<BannerImage | ||
link="" | ||
description="https://raw.githubusercontent.com/codedex-io/projects/main/projects/build-tic-tac-toe-with-javascript/header.png" | ||
uid={true} | ||
/> | ||
|
||
**Prerequisites**: JavaScript fundamentals | ||
**Version**: Node.js 16.14.0 | ||
**Read Time**: 30 minutes | ||
|
||
## Introduction | ||
|
||
Even in ancient times, people across the world still enjoyed playing games. One game that has stood the test of time is Tic-Tac-Toe. From Egyptian merchants who played on roofing tiles to Roman Soldiers who knew a similar game as [“Terni Lapilli”](https://en.wikipedia.org/wiki/Tic-tac-toe_variants), Tic-Tac-Toe has been widely played and appreciated over many millennia. | ||
|
||
<RoundedImage | ||
link="https://i.imgur.com/o6imkVE.png" | ||
description="Tic-Tac-Toe Game on concerete" | ||
/> | ||
|
||
In this project tutorial, we will learn to build a Tic-Tac-Toe game with JavaScript in the console! ❌ ⭕️ | ||
|
||
<RoundedImage | ||
link="https://i.imgur.com/aimW37h.png" | ||
description="Tic-Tac-Toe Game displayed in terminal" | ||
/> | ||
|
||
You'll be using: | ||
|
||
- Node.js for running the game on the terminal. | ||
- The 'prompt-sync' package for handling user input. | ||
- A code editor (like VS Code) for building the game. | ||
|
||
## Setting Up | ||
|
||
Our first task is to install **Node.js**, a runtime environment that lets us run JavaScript code from the terminal rather than the browser. | ||
|
||
You can install it from the [Node.js website](https://nodejs.org/en/download/package-manager), or check if you already have it installed with the `node -v` command. If not, you can type `npm install prompt-sync` in the terminal to install the package after downloading from the Node.js website. To execute your program simply run `node <filename>` in the terminal and press <kbd>enter</kbd>. | ||
|
||
```js | ||
node tictactoe.js | ||
``` | ||
|
||
In this example, **tictactoe.js** is the name of the file you saved your code in. | ||
|
||
We will need to import the 'prompt-sync' package to handle user input. This can be done with the following code: | ||
|
||
```js | ||
const prompt = require('prompt-sync')(); | ||
``` | ||
|
||
Next, we need to set up the game variables such as the game board, player names, and state of the game. | ||
|
||
Below where we imported `prompt-sync`, let's define the following variables: | ||
|
||
- `gameBoard` | ||
- `currentPlayer` | ||
- `gameActive` | ||
|
||
Since the game will be played on the console, we can use blank spaces to represent the `gameBoard` as an array with 9 positions: | ||
|
||
```js | ||
let gameBoard = [' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ']; | ||
``` | ||
|
||
In addition to this, we also need to assign a name for players to track which players turn it is. | ||
|
||
While players would typically use Xs and Os to play Tic-Tac-Toe, we will use goats and grapes. Thus, player 1 be ‘🐐’, but you can assign any name to it. | ||
|
||
```js | ||
let currentPlayer = '🐐'; | ||
``` | ||
|
||
You may have noticed that we only assigned one name to a player, despite there being two players in a Tic-Tac-Toe game. Don’t worry – we’ll address this later in the program. | ||
|
||
We also need to set up a game state, so the program knows when to stop and finish. When we run the program, we can start with the `gameActive` variable being set to `true`. This ensures the game keeps going until there is a win or a draw. | ||
|
||
```js | ||
let gameActive = true; | ||
``` | ||
|
||
Now that we've initialized our essential variables, it's time to work on the game functionality. | ||
|
||
## Printing the board | ||
|
||
With the `gameBoard` array we defined previously, let's make a function that displays the current state of the game board. As time goes on and the game gets played, the array will alter itself so we need placeholders for the data being inserted. We will make a set of strings that can be easily manipulated, which is done via [string interpolation](https://en.wikipedia.org/wiki/String_interpolation). | ||
|
||
```js | ||
function printBoard() { | ||
console.log(` | ||
${gameBoard[0]} | ${gameBoard[1]} | ${gameBoard[2]} | ||
--------- | ||
${gameBoard[3]} | ${gameBoard[4]} | ${gameBoard[5]} | ||
--------- | ||
${gameBoard[6]} | ${gameBoard[7]} | ${gameBoard[8]} | ||
`); | ||
} | ||
``` | ||
|
||
This piece of code creates a multi-line string, with `${}` interpolation syntax being used to set values of the `gameBoard` array elements to their corresponding positions. `${gameboard[0]}` relates to the first value of the `gameBoard`, allowing us to easily manipulate each cell's contents on the Tic-tac-toe grid. Since arrays use 0-indexing, each number 0-8 corresponds to a cell on the TicTacToe grid: | ||
|
||
<RoundedImage | ||
link="https://i.imgur.com/jG3saIL.png" | ||
description="Tic-Tac-Toe grid with cell numbers and corresponding positions" | ||
/> | ||
|
||
## Making Moves | ||
|
||
In Tic-Tac-Toe, players cannot take a space that is already occupied by the opponent or themselves. As a result, we need to check if a space is taken. | ||
|
||
Since we assigned a blank space to each cell initially, a blank space means it isn't taken and we can update the board with the player's move. This turns out to be a relatively easy `if`/`else` statement that checks if the player's chosen cell is indeed empty, or prompting the player to try again if not. | ||
|
||
Let's make a `handleMove()` function that takes in the player's position as an argument: | ||
|
||
```js | ||
function handleMove(position) { | ||
if (gameBoard[position] === " ") { | ||
gameBoard[position] = currentPlayer; | ||
} else { | ||
console.log("Cell already taken, choose another one."); | ||
return false; | ||
} | ||
} | ||
``` | ||
|
||
Later, we will make a `checkWin()` function to tell if a player has won or not. But for now, after every move, we will just add an `if`-statement to the `handleMove()` function to check if a player has won. | ||
|
||
If a player has won, the game will no longer be active and the program will display a winning message! | ||
|
||
```js | ||
if (checkWin()) { | ||
printBoard(); | ||
console.log(`Player ${currentPlayer} wins!`); | ||
gameActive = false; | ||
return true; | ||
} | ||
``` | ||
|
||
In the real game, when all the cells are filled without there being a winner, it's a draw. Let's create a function that checks if all cells from the gameBoard array are filled, and if so, concludes the game with a draw. We'll use [arrow function](https://www.codedex.io/javascript/bonus/arrow-functions-in-javascript) syntax to make a `checkDraw()` function: | ||
|
||
```js | ||
if (gameBoard.every(cell => cell !== " ")) { | ||
printBoard(); | ||
console.log("It's a draw!"); | ||
gameActive = false; | ||
return true; | ||
} | ||
``` | ||
|
||
The `handleMove()` function runs every time a player makes a move, so after one player's move, we need to switch to the next player. This can be done by checking if the `currentPlayer` variable is equal to ‘🐐’, and if the condition is true it will be swapped out by `🍇`, and vice versa. In other words, we are toggling `currentPlayer` between `🐐` and `🍇` after every move by replacing the variable with it's alternative. | ||
|
||
We'll use a [ternerary operator](https://www.codedex.io/javascript/bonus/ternary-operators-in-javascript) to do this, which is a shorthand for an `if`/`else` statement. | ||
|
||
```js | ||
currentPlayer = currentPlayer === "🐐" ? "🍇" : "🐐"; | ||
return true; | ||
``` | ||
|
||
**Note:** The code from this section is in _one_ function! Make sure your `handleMove()` function looks like this, with indentation and brackets matching up before moving on. | ||
|
||
```js | ||
function handleMove(position) { | ||
if (board[position] === " ") { | ||
board[position] = currentPlayer; | ||
} else { | ||
console.log("Cell already taken, choose another one."); | ||
return false; | ||
} | ||
|
||
if (checkWin()) { | ||
printBoard(); | ||
console.log(`Player ${currentPlayer} wins!`); | ||
active = false; | ||
return true; | ||
} | ||
|
||
if (board.every(cell => cell !== " ")) { | ||
printBoard(); | ||
console.log("It's a draw!"); | ||
active = false; | ||
return true; | ||
} | ||
|
||
currentPlayer = currentPlayer === "🐐" ? "🍇" : "🐐"; | ||
return true; | ||
} | ||
``` | ||
|
||
## Finishing Touches | ||
|
||
Only a few more things must be done before the program is runnable and works! Let's now create the `checkWin()` function that's used in the `handleMove()` function: | ||
|
||
```js | ||
function checkWin() { | ||
const conditions = [ | ||
[0, 1, 2], | ||
[3, 4, 5], | ||
[6, 7, 8], | ||
[0, 3, 6], | ||
[1, 4, 7], | ||
[2, 5, 8], | ||
[0, 4, 8], | ||
[2, 4, 6] | ||
]; | ||
} | ||
``` | ||
|
||
This function determines if the current player has won, with conditions being an array of all the possible winning combinations as displayed on the board. For example, `[0,1,2]` corresponds to the first column being occupied by one player, which would yield a win. | ||
|
||
Now we need to evaluate the conditions. For each winning condition, we test a, b, and c to check if the values at these index properties all match the current player. As long as one condition is met, the player will win. | ||
|
||
Let's add this return statement after the `conditions` array inside of our `checkWin()` function: | ||
|
||
```js | ||
return conditions.some(condition => { | ||
const [a, b, c] = condition; | ||
return gameBoard[a] === currentPlayer && gameBoard[b] === currentPlayer && gameBoard[c] === currentPlayer; | ||
}); | ||
``` | ||
|
||
The last part of the program is creating a loop that allows the game to go on until someone wins or there is a draw. With the `gameState` boolean variable, we can use a `while` loop to do this. Since we want the 'position' variable to be an integer before we pass it on to the later code, we use the `parseInt()` function to convert from a string to an integer. | ||
|
||
We also have to make a domain for the possible moves. A single game lasts for 9 moves that must be made in a position between 0 and 8. This loop will continuously run until `gameActive` is `false`, so we want it to be independent from the `checkWin()` function. | ||
|
||
```js | ||
while (gameActive) { | ||
printBoard(); | ||
const position = prompt(`Player ${currentPlayer}, enter your move (0-8): `); | ||
|
||
if (position >= 0 && position <= 8) { | ||
handleMove(parseInt(position)); | ||
} else { | ||
console.log("Invalid position, enter a number between 0 and 8."); | ||
} | ||
} | ||
``` | ||
|
||
We repeat the loop until `gameActive` is `false`. After each move, we log (or print) the updated `gameBoard` to the console and prompt the next player for their move. Since the board only has a total of 9 cells (0 - 8), the player must pick again if their move is out of range. Otherwise, a move within range is sent back to the `handleMove()` function to be processed. | ||
|
||
At this point, go ahead and save your **tictactoe.js** file and run again in the console with `node tictactoe.js`. You should now see the following: | ||
|
||
<ImageZoom src="https://i.imgur.com/B89cOwB.gif" alt="Final output of Tic Tac Toe project"/> | ||
|
||
## Congrats! | ||
|
||
You have made it to the end! With control flow, arrays, input handling, and game logic, you've coded Tic-Tac-Toe right on your computer! If you would like to challenge yourself more, create a tally or score system to keep track of how many times each player has won. | ||
|
||
### More Resources | ||
|
||
- [Source code](https://github.com/codedex-io/projects/blob/main/projects/build-tic-tac-toe-with-javascript/tictactoe.js) | ||
- [Node.js official site](https://nodejs.org) | ||
- [Tic-tac-toe (Wikipedia)](https://en.wikipedia.org/wiki/Tic-tac-toe) |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
const prompt = require('prompt-sync')(); | ||
|
||
let board = [" ", " ", " ", " ", " ", " ", " ", " ", " "]; | ||
let currentPlayer = "🐐"; | ||
let active = true; | ||
|
||
function printBoard() { | ||
console.log(` | ||
${board[0]} | ${board[1]} | ${board[2]} | ||
--------- | ||
${board[3]} | ${board[4]} | ${board[5]} | ||
--------- | ||
${board[6]} | ${board[7]} | ${board[8]} | ||
`); | ||
} | ||
|
||
function handleMove(position) { | ||
if (board[position] === " ") { | ||
board[position] = currentPlayer; | ||
} else { | ||
console.log("Cell already taken, choose another one."); | ||
return false; | ||
} | ||
|
||
if (checkWin()) { | ||
printBoard(); | ||
console.log(`Player ${currentPlayer} wins!`); | ||
active = false; | ||
return true; | ||
} | ||
|
||
if (board.every(cell => cell !== " ")) { | ||
printBoard(); | ||
console.log("It's a draw!"); | ||
active = false; | ||
return true; | ||
} | ||
|
||
currentPlayer = currentPlayer === "🐐" ? "🍇" : "🐐"; | ||
return true; | ||
} | ||
|
||
function checkWin() { | ||
const conditions = [ | ||
[0, 1, 2], | ||
[3, 4, 5], | ||
[6, 7, 8], | ||
[0, 3, 6], | ||
[1, 4, 7], | ||
[2, 5, 8], | ||
[0, 4, 8], | ||
[2, 4, 6] | ||
]; | ||
|
||
return conditions.some(condition => { | ||
const [a, b, c] = condition; | ||
return board[a] === currentPlayer && board[b] === currentPlayer && board[c] === currentPlayer; | ||
}); | ||
} | ||
|
||
while (active) { | ||
printBoard(); | ||
const position = prompt(`Player ${currentPlayer}, enter your move (0-8): `); | ||
|
||
if (position >= 0 && position <= 8) { | ||
handleMove(parseInt(position)); | ||
} else { | ||
console.log("Invalid position, enter a number between 0 and 8."); | ||
} | ||
} |