Frontend - Design Classic Tic Tac Toe ( N * N grid )
Anonymous User
915

Design classic tic tac toe game.
There are two players.Player 1 plays with 'X' , Player 2 plays with 'O'.
Player will win if able to fill in every cell in a row or a column or left/right diagonally.
If no one wins,but all the cells are marked,game is over.
-Update Player's turn
-Check if its a valid move ( for e.g once a cell is marked cant be changed or once the game is over)
-Find the game's status / winner

Here is my solution

HTML

<div id="status" data-status="start" style="color:blue;"></div>
<p>PLAYER 1 plays with  X</p><p>PLAYER 2 plays with O</p>
<button 
 class="btnPlayAgain" id="btnPlayAgain" type="button">Play Again</button>

<table id="board" role="grid" class="board"></table>

<div>Now its turn for:</div>
<div id="player"></div>

CSS

.board {
  height:200px;
  width:500px;
  table-layout: fixed;
  overflow:hidden;
}


.cell{
    border: 1px solid grey;
    background:green;
    color:yellow;
    text-align:center;
}
.btnPlayAgain{
  display:none;
}
.show{
  display:block;
}

Javascript

class TicTacToeGame{
  constructor(n,m){
    //App state
    this.rows=n; // number of rows in the Grid
    this.columns=m;// number of columns in the Grid
    this.board=new Array(this.rows).fill(''); // only the rows, columns will be added later
    
    this.playersMark=new Map([[0,'X'],[1,'O']]) // 1st player can use mark X and 2nd player can use mark O
    this.currentPlayer=0; // this denotes current player's turn.By Default it is first player.
    this.playersMoveHistory=new Map([[0,[]],[1,[]]]); // this keeps track of player's move history.and this also helps in deciding the game status
    this.status={ // all the possible statuses
        "start":"Game is about to begin!!",
        "pending":"Game in progress.....",
        "gameover":"Game Over! It's a Draw",
        "0":"Player 1 Won !!!",
        "1":"Player 2 Won !!!"
    };
    //All the possible right diagonal winning combinations - for the n*m grid
    this.rightDiagonals=new Map();
    for(let i=0,j=this.columns-1; i<this.rows,j>=0; i++,j--){
      this.rightDiagonals.set(i,j);
    }
    
    // App View
    this.boardContainer=document.querySelector('#board');
    this.playerContainer=document.querySelector('#player');
    this.statusContainer=document.querySelector('#status');
    this.btnPlayagain=document.querySelector('#btnPlayAgain');
    
  }
  
  // on initialization of the app
  init(){
    this.updateGameStatusView('start'); // shows the status of the game - which is "start" in the very beginning
    this.updatePlayersTurnView();//shows the current player
    
    this.showBoard(); // builds the columns in the grid, responsible for showing the tictactoe board
    this.bindEventListener();// adds event to the board (event delegation) - each of the cells are clickable if a valid move
  }
 
 
  // displays the board view 
   
  // creates board's row
  createRow(columns=[],rowIdx){
     if(columns.length==0) return;
    
    return `<tr role="row" class="row">${columns.map((col,colIdx)=>{
         return this.createColumn(rowIdx,colIdx,col);
    }).join('')}</tr>`;
  }
  
  // creates board's column
  createColumn(rowIdx,colIdx,content=''){
    return `<td data-row=${rowIdx} data-col=${colIdx} role="cell" class="cell">${content}</td>`;
  }
  
  getBoard(){
   return this.board.map((row,rowIdx)=>{
       const columnsArr=new Array(this.columns).fill('');
       return this.createRow(columnsArr,rowIdx);
    }).join('')
  }
  
  showBoard(){
     this.boardContainer.insertAdjacentHTML('beforeend',this.getBoard());
  }
 
  
  // play again, event handler
  handlePlayAgain=(e)=>{
      const cells=[...this.boardContainer.querySelectorAll('.cell')];
    
      this.boardContainer.addEventListener('click',this.handleMove);
    
      this.currentPlayer=0;
      this.updatePlayersTurnView();
    
      this.updateGameStatusView('start');
    
      cells.forEach(cell=> cell.innerText='');
    
      this.btnPlayagain.classList.remove('show');
  }
  
