Recriando o Sudoku com React e TypeScript

Recriando o Sudoku com React e TypeScript

O Sudoku é um clássico do gênero puzzle. Se você nunca ouviu falar, funciona assim:

  • O jogo começa com uma matriz 9⨉9 parcialmente preenchida com números de 1 a 9.
  • O objetivo é preencher todos os espaços vazios com números de 1 a 9,  de modo que cada linha, coluna ou “caixa” não contenha repetições.

Exemplo:

Fonte: https://en.wikipedia.org/wiki/Sudoku

Na imagem da esquerda, as “caixas”, ou boxes, são as submatrizes 3⨉3 determinadas pelas linhas(verticais e horizontais) mais grossas.

TypeScript

TypeScript é uma linguagem que visa adicionar tipos estáticos à linguagem JavaScript. Isso permite detectar uma série de bugs durante o desenvolvimento de projetos visto que editores como o VS Code ou o WebStorm analisam o código em tempo real em busca de inconsistências. Além disso, o compilador oficial TypeScript(que se chama tsc), além de analisar o código, também gera código JavaScript puro de acordo com a versão que for estipulada no arquivo de configuração .tsconfig.

React

React é uma biblioteca JavaScript criada pelo Facebook e usada por outras grandes empresas como Instagram, Netflix e Uber, para aumentar a produtividade dos desenvolvedores ao criarem interfaces de sites e aplicações. É uma biblioteca baseada no conceito de componentes, o que significa que você escreve código especificando como sua interface deve ser, ao invés de descrever exatamente as modificações necessárias no DOM, o que aumenta a reusabilidade do código.

Mas toda essa conveniência tem um preço, visto que configurar manualmente o React em um novo projeto pode ser uma tarefa hercúlea, ainda mais para um iniciante. Porém, é possível contornar esse problema utilizando um framework.

Usaremos o framework Next.js pois é uma das formas recomendadas para começar um novo projeto de acordo com a documentação oficial do React.

Next.js

Next.js é um framework construído em cima do React e do React DOM que já vem pronto para usar, no sentido que requer pouca ou nenhuma configuração. Por exemplo, ele traz renderização do lado do servidor(SSR), geração de sites estáticos(SSG) e integração com TypeScript, além de várias soluções de estilização como CSS Modules e Tailwind CSS.

Configurando nossa aplicação Next.js

Para criar uma aplicação Next.js, utilizaremos o pacote create-next-app. Dependendo do seu gerenciador de pacotes favorito, o comando adequado para executar a instalação a partir de um terminal será um dos seguintes:

npx create-next-app@latest
# ou
yarn create next-app
# ou
pnpm create next-app

Isso iniciará um processo interativo onde será necessário responder a algumas perguntas. Abaixo de cada pergunta vou deixar registrado o que escolhi para o meu projeto:

Daqui em diante assumirei que as opções são como acima.

Em seguida, precisamos instalar o pacote sudoku-gen pois é com ele que iremos gerar os puzzles e suas soluções:

npm install sudoku-gen
#ou
yarn add sudoku-gen
#ou
pnpm add sudoku-gen

Agora, apague o conteúdo dos arquivos: ./sudoku/app/page.tsx e ./sudoku/app/globals.css.

Estado e transições

Uma das ideias centrais do React é que a interface deve refletir o estado da aplicação. Sendo assim, vamos começar definindo o tipo do estado:

type GameState = {
  puzzle: string; // O puzzle gerado pelo pacote sudoku-gen
  solution: string; // A solução do puzzle
  cellValues: string; // Os valores de cada célula da matriz
  selectedCellId: number; // O id da célula selecionada
  selectedDifficulty: string; // A dificuldade selecionada
};

O campo puzzle é uma string contendo 81 caracteres dentre o símbolo “-” e os dígitos de 1 a 9. O símbolo “-” denota uma célula vazia. O mesmo vale para os campos solution e cellValues. Já selectedCellId representa a identificação de uma célula e selectedDifficulty é um valor entre easy, medium, hard e expert.

Feito isso, consideremos as possíveis ações do usuário que resultam em alteração do estado:

  • Iniciar um novo jogo;
  • Selecionar uma célula da matriz;
  • Apertar uma tecla numérica para atualizar a célula selecionada;
  • Limpar a célula selecionada;
  • Escolher a dificuldade do jogo;
  • Exibir a solução(vulgo se dar por vencido).

Traduzindo para TypeScript, fica assim:

type ActionType =
  | { type: "startedNewGame" }
  | { type: "selectedCell"; cellId: number }
  | { type: "selectedValue"; value: string }
  | { type: "selectedDifficulty"; difficulty: string }
  | { type: "clearedSelectedCell" }
  | { type: "gaveUp" };

Na definição do tipo acima usamos uma técnica conhecida como discriminated union.

Com isso, temos em mãos toda a informação necessária para gerenciar o estado do jogo. Se você tem familiaridade com o React, já deve ter ficado claro que pretendo usar o hook useReducer.

Para isso, precisamos definir uma função reducer, cujo papel é gerar um novo estado a partir do estado anterior e da ação do usuário. A implementação completa é a seguinte:

A função validateMove abaixo é responsável por determinar se é possível marcar o valor informado na célula escolhida, de acordo com o estado atual. Para isso, ela verifica a linha, coluna e “caixa”(box) da célula escolhida em busca de possíveis conflitos.

A seguinte função auxiliar é utilizada para converter uma identificação de célula para sua posição, isto é, sua linha, coluna e “caixa”.

A seguinte é a função inversa da função anterior. Ou quase, já que só declaramos linha e coluna como parâmetros, mas isso já é suficiente para localizar a célula na matriz.

