Penerapan JavaScript Tic Tac Toe

Oct 01 2020

Saya telah mencoba masuk ke pengembangan web, jadi saya membuat implementasi Tic Tac Toe dengan cepat. Ini terasa berantakan bagi saya, tetapi saya tidak yakin apa yang benar-benar dianggap sebagai latihan yang baik dan apa yang tidak. Adakah praktik yang buruk dalam kode saya, atau hal-hal yang dapat saya tingkatkan?

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>

Jawaban

1 Stuart Oct 26 2020 at 17:10

Berikut adalah beberapa saran dan versi yang mencoba mengilustrasikan sebagian besar poin ini.

1. Pemisahan kekhawatiran

Harus ada beberapa pemisahan antara kode yang menangani (i) tampilan dan (ii) logika permainan yang mendasari dan representasi papan. Orang sering menggunakan pola desain seperti model-view-controller .

Cara klasik untuk memisahkan mereka adalah dengan membuat kelas, modul atau objek untuk tampilan dan lainnya untuk logika permainan yang mendasarinya. Kedua objek tersebut hanya memanggil metode satu sama lain dalam kasus yang terbatas dan terdefinisi dengan baik. Jika Anda tidak ingin menggunakan objek, Anda dapat menggunakan nama fungsi dan komentar untuk memiliki demarkasi yang lebih jelas antara tampilan / penanganan acara dan logika permainan.

Sebaliknya, Anda berada di berbagai titik menggunakan html baik untuk merepresentasikan data maupun tampilan. Untuk program sederhana, ini mungkin berfungsi dan bahkan menyederhanakan kodenya, tetapi itu tidak bagus untuk program Anda karena Anda memiliki fungsi yang Anda inginkan untuk dapat menangani papan 'imajiner' dan papan sebenarnya yang ditampilkan di layar. Untuk game yang lebih kompleks, akan semakin rumit bekerja dengan representasi html dari papan. Jika Anda mengizinkan setiap fungsi untuk mengubah atau membaca html secara langsung, akan sulit untuk melacak dari mana asal perubahan tertentu. Itu juga mengikat program Anda terlalu dekat dengan representasi tertentu. Misalnya, bayangkan Anda ingin menggunakan canvasatau svggrafis bukan hanya teks dan css. Ini akan membutuhkan penulisan ulang semuanya dengan pengaturan saat ini.

getWinsangat membingungkan karena tampaknya berfungsi sepenuhnya berbeda tergantung pada apakah Anda meneruskan parameter papan atau tidak. Anda mungkin mencoba untuk mengoptimalkan hanya dengan memeriksa baris yang berisi x atau y tertentu, tetapi jauh lebih mudah untuk menulis fungsi umum yang akan memeriksa papan manapun. Fungsi mengembalikan urutan yang merupakan urutan kemenangan untuk menang, atau keseluruhan kisi jika seri.

2. Fungsi kecil yang melakukan satu hal

Fungsi Anda tampak berantakan, sebagian karena mereka menggunakan campuran representasi papan di layar dan representasi larik terpisah untuk mengetahui apakah suatu ruang terisi atau tidak, tetapi juga karena mereka mencoba bekerja dalam banyak kasus yang berbeda. misalnya getWin(seperti yang disebutkan di atas) dan getSpaceValue, yang menerima nilai xnull, elemen html, atau angka, dan yjuga bisa tidak ditentukan atau angka. Sebagian besar fungsi harus menerima input dari satu jenis (pengecualian terkadang memungkinkan argumen dihilangkan dengan default) dan mengembalikan nilai pengembalian yang dapat diprediksi.

3. Gunakan Arraymetode modern lebih banyak

filter, find, mapSemua bisa membantu menulis kode lebih pendek dan lebih mudah dibaca (meskipun juga dapat menjadi tidak terbaca jika digunakan untuk berkemas terlalu banyak ke dalam satu-liner)

4. Gunakan fungsi utilitas

