5
\$\begingroup\$

I have a bit of experience in programming but not that much. I was trying to create a TicTacToe Game in Javascript. I was doing my best to have a good structure. I was thinking about using classes, but for this Project I wanted to stay to the basics and don't make it to complicated for me.

I tried to structure the code in small groups. I would like to have something like a game-loop in the main function. But I don't know how to implement this without being stuck in a endless loop (what happened as I tried it).

Right now my "Game Loop" is being placed in the buttonClicked Function. But perhaps if you are not the one who wrote the program, then it's not really comprehensible. (Actually I don't know if having a real main that controls the game in javascript, because you are using the eventListeners)

Can you give me some tips for:

  1. How to improve my general Code Style
  2. Maybe also Ideas for the "game loop" (or explanation if you are using it or not)

This is my Source Code. There is a folder with assets missing(the cross and circle Symbol, that I can not upload here). But you can also access the game with this link via Github.

https://bodensteiner23.github.io/LearnToCode/

/* ============================= Variables ============================ */
const heading = document.getElementById("heading");
const buttons = document.querySelectorAll(".buttons");
const startButton = document.getElementById("start_game_button");
const newGameButton = document.getElementById("start_new_game_button");

let playerOneTurn = null;
let turnCounter = null;

let playingFielddArray = [0, 0, 0,
                          0, 0, 0,
                          0, 0, 0]

/* ============================= Functions ============================ */
/* ========================= Button Functions ========================= */
/**
 *  @brief  Initialization of all button event listeners
 */
function initButtonEventListeners() {
    // Start the game Button
    if (startButton) {
        startButton.addEventListener("click", startButtonClicked);
    }

    // Game field buttons
    for(let i = 0; i < buttons.length; i++) {
        buttons[i].addEventListener("click", buttonClicked)
        // @ts-ignore
        buttons[i].disabled = true;
    }

    // New Game Button
    if (newGameButton) {
        newGameButton.addEventListener("click", newGameButtonClicked)
    }
}

/**
 *  @brief  Logic if start button is clicked
 */
function startButtonClicked() {
    if (startButton) {
        startButton.style.display = "none";
    }
    //Set Player One Turn to True
    playerOneTurn = true;
    updateHeading("Player 1 turn");

    enableDisableButtons("enable")
}

/**
 *  @brief  Logic if game-field buttons are clicked. Handles Game Structure.
 * 
 *  @param  event Object that contains information about the button click
 */
function buttonClicked(event) {
    // Check if Button has already been clicked
    if (event.target.hasAttribute('data-clicked')) {
        // The button has been clicked before

        return;
    } else {
        // The button has not been clicked before
        event.target.setAttribute('data-clicked', 'true');
        
        updatePlayingField(event.target.id, playerOneTurn)

        let gameOver = checkWin(playerOneTurn)
        
        wonGame(gameOver, playerOneTurn)
    
        addImageToButton(event, playerOneTurn)
        
        // Check if game is over
        if (gameOver) {
            enableDisableButtons("disable")
        } else {
            if (playerOneTurn) {
                updateHeading("Player 1 turn")
            } else {
                updateHeading("Player 2 turn")
            }
        }

        turnCounter += 1;
        checkDraw(gameOver, turnCounter)
    }
}

/**
 *  @brief  Enable or disable game-field buttons
 * 
 *  @param  state State the buttons should be changed to: "enable"/"disable"
 */
function enableDisableButtons(state) {
    for (let i = 0; i < buttons.length; i++) {
        if (state == "enable") {
            // @ts-ignore
            buttons[i].disabled = false;    
        } else {
            // State is disable
            // @ts-ignore
            buttons[i].disabled = true; 
        }
    }
}

/**
 *  @brief  Add Image class to the button that was clicked
 * 
 *  @param  event Object that contains information about the button click
 *  @param  playersTurn True if Player1 / False if Player2
 */
function addImageToButton(event, playersTurn) {
    if (playersTurn) {
        event.target.classList.add("cross");
        
        playerOneTurn = false
    } else {
        // Check if button is already clicked
        if (event.target.classList.contains("cross")) {
            // Skip
        } else {
            event.target.classList.add("circle");
        }
        playerOneTurn = true;
    }
}

