I wanted to do a quick review on OOP. And thought writing a game would be the best way. I'm thinking of copying one of the Orisinal games. But for now, tic-tac-toe was a quick and easy refresher. Github Repo.
Enter players:
Javascript:
class TicTacToe {
/**
* board - 3x3 matrix of empty strings.
* players - Object representing the players.
* score - Object representing scores of players.
* currentPlayer - Current player, initialized to x.
* moveCount - Current move count
* @param {string} player1
* @param {string} player2
*/
constructor(player1, player2) {
this.board = null
this.players = {
x: { name: player1.toLowerCase(), token: 'x' },
o: { name: player2.toLowerCase(), token: 'o' },
}
this.score = { x: 0, o: 0 }
this.currPlayer = this.players.x
this.moveCount = 0
this.winner = null
// Bind click handlers so context of this is instance of game.
this.handleClickCell = this.handleClickCell.bind(this)
this.handleClickReplay = this.handleClickReplay.bind(this)
}
/** Create new board, render the score board, game board and replay button. */
init() {
this.createNewBoard()
this.printScoreBoard(this.score)
this.printGameBoard(this.board)
this.printReplayButton()
}
/** Create and return DOM element with optional class, id and dataset. */
createElement(tag, className, id, data) {
const element = document.createElement(tag)
if (className) element.classList.add(className)
if (id) element.id = id
if (data) {
for (const [key, val] of Object.entries(data)) {
element.dataset[key] = val
}
}
return element
}
/**
* Retrieve existing DOM element.
* @param {string} selector
* */
getElement(selector) {
return document.querySelector(selector)
}
/**
* Retrieve DOM element and append specified value.
* @param {string} selector
* @param {string} newVal
* */
appendToElement(selector, newVal) {
const element = this.getElement(selector)
element.append(newVal)
}
/** Create a new board with empty string for cell vals */
createNewBoard() {
const board = []
for (let i = 0; i < 3; i++) {
board.push(['', '', ''])
}
this.board = board
}
/**
* Update the game board with current player's cell with player's token.
* @param {string} row
* @param {string} col
* @param {string} playerToken
* */
updateGameBoard(row, col, playerToken) {
const board = this.board
board[row][col] = playerToken
const cellVal = this.createElement('span', `cell-${playerToken}`)
cellVal.innerText = playerToken
this.appendToElement(`#c-${row}-${col}`, cellVal)
}
/**
* Create game board and append to DOM
* @param {Array[]} board - 2d array of rows and cols of game board
*/
printGameBoard(board) {
const game = this.getElement('#game'),
gameBoard = this.createElement('div', 'board')
game.append(gameBoard)
for (let i = 0; i < board.length; i++) {
const row = board[i],
boardRow = this.createElement('div', 'row', i)
gameBoard.append(boardRow)
for (let j = 0; j < row.length; j++) {
const boardCol = this.createElement('div', 'col', `c-${i}-${j}`, {
row: i,
col: j,
})
boardRow.append(boardCol)
boardCol.addEventListener('click', this.handleClickCell)
}
}
}
/** Get the winner and if the winner exists, increment winning player's score. */
updateScore() {
const winner = this.winner && this.winner.token
if (winner) this.score[winner] = this.score[winner] + 1
}
/** Update the winning player's score on the score board */
updateScoreBoard() {
const token = this.winner.token,
score = this.score[token]
const scoreBoard = this.getElement(`#score-${token}`)
scoreBoard.innerText = `${score}`
}
/**
* Create score board and append to DOM
* @param {Number} score
*/
printScoreBoard(score) {
const game = this.getElement('#game'),
scoreBoard = this.createElement('div', 'score-board')
game.append(scoreBoard)
const p1 = this.createElement('div', 'score'),
p2 = this.createElement('div', 'score'),
p1Label = this.createElement('span', 'score-label'),
p2Label = this.createElement('span', 'score-label'),
p1Score = this.createElement('span', null, 'score-x'),
p2Score = this.createElement('span', null, 'score-o')
p1Label.innerText = `${this.players.x.name}: `
p2Label.innerText = `${this.players.o.name}: `
p1Score.innerText = `${score.x}`
p2Score.innerText = `${score.o}`
p1.append(p1Label)
p1.append(p1Score)
p2.append(p2Label)
p2.append(p2Score)
scoreBoard.append(p1)
scoreBoard.append(p2)
}
/**
* Click handler for when a cell is clicked.
* Check if a move is valid. A move is valid if cell is empty and a winner hasn't been decided.
* If play isn't valid, return.
* Otherwise update the game board with current player's token, increment moveCount.
* Check if each play results in a win and end game if current player wins.
* Check if game is over each turn and switch player to next player.
* @param {MouseEvent} e
*/
handleClickCell(e) {
const { row, col } = e.target.dataset,
validPlay = !this.board[row][col] && !this.winner
if (!validPlay) return
this.updateGameBoard(row, col, this.currPlayer.token)
this.moveCount++
if (this.didPlayerWin(row, col)) {
this.winner = this.currPlayer
this.handleGameOver()
return
}
if (this.isGameOver()) this.handleGameOver()
this.nextPlayer()
}
/** Check if game is over */
isGameOver() {
return this.moveCount >= 9
}
/**
* Reset the game by setting current player to player 'x',
* move count to 0, winner to null.
* Create a new board and reset cell vals to ''.
*/
resetGame() {
this.currPlayer = this.players.x
this.moveCount = 0
this.winner = null
this.createNewBoard()
const cells = document.querySelectorAll('.col')
for (const cell of cells) {
cell.innerText = ''
}
}
/**
* If there is a winner, update the score and scoreboard.
* Remove click event listener from cells.
* Print out game end message and show the replay button.
*/
handleGameOver() {
if (this.winner) {
this.updateScore()
this.updateScoreBoard()
}
const cells = document.querySelectorAll('.col')
for (const cell of cells) {
cell.removeEventListener('click', this.handleClickCell)
}
this.printEndMessage(this.winner)
this.toggleReplayButton(true)
}
/**
* Click handler for replay button.
* Add click event listeners back to cells,reset the game,
* clear message and hide replay button
*/
handleClickReplay() {
const cells = document.querySelectorAll('.col')
for (const cell of cells) {
cell.addEventListener('click', this.handleClickCell)
}
this.resetGame()
this.clearMessage()
this.toggleReplayButton(false)
}
/** Create replay button and append to DOM */
printReplayButton() {
const game = this.getElement('#game'),
button = this.createElement('button', 'btn', 'replay')
button.innerText = 'Replay'
button.addEventListener('click', this.handleClickReplay)
game.append(button)
}
/**
* Toggle replay button display
* @param {Boolean} display
*/
toggleReplayButton(display) {
const replayBtn = this.getElement('#replay')
replayBtn.style.display = display ? 'block' : 'none'
}
/**
* Check horizontal, vertical and diagonal lines of board to see if current player won.
* Return true if the player won, otherwise return false.
* @param {string} row
* @param {string} col
*/
didPlayerWin(row, col) {
const b = this.board,
currP = this.currPlayer.token
if (
// Horizontal
(b[row][0] === currP && b[row][1] === currP && b[row][2] === currP) ||
// Vertical
(b[0][col] === currP && b[1][col] === currP && b[2][col] === currP) ||
// Diagonal
(b[0][0] === currP && b[1][1] === currP && b[2][2] === currP) ||
(b[2][0] === currP && b[1][1] === currP && b[0][2] === currP)
)
return true
return false
}
/** Switch to next player */
nextPlayer() {
this.currPlayer =
this.currPlayer === this.players.x ? this.players.o : this.players.x
}
/**
* Create game end message and append to DOM
* @param {Object} winner
*/
printEndMessage(winner) {
const message = winner ? `${winner.name} won!` : 'Nobody won.',
game = this.getElement('#game'),
element = this.createElement('div', 'message')
element.innerText = message
game.append(element)
}
/** clear out game end message */
clearMessage() {
const message = this.getElement('.message')
message.remove()
}
}
module.exports = TicTacToe
HTML:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Tic Tac Toe</title>
<link href="style.css" rel="stylesheet" />
</head>
<body>
<div class="players" id="players">
<h1>Enter players:</h1>
<div class="player">
<label for="player-1">Player 1: </label>
<input type="text" name="player-1" id="player-1" />
</div>
<div class="player">
<label for="player-2">Player 2: </label>
<input type="text" name="player-2" id="player-2" />
</div>
<button id="start" class="btn">Start</button>
</div>
<div id="game"></div>
<script src="tictactoe.js"></script>
<script>
/** click handler for start button.
* Take the names from the input field and initialize a new Game.
*/
function handleClickStart() {
const p1 = document.querySelector('#player-1').value || 'player 1',
p2 = document.querySelector('#player-2').value || 'player 2'
const game = new TicTacToe(p1, p2)
document.querySelector('#game').style.display = 'block'
game.init()
document.querySelector('#players').style.display = 'none'
}
/** Add event listener to the start button. */
document
.querySelector('#start')
.addEventListener('click', handleClickStart)
</script>
</body>
</html>
CSS:
* {
box-sizing: border-box;
}
html {
text-align: center;
font-size: 1rem;
font-family: sans-serif;
}
button.btn {
padding: 1rem;
padding: 1rem 2rem;
text-align: center;
cursor: pointer;
font-size: 1rem;
text-transform: uppercase;
margin: 0 auto;
transition-duration: 0.4s;
background-color: white;
color: black;
border: 2px solid #fb0093;
}
button.btn:hover {
background-color: #fb0093;
color: white;
}
.players {
margin-top: 4rem;
}
.player {
display: block;
margin: 0 auto;
margin-bottom: 1rem;
font-size: 1.5rem;
}
.player input {
border: 1px solid rgba(0, 0, 0, 0.2);
}
#game {
max-width: 450px;
margin: 0 auto;
margin-top: 4rem;
display: flex;
flex-direction: column;
display: none;
}
.board {
margin-bottom: 2rem;
}
.row,
.col {
display: flex;
align-items: center;
justify-content: center;
font-size: 2.5rem;
}
.row:first-of-type .col {
border-top: 2px solid black;
}
.col {
cursor: pointer;
border: 2px solid black;
border-left: 0;
border-top: 0;
height: 6rem;
width: 6rem;
}
.col:first-of-type {
border-left: 2px solid black;
}
.col:hover {
background-color: #e9e9e9;
}
.cell-x {
color: #0093fb;
}
.cell-o {
color: #fb6800;
}
.score-board,
.message {
margin: 1rem 0;
font-size: 1.5rem;
}
.score {
text-transform: lowercase;
padding-bottom: 1rem;
}
#score-x {
color: #0093fb;
}
#score-o {
color: #fb6800;
}
#replay {
display: none;
}