Anda mungkin tidak ingin menggunakan pustaka seperti lodash, tetapi beberapa fungsi utilitas standar akan membuat kode Anda lebih ringkas dan mudah dibaca, dan menggunakan pustaka populer berarti pemrogram lain akan dapat membaca kode Anda dan dengan cepat memahami apa yang dilakukannya. Misalnya maxBy dari lodash akan berguna dalam memilih gerakan AI yang optimal. Anda dapat dengan mudah menulis sendiri jika Anda tidak ingin menggunakan perpustakaan.

5. Kode untuk menentukan kemenangan

Kode Anda berfungsi dan pintar tetapi sangat berbelit-belit! Kadang-kadang Anda melewatkannya sebagai koord gerakan saat ini, tetapi pada satu titik Anda melewatkannya j, jsebagai koordinat, yang secara misterius masih berfungsi. Cara yang lebih sederhana dan lebih transparan untuk memeriksa kemenangan adalah dengan menyimpan larik urutan kemenangan yang mungkin ( [[0, 1, 2], [3, 4, 5], ...]) dan kemudian menemukan urutan pertama yang mengatur semua kotak papan 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));
}

Jika Anda dapat memberikan koordinat perpindahan saat ini, maka Anda dapat menulis versi yang berpotensi lebih efisien dari ini yang hanya memeriksa (maks.) 4 baris yang melewati koordinat tersebut:

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

Dalam praktiknya untuk papan 3x3, pengoptimalan ini tidak mungkin meningkatkan kecepatan (dan mungkin memperburuknya, karena lineslarik harus dibangun ulang setiap kali).

6. penangan acara html

Jangan letakkan penangan peristiwa di html ketika sebagian besar logika Anda ada dalam javascript. Itu terlalu berantakan dan membingungkan. Letakkan semua penanganan acara di javascript.

7. Komentar / dokumentasi

Saat Anda membagikan kode Anda dan meminta umpan balik, Anda harus mendokumentasikan kode Anda. Paling sederhana ini bisa menjadi satu atau dua baris komentar yang menjelaskan apa yang dilakukan setiap fungsi dan struktur keseluruhan. Tetapi ada cara standar untuk mendokumentasikan yang disebut jsdoc ( lihat juga pengantar ini ). Ini sangat penting jika Anda karena alasan tertentu masih perlu memiliki fungsi yang berfungsi dengan cara yang mengejutkan.

Reiki Oct 16 2020 at 07:51

Untuk masuk ke praktik terbaik akan membutuhkan volume dan memberi Anda banyak pendapat. Bagaimanapun, mencoba memprogram terlalu jauh di luar pemahaman Anda tidak baik jadi saya tidak akan membahasnya secara khusus. Secara umum, hal pertama yang terpikir oleh saya adalah bahwa jika Anda benar-benar serius tentang pemrograman, jadilah baik untuk masa depan Anda. Pemrograman dimulai sebagai proses berpikir dan pikiran Anda akan berubah seiring dengan perkembangan keterampilan Anda. Tinggalkan catatan liberal (komentar kode) dan enam bulan atau enam tahun dari sekarang ketika Anda (atau mungkin saya!) Mengunjungi kembali kode Anda, Anda dapat merekonstruksi proses pembuatan kode.

Kudos untuk menamai fungsi dan variabel Anda sehingga tujuannya jelas. Gaya indentasi dan pengkodean Anda konsisten dan itu penting. Sekarang biasakan untuk meninggalkan satu atau dua baris untuk setiap blok kode.

Saya telah memprogram untuk web selama beberapa dekade dan Anda tidak akan percaya tekanan untuk mencoba membuat perubahan kecil pada 600 baris logika yang tidak Anda pahami ketika perusahaan tempat Anda bekerja dengan biaya jutaan dolar. tahun dan mempekerjakan puluhan orang. Atau kaget ketika Anda menyadari itu adalah program Anda sendiri!