Para entender a utilidade da próxima função, é necessário entender a distinção entre dois tipos de identificação de uma célula. Sempre que há menção à identificação de uma célula, ou seja, um cellId, devemos pensar em um número de 0 a 80, que estão organizados da seguinte forma:

0  1  2 |  9 10 11 | 18 19 20
3  4  5 | 12 13 14 | 21 22 23
6  7  8 | 15 16 17 | 24 25 26

—-----------------------------

27 28 29 |
30 31 32 |...
33 34 35 |

.

.

.

E não da forma mais comum, que é a forma utilizada pelo pacote auxiliar sudoku-gen:

0  1  2 |  3  4  5 |  6  7  8
9 10 11 | 12 13 14 | 15 16 17
18 19 20 | 21 22 23 | 24 25 26
-------------------------------
27 28 29 | 30 31 32 | 33 34 35
.
.
.


Isso se dá pela forma como definimos o layout via CSS grids aninhados. Para converter da forma mais convencional para a forma alternativa é que existe a função seguinte:

Na estrutura do projeto, tudo relacionado à função reducer fica no arquivo ./sudoku/app/lib/sudokuReducer.ts.

Componentes e estilo

Agora, vamos conhecer os componentes visuais com seus respectivos estilos.

O componente raíz e responsável pelo estado é o componente Game. Dentro dele temos um componente Grid e um Menu. O Grid consiste de nove componentes Box arranjados em forma de matriz 3⨉3 e cada Box, por sua vez, é uma coleção de nove componentes de tipo Cell, arranjados numa matriz 3⨉3. O componente Menu permite iniciar um novo jogo, escolher o nível de dificuldade e mostrar a solução.

A hierarquia de componentes fica basicamente assim:

  • Game
  • Grid
  • Box
  • Cell
  • Menu

O ponto de entrada da nossa aplicação Next.js é o arquivo ./sudoku/app/page.tsx. É nele onde definimos o componente Game:

A diretiva “use client” na primeira linha é necessária pois a partir da versão 13, o framework Next.js trata todos os componentes como server components por padrão. Para nós, isso significa apenas que não poderíamos usar hooks de estado e outros dentro do nosso componente sem a diretiva.

Fora isso, também definimos nesse componente os event handlers, que são essas funções cujo nome começa com o prefixo handle e que são responsáveis por reagir às ações do usuário, influenciando o estado da aplicação.

Note ainda o uso do useEffect que é um hook que serve para sincronizar um componente React com algo que mantém um estado não controlado pelo React. Usamo-lo para detectar teclas pressionadas e delegar ao handler adequado.

A forma de estilização de componentes que usaremos são os CSS Modules. No arquivo ./sudoku/app/page.module.css definimos a classe .game:

E para usar esse estilo, iremos importá-lo em ./sudoku/app/page.module.tsx:

A magia acontece na linha:

Pois isso instrui o Next.js a injetar o estilo apenas nesse componente de modo que não precisamos nos preocupar com colisão de nomes de classes espalhadas por todo o projeto. CSS Modules e componentes são um “match made in heaven”.

Os outros componentes são estilizados de modo análogo.
Continuando as apresentações, falemos do componente Grid, localizado no arquivo ./sudoku/app/components/Grid.tsx. É um componente sem estado que aceita os seguintes props:

Sua definição é a seguinte:

Aqui foi usada a função estática Array.from para gerar um array verdadeiro a partir de um objeto array-like.

O estilo do componente Grid é definido em ./sudoku/app/components/Grid.module.css:

CSS Grids para estilizar o componente Grid! Isso foi muito satisfatório, but I digress.

O próximo componente é o Box, que é utilizado apenas para simplificar a estilização, pois o mesmo simplesmente aplica seu estilo e renderiza seus filhos. Sua definição encontrada em ./sudoku/app/components/Box.tsx é como segue:

E sua folha de estilos, que fica em ./sudoku/app/components/Box.module.css, é da forma a seguir:

Observe que este também é um CSS Grid.

Falemos agora do componente Cell, cuja implementação fica em ./sudoku/app/components/Cell.tsx. O mesmo aceita os seguintes props:

E sua implementação é a seguinte:

Note como o estilo muda de acordo com o estado do jogo, ou seja, se a célula já estava marcada no puzzle inicialmente gerado, se ela está selecionada ou se é só um espaço vazio.

O CSS correspondente se encontra em ./sudoku/app/components/Cell.module.css.

A regra:

É para impedir que o usuário possa selecionar o texto da célula pois isso quebra um pouco a imersão do jogo e fica parecendo que o jogador está num site normal e não em numa super aplicação React!

Essa palavra-chave composes é uma forma de herança entre classes CSS que é especificada pelo CSS Modules.

Considerações finais

Depois de pronto, o jogo tem essa cara:


As cores foram baseadas numa paleta para pessoas com dificuldade em enxergar certas cores. Um salve para os daltônicos!

Chegamos ao fim deste tutorial. Espero ter contribuído com sua jornada no aprendizado de desenvolvimento Frontend. Bons estudos e boa sorte!

Um repositório Git deste projeto se encontra em: https://github.com/lzralbu/sudoku

Uma versão live do projeto pode ser acessada em: https://sudoku-app-lzralbu.vercel.app/

💡
As opiniões e comentários expressos neste artigo são de propriedade exclusiva de seu autor e não representam necessariamente o ponto de vista da Revelo.

A Revelo Content Network acolhe todas as raças, etnias, nacionalidades, credos, gêneros, orientações, pontos de vista e ideologias, desde que promovam diversidade, equidade, inclusão e crescimento na carreira dos profissionais de tecnologia.