📜 ⬆️ ⬇️

Tic Tac Toe, Part 2: Undo / Redo with state storage

Tic Tac Toe, Part 0: Comparing Svelte and React
Tic Tac Toe, Part 1: Svelte and Canvas 2D
Tic Tac Toe, Part 2: Undo / Redo with state storage
Tic Tac Toe, Part 3: Undo / Redo with commands storage
Tic Tac Toe, Part 4: Interacting with the Flask backend using HTTP

Continuation of the article Tic Tac Toe, part 1 , in which we began the development of this game on Svelte . In this part we will complete the game to the end. Add Undo / Redo teams, random access to any step of the game, alternate moves with the opponent, display the status of the game, determine the winner.


Undo / Redo commands

REPL code


At this stage, Undo / Redo commands have been added to the application. The push and redo methods have been added to the history storage.


undo: () => update(h => { h.undo(); return h; }), redo: () => update(h => { h.redo(); return h; }), 

The push , redo , canUndo , canRedo methods have been added to the History class.


 canUndo() { return this.current > 0; } canRedo() { return this.current < this.history.length - 1; } undo() { if (this.canUndo()) this.current--; } redo() { if (this.canRedo()) this.current++; } 

The removal of all states from current to last has been added to the push method of the History class. If we execute the Undo command several times and click in the playing field, all the states to the right from the current to the last will be removed from the storage and a new state will be added.


 push(state) { // remove all redo states if (this.canRedo()) this.history.splice(this.current + 1); // add a new state this.current++; this.history.push(state); } 

The Undo and Redo buttons have been added to the App component. If the execution of commands is not possible, then they are deactivated.


 <div> {#if $history.canUndo()} <button on:click={history.undo}>Undo</button> {:else} <button disabled>Undo</button> {/if} {#if $history.canRedo()} <button on:click={history.redo}>Redo</button> {:else} <button disabled>Redo</button> {/if} </div> 

Turn change

REPL code


An alternate appearance of a cross or a zero is made after a mouse click.


The clickCell () method has been removed from their history stores, all method code has been moved to the BoardClock handleClick () handler.


 function handleClick(event) { let x = Math.trunc((event.offsetX + 0.5) / cellWidth); let y = Math.trunc((event.offsetY + 0.5) / cellHeight); let i = y * width + x; const state = $history.currentState(); const squares = state.squares.slice(); squares[i] = state.xIsNext ? 'X' : 'O'; let newState = { squares: squares, xIsNext: !state.xIsNext, }; history.push(newState); } 

Thus, the previously made mistake was eliminated, the storage was dependent on the logic of this particular game. This bug has now been fixed, and the storage can be reused in other games and applications without changes.


Previously, the status of the game step was described only by an array of 9 values. Now the state of the game is determined by the object containing the array and the xIsNext property. Initializing this object at the beginning of the game looks like this:


 let state = { squares: Array(9).fill(''), xIsNext: true, }; 

And it can also be noted that the history repository can now perceive the states described in any way.


Random access to move history

REPL code


In the history repository, the setCurrent (current) method was added, with which we set the selected current state of the game.


 setCurrent(current) { if (current >= 0 && current < this.history.length) this.current = current; } 

 setCurrent: (current) => update(h => { h.setCurrent(current); return h; }), 

In the App component added the output of the history of moves in the form of buttons.


 <ol> {#each $history.history as value, i} {#if i==0} <li><button on:click={() => history.setCurrent(i)}>Go to game start</button></li> {:else} <li><button on:click={() => history.setCurrent(i)}>Go to move #{i}</button></li> {/if} {/each} </ol> 

Determining the winner, displaying the status of the game

REPL code


Added the function of determining the winner calculateWinner () in a separate helpers.js file:


 export function calculateWinner(squares) { const lines = [ [0, 1, 2], [3, 4, 5], [6, 7, 8], [0, 3, 6], [1, 4, 7], [2, 5, 8], [0, 4, 8], [2, 4, 6], ]; for (let i = 0; i < lines.length; i++) { const [a, b, c] = lines[i]; if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) { return squares[a]; } } return null; } 

A derivative status repository has been added to determine the status of a game, here the outcome of the game is determined: winner or draw:


 export const status = derived( history, $history => { if ($history.currentState()) { if (calculateWinner($history.currentState().squares)) return 1; else if ($history.current == 9) return 2; } return 0; } ); 

The status of the game has been added to the App component:


 <div class="status"> {#if $status === 1} <b>Winner: {!$history.currentState().xIsNext ? 'X' : 'O'}</b> {:else if $status === 2} <b>Draw</b> {:else} Next player: {$history.currentState().xIsNext ? 'X' : 'O'} {/if} </div> 

In the Board component, restrictions have been added to the handleClick () click handler: it is not possible to perform a click in the filled cell even after the game is over.


 const state = $history.currentState(); if ($status == 1 || state.squares[i]) return; 

Game over! In the next article we will consider the implementation of the same game using the Command pattern, i.e. with storage commands Undo / Redo instead of storing individual states.


GitHub Repository

https://github.com/nomhoi/tic-tac-toe-part2


Installing the game on the local computer:


 git clone https://github.com/nomhoi/tic-tac-toe-part2.git cd tic-tac-toe-part2 npm install npm run dev 

Run the game in the browser at: http: // localhost: 5000 / .


')

Source: https://habr.com/ru/post/459630/


All Articles