Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
844 views
in Technique[技术] by (71.8m points)

scope - Are these unexpected effects in my TicTacToe just Javascript timing aspects I'm unaware of?

I made a TicTacToe game that happily works. I'm trying to solve two things though.

  1. The opponent's move in "DumbAI" shows immediately after I choose mine. When I impose a setTimeout(), and the AI opponent wins, the endgame sequence does not fire. It works when I win though.

The endgame sequence is that when anyone gets 3 in a row, an alert is supposed to flash, the 3 squares that won are highlighted and the eventlistener is removed so no more marks can be made.

Instead, the code lets me swap to the active player. And if the active player gets 3 in a row, the endgame sequence fires.

All these functions are in the same block. By putting a setTimeout() on the opponent's move, is it skipping over the endgame sequence?

  1. Similarly, when I break out the endgame sequence into a separate block, another issue occurs.

When I take the endgame sequence out of the block and I win, the code will flash the alert and highlight the spaces, but it will also allow the AI opponent to make an extra move.

By taking the endgame sequence out of the block, is the computer moving too quickly through the code by allowing opponent to take his turn before firing the endgame sequence?

script.js:

var ONE_CLASS
var TWO_CLASS

const btn = document.querySelector('#PlayerOneSymbol');

btn.onclick = function () {
    const XOs = document.querySelectorAll('input[name="choice"]');
    for (const XO of XOs) {
        if (XO.checked) {
          ONE_CLASS = XO.value
          TWO_CLASS = XO.value == 'X' ? 'O' : 'X'
          break;
        }
    }
    alert("First Move Belongs to " + ONE_CLASS + ". Select Player Two.");
    };

var playerTwoIdentity
  
const btn2 = document.querySelector('#PlayerTwoChoice');
btn2.onclick = function () {
    const Opponents = document.querySelectorAll('input[name="choice2"]');
    for (const Opponent of Opponents) {
        if (Opponent.checked) {
          playerTwoIdentity = Opponent.value
          break;
        }
    }
    alert("Your Opponent is "  + playerTwoIdentity + ". Start New Game.")
    };

let playerOneTurn 
    
function swapTurns() {
  playerOneTurn = !playerOneTurn
};

const winningTrios = [
    [0, 1, 2],
    [3, 4, 5],
    [6, 7, 8],
    [0, 3, 6],
    [1, 4, 7],
    [2, 5, 8],
    [0, 4, 8],
    [6, 4, 2]
]

restartBtn.addEventListener('click', startGame);

function startGame() {
  if (ONE_CLASS == undefined || playerTwoIdentity == undefined) {return alert ("Make sure players are defined")}
  console.log("player 1 = " + ONE_CLASS + ", player 2 = " + playerTwoIdentity)
  drawBoard();
  playerOneTurn = true;
}

const arrayfromBoxes = Array.from(document.getElementsByClassName('box'));
const stylingOfBoxes = document.querySelectorAll('.box');

function drawBoard() {
  console.log(stylingOfBoxes)
  for (let i = 0; i < stylingOfBoxes.length; i++) {
  stylingOfBoxes[i].addEventListener('click', boxmarked, {once: true});}
    stylingOfBoxes.forEach(gridBox => {
    gridBox.classList.remove(ONE_CLASS)
    gridBox.classList.remove(TWO_CLASS)
    gridBox.classList.remove('winner')
    gridBox.innerHTML = ""
    })
    }

function boxmarked(e) {
    const index = arrayfromBoxes.indexOf(e.target)
// how to consolidate? maybe I just let ONE_CLASS mark and then if the AI or player
// or do it even earlier and link it with playerTurn? 
    if(playerOneTurn) {
        arrayfromBoxes[index].classList.add(ONE_CLASS)
        e.target.innerHTML = ONE_CLASS
      } else {
        arrayfromBoxes[index].classList.add(TWO_CLASS)
        e.target.innerHTML = TWO_CLASS
      }

      // if (playerhasWon()) {
      //   declareWinner()
      //   return
      // } 
      
      // if (emptySpaceRemains() == false) {
      //   declareTie()
      //   return
      // }
    hasGameEnded()
    swapTurns()

    // eliminate repetition - 
    if(playerTwoIdentity === "Dumb AI") {
      var dumbAIArray = arrayfromBoxes.reduce((dumbAIArray, box, idx) => {
        if (box.innerHTML === "") {
          dumbAIArray.push(idx);
          }
          return dumbAIArray;
        }, []);
        let dumbAIpicked = dumbAIArray[Math.floor(dumbAIArray.length * (Math.random()))]
        arrayfromBoxes[dumbAIpicked].classList.add(TWO_CLASS)
        arrayfromBoxes[dumbAIpicked].innerHTML = TWO_CLASS

// why does Timeoutfunction prevent opponent sequence?
// setTimeout(() => {arrayfromBoxes[dumbAIpicked].classList.add(TWO_CLASS)}, 500);
// setTimeout(() => {arrayfromBoxes[dumbAIpicked].innerHTML = TWO_CLASS}, 500);

// if (playerhasWon()) {
//   declareWinner()
//   return
// } 

// if (emptySpaceRemains() == false) {
//   declareTie()
//   return
// }

  hasGameEnded()
  swapTurns()
    } else { console.log("Human")
    }
}

function hasGameEnded() {
      // fix declareWinner() appears before the added classes bc alert happens quicker than redraw
      // I also cannot pull these out because then the opponent move fires and shows
      // could have something to do with timing of in-block code
      if (playerhasWon()) {
        declareWinner()
        return
      } 
      
      if (emptySpaceRemains() == false) {
        declareTie()
        return
      }
}