/**
 *  @brief  Logic if the new game button is clicked. Resets all states for new game
 */
function newGameButtonClicked() {
    // Game field buttons
    enableDisableButtons("enable")
    for(let i = 0; i < buttons.length; i++) {
        buttons[i].removeAttribute("data-clicked")
        buttons[i].classList.remove("cross")
        buttons[i].classList.remove("circle")
    }

    if (newGameButton) {
        newGameButton.style.display = "none"
    }
    
    updateHeading("Player 1 turn")

    playerOneTurn = true

    turnCounter = null
    
    playingFielddArray = [0, 0, 0,
                          0, 0, 0,
                          0, 0, 0]
}


/* ================================ Text ============================== */
/**
 *  @brief  Updates the Heading of the App
 * 
 *  @param  text Text that should be displayed
 */
function updateHeading(text) {
    if (heading) {
        heading.textContent = text;
    }
}
    

/* ============================= Game Logic =========================== */
/**
 *  @brief  Checks what Player Turn it is and returns the symbol of that player
 * 
 *  @param  playerOneTurn True if Player1 / False if Player2
 */
function setPlayerTurn(playerOneTurn) {
    let playerSymbol = 0
    
    if (playerOneTurn) {
        playerSymbol = 1
    } else {
        playerSymbol = 2
    }

    return playerSymbol
}

/**
 *  @brief  Updates playing field array, that represents the playing field for the user
 * 
 *  @param  buttonClicked Id of the Button that was clicked
 *  @param  playerOneTurn True if Player1 / False if Player2
 */
function updatePlayingField(buttonClicked, playerOneTurn) {
    // Check which Player is playing
    let playerSymbol = setPlayerTurn(playerOneTurn)

    switch(buttonClicked) {
        case "top_left":
            playingFielddArray[0] = playerSymbol
            break;
        case "top_middle":
            playingFielddArray[1] = playerSymbol
            break;
        case "top_right":
            playingFielddArray[2] = playerSymbol
            break;
        case "middle_left":
            playingFielddArray[3] = playerSymbol
            break;       
        case "middle_middle":
            playingFielddArray[4] = playerSymbol
            break
        case "middle_right":
            playingFielddArray[5] = playerSymbol
            break
        case "bottom_left":
            playingFielddArray[6] = playerSymbol
            break
        case "bottom_middle":
            playingFielddArray[7] = playerSymbol
            break
        case "bottom_right":
            playingFielddArray[8] = playerSymbol
            break
    }

}

/**
 *  @brief  Checks for winning conditions of TicTacToe Game
 * 
 *  @param  playerOneTurn True if Player1 / False if Player2
 * 
 *  @retval True if game is over / False if not
 */
function checkWin(playerOneTurn) {
    let playerSymbol = setPlayerTurn(playerOneTurn) 
    let gameOver = false
    
    for (let i = 0; i < 3; i++) {
        // Test horizontal fields
        let verticalFields = [i, i + 3, i + 6]
        if (verticalFields.every(index => playingFielddArray[index] === playerSymbol)) {
            gameOver = true
        }   // Should be ok
        
        // Test vertical fields
        let horizontalFields = [i*3, i*3 + 1, i*3 + 2]
        if (horizontalFields.every(index => playingFielddArray[index] === playerSymbol)) {
            gameOver = true
        }   // Should be okay
        
    }
    // Test diagonall fields
    let diagonallFieldsLeftToRight = [0, 4, 8]
    let diagonallFieldsRightToLeft = [2, 4, 6]
    
    if (diagonallFieldsRightToLeft.every(index => playingFielddArray[index] === playerSymbol)) {
        gameOver = true
    } else if (diagonallFieldsLeftToRight.every(index => playingFielddArray[index] === playerSymbol)) {
        gameOver = true
    }

    return gameOver
}

/**
 *  @brief  Display winning message and new game Button
 * 
 *  @param  gameOver True if game is over / False if not
 *  @param  playerOneTurn True if Player1 / False if Player2
 */
