Tic Tac Toe Part 2: Stateless Undo / Redo
- Tutorial
Tic Tac Toe, part 0: Comparison of 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 storage of Tic Tac commands
Toe Part 4: Interacting with the Flask Backend Using HTTP
Continuation of the article by Tic Tac Toe, part 1 , in which we started developing this game on Svelte . In this part we will finish the game to the end. Add Undo / Redo teams , random access to any step of the game, alternating moves with the opponent, displaying the status of the game, determining the winner.
Undo / Redo Commands
At this point, Undo / Redo commands were added to the application. Added push and redo methods to the history store .
undo: () => update(h => { h.undo(); return h; }),
redo: () => update(h => { h.redo(); return h; }),
Added methods push , redo , canUndo , canRedo to the History class .
canUndo() {
returnthis.current > 0;
}
canRedo() {
returnthis.current < this.history.length - 1;
}
undo() {
if (this.canUndo())
this.current--;
}
redo() {
if (this.canRedo())
this.current++;
}
The method push class History added removing all states from the current to the latter. If we execute the Undo command several times and click in the playing field, then all the states to the right of the current to the last will be deleted from the store and a new state will be added.
push(state) {
// remove all redo statesif (this.canRedo())
this.history.splice(this.current + 1);
// add a new statethis.current++;
this.history.push(state);
}
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()}
<buttonon:click={history.undo}>Undo</button>
{:else}
<buttondisabled>Undo</button>
{/if}
{#if $history.canRedo()}
<buttonon:click={history.redo}>Redo</button>
{:else}
<buttondisabled>Redo</button>
{/if}
</div>
Change of course
An alternating appearance of a cross or a toe after a mouse click is performed.
The clickCell () method has been removed from their history repository , all the method code has been transferred to the handleClick () handler of the Board component .
functionhandleClick(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 repository was dependent on the logic of this particular game. Now this error has been fixed, and the repository can be reused in other games and applications without changes.
Previously, the state of a 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. Initialization of 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 store can now perceive states described in any way.
Random access to move history
In the history store , 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 display of the history of moves in the form of buttons.
<ol>
{#each $history.history as value, i}
{#if i==0}
<li><buttonon:click={() => history.setCurrent(i)}>Go to game start</button></li>
{:else}
<li><buttonon:click={() => history.setCurrent(i)}>Go to move #{i}</button></li>
{/if}
{/each}
</ol>
Determining the winner, displaying the status of the game
Added function to determine the winner calculateWinner () in a separate helpers.js file :
exportfunctioncalculateWinner(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];
}
}
returnnull;
}
Derived status storage has been added to determine the status of the game, here the outcome of the game is determined: winner or draw:
exportconst status = derived(
history,
$history => {
if ($history.currentState()) {
if (calculateWinner($history.currentState().squares))
return1;
elseif ($history.current == 9)
return2;
}
return0;
}
);
The output of the game status has been added to the App component :
<divclass="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 , limitations have been added to the handle of the handleClick () click handler : it is impossible to click in the filled cell even after the end of the game.
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 storing Undo / Redo commands 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
We launch the game in a browser at the address: http: // localhost: 5000 / .