I'm working on building a simple game in vanilla JS (tic-tac-toe). Some weeks ago I created a functional MVP and asked some questions about it (Vanilla JS Tic-Tac-Toe). The code worked, but it was a single monolithic mess and the UI was ugly, so I decided to improve it.
So, I refactored the code. I organized it into three files:
- main.js - contains the state and controls the game flow
- UIFunctions.js - UI related (painting cells, cleaning the screen, etc)
- gameCheckerFunctions - process the state and verifies if we have a winner
- gameStateFunctions - basically saves moves made and change the current player
So, the questions are:
How to implement a MVC pattern? Right now, the state and HTML elements are global variables, but I think this could be stored in state and UI classes Also, I'm having troubles with the concept of controller. Is main.js the code that will be the controller in an MVC pattern?
There's a better algorithm to check for the winner?
Is it a good looking code? How it could be improved? What looks ugly and could be done in a better way? I really would like to develop good practices from the beginning.
Some features that I would like to implement are a CPU player, storing the score in localStorage, remember the player name, auth, two-player network gamming, etc. I'm in the correct way of doing things?
Game can be played https://nabla-f.github.io/
The code:
app.js
// IMPORTS;
import checker from './refactored/gameCheckerFunctions.js';
import game from './refactored/gameStateFunctions.js';
import ui from './refactored/UIFunctions.js';
// CONFIG
const P1_CLASS = 'player1'; // Defined in styles.css
const P2_CLASS = 'player2'; // Defined in styles.css
// STATE
let state = {
activePlayer: P1_CLASS,
boardArray: [[null, null, null], [null, null, null], [null, null, null]],
boardDim: 3,
moves: 0
}
let view = {
button: null,
cells: null,
resultScreen: {
screen: null,
text: null,
img: null
}
};
// WAIT FOR THE DOM
window.addEventListener('DOMContentLoaded', (event) => {
console.log('DOM fully loaded and parsed');
main();
});
// MAIN FUNCTION
function main() {
// GET ELEMENTS FROM DOM AND ADD EVENT LISTENERS
view.button = document.querySelector("#resetBtn");
view.cells = document.querySelectorAll(".cell");
view.resultScreen.screen = document.querySelector("#done");
view.resultScreen.text = document.querySelector('.lead');
view.resultScreen.img = document.getElementById('winnerImg');
view.button.addEventListener('click', () => {ui.restartGameUI(view.cells, view.resultScreen)});
view.button.addEventListener('click', () => {game.restartGame(state, P1_CLASS); console.log(state)});
view.cells.forEach( cell => cell.addEventListener('click', turn) );
view.cells.forEach( cell => cell.classList.add('player1Turn') );
// GAME FLOW FUNCTION EXECUTED EACH TIME A PLAYER MAKES A MOVE
function turn() {
console.log(`Now playing: ${state.activePlayer}`)
let cell = this; // currently clicked cell
if (!cell.hasAttribute('data-disabled')) {
game.storeMove(cell, state.boardArray, state.activePlayer);
state.moves += 1
ui.paintCell(cell, state.activePlayer);
if (checker.checkWinner(state.boardArray, state.boardDim, state.activePlayer)) {
ui.paintWinner(view.resultScreen, state.activePlayer)
}
else if (checker.checkDraw(state.moves)) {
ui.paintDraw(view.resultScreen);
}
else {
state.activePlayer = game.changeTurn(state.activePlayer, P1_CLASS, P2_CLASS);
ui.changeTurnHover(view.cells, state.activePlayer, P1_CLASS, P2_CLASS);
}
}
};
};
UIFunctions.js
// FUNCTIONS
export default class UIFunctions {
static paintCell(cell, playerClass) {
console.log('cell clicked: ' + cell.id);
cell.classList.add(playerClass);
cell.setAttribute('data-disabled', 'true');
}
static paintWinner(screen, playerClass) {
screen.screen.classList.add(`${playerClass}winner`);
screen.img.classList.add(`${playerClass}win-img`);
}
static paintDraw(screen) {
screen.screen.classList.add(`drawgame`);
screen.text.innerText = '...is a draw...';
}
static changeTurnHover(cells, activePlayer, p1Class, p2Class) {
cells.forEach( cell => cell.classList.remove(`${p1Class}Turn`) );
cells.forEach( cell => cell.classList.remove(`${p2Class}Turn`) );
cells.forEach( cell => {
if (!cell.hasAttribute('data-disabled')) {
cell.classList.add(`${activePlayer}Turn`);
}
})
}
static restartGameUI(cells, screen) {
cells.forEach( cell => cell.classList.remove('player1') );
cells.forEach( cell => cell.classList.remove('player2') );
cells.forEach( cell => cell.classList.remove('player2Turn') );
cells.forEach( cell => cell.classList.add('player1Turn') );
cells.forEach( cell => cell.removeAttribute('data-disabled') );
screen.screen.classList.remove('player1winner');
screen.screen.classList.remove('player2winner');
screen.screen.classList.remove('drawgame');
screen.img.classList.remove('player1win-img');
screen.img.classList.remove('player2win-img');
screen.text.innerText = 'Winner is ';
}
}
gameCheckerFunctions.js
export default class gameCheckerFunctions {
static INDEXES = [0, 1, 2]
static DIAG_INDEXES = [ [0, 1, 2], [2, 1, 0] ];
static getValueFromArray(array, row, column) {
return array[row][column]
}
static checkRows(boardArray, boardDim, playerClass) {
for (let i=0; i < boardDim; i++) {
let movesInLine = [];
for (let column of this.INDEXES) {
movesInLine.push(boardArray[i][column]);
}
if ( movesInLine.every( cell => cell == playerClass ) ) {return true;}
}
return false;
}
static checkColumns(boardArray, boardDim, playerClass) {
for (let i=0; i < boardDim; i++) {
let movesInLine = [];
for (let row of this.INDEXES) {
movesInLine.push(boardArray[row][i]);
}
if ( movesInLine.every( cell => cell == playerClass ) ) {return true;}
}
return false;
}
static checkDiags(boardArray, playerClass) {
for (let diag of this.DIAG_INDEXES) {
let movesInLine = [];
for (let i=0; i <= 2; i++) {
let move = boardArray[i][diag[i]];
movesInLine.push(move);
}
if ( movesInLine.every( cell => cell == playerClass ) ) {return true;}
}
return false;
}
static checkWinner(boardData, boardDim, playerClass) {
if ( this.checkRows(boardData, boardDim, playerClass)
|| this.checkColumns(boardData, boardDim, playerClass)
|| this.checkDiags(boardData, playerClass) ) {
console.log("This function works, there's a winner!");
return true
}
else {
console.log('No winner this time');
return false
}
}
static checkDraw(moves) {
if (moves >= 9) {
return true;
}
else {
return false
}
}
constructor() {
throw new Error(" This class can't be instantiated");
}
}
gameStateFunctions.js
// FUNCTIONS
export default class gameStateFunctions {
static changeTurn(activePlayer, p1Class, p2Class) {
//turnFlag = !turnFlag;
const newPlayer = (activePlayer == p1Class) ? p2Class : p1Class
return newPlayer
}
static storeMove(cell, boardArray, activePlayer) {
console.log(`current state in storeMove(): `)
console.log(boardArray);
let coordinates = cell.dataset.coord.split(':')
const [x, y] = coordinates;
if (boardArray[x][y] === null) {
boardArray[x][y] = activePlayer;
}
}
static restartGame(state, startingPlayerClass) {
state.activePlayer = startingPlayerClass;
state.moves = 0;
state.boardArray = [[null, null, null], [null, null, null], [null, null, null]];
console.clear();
console.log('GAME RESTARTED');
}
constructor() {
throw new Error(" This class can't be instantiated");
}
}
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script type="module" src="app.js"></script>
<link rel="stylesheet" href="styles/normalize.css">
<link rel="stylesheet" href="styles/styles.css">
<title>Tic-Tac-Toe vanilla JS</title>
</head>
<body>
<div id="board">
<div class="cells">
<div class="cell" id="cell-1" data-coord="0:0"></div>
<div class="cell" id="cell-2" data-coord="0:1"></div>
<div class="cell" id="cell-3" data-coord="0:2"></div>
<div class="cell" id="cell-4" data-coord="1:0"></div>
<div class="cell" id="cell-5" data-coord="1:1"></div>
<div class="cell" id="cell-6" data-coord="1:2"></div>
<div class="cell" id="cell-7" data-coord="2:0"></div>
<div class="cell" id="cell-8" data-coord="2:1"></div>
<div class="cell" id="cell-9" data-coord="2:2"></div>
</div>
<div id="done">
<div class="lead">
Winner is
</div>
<div id="winnerImg"></div>
<button id="resetBtn" type="reset">Play Again</button>
</div>
</div>
</body>
</html>