function wonGame(gameOver, playerOneTurn) {

    let player = setPlayerTurn(playerOneTurn)

    if (gameOver) {
        updateHeading("Player " + player + " has won!")
        
        // Display new Game Button
        if (newGameButton) {
            newGameButton.style.display = "flex"
        }
    }
}

/**
 *  @brief  Checks if the game is in a draw state
 * 
 *  @param  gameOver True if game is over / False if not
 *  @param  turnCounter Counter for how many rounds have already been played
 */
function checkDraw(gameOver, turnCounter){
    
    if (!gameOver && turnCounter == 9) {
        updateHeading("Draw! Nobody has won!")

        // Display new Game Button
        if (newGameButton) {
            newGameButton.style.display = "flex"
        }
    }

}

// Main Function
function main() {

    // Init 
    initButtonEventListeners()


}

// Calling main Function
main()
/* Settings for the whole body */
body {
    background-color: hsla(0, 2%, 12%, 1); 
    font-family: 'Arial', sans-serif;
    margin: 0;
    padding: 0;
}

/* Settings for game field buttons */
.buttons {
    width: 20vmin;
    height: 20vmin;
    background: white;
    border: 1em solid black;
    margin: 0;
    font: 0/0 transparent;
}

/* Settings for start game button */
#start_game_button {
    display: flex;
    justify-content: center;
    margin-top: 3vmin;
}

#start_game_button button{
    font-size: 2em;
    padding: 1vmin 2vmin;
    border-radius: 1vmin;
    
}

#start_game_button button:hover {
    cursor: pointer;
}

/* Settings for new game button */
#start_new_game_button {
    display: flex;
    justify-content: center;
    margin-top: 3vmin;
    display: none;
}

#start_new_game_button button {
    font-size: 2em;
    padding: 1vmin 2vmin;
    border-radius: 1vmin;
}

/* Settings for the heading */
#heading {
    text-align: center;
    color: white;
    font-size: 6em;
    margin-bottom: 20px;
}

/* Settings for the game field */
#tictactoe-field {
    width: 60vmin;
    height: 60vmin;
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: 10px;
    margin: 0 auto;
}

/* Settings for the SVG */
.game-field .cross {
    background-image: url('assets/Cross_Image.svg');
    background-size: 100%;
    background-repeat: no-repeat;
    background-position: center;
}

.game-field .circle {
    background-image: url('assets/Circle_Image.svg');
    background-size: 80%;
    background-repeat: no-repeat;
    background-position: center;
}

/* Responsive Design */
@media screen and (max-width: 600px) {
    #tictactoe-field {
        width: 90%;
        height: auto;
    }

    .buttons {
        width: 28vmin;
        height: 28vmin;
    }

    #heading {
        font-size: 3em;
    }

    #start_game_button button,
    #start_new_game_button button {
        font-size: 1.5em;
        padding: 1vmin 2vmin;
    }
}
<!DOCTYPE html>
<html lang="en">

<!---------------------- Head ---------------------->
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0", maximum-scale=1.0, user-scalable=no">
    <title>TicTacToe</title>

    <link rel="stylesheet" href="styles.css">
    
</head>

<!---------------------- Body ---------------------->
<body>
    <div id="heading">
        TicTacToe
    </div>
    
    <div id="tictactoe-field" class="game-field">
        <button id="top_left" class="buttons">top_left</button>
        <button id="top_middle" class="buttons">top_middle</button>
        <button id="top_right" class="buttons">top_right</button>
        
        <button id="middle_left" class="buttons">middle_left</button>
        <button id="middle_middle" class="buttons">middle_middle</button>
        <button id="middle_right" class="buttons">middle_right</button>
        
        <button id="bottom_left" class="buttons">bottom_left</button>
        <button id="bottom_middle" class="buttons">bottom_middle</button>
        <button id="bottom_right" class="buttons">bottom_right</button>
    </div>

    <div id="start_game_button">
        <button>Start Game</button>
    </div>
    <div id="start_new_game_button">
        <button>New Game</button>
    </div>
    
    <script src="tictactoe.js"></script>
    
</body>
</html>

