Hi there👋🏽.
Introduction
As a beginner the best way to level up is to actually build complete programs, I use the word "complete" loosely. I am hoping to write a series where we will build 24 applications or programs together, varying in levels from beginner to advanced. for example : this tic tac toe mini series
for beginner level we will build just a working tac in the console,
for intermediate we will migrate tac to the canvas(mapping a data structure to canvas objects)
for advanced we will implement minMax algorithm
most of the projects in what I call a t24
series will follow this three part layer. These projects were actually suppose to be part of an ebook I was working on, to release under pay what you want, but I decided it is way easier to write articles and make it accessible for everyone, and to be fair I also want to improve my writing as I am not a native English speaker, I thought this was a perfect way, lot of writing etc.
let's get coding
here is my folder structure, we will update it as we go along, I do actually suggest that you put all the projects under the same folder prolly called t24.
if you get stuck: repo
prerequisites
I am assuming you are familiar with JavaScript fundamentals: Objects, Arrays and Functions, if you want to level up a bit I also have a series for intermediate JS(which will be helpful as we move along the levels) you can check it out at your own leisure.
You need to be also familiar with tic tac toe or as we call it here "X's and O's"
tic tac toe
we will use a hardcoded 3 x 3 grid to represent our game space, we will prolly make this dynamic in other levels
file structure:
tic\
tic.js
open tic.js and let's start coding
representing the board:
// array of arrays
const board = [[" ", " " , " "],
[" ", " " , " "],
[" ", " " , " "],
]
the idea here is simple since we are in the console we will console.log
our board after every update
.
update being whether the player or computer puts an X or O on a free space in our board, it will all make sense as we code.
we will use an object to encapsulate all of our gaming logic
const game = {
update : function(){},
isgameOver: function(){},
move : function(c){},
possibleMoves: function(){},
computer: function(){},
updateBoard: function(){}
}
I suggest typing along as we fill these functions rather than copying as you'll learn more.
updateBoard
prolly the simplest function, all we need to do is loop over our array of arrays and log them as a tic tac toe board
| |
| |
| |
updateBoard: function(){
console.log(" ") // empty space before a board
board.forEach((arr, i)=> { // looping over each array in the board
console.log(arr.toString().replace(/,/g, "|"))
})
}
the magic in update board happens here:
arr.toString().replace(/,/g, "|")
if you understand this you good to go, basically what we are doing here is turning an array [" ", " " , " "]
to a string and replacing every ,
with a |
symbol, basically if we had an array ["O", "X" , "O"]
the above code will turn it to string O|X|O
, which is what we are doing to log our board, you should end with a 3 x 3 board like this:
this function is actually complete, next let's implement possibleMoves, the second easiest func to implement, we will worry about composing everything together later
possibleMoves
possibleMoves: function(){
const p = []
// just looping over the board O(n*2)
for(let i = 0; i < board.length; i++){
for(let j = 0; j < board[0].length; j++){
if(board[i][j] === " "){ // if we find a space without X or O add it to p array(it's a possible move)
p.push({row: i, col: j})
}
}
}
return p
}
Our little loop takes O(n*2) time, which is not bad considering our board size, with wich we got coordinates for free spaces in an array.
Coordinates in this case are indexes to our array of arrays e.g
board[p.row][p.col]
is how access a space.
let's bring some of this together, by implementing a part of the update function
update
let m = [] // place this outside our object, under board,
// m will hold all current possible moves
update : function(){
this.updateBoard();
m = this.possibleMoves();
}
for now the update function is just drawing the board and getting all possible moves
let's implement a simple computer function, that will pick a random move, from possible moves and place an O, we will handle turns etc later
computer
let turn = true // declare turn outside we will use it to change turns later
computer: function(){
console.log(m) // see all possible moves(for debugging purposes)
if(m.length > 0){
let ra = Math.round(Math.random() * (m.length - 1)) // picking a random number in a range of our board len
board[m[ra].row][m[ra].col] = "O" // using that index to pick a random coordinate
}
turn = true;
this.update() // important
console.log("ur turn")
},
believe it or not, that's all for our computer, now let's implement the player which is responsible for starting the game via the move function, hang in there we are about to bring everything together, it will all make sense.
move
now this is the complicated part especially if you are not familiar with node js, I will try to explain everything
basically we need a way to process player input and get coordinates to make a move on the board(put an X)
// declare var on top
let combo = {
row: undefined,
col: undefined
}
combo will hold user input, the idea is simple when both row and col are other values than undefined
e.g
combo = {
row: 0,
col: 0
}
move will put an x on 0,0. and hand over the turn to the computer function vice versa
getting user input
const readline = require("readline") // way to import a module into node(comes with node)
you do not need to know how the readline module works, just the part we interested in
at the bottom of everything write the following, which is responsible for waiting for user input in the console(think of it as a while true loop) but waiting for user input
readline.emitKeypressEvents(process.stdin);
process.stdin.setRawMode(true);
// the following code is all you need to understand for now
// all it is saying is when we receive a key press event react
process.stdin.on('keypress', (str, key) => {
// we only care about the key
if (key.ctrl && key.name === 'c') {
// if we receive ctrl-c we exit the "loop"
process.exit(); // exits the program
} else {
// else if we get other keys and it's the players turn
if(turn){
if(combo.row){
// if we got the coord for row fill col
combo.col = key.name
turn = false
// basically we have all coordinates to make a move
game.move(combo)
}else{
// the first key we want is the row
combo.row = key.name
}
}else{
// pressing a key while it's computer's turn
console.log("wait your turn")
}
}
});
now we can implement the move function, as we can now get user input
move
remember move will only be invoked when combo has both row and col coordinates
move : function(c){
board[+c.row][+c.col] = "x"
// turning combo back to undefined
// so we can take coordinates for the next turn for player
// if we don't do this we will never get new coords
combo.row = undefined
combo.col = undefined
this.update()
setTimeout(() => {
// after making our move we give CPU(I am tired of writing computer😂, it's cpu from now on)
// the turn
this.computer()
}, 3000);
},
if you run the program now it should hang and wait for coordinate(input numbers), then it will execute move, which will call update and CPU, vice versa
we almost done, all we need now is gameover logic
gameover
// write this function on top of everything
const allequal = arr => arr.every(v => v !== " " && v === arr[0]);
if you understand the above function, you are basically done with gameover.
what this func is basically doing is checking for same contiguous values
e.g [1, 1, 1] it will return true, for [1, 2, 1] false,
meaning we can just put ["X", "X", "X"], and we will know X has won vice versa, all we need now is dumb logic to check all possible combos basically:
that is basically what the code below is doing
first on top declare winner and gameOver vars
let winner = ""
let gameOver = false;
dumb logic
isgameOver: function(){
if(allequal(board[0])){
gameOver = true;
winner = board[0][0]
}
if(allequal(board[1])){
gameOver = true;
winner = board[1][0]
}
if(allequal(board[2])){
gameOver = true;
winner = board[2][0]
}
if(allequal([board[0][0], board[1][0], board[2][0]])){
gameOver = true;
winner = board[0][0]
}
if(allequal([board[0][1], board[1][1], board[2][1]])){
gameOver = true;
winner = board[0][1]
}
if(allequal([board[0][2], board[1][2], board[2][2]])){
gameOver = true;
winner = board[0][2]
}
if(allequal([board[0][0], board[1][1], board[2][2]])){
gameOver = true;
winner = board[0][0]
}
if(allequal([board[0][2], board[1][1], board[2][0]])){
gameOver = true;
winner = board[0][2]
}
}
all we need to do now is bring everything together in update
update
update : function(){
this.isgameOver(); // checking if game is over
if(gameOver){
this.updateBoard()
console.log(`Game over ${winner} won!`)
process.exit(); // exit ("loop")
}
this.updateBoard();
m = this.possibleMoves();
// if there no possible moves anymore it's a draw
if(m.length === 0){
gameOver = true;
console.log("Game over by draw")
process.exit();
}
}
with that we are done with the beginner level, next article we are making it graphical and do a bit of clean up and error handling, you can do it as a challenge if you want
you can find the repo here for this code so far, watch, fork, star the repo all that good stuff I will be updating it as we go along, you can get the code even before the article for intermediate as I will commit as I code too
Conclusion
I will try to make this series as consistent as possible, More interesting projects coming, including a mini compiler👌🧙♂️, yes I am going all out, teaching is the best way to learn too,.
if you would like to support me, you can buy me a coffee below, I will highly appreciate it❤❤, you certainly don't have to if you can't.
with that cheers till next time, Happy Hacking!!