1

I'm making a tic-tac-toe game, my problem is that the game function doesn't wait for the user to choice where he want to play his move, it just run the (gameOver) function immediatly after I press start game button.

can anyone tell me what is wrong with my codes and help me to fix it ?

```
const start = document.getElementById('start');
const table = document.getElementById('table');
places = ["one", "two", "three", "four", "five", "six", "seven", 'eight', "nine"];
let move = 0;
start.addEventListener('click', function(){
startGame();
});

function startGame(){
console.log("Game started");
user();
}

function gameOver(){
console.log("Game Over");
}

function computer(){
let index = places[Math.floor(Math.random() * places.length)];
let pos = document.getElementById(index);
if (/[1-9]/.test(pos.innerHTML)){
    pos.innerHTML = "O";
    move += 1;
    places.splice(places.indexOf(pos), 1 );
}
if (move > 8){
    gameOver();
} else {
    user();
}
}

function user(){
table.addEventListener('click', function(event){
    let pos = event.target;
    let Id = event.target.id;
    if (/[1-9]/.test(pos.innerHTML)){
        pos.innerHTML = "X";
        move += 1;
        places.splice(places.indexOf(Id), 1 );
    }
    if (move > 8){
        gameOver();
    } else {
        computer();
    }
});
}
```
<div class="col text-center">
            <table class="table text-center">
                <tbody id="table">
                    <tr>
                        <td id="one">1</td>
                        <td id="two">2</td>
                        <td id="three">3</td>
                    </tr>
                    <tr>
                        <td id="four">4</td>
                        <td id="five">5</td>
                        <td id="six">6</td>
                    </tr>
                    <tr>
                        <td id="seven">7</td>
                        <td id="eight">8</td>
                        <td id="nine">9</td>
                    </tr>
                </tbody>
            </table>
            <br />
            <button class="btn btn-primary" type="button" id="start">Start Game</button>
        </div>
  • You can't, if a function is running, all the events are blocked to fire. – Teemu Jan 25 '19 at 13:05
  • You are trying to mix async and sync. In `user()` you are just adding an event listener and the function returns immediately. Thats why you are seeing the GameOver right away. You need another approach ... maybe try it with `window.setTimeout` and check the current game status in there – seasick Jan 25 '19 at 13:09
  • @seasick I did not understand what you mean, please edit the code for me if you can. –  Jan 25 '19 at 13:11
  • See https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/setTimeout – seasick Jan 25 '19 at 13:13
  • where should i put this ? –  Jan 25 '19 at 13:14
  • you should change your whole code ... see also ZA_ITAs answer – seasick Jan 25 '19 at 13:15
  • @seasick `you should change your whole code` Not really, using `async / await` you could keep the logic pretty much as is. – Keith Jan 25 '19 at 13:17
  • 1
    There's a working async await sample below but instead the chosen answer gives a vague explanation of why binding multiple click handlers is not a good idea. I'm out for the night. – BlueWater86 Jan 25 '19 at 13:24
  • @BlueWater86 Yes, `async / await` is the way I would go, btw. Your snippet errors for me. Also the amount of changes required to make it work `async / await` is a lot less. I'll post a version, for you to see what I mean. – Keith Jan 25 '19 at 13:34
  • You're in a legacy browser? – BlueWater86 Jan 25 '19 at 13:36
  • @BlueWater86 `You're in a legacy browser?` no, latest Chrome. I've done a working snippet below that's working fine. – Keith Jan 25 '19 at 13:42
  • Tomatos, tomatos. – BlueWater86 Jan 25 '19 at 13:46

5 Answers5

1

The main problem is that you made a while loop to run the game and you have an event "on player click" that is asyncronous and doesn't pause the game.

The preferred way is to make the game asyncronous at all, without using the while loop, and check the move counter every time computer or player make a move.

1) On start set the move count to 0

2) On player click increase the move count and eventually run the computer move or the "game over" function if move_count >8

Note1: remember to increase move count and check the end also when computer move.

Note2: using this solution the player will move always first.

ZA_ITA
  • 51
  • 4
1

Let's analyze your user() function:

const table = document.getElementById('table');
...
function user(){
    table.addEventListener('click', function(event){
        let pos = event.target;
        if (/[1-9]/.test(pos.innerHTML)){
            pos.innerHTML = "X";
            player = true;
        }
    });
}