\$\endgroup\$
1
  • \$\begingroup\$ How to improve my general Code Style -> Get the book Code Complete and focus on the middle third of it. \$\endgroup\$
    – radarbob
    Commented Apr 9 at 3:11

2 Answers 2

8
\$\begingroup\$

Game-Loops

You ask

"Maybe also Ideas for the "game loop" (or explanation if you are using it or not)"

Game-Loops are used for animated games (Space Invaders, Pacman, Doom, etc...). Though tic-tac-toe could be animated it is generally a turn based game with user input (button clicks) driving the game state. As such it does not need a Game-Loop.

In Javascript game loops are driven by a timer event that is synced with the display hardware to ensure (if possible) that the animation is presented to the display at a consistent frame rate (generally 60FPS but can be higher).

The event is requested once every display frame using requestAnimationFrame

Below is an example of a game loop using requestAnimationFrame

requestAnimationFrame(myGameLoop);  // Request first frame
function myGameLoop(time) {
    // update game state and render 
    requestAnimationFrame(myGameLoop);  // Request next frame
}

In most languages game loops are tied to the display hardware to ensure the best quality animation.

At the lowest level the game loop is not a loop. It is some code that is called via an hardware interrupt (event), updates and renders the game state and then exits and does nothing. The hardware (via an interrupt) will call the code at the beginning of the next display frame, the code does not loop.

Bad Code Comments

From your code looking at the comments.

/**
 *  @brief  Checks if the game is in a draw state
 * 
 *  @param  gameOver True if game is over / False if not
 *  @param  turnCounter Counter for how many rounds have already been played
 */
function checkDraw(gameOver, turnCounter) {    
    if (!gameOver && turnCounter == 9) {
        updateHeading("Draw! Nobody has won!")

        // Display new Game Button
        if (newGameButton) {
            newGameButton.style.display = "flex"
        }
    }
}

// Main Function
function main() {

    // Init 
    initButtonEventListeners()
}

// Calling main Function
main()

The comments just repeat the code and read like instructions to a programming novice // Calling main Function followed by main() Really?

Comments should add to the readability , if they just repeat what is obvious then the comment detracts from readability. It is just noise.

Also take care not to confuse your intent with badly worded comments. You have /* Checks if the game is in a draw state */ I read that as draw (as in render) better maybe as /* Checks if game is a draw */ Comments should never be ambiguous.

Generally your naming is good (a little too long in places). Good naming (self commenting code) reduces the need for comments and thus reducing code noise, improving readability and maintainability.

Remember that when you maintain code, if you have lots of comments that too must be maintained, something to avoid if possible, no?

\$\endgroup\$
1
  • \$\begingroup\$ +10 for the comments paragraph (if I could...) \$\endgroup\$ Commented Apr 9 at 16:01
7
\$\begingroup\$
  1. You don't really need a game loop in this situation because tic-tac-toe is not an animated game. If you ever wanted to use it, you should use requestAnimationFrame() method because it does not result in any flickering and also provides frame timings which you can use to calculate delta time. It is not a loop but a recursive function (function which calls itself). Here is a basic game loop with requestAnimationFrame() method:
// performance.now() provides a better timestamp.
let lastFrameTime = performance.now();
const gameLoop = (currentFrameTime) => {
    requestAnimationFrame(gameLoop);
    const deltaTime = (currentFrameTime - lastFrameTime) / 1000;
    lastFrameTime = currentFrameTime;

    // Place you code here
}
requestAnimationFrame(gameLoop); // Initializing the game loop
  1. Try to make your code shorter. You don't have to use switch statements and if conditions over and over in the code. I saw that you have used if conditions and switch statements in the function where you would have done the same thing with minimal code without matching classes instead you can get their index and used that.

You can apply same CSS to multiple elements with different ids' and classes by using a comma(,). For example:

#element1,
#element2 {
    display: flex;
}
\$\endgroup\$
1
  • 2
    \$\begingroup\$ Technically it is not "a recursive function", it uses an event to execute the function. The function gameLoop has exited, its context removed from the call stack, before it runs again. \$\endgroup\$
    – Blindman67
    Commented Apr 8 at 18:55

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.