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
In the article "Comparison: Svelte and React" I tried to repeat the development of the game Tic Tac Toe. There, I performed only the first part of the source tutorial for React without support for the move history. In this article we will start the development of this game using the Svelte framework with support for the history of moves. The move history is actually the Undo / Redo system. In the original tutorial on React, the Undo / Redo system is implemented with state storage with random access to any state. When implementing the Undo / Redo system, the Command pattern is usually used, and the Undo / Redo commands are stored in the command list. We will try to implement this approach later, now we will execute the Undo / Redo system with state storage.
In the development applied architectural solution Flux using storage. This is a separate section in the article.
<script> import Board from './Board.svelte'; </script> <div class="game"> <div class="game-board"> <Board /> </div> <div class="game-info"> <div class="status">Next player: X</div> <div></div> <ol></ol> </div> </div> <style> .game { font: 14px "Century Gothic", Futura, sans-serif; margin: 20px; display: flex; flex-direction: row; } .game-info { margin-left: 20px; } .status { margin-bottom: 10px; } ol { padding-left: 30px; } </style>
<script> import { onMount } from 'svelte'; export let width = 3; export let height = 3; export let cellWidth = 34; export let cellHeight = 34; export let colorStroke = "#999"; let boardWidth = 1 + (width * cellWidth); let boardHeight = 1 + (height * cellHeight); let canvas; onMount(() => { const ctx = canvas.getContext('2d'); ctx.clearRect(0, 0, boardWidth, boardHeight); ctx.beginPath(); // vertical lines for (let x = 0; x <= boardWidth; x += cellWidth) { ctx.moveTo(0.5 + x, 0); ctx.lineTo(0.5 + x, boardHeight); } // horizontal lines for (let y = 0; y <= boardHeight; y += cellHeight) { ctx.moveTo(0, 0.5 + y); ctx.lineTo(boardWidth, 0.5 + y); } // draw the board ctx.strokeStyle = colorStroke; ctx.stroke(); ctx.closePath(); }); </script> <canvas bind:this={canvas} width={boardWidth} height={boardHeight} ></canvas>
The start code displays an empty grid. It is rendered using the HTML5 canvas element. More details about using this element are described in the previous article Developing a Breakout Game on Svelte . How to draw a grid peeped here . The Board component can be reused in other games. By changing the variables width and height you can change the size of the grid, by changing the values of the variables cellWidth and cellHeight you can change the size of the cell.
REPL code
In the onMount () function added the output of zeros in the cells after the grid output. And some magic numbers associated with the positioning values in the cells.
ctx.beginPath(); ctx.font = "bold 22px Century Gothic"; let d = 8; for (let i = 0; i < height; i+=1) { for (let j = 0; j < width; j+=1) { ctx.fillText("O", j * cellWidth + d + 1, (i + 1) * cellHeight - d); } } ctx.closePath();
REPL code
In this section, we check the state changes using the custom store . Added state storage in a separate stores.js file, this storage is imported into both components: App and Board. In this repository, state1 and state2 methods that change the state of the game are defined.
import { writable } from 'svelte/store'; function createState() { const { subscribe, set, update } = writable(Array(9).fill('O')); return { subscribe, state1: () => set(Array(9).fill('1')), state2: () => set(Array(9).fill('2')), }; } export const state = createState();
In the App component added two buttons State 1 and State 2 . Clicking on the buttons we call the appropriate methods in the repository.
<button on:click={state.state1}>State 1</button> <button on:click={state.state2}>State 2</button>
In the Board component, I changed the line for outputting zeros with the data from their state repository. Here we use an auto-subscription to the repository .
ctx.fillText($state[k], j * cellWidth + d + 1, (i + 1) * cellHeight - d);
At this stage, the playing field is filled with zeros by default, click on the State 1 button - the field is filled with ones, click on the State 2 button - the field is filled with twos.
REPL code
In the state repository, the setCell () method was added, which fills the selected cell with a cross.
setCell: (i) => update(a => {a[i] = 'X'; return a;}),
An event handler was added to canvas by mouse click, here we define the cell index and call the state's setCell () method.
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; state.setCell(i); }
The playing field by default is filled with zeros, click on any cell, the zero is replaced by a cross.
REPL code
Let me remind you that we are now running the Undo / Redo system with the storage of states with random access.
import { writable } from 'svelte/store'; class History { constructor() { this.history = new Array; this.current = -1; } currentState() { return this.history[this.current]; } push(state) { // TODO: remove all redo states this.current++; this.history.push(state); } } function createHistory() { const { subscribe, set, update } = writable(new History); return { subscribe, push: (state) => update(h => { h.push(state); return h; }), }; } export const history = createHistory();
State repository deleted, history repository added to store history of moves. We describe it with the help of the History class. To store the state, use the history array. Sometimes when implementing the Undo / Redo system, two LIFO stacks are used: undo-stack and redo-stack. We use a single history array to store states in History . The current property is used to determine the current state of the game. All states in history from the beginning of the array to the state with the current index can be said to be in the Undo list, and all the others in the Redo list. By reducing or increasing the current property, in other words, by executing Undo or Redo commands, we select the state closer to the beginning, or to the end of the game. The undo and redo methods are not yet implemented, they will be added later. The currentState () and push () methods are added to the History class. The currentState () method returns the current state of the game, using the push () method we add a new state to the history of moves.
In the App component, we removed the State 1 and State 2 buttons. And added a push button:
<button on:click={() => history.push(Array(9).fill($history.current + 1))}>Push</button>
By clicking on this button a new state is added to the move history, the history array is simply filled with the value of the current state index current .
In the Board component, we display the current state from the move history. This demonstrates the use of auto-subscription to the repository :
ctx.fillText($history.currentState()[k], j * cellWidth + d + 1, (i + 1) * cellHeight - d);
In the push method of the history repository, you can add output to the browser console and observe how it changes after clicking the Push button.
h.push(state); console.log(h); return h;
REPL code
In the App component removed the push button.
The history repository has a clickCell method defined . Here we create a complete copy of the game state, change the state of the selected cell and add a new state to the move history:
clickCell: (i) => update(h => { // create a copy of the current state const state = h.currentState().slice(); // change the value of the selected cell to X state[i] = 'X'; // add the new state to the history h.push(state); console.log(h.history); return h; }),
In the Board component, a call to the clickCell () storage method was added to the handleClick () function:
history.clickCell(i);
In the browser console here also we can observe how the state of the history of moves changes after each mouse click.
In the following articles, we will complete the game to the end, with an undo / redo interface and random access to any step of the game. Consider an implementation of the Undo / Redo system using the Command design pattern. Consider interaction with the backend, the player will compete with an intelligent agent in the backend.
In the development of this game is observed the use of architectural solutions Flux . Actions are implemented as methods in the definition of the history repository in the stores.js file. There is a history repository , which is described in the form of the History class. Views are implemented as App and Board components. So for me, all this is the same as MVC architecture , side view. Actions - controller, storage - model, view - view. The descriptions of both architectures are almost identical.
https://github.com/nomhoi/tic-tac-toe-part1
Installing the game on the local computer:
git clone https://github.com/nomhoi/tic-tac-toe-part1.git cd tic-tac-toe-part1 npm install npm run dev
Run the game in the browser at: http: // localhost: 5000 / .
Source: https://habr.com/ru/post/458752/
All Articles