The catch here is that JavaScript is a highly Asynchronous language. The addEventListener function, when executed, adds the event listener and then returns, which means the user() function has completed. This listener, then, will trigger the corresponding function with every click, it will not stop the code and wait for a click input. Then, since your code is within a while and the user function is executed completely (remember, it has only one statement that is addEventListener), the code finishes rather quickly.

To solve it, call addEventListener at the beginning of the start function, then place the corresponding logic inside the corresponding click function. This way, your code will be executed only when the user clicks, you can call the computers move or the gameOver function from there.

Daniel Turcich
  • 1,764
  • 2
  • 26
  • 48
Danilo Filippo
  • 762
  • 1
  • 6
  • 12
1

Another way is to create the game as iterator which can be paused and wait for the user input.

var game; //variable to reference the currently running game

function* newGame() { //note the '*' which creates an iterator
    while (i <= 9){
        if (player === true) {
            computer();
        } else {
            user();
            yield i; //pause the method (must return a value)
        }
        i++;
    }
    gameOver();
}

function startGame(){
    game = newGame(); //start the game by creating new iterator
    game.next(); //perform first step
}

function user(){
    table.addEventListener('click', function(event){
        let pos = event.target;
        if (/[1-9]/.test(pos.innerHTML)){
            pos.innerHTML = "X";
            player = true;
            game.next(); //continue with the game...
        }
    });
}

Note that the way it's written now you will assign 4 different click handlers. You should also call removeEventListener() because the listener is not automatically cleared after it's called! But you will find out once the game start working ;).

Radek Pech
  • 3,032
  • 1
  • 24
  • 29
0

Thanks to some mentoring from @Keith, I'm editing this answer.

User input to the browser is asynchronous by nature. Maybe some day we will have an API for awaiting events but until then we are left monkey patching with Promises.

A pattern that has been useful on several projects is the resolving of a Promise outside of it's scope. In this pattern, the resolver resembles a deferred. This pattern allows shipping of the resolver into a unit of work other than the promise constructor. Here is a comparison of the two ways that you can asynchronously await a DOM event:

(async () => {
  const button1 = document.createElement("button")
  button1.innerText = "Resolve it out of scope"
  document.body.appendChild(button1)
  const button2 = document.createElement("button")
  button2.innerText = "Resolve it in scope"
  document.body.appendChild(button2)
  
  const remove = button => button.parentNode.removeChild(button);
  
  const asyncResolveOutScope = async () => {
    let resolver
    button1.onclick = () => {
      remove(button1)
      resolver()
    }
    return new Promise(resolve => resolver = resolve)
  }

  const asyncResolveInScope = async () => {
    return new Promise(resolve => {
      button2.onclick = () => {
        remove(button2)
        resolve()
      }
    })
  }

  console.log("Click the buttons, I'm waiting")
  await Promise.all([asyncResolveOutScope(), asyncResolveInScope()])
  
  console.log("You did it")
})()

I'm not sure of how helpful it will be for you but here is an example of a working tic-tac-toe game that uses the shipped (resolve out of scope) Promise pattern:

