Recreando Sudoku con React y TypeScript

Recreando Sudoku con React y TypeScript

El Sudoku es un clásico del género puzzle. Si nunca has oído hablar de él, funciona así:

  • El juego comienza con una matriz 9×9 parcialmente llenada con números del 1 al 9.
  • El objetivo es llenar todos los espacios vacíos con números del 1 al 9, de modo que cada fila, columna o “caja” no contenga repeticiones.

Ejemplo:

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

En la imagen de la izquierda, las “cajas” son las submatrices 3×3 determinadas por las líneas (verticales y horizontales) más gruesas.

TypeScript

TypeScript es un lenguaje que busca agregar tipos estáticos al lenguaje JavaScript. Esto permite detectar una serie de errores durante el desarrollo de proyectos, ya que editores como VS Code o WebStorm analizan el código en tiempo real en busca de inconsistencias. Además, el compilador oficial de TypeScript (que se llama tsc), además de analizar el código, también genera código JavaScript puro de acuerdo con la versión que se estipule en el archivo de configuración .tsconfig.

React

React es una biblioteca de JavaScript creada por Facebook y usada por otras grandes empresas como Instagram, Netflix y Uber, para aumentar la productividad de los desarrolladores al crear interfaces de sitios web y aplicaciones. Es una biblioteca basada en el concepto de componentes, lo que significa que escribes código especificando cómo debe ser tu interfaz, en lugar de describir exactamente las modificaciones necesarias en el DOM, lo que aumenta la reutilización del código.

Pero toda esta conveniencia tiene un precio, ya que configurar manualmente React en un nuevo proyecto puede ser una tarea titánica, especialmente para un principiante. Sin embargo, es posible solucionar este problema utilizando un framework.

Usaremos el framework Next.js porque es una de las formas recomendadas para comenzar un nuevo proyecto según la documentación oficial de React.

Next.js

Next.js es un framework construido sobre React y React DOM que ya está listo para usar, en el sentido de que requiere poca o ninguna configuración. Por ejemplo, ofrece renderización del lado del servidor (SSR), generación de sitios estáticos (SSG) e integración con TypeScript, además de varias soluciones de estilización como CSS Modules y Tailwind CSS.

Configurando nossa aplicação Next.js

Para crear una aplicación Next.js, utilizaremos el paquete create-next-app. Dependiendo de tu gestor de paquetes favorito, el comando adecuado para ejecutar la instalación desde una terminal será uno de los siguientes:

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

Esto iniciará un proceso interactivo donde será necesario responder algunas preguntas. A continuación de cada pregunta voy a dejar registrado lo que elegí para mi proyecto:

De aquí en adelante asumiré que las opciones son como arriba.

En seguida, necesitamos instalar el paquete sudoku-gen, pues con él vamos a generar los puzzles y sus soluciones:

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

Ahora, borra el contenido de los archivos: ./sudoku/app/page.tsx y ./sudoku/app/globals.css.

Estado y transiciones

Una de las ideas centrales de React es que la interfaz debe reflejar el estado de la aplicación. Así que, vamos a empezar definiendo el tipo de 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
};

El campo puzzle es una cadena que contiene 81 caracteres entre el símbolo “-” y los dígitos del 1 al 9. El símbolo “-” denota una celda vacía. Lo mismo aplica para los campos solution y cellValues. Por su parte, selectedCellId representa la identificación de una celda y selectedDifficulty es un valor entre easy, medium, hard y expert.

Hecho esto, consideremos las posibles acciones del usuario que resultan en la alteración del estado:

  • Iniciar un nuevo juego;
  • Selecionar una celda de la matriz;
  • Presionar una tecla numérica para actualizar la celda seleccionada;
  • Limpiar la celda seleccionada;
  • Elegir la dificultad del juego;
  • Mostrar la solución (vulgo darse por vencido).

Traducido a TypeScript, queda así:

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

En la definición del tipo anterior usamos una técnica conocida como discriminated union.

Con esto, tenemos en nuestras manos toda la información necesaria para gestionar el estado del juego. Si tienes familiaridad con React, ya debe estar claro que pretendo usar el hook useReducer.

Para eso, necesitamos definir una función reducer, cuyo papel es generar un nuevo estado a partir del estado anterior y la acción del usuario. La implementación completa es la siguiente:

La función validateMove a continuación es responsable de determinar si es posible marcar el valor informado en la celda elegida, de acuerdo con el estado actual. Para eso, verifica la fila, columna y “caja” (box) de la celda elegida en busca de posibles conflictos.

La siguiente función auxiliar se utiliza para convertir una identificación de celda a su posición, es decir, su fila, columna y “caja”.

La siguiente es la función inversa de la función anterior. O casi, ya que solo declaramos fila y columna como parámetros, pero eso ya es suficiente para localizar la celda en la matriz.

Para entender la utilidad de la próxima función, es necesario comprender la distinción entre dos tipos de identificación de una celda. Siempre que hay mención a la identificación de una celda, o sea, un cellId, debemos pensar en un número del 0 al 80, organizados de la siguiente 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 |

