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:
- How to improve my general Code Style
- 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>