function checkClass() {
  if(playerOneTurn) {
  return ONE_CLASS
} else {
  return TWO_CLASS
};}

function emptySpaceRemains() {
  var innerHTMLempty = (insidebox) => insidebox.innerHTML===""
  console.log(arrayfromBoxes.some(innerHTMLempty))
  return (arrayfromBoxes.some(innerHTMLempty))
}

function declareTie() {
  setTimeout(alert ("TIE GAME"), 1000)}

function playerhasWon() {
    var indexOfSelected = arrayfromBoxes.reduce((indexOfSelected, box, idx) => {
        if (box.classList[1] === checkClass()) {
            indexOfSelected.push(idx);
        }
        return indexOfSelected;
    }, []);

  const winningThreeIndexes = winningTrios
  .map(trio => trio.filter(i => indexOfSelected.includes(i)))
  .filter(i => i.length === 3);

  console.log(winningThreeIndexes)
  console.log(winningThreeIndexes.length)

  if (winningThreeIndexes.length === 1) {winningThreeIndexes[0].map((index) => {arrayfromBoxes[index].className += ' winner'})}
 
   var isThereAWinner = 
    winningTrios.some(trio => {return trio.every(i => indexOfSelected.includes(i))});
       console.log({isThereAWinner});
   return isThereAWinner
      }

function declareWinner() {
  setTimeout(alert (checkClass() + " WINS"), 1000);
  for (let i=0; i < stylingOfBoxes.length; i++) {
    stylingOfBoxes[i].removeEventListener('click', boxmarked, {once: true});}
}

index.html:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Tic Tac Toe</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
<div class="container">
        <h1 id="playtext">Let's Play</h1>

    <div class="radioContainer">
        <div id="playerOne">
        <h3>Player One</h3>
        <form>
            <input type="radio" name="choice" value="X"> X<br>
            <input type="radio" name="choice" value="O"> O<br>
            <input type="button" id="PlayerOneSymbol" value="Confirm">
        </form>
        </div>

        <div id="playerTwo">
        <h3>Player Two</h3>
        <form>
            <input type="radio" name="choice2" value="Human"> Human<br>
            <input type="radio" name="choice2" value="Dumb AI"> Dumb AI<br>
            <input type="radio" name="choice2" value="Smart AI"> Smart AI<br>
            <input type="button" id="PlayerTwoChoice" value="Confirm">
        </form>
        </div>
    </div>

    <div class="buttonHolder">
        <div class="buttonWrapper">
    <button id="restartBtn">Start New Game</button>
        </div>
    </div>

    <div class="gameboard">
        <div class="box" ></div>
        <div class="box" ></div>
        <div class="box" ></div>
        <div class="box" ></div>
        <div class="box" ></div>
        <div class="box" ></div>
        <div class="box" ></div>
        <div class="box" ></div>
        <div class="box" ></div>
    </div>
</div>
    <script src="script.js"></script>
</body>
</html> 

style.css:

* {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

body {
  display: flex;
  justify-content: center;
}

#playtext {
  text-align: center;
  padding: 10px;
}

.buttonHolder {
  height: 60px;
  width: 100%;
  float: left;
  position: relative;
  background-color: purple;
}

.buttonWrapper {
position: absolute;
top: 50%;
left: 50%; 
-ms-transform: translate(-50%, -50%);
transform: translate(-50%, -50%);
}

.container {
  background-color: purple;
  justify-content: center;
  /* display: flex;
  flex-wrap: wrap; */
  width: 400px;
  height: 600px;
}

#gameboard {
border-top:10px;
border-bottom: 4px;
border-bottom-color: black;
background-color: chartreuse;
}

.box {
  background-color: yellow;
  width: 125px;
  height: 125px;
  float: left;
  width: 33.33%;
}

button:hover {
  cursor: pointer;
  transform: translateY(-2px);
}


.winner {
  background-color: black;
}

.X {
  content: 'X';
  font-size: 135px;

}

.O {
  content: 'O';
  font-size: 135px;
}

#spacer {
height: 10px;
width: 100%;
background-color: purple;
padding: 10px;
}

#playerOne {
background-color: blanchedalmond;
padding: 5px;
height: 110px;
float: left;
width: 50%;
}

#playerTwo {
background-color: mintcream;
padding: 5px;
height: 110px;
float: left;
width: 50%;
}
question from:https://stackoverflow.com/questions/65860987/are-these-unexpected-effects-in-my-tictactoe-just-javascript-timing-aspects-im

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

A friend helped me figure out what's happening in #1 -- I think. He points out that JS is asynchronous. I had three functions:

  1. Opponent puts down marker in a space.
  2. Board is evaluated to see if opponent won.
  3. If not, it switches turns and lets player pick a space.
  • If so, it ends the game and prevents picking a space

I was hoping when (1) was delayed, (2) and (3) wouldn't fire until (1) did.

But in reality (1) is delayed, so (2) runs anyway and doesn't see the opponent win and so (3) lets player pick a space.

So to fix this, I put the timeout on all 3 functions:

  setTimeout(() => {
        let dumbAIpicked = dumbAIArray[Math.floor(dumbAIArray.length * (Math.random()))]
        arrayfromBoxes[dumbAIpicked].classList.add(TWO_CLASS)
        arrayfromBoxes[dumbAIpicked].innerHTML = TWO_CLASS

        if (playerhasWon()) {
        declareWinner()
        return
        } 
        if (emptySpaceRemains() == false) {
        declareTie()
        return
        }
        // hasGameEnded()
        swapTurns()
``}, 500);

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...