.

.

.

Y no de la forma más común, que es la forma utilizada por el paquete 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
.
.
.


Esto se debe a la forma en que definimos el diseño mediante grids CSS anidados. Para convertir de la forma más convencional a la forma alternativa es que existe la siguiente función:

En la estructura del proyecto, todo lo relacionado con la función reducer queda en el archivo ./sudoku/app/lib/sudokuReducer.ts.

Componentes y estilo

Ahora, vamos a conocer los componentes visuales con sus respectivos estilos.

El componente raíz y responsable del estado es el componente Game. Dentro de él tenemos un componente Grid y un Menu. El Grid consiste en nueve componentes Box organizados en forma de matriz 3⨉3 y cada Box, a su vez, es una colección de nueve componentes de tipo Cell, organizados en una matriz 3⨉3. El componente Menu permite iniciar un nuevo juego, elegir el nivel de dificultad y mostrar la solución.

La jerarquía de componentes es básicamente así:

  • Game
  • Grid
  • Box
  • Cell
  • Menu

El punto de entrada de nuestra aplicación Next.js es el archivo ./sudoku/app/page.tsx. Es en él donde definimos el componente Game:

La directiva “use client” en la primera línea es necesaria porque a partir de la versión 13, el framework Next.js trata todos los componentes como server components por defecto. Para nosotros, esto significa solo que no podríamos usar hooks de estado y otros dentro de nuestro componente sin la directiva.

Fuera de eso, también definimos en este componente los event handlers, que son esas funciones cuyo nombre comienza con el prefijo handle y que son responsables de reaccionar a las acciones del usuario, influyendo en el estado de la aplicación.

Nota también el uso de useEffect que es un hook que sirve para sincronizar un componente React con algo que mantiene un estado no controlado por React. Lo usamos para detectar teclas presionadas y delegar al handler adecuado.

La forma de estilización de componentes que usaremos son los CSS Modules. En el archivo ./sudoku/app/page.module.css definimos la clase .game:

Y para usar ese estilo, lo importaremos en ./sudoku/app/page.module.tsx:

La magia ocurre en la línea:

Pues esto instruye a Next.js a inyectar el estilo solo en este componente de modo que no necesitamos preocuparnos por la colisión de nombres de clases esparcidas por todo el proyecto. CSS Modules y componentes son un “match made in heaven”.

Los otros componentes son estilizados de manera análoga. Continuando las presentaciones, hablemos del componente Grid, localizado en el archivo ./sudoku/app/components/Grid.tsx. Es un componente sin estado que acepta los siguientes props:

Su definición es la siguiente:

Aquí se utilizó la función estática Array.from para generar un array verdadero a partir de un objeto array-like.

El estilo del componente Grid está definido en ./sudoku/app/components/Grid.module.css:

¡CSS Grids para estilizar el componente Grid! Eso fue muy satisfactorio, but I digress.

El siguiente componente es el Box, que se utiliza solo para simplificar la estilización, pues simplemente aplica su estilo y renderiza sus hijos. Su definición, encontrada en ./sudoku/app/components/Box.tsx es la siguiente:

Y su hoja de estilos, que se encuentra en ./sudoku/app/components/Box.module.css, es de la siguiente manera:

Observa que este también es un CSS Grid.

Hablemos ahora del componente Cell, cuya implementación está en ./sudoku/app/components/Cell.tsx. Este acepta los siguientes props:

Y su implementación es la siguiente:

Nota cómo el estilo cambia de acuerdo con el estado del juego, es decir, si la celda ya estaba marcada en el puzzle inicialmente generado, si está seleccionada o si es solo un espacio vacío.

El CSS correspondiente se encuentra en ./sudoku/app/components/Cell.module.css.

La regla:

Es para impedir que el usuario pueda seleccionar el texto de la celda, pues eso rompe un poco la inmersión del juego y parece que el jugador está en un sitio normal y no en una superaplicación React.

¡Esa palabra clave composes es una forma de herencia entre clases CSS especificada por los CSS Modules!

Consideraciones finales

Una vez terminado, el juego tiene este aspecto:


Los colores se basaron en una paleta para personas con dificultades para ver ciertos colores. ¡Un saludo para los daltónicos!

Hemos llegado al final de este tutorial. Espero haber contribuido a tu aprendizaje en el desarrollo Frontend. ¡Buenos estudios y buena suerte!

Un repositorio Git de este proyecto se encuentra en: https://github.com/lzralbu/sudoku

Una versión en vivo del proyecto puede ser en: https://sudoku-app-lzralbu.vercel.app/

💡
Las opiniones y comentarios emitidos en este artículo son propiedad única de su autor y no necesariamente representan el punto de vista de Listopro.

Listopro Community da la bienvenida a todas las razas, etnias, nacionalidades, credos, géneros, orientaciones, puntos de vista e ideologías, siempre y cuando promuevan la diversidad, la equidad, la inclusión y el crecimiento profesional de los profesionales en tecnología.