class game {
  constructor(name, order=["Computer", "User"]) {
    this.userTurnResolver
    this.name = name
    this.players = [
      {name: "Computer", turn: async() => {
        const cells = this.unusedCells
        return cells[Math.floor(Math.random() * cells.length)]
      }},
      {name: "User", turn: async() => new Promise(resolve => this.userTurnResolver = resolve)}
    ].sort((a, b) => order.indexOf(a.name) - order.indexOf(b.name))
    this.players[0].symbol = "X"
    this.players[1].symbol = "O"
  }
  log(...args) {
    console.log(`${this.name}: `, ...args)
  }
  get cells() {
    return Array.from(this.table.querySelectorAll("td")).map(td => td.textContent)
  }
  get unusedCells() {
    return Array.from(this.table.querySelectorAll("td")).filter(td => !isNaN(td.textContent)).map(td => td.textContent)
  }
  userClick(e) {
    const cell = isNaN(e.target.textContent) ? false : parseInt(e.target.textContent)
    if (cell && this.userTurnResolver) this.userTurnResolver(cell)
  }
  render() {
    //This would usually be done with HyperHTML. No need for manual DOM manipulation or event bindings.
    const container = document.createElement("div")
    container.textContent = this.name
    document.body.appendChild(container)
    this.table = document.createElement("table")
    this.table.innerHTML = "<tbody><tr><td>1</td><td>2</td><td>3</td></tr><tr><td>4</td><td>5</td><td>6</td></tr><tr><td>7</td><td>8</td><td>9</td></tr></tbody>"
    this.table.onclick = e => this.userClick(e)
    container.appendChild(this.table)
  }
  async start() {
    this.render()
    this.log("Game has started")

    const wins = [
      {desc: "First Row", cells: [0, 1, 2]}, {desc: "Second Row", cells: [3, 4, 5]},
      {desc: "Third Row", cells: [6, 7, 8]}, {desc: "Diagonal", cells: [0, 4, 8]},
      {desc: "Diagonal", cells: [2, 4, 6]}, {desc: "First Column", cells: [0, 3, 6]},
      {desc: "Second Column", cells: [1, 4, 7]}, {desc: "Third Column", cells: [2, 5, 8]}
    ]
    const checkWin = symbol => wins.find(win => win.cells.every(i => this.cells[i] === symbol))

    const takeTurn = async ({name, turn, symbol}, round, i) => {
      this.log(`It is ${name}'s turn (round ${round}, turn ${i})`)
      const choice = await turn()
      this.log(`${name} had their turn and chose ${choice}`)
      this.table.querySelectorAll("td")[choice-1].textContent = symbol
      return checkWin(symbol)
    }

    let win, round = 0, i = 0
    while (!win && i < 9) {
      round++
      for (let player of this.players) {
        i++
        win = await takeTurn(player, round, i)
        if (win) {
          this.log(`${player.name} is the winner with ${win.desc}`)
          break
        }
        if (i===9) {
          this.log(`We have a stalemate`)
          break
        }
      }
    }
    this.log("Game over")
  }
}

(new game("Game 1", ["User", "Computer"])).start();
(new game("Game 2")).start();
(new game("Game 3")).start();
BlueWater86
  • 1,773
  • 12
  • 22
  • Using `async / await`, is great. But some advice here. Try and avoid using a deferred, it's a promise anti-pattern, especially if used like this. By doing this you have also locked in the ordering of players, eg. If you now wanted the user to go first, and then computer you will now need to change more code. btw. The error I got was not clicking `start game` first. – Keith Jan 25 '19 at 14:11
  • There's no more of a deferred antipattern here than there is in your answer Keith. – BlueWater86 Jan 25 '19 at 14:54
  • https://stackoverflow.com/questions/26150232/resolve-javascript-promise-outside-function-scope – BlueWater86 Jan 25 '19 at 15:00
  • There is no deferred in my code. Your `userPromise` is basically creating a defer. There are good reasons why Deferred has been made obsolete. https://developer.mozilla.org/en-US/docs/Mozilla/JavaScript_code_modules/Promise.jsm/Deferred – Keith Jan 25 '19 at 15:46
  • Lol, did you have a look through the answer provided? There’s consideration to officially add this pattern for ES6 promise. Your “mentoring” is unnecessary. – BlueWater86 Jan 25 '19 at 22:38
  • I'm not really sure what your referring too in the link I posted. Rather than me explain what your doing wrong, here is a link -> https://github.com/petkaantonov/bluebird/wiki/Promise-Anti-patterns btw. Did you really do LOL, if the 86 is your DOB, it's a bit of a surprising response more akin to a 13/14 year old. – Keith Jan 25 '19 at 22:52
  • The document on promise antipatterns is great. Making non-thenable code awaitable by using a Promise resolved outside of scope or using a deferred is not an antipatern. – BlueWater86 Jan 25 '19 at 22:59
0

Javascript is a single threaded runtime, as such a lot of things you do are known as asynchronous, things like getting mouse click's, doing an ajax / fetch request. All these things do not block the Javascript thread, but instead use callback etc.

Luckily, modern JS engines have a feature called async / await, what this basically does it make it so asynchronous code looks synchronous.

Below is your code slightly modified, the main change was to make your user function into a promise, so that it can be awaited.

btw. There are other bugs with this, like clicking on a cell that's already been used, but I've left that out here, so it's easy for people to see what changes I made to get your script running.

const start = document.getElementById('start');
const table = document.getElementById('table');
places = ["one", "two", "three", "four", "five", "six", "seven", 'eight', "nine"];
let i = 1;
let player = true;
start.addEventListener('click', function(){
    startGame();
});

async function startGame(){
    while (i <= 9){
        if (player === true) {
            computer();
        } else {
            await user();
        }
        i++;
    }
    gameOver();
}

function gameOver(){
    console.log("Game Over");
}

