简体   繁体   中英

How to make the function wait for an event to occur?

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> 

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.

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.

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 ;).

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(); 

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> 

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM