interface PlayerToken { x : string ; o : string ; [ key ] : string ; } interface Score { x : number ; o : number ; [ key ] : number ; } interface Display { bindHandler ( clickHandler ) : void ; createElement ( tag , className ? , dataset ? ) : HTMLElement ; getElement ( selector ) : HTMLElement ; getAllElements ( selector ) : NodeList ; printGameBoard ( boardData ) : void ; updateBoard ( row , col , currentPlayer ) : void ; clearGameBoard ( ) : void ; printScoreBoard ( scoreData ) : void ; updateScore ( currentScore , currentPlayer ) : void ; printMessage ( winner ? ) : void ; clearMessage ( ) : void ; } class DOMDisplay implements Display { param { requestCallback } clickHandler bindHandler ( clickHandler ) { document . addEventListener ( 'click' , ( event ) => { const clicked = < HTMLElement > event . target ; const isColumn = clicked . className === 'col' ; if ( isColumn ) { const cell = clicked ; const row = + cell . parentElement ! . dataset . row ! ; const col = + cell . dataset . col ! ; clickHandler ( row , col ) ; } } ) ; } param { string } tag param { string } className param { Object [ ] } dataset return { HTMLElement } createElement = ( tag , className ? , dataset ? ) : HTMLElement => { const element = document . createElement ( tag ) ; if ( className ) element . classList . add ( className ) ; if ( dataset ) element . dataset [ dataset [ 0 ] ] = dataset [ 1 ] ; return element ; } ; param { string } selector return { HTMLElement } getElement = ( selector ) : HTMLElement => < HTMLElement > document . querySelector ( selector ) ; param { string } selector return { NodeList } getAllElements = ( selector ) : NodeList => < NodeList > document . querySelectorAll ( selector ) ; param { Object [ ] } boardData printGameBoard = ( boardData ) : void => { const game = this . getElement ( '#game' ) ; const gameBoard = this . createElement ( 'div' , 'board' , undefined ) ; game . append ( gameBoard ) ; boardData . forEach ( ( row , i ) => { const boardRow = this . createElement ( 'div' , 'row' , [ 'row' , i ] ) ; gameBoard . append ( boardRow ) ; row . forEach ( ( col , j ) => { const boardCol = this . createElement ( 'div' , 'col' , [ 'col' , j ] ) ; boardRow . append ( boardCol ) ; } ) ; } ) ; } ; param { number } row param { number } col param { string } currentPlayer updateBoard = ( row , col , currentPlayer ) : void => { const playerToken = this . createElement ( 'span' , currentPlayer , undefined ) ; playerToken . textContent = currentPlayer ; const boardRow = this . getElement ( `[data-row="${ row }"]` ) ; const cell = < HTMLElement > boardRow . querySelector ( `[data-col="${ col }"]` ) ; cell . append ( playerToken ) ; } ; /**
     * Set all cells in the board to empty strings
     */ clearGameBoard = ( ) : void => { const cells = this . getAllElements ( '.col' ) ; cells . forEach ( cell => { cell . textContent = '' ; } ) ; } ; param { Score } scoreData printScoreBoard = ( scoreData ) : void => { const game = this . getElement ( '#game' ) ; const scoreBoard = this . createElement ( 'div' , 'score' ) ; game . append ( scoreBoard ) ; const playerOneScore = this . createElement ( 'div' , 'x' ) ; playerOneScore . textContent = `Player 1: ${ scoreData . x }` ; playerOneScore . id = 'score-x' ; const playerTwoScore = this . createElement ( 'div' , 'o' ) ; playerTwoScore . textContent = `Player 2: ${ scoreData . o }` ; playerTwoScore . id = 'score-o' ; scoreBoard . append ( playerOneScore , playerTwoScore ) ; } ; param { Score } currentScore param { string } currentPlayer updateScore = ( currentScore , currentPlayer ) : void => { const currentPlayerScore = this . getElement ( `#score-${ currentPlayer }` ) ; const player = currentPlayer === 'x' ? 'Player 1' : 'Player 2' ; const d = currentScore [ currentPlayer ] ; currentPlayerScore . textContent = `${ player }: ${ d }` ; } ; param { string } winner printMessage = ( winner ) : void => { const message = this . createElement ( 'div' , 'message' ) ; const player = winner === 'x' ? 'Player 1' : 'Player 2' ; message . textContent = winner ? `${ player } wins!` : 'Nobody wins!' ; const game = this . getElement ( '#game' ) ; game . append ( message ) ; } ; /**
     * Clear message from the screen
     */ clearMessage = ( ) : void => { const message = this . getElement ( '.message' ) ; message . remove ( ) ; } ; } class TicTacToe { param { Display } display display : Display ; board : Array < Array < string > > ; players : PlayerToken ; wait : number ; waiting : boolean ; score : Score ; currentPlayer : string ; constructor ( display ) { this . display = display ; this . board = this . createBoard ( ) ; this . players = { x : 'x' , o : 'o' } ; this . wait = 1500 ; this . waiting = false ; this . score = { x : 0 , o : 0 } ; this . currentPlayer = this . players . x ; this . display . bindHandler ( this . clickCell ) ; } /**
     * Click a cell in the game board and determine if its a win, a stalemate, or
     * the game continues. Game over or switch player.
     */ clickCell = ( row , col ) => { const canContinue = this . board [ row ] [ col ] === '' ; if ( canContinue && ! this . waiting ) { this . board [ row ] [ col ] = this . currentPlayer ; this . display . updateBoard ( row , col , this . currentPlayer ) ; const win = this . isGameWon ( row , col ) ; const stalemate = this . board . map ( row => row . filter ( col => col === '' ) ) . filter ( row => row . length > 0 ) ; if ( ! this . waiting ) { if ( win ) { this . increaseScore ( ) ; this . display . updateScore ( this . score , this . currentPlayer ) ; this . gameOver ( this . currentPlayer ) ; } else if ( stalemate . length < 1 ) { this . gameOver ( ) ; } else { this . switchPlayer ( ) ; } } } } ; /**
     * Reset the board after a delay after win or stalemate
     */ gameOver = ( winner ? ) => { this . waiting = true ; this . display . printMessage ( winner ) ; setTimeout ( ( ) => { this . resetBoard ( ) ; this . waiting = false ; } , this . wait ) ; } ; return { Object [ ] } createBoard = ( ) : Array < Array < string > > => [ [ '' , '' , '' ] , [ '' , '' , '' ] , [ '' , '' , '' ] ] ; /**
     * Restore the board to its original empty state
     */ resetBoard = ( ) : void => { this . display . clearMessage ( ) ; this . display . clearGameBoard ( ) ; this . board = this . createBoard ( ) ; } ; param { number } row param { number } col return { boolean } isGameWon = ( row , col ) : boolean => { if ( ( this . board [ row ] [ 0 ] === this . currentPlayer && this . board [ row ] [ 1 ] === this . currentPlayer && this . board [ row ] [ 2 ] === this . currentPlayer ) || ( this . board [ 0 ] [ col ] === this . currentPlayer && this . board [ 1 ] [ col ] === this . currentPlayer && this . board [ 2 ] [ col ] === this . currentPlayer ) || ( ( this . board [ 0 ] [ 0 ] === this . currentPlayer && this . board [ 1 ] [ 1 ] === this . currentPlayer && this . board [ 2 ] [ 2 ] === this . currentPlayer ) || ( this . board [ 2 ] [ 0 ] === this . currentPlayer && this . board [ 1 ] [ 1 ] === this . currentPlayer && this . board [ 0 ] [ 2 ] === this . currentPlayer ) ) ) return true ; return false ; } ; /**
     * Switch to the next player
     */ switchPlayer = ( ) : void => { this . currentPlayer = this . currentPlayer === this . players . x ? this . players . o : this . players . x ; } ; /**
     * Increase the score of the winning player
     */ increaseScore = ( ) : void => { this . score [ this . currentPlayer ] += 1 ; } ; /**
     * Render score board and game board
     */ startGame ( ) { this . display . printScoreBoard ( this . score ) ; this . display . printGameBoard ( this . board ) ; } } const ticTacToe = new TicTacToe ( new DOMDisplay ( ) ) ; ticTacToe . startGame ( ) ;