function computer(){
    let index = places[Math.floor(Math.random() * places.length)];
    let pos = document.getElementById(index);
    if (/[1-9]/.test(pos.innerHTML)){
        pos.innerHTML = "O";
        player = false;
    }
}

function user(){
  return new Promise((resolve) => {
    function evClick(event) {
      table.removeEventListener('click', evClick);
      let pos = event.target;
      if (/[1-9]/.test(pos.innerHTML)){
          pos.innerHTML = "X";
          player = true;
      }
      resolve();
    }
    table.addEventListener('click', evClick)
  });
}
td {
  border: 1px solid black;
  margin: 15px;
  padding: 15px;
}
<div class="col text-center">
            <table class="table text-center">
                <tbody id="table">
                    <tr>
                        <td id="one">1</td>
                        <td id="two">2</td>
                        <td id="three">3</td>
                    </tr>
                    <tr>
                        <td id="four">4</td>
                        <td id="five">5</td>
                        <td id="six">6</td>
                    </tr>
                    <tr>
                        <td id="seven">7</td>
                        <td id="eight">8</td>
                        <td id="nine">9</td>
                    </tr>
                </tbody>
            </table>
            <br />
            <button class="btn btn-primary" type="button" id="start">Start Game</button>
        </div>
Keith
  • 22,005
  • 2
  • 27
  • 44
  • Hi Keith, Your use of the ES6 Promise is great but just a bit of advice. You can avoid needing to add and remove that event listener by moving it out into main function body. Seems a bit silly to add it and remove it again on each click. You can achieve this by resolving your Promise outside of its scope. – BlueWater86 Jan 25 '19 at 15:13
  • @BlueWater86 No, it's best to keep as is. It's called separation of concerns. In the long run it makes much more scale-able / reusable code. IOW: `function user()` is self contained, requires no external dependencies. And adding / removing event listeners has little / if any performance hit. It's a win / win. :) – Keith Jan 25 '19 at 15:52
  • @BlueWater86 Just FYI, it was me who up-voted your comment `There's a working async await sample`. This was because I believed the `async / await` route was the correct one. Unfortunately the advice I then gave, seems to have insulted you, as your copy/paste parody seems to imply. I'd rather not get into an argument about what's best. If your happy the way your using promises & works for you, then that's great. I'll stick to my way, you stick to yours. Everyone's happy. :) – Keith Jan 25 '19 at 16:18
  • Well my code has less flaws than yours, so I'm happy. eg. Click on the board in your snippet before you click the start, and watch the error appear. Next make your code have the player take the turn first. ps. The flaw in my code allows me to pretty much do -> `await user(); computer()` Like I say, if your happy using defer, it's fine by me. Not really sure why your so angry, I was with you on the accepted answer bit at the beginning, but since then you seem to have gone really defensive. – Keith Jan 25 '19 at 23:00
  • Not defensive at all. Overall the code quality above is not here nor there. Your suggestion of an antipattern is incorrect. – BlueWater86 Jan 25 '19 at 23:05
  • I'm not going to argue with you about this, if you don't believe defer is an anti-pattern, take that up with the Promise spec people, seen as defer is not a standard, you might be able to convince them there wrong about it. Personally I'd rather they concentrate on promise cancellation, than bringing in the use of buggy deferables. (As proven by your snippet).. But that's just me. – Keith Jan 25 '19 at 23:10
  • The deferred antipatern: >creating deferred object when you already have a promise or thenable or >manually wrap a callback API and finally >So when should deferred be used? ...when wrapping a callback API that doesn't follow the standard convention. Like setTimeout. In this case our callback API is the DOM and a click event. The snippet may be able to be improved and probably would have had you not insisted that there was an antipattern. – BlueWater86 Jan 26 '19 at 02:25
  • @BlueWater86 Quote: `So when should deferred be used? Well simply, when you have to.` As proven by my snippet, it certainly wasn't required. Also setTimeout is a bad example, as that doesn't require a defer either. `The snippet may be able to be improved` I'm not stopping you. – Keith Jan 26 '19 at 02:33
  • I get it Keith you've been contributing on StackOverflow for longer than I have. You are the god of Promises. You can not be reasoned with. – BlueWater86 Jan 26 '19 at 02:35
  • @BlueWater86 At the begging of these comments I said -> `If your happy the way your using promises & works for you, then that's great. I'll stick to my way, you stick to yours. Everyone's happy. :) ` And yet your implying I'm the one not to be reasoned with. Or is a person who is to be reasoned with have to agree with you? – Keith Jan 26 '19 at 02:53