Tic Tac Toe Parte 1: Svelte e Canvas 2D

Tic Tac Toe Parte 0: Comparando Svelte e Reagir
Tic Tac Toe Parte 1: Svelte e Canvas 2D
Tic Tac Toe Parte 2: Desfazer / Refazer sem estado
Tic Tac Toe, parte 3: Desfazer / Refazer com armazenamento de comando
Tic Tac Toe Parte 4: Interagindo com o back-end do frasco usando HTTP

No artigo "Comparison: Svelte and React" , tentei repetir o desenvolvimento do jogo Tic Tac Toe. Lá, completei apenas a primeira parte do tutorial original do React sem apoiar o histórico de movimentos. Neste artigo, iniciaremos o desenvolvimento deste jogo usando a estrutura Svelte, com suporte para o histórico de jogadas. A história dos movimentos é na verdade um sistema de Desfazer / Refazer. No tutorial original sobre React, o sistema Desfazer / Refazer com armazenamento de estado com acesso aleatório a qualquer estado é implementado. Ao implementar o sistema Desfazer / Refazer, o padrão Comando geralmente é usado e os comandos Desfazer / Refazer são armazenados na lista de comandos. Tentaremos implementar essa abordagem mais tarde; agora, executaremos o sistema Desfazer / Refazer com armazenamento de estado.


O desenvolvimento usa a solução arquitetônica Flux usando armazenamento. Esta é uma seção separada no artigo.


Código inicial

Código REPL


App.svelte
<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> 

Board.svelte
 <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> 

O código inicial exibe uma grade vazia. É exibido usando o elemento de tela HTML5. Para obter mais informações sobre o uso desse elemento, consulte o artigo anterior Developing Breakout on Svelte . Como desenhar uma grade espionada aqui . O componente do tabuleiro pode ser reutilizado em outros jogos. Alterando as variáveis width e height , é possível redimensionar a grade; alterando os valores das variáveis ​​cellWidth e cellHeight, você pode redimensionar a célula.


Preencher células com zeros

Código REPL
Na função onMount (), adicionou a saída de zeros nas células após a saída da grade. E alguns números mágicos relacionados ao posicionamento de valores nas células.


 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(); 

Adicionar armazenamento de estado

Código REPL
Nesta seção, verificamos as alterações de estado usando o repositório do usuário . Adicionado armazenamento de estado em um arquivo stores.js separado, esse armazenamento é importado para os dois componentes: App e Board. Os métodos state1 e state2 que alteram o estado do jogo são definidos nesta loja.


 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(); 

Adicionados dois botões Estado 1 e Estado 2 ao componente App. Ao clicar nos botões, chamamos os métodos correspondentes no repositório.


 <button on:click={state.state1}>State 1</button> <button on:click={state.state2}>State 2</button> 

No componente Placa, mudei a linha de saída de zeros para a saída de dados de seu armazenamento de estado . Aqui usamos a assinatura automática para armazenamento .


 ctx.fillText($state[k], j * cellWidth + d + 1, (i + 1) * cellHeight - d); 

Nesta fase, o campo do jogo é preenchido com zeros por padrão, clique no botão Estado 1 - o campo é preenchido com unidades, clique no botão Estado 2 - o campo é preenchido com duques.


Preenchendo uma célula com um clique do mouse

Código REPL
No armazenamento de estado , adicionei o método setCell () , que preenche a célula selecionada com uma cruz.


 setCell: (i) => update(a => {a[i] = 'X'; return a;}), 

Um manipulador de eventos com clique do mouse foi adicionado à tela, aqui determinamos o índice da célula e chamamos o método setCell () do armazenamento de estado .


 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); } 

Por padrão, o campo de jogo é preenchido com zeros, clicamos em qualquer célula e o zero é substituído por uma cruz.


História dos movimentos

Código REPL
Deixe-me lembrá-lo de que atualmente estamos executando o sistema Desfazer / Refazer com armazenamento de estado com acesso aleatório.


 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(); 

O armazenamento de estado foi excluído, o armazenamento de histórico foi adicionado para armazenar o histórico de movimentos. Nós o descrevemos usando a classe History . Para armazenar os estados, use a matriz do histórico . Às vezes, ao implementar o sistema Desfazer / Refazer, duas pilhas LIFO são usadas: desfazer e refazer a pilha. Usamos uma única matriz de histórico para armazenar estados no Histórico . A propriedade atual é usada para determinar o estado atual do jogo. Todos os estados do histórico, desde o início da matriz até o estado com o índice atual , por assim dizer, estão na lista Desfazer e todos os outros na lista Refazer. Diminuindo ou aumentando a propriedade atual , ou seja, executando os comandos Desfazer ou Refazer, selecionamos o estado mais próximo do início ou do final do jogo. Os métodos de desfazer e refazer ainda não foram implementados; eles serão adicionados posteriormente. Os métodos CurrentState () e push () foram adicionados à classe History . O método currentState () retorna o estado atual do jogo, usando o método push () , adicionamos um novo estado ao histórico de movimentação.


No componente App , removemos os botões Estado 1 e Estado 2 . E adicionou um botão:


 <button on:click={() => history.push(Array(9).fill($history.current + 1))}>Push</button> 

Ao clicar neste botão, um novo estado é adicionado ao histórico de movimentos, a matriz do histórico é simplesmente preenchida com o valor atual do índice de estado atual .


No componente Placa , exiba o estado atual do histórico de movimentos. Veja como usar a assinatura automática para armazenamento :


 ctx.fillText($history.currentState()[k], j * cellWidth + d + 1, (i + 1) * cellHeight - d); 

No método push do armazenamento de histórico , você pode adicionar saída ao console do navegador e observar como ela muda depois de clicar no botão Push .


 h.push(state); console.log(h); return h; 

Clicando em uma célula

Código REPL
No componente App , o botão Push foi removido.


O método clickCell foi definido no armazenamento de histórico . Aqui, criamos uma cópia completa do estado do jogo, alteramos o estado da célula selecionada e adicionamos um novo estado ao histórico de movimentação:


 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; }), 

No componente Placa , a chamada do método de armazenamento callClick () foi adicionada à função handleClick () :


 history.clickCell(i); 

No console do navegador, também podemos ver como o estado do histórico de movimentos muda após cada clique do mouse.


Nos artigos a seguir, terminaremos o jogo até o fim, com uma interface para cancelar / retornar etapas e acesso aleatório a qualquer etapa do jogo. Considere uma implementação do sistema Desfazer / Refazer usando o padrão de design do Comando. Considere a interação com o back-end, o jogador competirá com o agente intelectual no back-end.


Arquitetura de fluxo

No desenvolvimento deste jogo, é observado o uso da solução arquitetônica do Flux . As ações são implementadas como métodos na definição de armazenamento do histórico no arquivo stores.js . Existe um repositório de histórico, descrito na forma da classe Histórico . As visualizações são implementadas como componentes de App e Board . Para mim, tudo isso é igual à arquitetura MVC , vista lateral. Ações - controlador, armazenamento - modelo, visão - visão. As descrições de ambas as arquiteturas praticamente coincidem.


Repositório do GitHub

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


Instalando o jogo no computador local:


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

Iniciamos o jogo em um navegador no endereço: http: // localhost: 5000 / .

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


All Articles