I would go with the strategy pattern.
class Player {
async getNextMove() {
throw new Error('not implemented');
};
}
class AiPlayer extends Player {
async getNextMove() {
/* Your AI LOGIC*/
return 0;
};
}
class HumanPlayer extends Player {
async getNextMove() {
await /*deal with user input*/
};
}
// gameLogic:
let playerOne = new AiPlayer();
let playerTwo = new HumanPlayer();
let players = [playerOne, playerTwo];
let currentPlayer = 0;
let gameIsRuning = true;
while (gameIsRuning) {
let playerMove = await playerOneplayers[currentPlayer].getNextMove();
await// playerTwo.getNextMove();validate the input
// recalculate the game state
// display board if not headless
if (/*function to check game is over*/) {
gameIsRuning = false;
}
currentPlayer = (currentPlayer++) % 2;
}
In that case waiting for player inputs is blocks the loop, ai is not.