การใช้งาน JavaScript Tic Tac Toe

Oct 01 2020

ฉันพยายามพัฒนาเว็บดังนั้นฉันจึงใช้ Tic Tac Toe อย่างรวดเร็ว มันรู้สึกยุ่งสำหรับฉัน แต่ฉันไม่แน่ใจว่าอะไรคือแนวทางปฏิบัติที่ดีและอะไรไม่จริง การปฏิบัติที่ไม่ดีในรหัสของฉันหรือสิ่งที่ฉันสามารถปรับปรุงได้?

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>

คำตอบ

1 Stuart Oct 26 2020 at 17:10

ต่อไปนี้เป็นคำแนะนำและเวอร์ชันที่พยายามอธิบายประเด็นส่วนใหญ่เหล่านี้

1. แยกความกังวล

ควรมีการแยกบางส่วนระหว่างรหัสที่จัดการกับ (i) การแสดงผลและ (ii) ตรรกะของเกมและการเป็นตัวแทนของกระดาน คนมักจะใช้รูปแบบการออกแบบเช่นรุ่น View-Controller

วิธีการแยกแบบคลาสสิกคือการสร้างคลาสโมดูลหรือวัตถุสำหรับการแสดงผลและอีกวิธีหนึ่งสำหรับตรรกะของเกม วัตถุทั้งสองเรียกเฉพาะเมธอดของกันและกันในกรณีที่ จำกัด และกำหนดไว้อย่างชัดเจน หากคุณไม่ต้องการใช้วัตถุคุณสามารถใช้ชื่อฟังก์ชันและความคิดเห็นเพื่อแบ่งเขตที่ชัดเจนขึ้นระหว่างการจัดการการแสดงผล / เหตุการณ์และตรรกะของเกม

แต่คุณอยู่ในจุดต่างๆโดยใช้ html ทั้งเพื่อแสดงข้อมูลและการแสดงผล สำหรับโปรแกรมง่ายๆวิธีนี้อาจใช้งานได้และทำให้โค้ดง่ายขึ้น แต่ก็ไม่ดีสำหรับโปรแกรมของคุณเนื่องจากคุณมีฟังก์ชันที่คุณต้องการจัดการทั้งกระดาน 'จินตภาพ' และกระดานจริงที่แสดงบนหน้าจอ สำหรับเกมที่ซับซ้อนมากขึ้นจะมีความซับซ้อนมากขึ้นในการทำงานกับการแสดง HTML ของกระดาน หากคุณอนุญาตให้ทุกฟังก์ชันเปลี่ยนหรืออ่าน html โดยตรงจะยากที่จะติดตามว่าการเปลี่ยนแปลงนั้นมาจากที่ใด นอกจากนี้ยังผูกโปรแกรมของคุณไว้ใกล้ชิดเกินไปกับการแสดงเฉพาะ ตัวอย่างเช่นสมมติว่าคุณต้องการใช้canvasหรือใช้svgกราฟิกแทนข้อความและ css สิ่งนี้จะต้องมีการเขียนทุกอย่างใหม่ด้วยการตั้งค่าปัจจุบัน

getWinมีความสับสนเป็นพิเศษเนื่องจากดูเหมือนว่าจะทำงานแตกต่างกันอย่างสิ้นเชิงขึ้นอยู่กับว่าคุณส่งผ่านพารามิเตอร์ของบอร์ดหรือไม่ คุณอาจพยายามเพิ่มประสิทธิภาพโดยตรวจสอบเฉพาะแถวที่มี x หรือ y เฉพาะ แต่การเขียนฟังก์ชันทั่วไปที่จะตรวจสอบบอร์ดใด ๆ นั้นง่ายกว่ามาก ฟังก์ชันจะส่งคืนลำดับซึ่งเป็นลำดับการชนะสำหรับการชนะหรือทั้งกริดหากเป็นการเสมอกัน

2. ฟังก์ชั่นเล็ก ๆ ที่ทำสิ่งเดียว

ฟังก์ชั่นของคุณดูยุ่งเหยิงส่วนหนึ่งเป็นเพราะพวกเขาใช้ส่วนผสมของการแสดงบนหน้าจอของบอร์ดและการแทนอาร์เรย์แยกต่างหากเพื่อค้นหาว่ามีช่องว่างเต็มหรือไม่ แต่ยังเป็นเพราะพวกเขาพยายามทำงานในกรณีต่างๆมากเกินไป เช่นgetWin(ตามที่กล่าวไว้ข้างต้น) และgetSpaceValueซึ่งยอมรับค่าxที่เป็นโมฆะองค์ประกอบ html หรือตัวเลขและyยังสามารถระบุไม่ได้หรือตัวเลข ฟังก์ชันส่วนใหญ่ควรยอมรับอินพุตประเภทเดียว (ข้อยกเว้นบางครั้งอนุญาตให้ละเว้นอาร์กิวเมนต์ด้วยค่าเริ่มต้น) และส่งคืนค่าส่งคืนที่คาดเดาได้

3. ใช้Arrayวิธีการที่ทันสมัยมากขึ้น

filter, find, mapสามารถช่วยเหลือทั้งหมดเขียนรหัสสั้นและอ่านได้มากขึ้น ( แต่ยังสามารถกลายเป็นอ่านไม่ได้ถ้าใช้ในการแพ็คมากเกินไปในหนึ่งซับ)

4. ใช้ฟังก์ชันยูทิลิตี้

