Implementazione JavaScript Tic Tac Toe

Oct 01 2020

Ho cercato di entrare nello sviluppo web, quindi ho realizzato una rapida implementazione Tic Tac Toe. Mi sembra disordinato, ma non sono sicuro di cosa sia veramente considerata una buona pratica e cosa no. Qualche cattiva pratica nel mio codice o cose che potrei migliorare?

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>

Risposte

1 Stuart Oct 26 2020 at 17:10

Ecco alcuni suggerimenti e una versione che cerca di illustrare la maggior parte di questi punti.

1. Separazione delle preoccupazioni

Dovrebbe esserci una certa separazione tra il codice che gestisce (i) la visualizzazione e (ii) la logica di gioco sottostante e la rappresentazione del tabellone. Le persone spesso usano modelli di progettazione come controller di visualizzazione del modello .

Un modo classico per separarli sarebbe creare una classe, un modulo o un oggetto per la visualizzazione e un altro per la logica di gioco sottostante. I due oggetti chiamano i metodi degli altri solo in casi limitati e ben definiti. Se non vuoi usare oggetti, puoi semplicemente usare nomi di funzioni e commenti per avere una demarcazione più chiara tra la visualizzazione / gestione degli eventi e la logica del gioco.

Invece, ci si trova in vari punti utilizzando l'html sia per rappresentare i dati che per la visualizzazione. Per programmi semplici, questo potrebbe funzionare e persino semplificare il codice, ma non è eccezionale per il tuo programma perché hai funzioni che vuoi essere in grado di gestire sia una scheda "immaginaria" che la scheda effettiva mostrata sullo schermo. Per giochi più complessi, diventerà sempre più complicato lavorare con la rappresentazione html del tabellone. Se permetti a ogni funzione di cambiare o leggere direttamente l'html, diventa difficile tracciare da dove proviene un particolare cambiamento. Inoltre, lega il tuo programma troppo strettamente a una particolare rappresentazione. Ad esempio, immagina di voler usare canvaso svggrafica invece di solo testo e css. Ciò richiederebbe la riscrittura di tutto con l'attuale configurazione.

getWinè particolarmente confuso in quanto sembra funzionare in modo completamente diverso a seconda che gli si passi o meno il parametro board. Potresti provare a ottimizzare controllando solo le righe contenenti la particolare x o y, ma è molto più facile scrivere una funzione generica che controllerà qualsiasi scheda. La funzione restituisce una sequenza che è la sequenza vincente per una vincita o l'intera griglia se si tratta di un pareggio.

2. Piccole funzioni che fanno una cosa

Le tue funzioni sembrano disordinate, in parte perché utilizzano una combinazione della rappresentazione sullo schermo della scacchiera e una rappresentazione di matrice separata per scoprire se uno spazio è pieno o meno, ma anche perché stanno cercando di funzionare in troppi casi diversi. ad esempio getWin(come menzionato sopra) e getSpaceValue, che accetta valori xnulli, un elemento html o un numero e ypuò anche essere indefinito o un numero. La maggior parte delle funzioni dovrebbe accettare input di un singolo tipo (un'eccezione a volte consente di omettere gli argomenti con i valori predefiniti) e restituire un valore di ritorno prevedibile.

3. Usa di Arraypiù metodi moderni

filter, find, mapPossono tutti aiutare a scrivere codice più breve e più leggibile (anche se può anche diventare illeggibili se utilizzato per il confezionamento di troppo in un one-liner)

4. Utilizzare le funzioni di utilità

Potresti non voler utilizzare una libreria come lodash, ma alcune funzioni di utilità standard renderebbero il tuo codice più conciso e leggibile e l'utilizzo di una libreria popolare significa che altri programmatori saranno in grado di leggere il tuo codice e capire rapidamente cosa sta facendo. Ad esempio il maxBy di lodash sarebbe utile per scegliere la mossa AI ottimale. Puoi facilmente scrivere il tuo se non vuoi usare una libreria.

5. Codice per determinare una vittoria

Il tuo codice funziona ed è intelligente in un certo senso ma molto contorto! A volte gli passi j, jle coordinate della mossa corrente, ma a un certo punto passi come le coordinate, che misteriosamente funziona ancora. Un modo più semplice e trasparente per verificare una vincita consiste nel memorizzare una serie di possibili sequenze vincenti ( [[0, 1, 2], [3, 4, 5], ...]) e quindi trovare la prima sequenza per la quale sono impostati tutti i quadrati del tabellone 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 puoi fornire le coordinate della mossa corrente, puoi scrivere una versione potenzialmente più efficiente di questa che controlla solo le (max.) 4 linee che passano attraverso quelle coordinate:

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));
}

In pratica per una scheda 3x3 è improbabile che questa ottimizzazione migliori la velocità (e potrebbe peggiorarla, poiché l' linesarray deve essere ricostruito ogni volta).

6. gestori di eventi html

Non inserire gestori di eventi nell'html quando la maggior parte della logica è in javascript. È troppo disordinato e confuso. Metti tutta la gestione degli eventi in javascript.

7. Commenti / documentazione

Poiché condividi il codice e chiedi feedback, dovresti documentare il codice. Nel modo più semplice questa potrebbe essere una o due righe di commenti che spiegano cosa fa ciascuna funzione e la struttura complessiva. Ma esiste un modo standard di documentare chiamato jsdoc ( vedi anche questa introduzione ). Ciò è particolarmente importante se per qualche motivo è ancora necessario disporre di funzioni che funzionino in modi sorprendenti.

Reiki Oct 16 2020 at 07:51

Per entrare nelle migliori pratiche sarebbero necessari volumi e ottenere molte opinioni. In ogni caso, cercare di programmare troppo oltre la tua comprensione non va bene, quindi non entrerò nei particolari. In generale, la prima cosa che mi viene in mente è che se sei veramente serio riguardo alla programmazione, sii buono con te stesso. La programmazione inizia come un processo di pensiero e i tuoi pensieri cambieranno man mano che le tue capacità crescono. Lascia note liberali (commenti sul codice) e tra sei mesi o sei anni da ora quando tu (o forse io!) Rivisiti il ​​tuo codice, puoi ricostruire il processo che ha creato il codice.

Complimenti per aver nominato le tue funzioni e variabili in modo che il loro intento sia chiaro. Il tuo stile di indentazione e codifica è coerente e questo è importante. Ora prendi l'abitudine di lasciare una o due righe per ogni blocco di codice.

Sto programmando per il web da un paio di decenni e non crederai allo stress di provare a fare una piccola modifica a 600 linee di logica che non capisci quando l'azienda per cui lavori costa un milione di dollari anno e impiega decine di persone. O lo shock quando ti rendi conto che è la tua programmazione!