  // on player's move, click event handler
  handleMove=(e)=>{
       const { target }=e;
       if(target.classList.contains('cell')){
         
         // first check if it is a valid move
          if(target.innerText!==''){ // (if already marked donot mark again)
            return;
          }
          
         // update the current targetted cell's view with the current player's mark ( X or O )
          target.innerText=this.playersMark.get(this.currentPlayer);
         
         // keep track of current player's move in the playersMoveHistory map ( this will help us to find the winner)
          this.playersMoveHistory.set(this.currentPlayer,[...this.playersMoveHistory.get(this.currentPlayer),[parseInt(target.getAttribute('data-row')),parseInt(target.getAttribute('data-col'))]]);
         
          // after every valid move lets check if we already found our winner
          this.findWinner();
         
          // if nobody won / game is still not over its still in progress , so update the player's turn
          if(this.statusContainer.getAttribute('data-status')==='pending'){
             this.updateTurn();
             this.updatePlayersTurnView();
          }
         
       }
    };
  
  // event listeners for the board cells and the play again button
  bindEventListener(){
    this.btnPlayagain.addEventListener('click',this.handlePlayAgain);
    this.boardContainer.addEventListener('click',this.handleMove);
  }
  
  
  
    // after the current game ends
  endGame(cells){
    this.boardContainer.removeEventListener('click',this.handleMove); // since the current game is over , player is not allowed to make a move until play agin is clicked
    // reset playersMoveHistory
    this.playersMoveHistory=new Map([[0,[]],[1,[]]]);
    // show the play again button
    this.btnPlayagain.classList.add('show');
    // players turn over for the current game
    this.currentPlayer=null;
    this.updatePlayersTurnView();
  }
  
  // main logic to check if
  //1.game over / game is a draw 
  //2.game is still pending / inprogress
  // 3. either of the player has won the game - 
 // a. by row 
  //b.by column
  //c.by left/right diagonals
  
  findWinner(){
    const cells=[...this.boardContainer.querySelectorAll('.cell')];
   
    //  player 0 or 1 Won
     const currentPlayersMoves=this.playersMoveHistory.get(this.currentPlayer);
          let allrows={};
          let allcols={};
          currentPlayersMoves.forEach(([row,col])=> {
                allrows[row]=(allrows[row] || 0)+1;
                allcols[col]=(allcols[col] || 0)+1;
          })
    
        
       
        if(Object.values(allrows).includes(this.rows) || Object.values(allcols).includes(this.columns)){
             this.updateGameStatusView(this.currentPlayer.toString());
             this.endGame(cells);
             return;
        }
    
    //left diagonals
    
    if(this.rows===currentPlayersMoves.filter(([row,col])=>{
      return (row===col);
    }).length){
          this.updateGameStatusView(this.currentPlayer.toString());
          this.endGame(cells);
          return;
    }
    
    //Right Diagonals
    let count=0;
   
    currentPlayersMoves.forEach(([row,col])=>{
       if(this.rightDiagonals.has(row) && this.rightDiagonals.get(row)===col){
         count++;
       }
    });
    
  
    if(count===this.rows)
    {
          this.updateGameStatusView(this.currentPlayer.toString());
          this.endGame(cells);
          return;
    }
       
       // Game Over
      const isAllMarked=cells.every(cell=> cell.innerText!=='');

       if(isAllMarked){
         this.updateGameStatusView('gameover');
         this.endGame(cells);
         return;
       }

    
      // Pending
       this.updateGameStatusView('pending');
  }
  
  // updates the current player's state
  updateTurn(){
    this.currentPlayer=this.currentPlayer===0 ? 1 : 0;
  }
  
  // display's the current player's turn
  updatePlayersTurnView(){
     if(this.currentPlayer===null){
        this.playerContainer.innerText='--The End---';
        return;
     }
     this.playerContainer.innerText=this.currentPlayer===0 ? 'Player 1':'Player 2';
  }
  
  // display's the current status in the top bar
  updateGameStatusView(statusText='pending'){
      this.statusContainer.innerText=this.status[statusText];
      this.statusContainer.setAttribute('data-status',statusText);
  }
 
}

// create an instance of tictactoe and call init function
const game=new TicTacToeGame(4,4);
window.addEventListener('DOMContentLoaded',()=>{
    game.init();
});
Comments (2)