คุณอาจไม่ต้องการใช้ไลบรารีเช่น lodash แต่ฟังก์ชันยูทิลิตี้มาตรฐานบางอย่างจะทำให้โค้ดของคุณกระชับและอ่านได้ง่ายขึ้นและการใช้ไลบรารียอดนิยมหมายความว่าโปรแกรมเมอร์คนอื่น ๆ จะสามารถอ่านโค้ดของคุณและเข้าใจได้อย่างรวดเร็วว่ากำลังทำอะไรอยู่ ตัวอย่างเช่น maxBy ของlodashจะมีประโยชน์ในการเลือกการย้าย AI ที่ดีที่สุด คุณสามารถเขียนของคุณเองได้อย่างง่ายดายหากคุณไม่ต้องการใช้ห้องสมุด

5. รหัสสำหรับพิจารณาการชนะ

รหัสของคุณใช้งานได้และฉลาดในทางหนึ่ง แต่ซับซ้อนมาก! บางครั้งคุณส่งต่อ coords ของการเคลื่อนไหวปัจจุบัน แต่เมื่อถึงจุดหนึ่งคุณผ่านj, jเป็นพิกัดซึ่งยังคงใช้งานได้อย่างลึกลับ วิธีที่ง่ายและโปร่งใสมากขึ้นในการตรวจสอบสำหรับผู้ชนะคือการเก็บอาร์เรย์ของลำดับที่ชนะไปได้ ( [[0, 1, 2], [3, 4, 5], ...]) 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));
}

หากคุณสามารถระบุพิกัดของการเคลื่อนไหวปัจจุบันคุณสามารถเขียนเวอร์ชันที่มีประสิทธิภาพมากกว่าซึ่งจะตรวจสอบเฉพาะ (สูงสุด) 4 บรรทัดที่ผ่านพิกัดเหล่านั้น:

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

ในทางปฏิบัติสำหรับบอร์ด 3x3 การเพิ่มประสิทธิภาพนี้ไม่น่าจะช่วยเพิ่มความเร็วได้ (และอาจแย่ลงเนื่องจากlinesต้องสร้างอาร์เรย์ใหม่ทุกครั้ง)

6. ตัวจัดการเหตุการณ์ html

อย่าใส่ตัวจัดการเหตุการณ์ใน html เมื่อตรรกะส่วนใหญ่ของคุณอยู่ในจาวาสคริปต์ มันยุ่งและสับสนเกินไป ใส่การจัดการเหตุการณ์ทั้งหมดในจาวาสคริปต์

7. ข้อคิดเห็น / เอกสารประกอบ

ในขณะที่คุณแบ่งปันรหัสและขอความคิดเห็นคุณควรบันทึกรหัสของคุณ อย่างง่ายที่สุดนี่อาจเป็นความคิดเห็นหนึ่งหรือสองบรรทัดเพื่ออธิบายว่าแต่ละฟังก์ชันทำหน้าที่อะไรและโครงสร้างโดยรวม แต่มีวิธีการจัดทำเอกสารมาตรฐานที่เรียกว่าjsdoc ( ดูคำนำนี้ด้วย ) นี่เป็นสิ่งสำคัญอย่างยิ่งหากคุณยังคงต้องมีฟังก์ชั่นที่ทำงานในรูปแบบที่น่าประหลาดใจด้วยเหตุผลบางประการ

Reiki Oct 16 2020 at 07:51

เพื่อให้ได้แนวทางปฏิบัติที่ดีที่สุดจะต้องมีปริมาณและขอความคิดเห็นมากมาย ไม่ว่าอย่างไรก็ตามการพยายามตั้งโปรแกรมให้ไกลเกินความเข้าใจของคุณนั้นไม่ดีดังนั้นฉันจะไม่อธิบายรายละเอียด โดยทั่วไปสิ่งแรกที่เกิดขึ้นกับฉันคือถ้าคุณจริงจังกับการเขียนโปรแกรมจริง ๆ จงเป็นประโยชน์ต่ออนาคตของคุณด้วย การเขียนโปรแกรมเริ่มต้นจากกระบวนการคิดและความคิดของคุณจะเปลี่ยนไปเมื่อทักษะของคุณเติบโตขึ้น ทิ้งบันทึกแบบเสรี (ความคิดเห็นเกี่ยวกับรหัส) และหกเดือนหรือหกปีนับจากนี้เมื่อคุณ (หรืออาจจะเป็นฉัน!) กลับมาดูรหัสของคุณอีกครั้งคุณสามารถสร้างกระบวนการที่สร้างรหัสขึ้นใหม่

ความรุ่งโรจน์ในการตั้งชื่อฟังก์ชันและตัวแปรของคุณเพื่อให้เจตนาของพวกเขาชัดเจน รูปแบบการเยื้องและการเข้ารหัสของคุณสอดคล้องกันและเป็นสิ่งสำคัญ ตอนนี้ทำความคุ้นเคยกับการเว้นบรรทัดหรือสองบรรทัดสำหรับแต่ละบล็อกโค้ด

ฉันเขียนโปรแกรมสำหรับเว็บมาสองสามทศวรรษแล้วและคุณจะไม่เชื่อว่าความเครียดจากการพยายามเปลี่ยนแปลงเล็กน้อยกับตรรกะ 600 บรรทัดที่คุณไม่เข้าใจเมื่อ บริษัท ที่คุณทำงานเพื่อเรียกเก็บเงินเป็นล้าน ปีและมีพนักงานหลายสิบคน หรือตกใจเมื่อรู้ว่าเป็นการเขียนโปรแกรมของคุณเอง!