I've been trying to get into web development, so I made a quick Tic Tac Toe implementation. It feels messy to me, but I'm not sure what is really considered to be good practice and what not. Any poor practice in my code, or things I could improve?
https://github.com/jason-shepherd/tictactoe
//Get the board, record, and select html elements
const gridSpaces = document.querySelectorAll('[data-spaces]');
const recordText = document.querySelector('[data-record]');
const difficultySelect = document.querySelector('[data-select]')
const gridWidth = Math.sqrt(gridSpaces.length);
let opponent = "O"
let player = "X"
let difficulty;
let record = {
X: 0,
O: 0,
ties: 0
}
let moveCount = 0;
let inPlay = true;
function init() {
updateDifficulty();
//Init the board spaces with an event listener
for(let i = 0; i < gridSpaces.length; i++) {
gridSpaces[i].addEventListener('click', () => {
if(!inPlay) {
reset();
return;
}
if(getSpaceValue(i) != '') return;
//Player's move
setSpaceValue(i, player);
gridSpaces[i].style.cursor = "default";
win = getWin(Math.floor(i % gridWidth), Math.floor(i / gridWidth), player);
displayWin(win, player);
moveCount++;
//AI move
if(inPlay) {
if(difficulty != 0)
makeAiMove();
else
player = player == "O" ? "X" : "O";
}
});
}
}
function checkRowSpace(index, x, y, board) {
return getGridSpace(index, y, board);
}
function checkColSpace(index, x, y, board) {
return getGridSpace(x, index, board);
}
function checkDiagonal(index, x, y, board) {
if(x == y)
return getGridSpace(index, index, board);
else
return null;
}
function checkAntiDiagonal(index, x, y, board) {
if(x + y == gridWidth - 1)
return getGridSpace(index, gridWidth - 1 - index, board);
else
return null
}
const checkFunctions = [checkRowSpace, checkColSpace, checkDiagonal, checkAntiDiagonal];
function getWin(x, y, currentPlayer, board) {
let winSequence = [];
for(let i = 0; i < 4; i++) {
for(let j = 0; j < gridWidth; j++) {
let currentSpace = checkFunctions[i](j, x, y, board);
if(board == undefined) {
if(getSpaceValue(currentSpace) != currentPlayer) {
winSequence = [];
break;
}
} else if(currentSpace != currentPlayer) {
winSequence = [];
break;
}
winSequence.push(currentSpace);
if(j == gridWidth - 1) {
return winSequence;
}
}
}
if(moveCount == Math.pow(gridWidth, 2) - 1) {
return gridSpaces;
}
return winSequence;
}
function displayWin(win, currentPlayer) {
if(win.length !== 0) {
let condition = "win";
if(win.length === gridSpaces.length) {
record.ties++;
condition = "draw";
} else {
record[currentPlayer]++;
}
recordText.textContent = `X ${record.X}-${record.ties}-${record.O} O`;
win.forEach(space => {
space.firstChild.classList.add(condition);
});
gridSpaces.forEach(space => {
space.style.cursor = "pointer";
});
inPlay = false;
return;
}
}
function makeAiMove() {
let bestVal = -11;
let bestMove;
let newBoard = [];
gridSpaces.forEach(space => {
newBoard.push(getSpaceValue(space));
});
let possibleMoves = getBoardChildren(newBoard, "O");
if(difficulty != 9)
possibleMoves.sort((a, b) => {return 0.5 - Math.random()})
possibleMoves.forEach(child => {
let value = minimax(child, difficulty, false);
if(value > bestVal) {
bestVal = value;
bestMove = child;
}
});
for(let i = 0; i < bestMove.length; i++) {
if(getSpaceValue(i) != bestMove[i]) {
setSpaceValue(i, 'O');
let win = getWin(Math.floor(i % gridWidth), Math.floor(i / gridWidth), opponent);
displayWin(win, opponent);
}
}
moveCount++;
}
function minimax(board, depth, maximizingPlayer) {
let score = scoreBoard(board, depth);
if(depth == 0 || isTerminating(board) || score != 0)
return score;
if(maximizingPlayer) {
let value = -10;
getBoardChildren(board, opponent).forEach(child => {
value = Math.max(value, minimax(child, depth - 1, false));
});
return value;
} else {
let value = 10;
getBoardChildren(board, player).forEach(child => {
value = Math.min(value, minimax(child, depth - 1, true));
});
return value;
}
}
function getBoardChildren(board, currentPlayer) {
let children = [];
for(let i = 0; i < board.length; i++) {
if(board[i] == '') {
board[i] = currentPlayer;
children.push([...board]);
board[i] = '';
}
}
return children;
}
function isTerminating(board) {
for(let i = 0; i < board.length; i++) {
if(board[i] == '')
return false;
}
return true;
}
function scoreBoard(board, depth) {
let currentPlayer = "O";
for(let i = 0; i < 2; i++) {
for(let j = 0; j < 3; j++) {
if(getWin(j, j, currentPlayer, board).length == 3) {
if(currentPlayer == "O")
return 10 - (difficulty - depth);
else
return -10 + (difficulty - depth);
}
}
currentPlayer = "X";
}
return 0;
}
function updateDifficulty() {
if(difficultySelect.value != "friend") {
switch(difficultySelect.value) {
case "easy":
difficulty = 1;
break;
case "medium":
difficulty = 4;
break;
case "unbeatable":
difficulty = 9;
break;
}
if(player == "O") {
player = "X";
makeAiMove();
}
} else {
difficulty = 0;
}
}
function reset() {
player = "X";
moveCount = 0;
inPlay = true;
for(let i = 0; i < gridSpaces.length; i++) {
gridSpaces[i].firstChild.classList.remove("win");
gridSpaces[i].firstChild.classList.remove("draw");
setSpaceValue(i, "");
}
}
function getSpaceValue(x, y, board) {
if(x == null)
return;
else if(typeof x === 'object')
return x.firstChild.textContent;
else if(y == undefined)
return gridSpaces[x].firstChild.textContent;
else
return gridSpaces[y * gridWidth + x].firstChild.textContent;
}
function setSpaceValue(index, value) {
gridSpaces[index].firstChild.textContent = value;
}
function getGridSpace(x, y, board) {
if(board != undefined)
return board[y * gridWidth + x];
else
return gridSpaces[y * gridWidth + x];
}
init();
body {
background-color: #353A47;
}
.tictactoe-container {
background-color: #353A47;
width: 77vh;
height: auto;
position: absolute;
left: 50%;
top: 20%;
transform: translate(-50%, -20%);
}
.grid-container {
background-color: #2B303B;
display: inline-grid;
width: auto;
height: auto;
grid-gap: 1vh;
grid-template-columns: repeat(3, 25vh);
grid-template-rows: repeat(3, 25vh);
}
.grid-item {
background-color: #353A47;
color: #F0F7EE;
display: flex;
cursor: pointer;
justify-content: center;
align-items: center;
font-family: 'Varela Round', sans-serif;
font-weight: bold;
font-size: 25vh;
}
.record {
color: #F0F7EE;
font-weight: bold;
text-align: center;
font-family: 'Varela Round', sans-serif;
font-size: 10vh;
white-space: nowrap;
margin: 2vh auto;
}
.ai-select {
color: #F0F7EE;
background-color: #353A47;
font-size: 3vh;
width: 40%;
height: 15%;
margin: 1vh 30%;
}
.win {
color: #4BB3FD;
animation: shake 0.5s;
animation-iteration-count: 3;
}
.draw {
color: #FF312E;
animation: shake 0.5s;
animation-iteration-count: 6;
}
@keyframes shake {
0% { transform: translate(1px, 1px) rotate(0deg); }
10% { transform: translate(-1px, -2px) rotate(-1deg); }
20% { transform: translate(-3px, 0px) rotate(1deg); }
30% { transform: translate(3px, 2px) rotate(0deg); }
40% { transform: translate(1px, -1px) rotate(1deg); }
50% { transform: translate(-1px, 2px) rotate(-1deg); }
60% { transform: translate(-3px, 1px) rotate(0deg); }
70% { transform: translate(3px, 1px) rotate(-1deg); }
80% { transform: translate(-1px, -1px) rotate(1deg); }
90% { transform: translate(1px, 2px) rotate(0deg); }
100% { transform: translate(1px, -2px) rotate(-1deg); }
}
.noselect {
-webkit-touch-callout: none; /* iOS Safari */
-webkit-user-select: none; /* Safari */
-khtml-user-select: none; /* Konqueror HTML */
-moz-user-select: none; /* Old versions of Firefox */
-ms-user-select: none; /* Internet Explorer/Edge */
user-select: none; /* Non-prefixed version, currently
supported by Chrome, Edge, Opera and Firefox */
}
@media screen and (orientation:portrait) {
.tictactoe-container {
width: 77vw;
}
.grid-container {
grid-gap: 1vw;
grid-template-columns: repeat(3, 25vw);
grid-template-rows: repeat(3, 25vw);
}
.grid-item {
font-size: 25vw;
}
.record {
font-size: 10vw;
}
.ai-select {
font-size: 3vw;
}
}
<!DOCTYPE html>
<html>
<meta charset="UTF-8" name="viewport" content="width=device-width, initial-scale=1">
<head>
<title>Tic Tac Toe</title>
<link rel="stylesheet" type = "text/css" href = "style.css">
<link href="https://fonts.googleapis.com/css2?family=Varela+Round&display=swap" rel="stylesheet">
</head>
<body>
<div class="tictactoe-container">
<p data-record class="record noselect">X 0-0-0 O</p>
<div class="grid-container noselect">
<div data-spaces class="grid-item"><p></p></div>
<div data-spaces class="grid-item"><p></p></div>
<div data-spaces class="grid-item"><p></p></div>
<div data-spaces class="grid-item"><p></p></div>
<div data-spaces class="grid-item"><p></p></div>
<div data-spaces class="grid-item"><p></p></div>
<div data-spaces class="grid-item"><p></p></div>
<div data-spaces class="grid-item"><p></p></div>
<div data-spaces class="grid-item"><p></p></div>
</div>
<select data-select class="ai-select" onchange="updateDifficulty()">
<option value="easy">Easy</option>
<option value="medium" selected="selected">Medium</option>
<option value="unbeatable">Unbeatable</option>
<option value="friend">Play with a friend</option>
</select>
</div>
<script src="tictactoe.js" defer></script>
</body>
</html>