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\$ Commented Apr 9, 2024 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\$
0
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\$ Commented Apr 8, 2024 at 18:55

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.