Implementação JavaScript Tic Tac Toe
Tenho tentado entrar no desenvolvimento da web, então fiz uma rápida implementação do jogo da velha. Parece complicado para mim, mas não tenho certeza do que é realmente considerado uma boa prática e o que não é. Alguma prática ruim em meu código ou coisas que eu poderia melhorar?
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>
Respostas
Aqui estão algumas sugestões e uma versão que tenta ilustrar a maioria desses pontos.
1. Separação de preocupações
Deve haver alguma separação entre o código que manipula (i) a exibição e (ii) a lógica do jogo subjacente e a representação do tabuleiro. As pessoas costumam usar padrões de projeto como model-view-controller .
Uma forma clássica de separá-los seria fazer uma classe, módulo ou objeto para a tela e outro para a lógica do jogo subjacente. Os dois objetos chamam os métodos um do outro apenas em casos limitados e bem definidos. Se você não quiser usar objetos, pode apenas usar nomes de função e comentários para ter uma demarcação mais clara entre a exibição / manipulação de eventos e a lógica do jogo.
Em vez disso, você está em vários pontos usando o html para representar os dados e também a exibição. Para programas simples, isso pode funcionar e até mesmo simplificar o código, mas não é ótimo para o seu programa porque você tem funções que deseja que sejam capazes de lidar com uma placa 'imaginária' e a placa real mostrada na tela. Para jogos mais complexos, será cada vez mais complicado trabalhar com a representação html do tabuleiro. Se você permitir que todas as funções alterem ou leiam o html diretamente, torna-se difícil rastrear de onde uma alteração específica está vindo. Também vincula seu programa muito estreitamente a uma representação particular. Por exemplo, imagine que você deseja usar gráficos canvasou em svgvez de apenas texto e css. Isso exigiria reescrever tudo com a configuração atual.
getWiné particularmente confuso, pois parece funcionar de forma completamente diferente dependendo se você passa o parâmetro da placa ou não. Você pode estar tentando otimizar verificando apenas as linhas que contêm x ou y específicos, mas é muito mais fácil escrever uma função genérica que verificará qualquer quadro. A função retorna uma sequência que é a sequência vencedora para uma vitória ou a grade inteira se for um empate.
2. Pequenas funções que fazem uma coisa
Suas funções parecem confusas, em parte porque estão usando uma mistura da representação na tela do quadro e uma representação de matriz separada para descobrir se um espaço está preenchido ou não, mas também porque estão tentando funcionar em muitos casos diferentes. por exemplo, getWin(como mencionado acima) e getSpaceValue, que aceita valores de xque são nulos, um elemento html ou um número e ytambém pode ser indefinido ou um número. A maioria das funções deve aceitar entradas de um único tipo (uma exceção às vezes é permitir que os argumentos sejam omitidos com os padrões) e retornar um valor de retorno previsível.
3. Use Arraymétodos modernos mais
filter, find, mapPodem todos ajudar a escrever mais curto e mais legível código (embora também pode se tornar ilegível se usado para embalar demasiado em um one-liner)
4. Use funções utilitárias
Você pode não querer usar uma biblioteca como o lodash, mas algumas funções de utilitário padrão tornariam seu código mais conciso e legível, e usar uma biblioteca popular significa que outros programadores serão capazes de ler seu código e entender rapidamente o que ele está fazendo. Por exemplo, maxBy de Lodash seria útil na escolha do movimento AI ideal. Você pode escrever seu próprio facilmente se não quiser usar uma biblioteca.
5. Código para determinar uma vitória
Seu código funciona e é inteligente, mas muito complicado! Às vezes você passa j, jpelas coordenadas do movimento atual, mas em um ponto você passa pelas coordenadas, o que misteriosamente ainda funciona. Uma maneira mais simples e transparente de verificar se há uma vitória é armazenar uma série de possíveis sequências vencedoras ( [[0, 1, 2], [3, 4, 5], ...]) e, em seguida, encontrar a primeira sequência para a qual todos os quadrados do tabuleiro estão definidos player.
const range = [...Array(gridWidth).keys()];
const lines = [
...range.map(i => range.map(j => i * gridWidth + j)), // rows
...range.map(i => range.map(j => i + j * gridWidth)), // columns
range.map(j => j * (gridWidth + 1)), // diagonal
range.map(j => (gridWidth - 1) * (j + 1)) // antidiagonal
];
function win(player, board) {
return lines.find(line => line.every(i => board[i] === player));
}
Se você puder fornecer as coordenadas do movimento atual, poderá escrever uma versão potencialmente mais eficiente disso, que verifica apenas as (máx.) 4 linhas que passam por essas coordenadas:
const range = [...Array(gridWidth).keys()];
function win(player, x, y, board) {
let lines = [
range.map(i => y * gridWidth + i), // current row
range.map(i => i * gridWidth + x) // current column
];
if (x === y) { // diagonal
lines.push(range.map(i => i * gridWidth + i));
}
if (gridWidth - x === y) { // antidiagonal
lines.push(range.map(i => (gridWidth - 1) * (i + 1)));
}
return lines.find(line => line.every(i => board[i] === player));
}
Na prática, para uma placa 3x3, é improvável que essa otimização melhore a velocidade (e pode piorá-la, pois o linesarray precisa ser reconstruído a cada vez).
6. manipuladores de eventos html
Não coloque manipuladores de eventos no html quando a maior parte de sua lógica está em javascript. É muito bagunçado e confuso. Coloque todo o tratamento de eventos em javascript.
7. Comentários / documentação
Como você está compartilhando seu código e pedindo feedback, você deve documentar seu código. Na forma mais simples, isso poderia ser uma linha ou duas de comentários explicando o que cada função faz e a estrutura geral. Mas existe uma forma padrão de documentar chamada jsdoc ( veja também esta introdução ). Isso é especialmente importante se você, por algum motivo, ainda precisar de funções que funcionem de maneiras surpreendentes.
Entrar nas melhores práticas exigiria volumes e obteria muitas opiniões. De qualquer forma, tentar programar muito além de sua compreensão não é bom, então não entrarei em detalhes. Em geral, a primeira coisa que me ocorre é que, se você realmente quer programar, seja bom para o seu futuro eu. A programação começa como um processo de pensamento e seus pensamentos mudarão à medida que suas habilidades crescerem. Deixe notas liberais (comentários de código) e daqui a seis meses ou seis anos, quando você (ou talvez eu!) Revisitar seu código, você pode reconstruir o processo que fez o código.
Parabéns por nomear suas funções e variáveis para que sua intenção seja clara. Seu estilo de recuo e codificação é consistente e isso é importante. Agora, adquira o hábito de deixar uma ou duas linhas para cada bloco de código.
Eu tenho programado para a web por algumas décadas e você não vai acreditar no estresse de tentar fazer uma pequena mudança em 600 linhas de lógica que você não entende quando a empresa para a qual você trabalha fatura um milhão por ano e emprega dezenas de pessoas. Ou o choque ao perceber que é sua própria programação!