Comenzamos con un nuevo capítulo donde vamos a empezar a trabajar de manera práctica con un proyecto sencillo del clásico juego Piedra, Papel y Tijera
.
Está basado en un antiguo artículo que escribí para mostrar como se hacía con Angular, será prácticamente lo mismo, pero adaptado a Qwik 100%.
El objetivo principal será practicar aspectos de Qwik obtenidos a lo largo del libro con el objetivo de hacer algo que nos pueda entretener en momentos puntuales y a la vez nos sirva para afianzar conocimientos.
El juego en si es muy sencillo, pero es super útil para poder repasar algunos conceptos adquiridos durante el libro.
Lo que conseguiremos al final del capítulo será algo similar a lo siguiente:
A continuación os proporciono los puntos que vamos a ver en este capítulo.
Estos pasos los vamos a completar hasta publicar un proyecto Qwik con Netlify.
En este capítulo vamos a crear el mítico juego Piedra, Papel o tijera
que seguramente lo conocéis muy bien y seguramente muchos/as de nosotros/as hemos jugado cuando estábamos en etapas escolares de primaria.
Si no lo habéis jugado, tranquilidad, que la mecánica del juego es super sencilla:
Tenemos 3 elementos, piedra, papel o tijera y en cada jugada cada participante (normalmente es uno contra uno) saca uno de esos 3 elementos, con el objetivo de vencer a su contrincante.
Estas serán las 3 opciones, representadas cada una de ellas por una imagen:
Las combinaciones ganadoras son las siguientes:
Si queréis más información del juego y otros temas relacionados, os invito a que os animéis a acceder a la información de Wikipedia del juego.
Iniciamos un nuevo proyecto llamado 22-paper-scissors-rock.
El proceso para realizarlo ya lo conocéis de sobra
Comenzamos con el apartado visual, donde daremos todos los pasos necesarios hasta llegar a la siguiente apariencia, sin implementar la lógica.
Una vez que ya hemos creado el proyecto, descargamos las tres imágenes que necesitamos para las diferentes opciones del juego.
Estas imágenes nos servirán para ejecutar nuestras jugadas con piedra, papel y tijera.
Añadimos las imágenes dentro del directorio public
con un nuevo directorio que usaremos para almacenar las imágenes, llamado img
(quedando public/img/
), para poder utilizarlas desde cualquier parte de nuestra app accediendo fácilmente.
Descargamos las imágenes desde aquí
Al descargar y añadirlas, si las abrimos, se mostrarán de la siguiente forma (aquí con la correspondiente a tijeras):
Aplicamos el siguiente código CSS en el index.css
(después de crearlo) dentro de src/routes
:
.choice img {
width: 100px !important;
height: 100%;
}
Ya tenemos preparadas las imágenes y podemos añadirlas accediendo a ellas mediante el directorio public
, que será añadiendo en el atributo src
el caracter /
y luego la ruta relativa que corresponde a los ficheros, que se encontrarán dentro del directorio img
, siendo su ruta original public/img/<imagen>.png
.
Por lo tanto, se reflejará el atributo src
con el valor /img/<imagen>.png
, quedando así el contenido de src/routes/index.tsx
teniendo en cuenta que añadimos la referencia al CSS:
import { component$, useStyles$ } from "@builder.io/qwik";
import indexCss from './index.css?inline';
export default component$(() => {
useStyles$(indexCss);
return (
<>
<h1>Rock, Paper and Scissors</h1>
<div class="choice">
<img src="/img/paper.png" />
<img src="/img/rock.png" />
<img src="/img/scissors.png" />
</div>
</>
);
});
Una vez que guardamos los cambios, se muestra la siguiente apariencia:
Debemos de solucionar el problema que se da con el tamaño de la imagen tal y como se ha visto en el capítulo capítulo 6 donde se asigna un tamaño fijo, ya que Qwik requiere de esta propiedad.
Al realizar las correcciones, este será el código resultante:
import { component$, useStyles$ } from "@builder.io/qwik";
import indexCss from './index.css?inline';
export default component$(() => {
useStyles$(indexCss);
return (
<>
<h1>Rock, Paper and Scissors</h1>
<div class="choice">
<img src="/img/paper.png" width={200} height={200}/>
<img src="/img/rock.png" width={200} height={200}/>
<img src="/img/scissors.png" width={200} height={200} />
</div>
</>
);
});
Al guardar, desaparecen las advertencias:
Hemos solucionado el problema de la advertencia, aunque si nos fijamos en la consola, nos encontramos un aviso como el siguiente (el aviso se ha empezado observar a partir de la versión 1.5.0
):
En este caso se nos está recomendando optimizar las imágenes con unas sencillas instrucciones que en este caso, no las seguiremos, ya que vamos a dinamizar las imágenes y con esta operación, conseguiremos eliminar esta advertencia.
Lo que nos vamos a centrar es en construir el juego y en que funcione, luego ya nos centramos en mejoras y optimizaciones.
Aplicando varios cambios en el index.css
donde especificamos cómo estará lo que es el contenedor principal del body
con su estilo, conseguiremos dejarlo sin márgenes, ni relleno para aplicarle un fondo de color personalizado.
/* Para que ocupe toda la ventana y se expanda sin márgenes ni relleno */
* {
margin:0;
padding: 0;
box-sizing: border-box;
}
body{
background-color:#110F26;
color: white;
}
... (lo anterior) ...
Quedará de la siguiente manera, donde queda evidente que tenemos que mejorar mucho el tema de las opciones de juego para considerarlo presentable.
Añadimos varios cambios en el apartado del index.css
en relación a las opciones del juego.
Borde en las opciones.
Centrar el contenido de manera horizontal mediante Flexbox.
Aplicarle un fondo a las imágenes para conseguir una visualización mejor.
Aplicando estos cambios en el index.css:
... (lo inicial) ...
/* Para ubicar las opciones de izquierda a derecha*/
.choices {
margin: 10px;
display: flex; /* or inline-flex */
flex-direction: row;
justify-content: center;
min-height: 100px;
}
.choice img {
width: 100px !important;
height: 100%;
background-color: white;
padding: 15px;
border: 4px solid #333333;
border-radius: 20px;
}
Quedará de la siguiente manera:
Es evidente que ha mejorado bastante la apariencia, quedando muchísimo mejor en lo que respecta a las opciones del juego.
Eso sí, cada uno/a de vosotros/as puede seleccionar los colores y estilos que queráis, esto es una propuesta personal que gustará o no, como es obvio.
En este caso vamos a aplicar los selectores hover
y active
en las opciones del juego mediante CSS en el index.css
, como hasta ahora.
Aplicaremos lo siguiente:
...( lo definido hasta ahora ) ...
.choice img:hover{
cursor: pointer;
background-color:#86849d;
}
.choice img:active{
background-color:#626078;
}
Al guardar y poner el cursor sobre cualquiera de las opciones (en este caso en tijeras):
Ahora nos centramos en el aspecto del título del juego, lo que queremos es que tenga estas características:
Añadimos lo siguiente en los estilos del fichero index.css
:
.title {
width: 100%;
background: white;
color: #110F26;
padding: 20px;
text-align: center;
}
Y una vez añadido el CSS, cambiamos lo que tenemos en src/routes/index.tsx
:
import { component$, useStyles$ } from "@builder.io/qwik";
import indexCss from './index.css?inline';
export default component$(() => {
useStyles$(indexCss);
return (
<>
<h1>Rock, Paper and Scissors</h1>
<div class="choice">
<img src="/img/paper.png" width={200} height={200}/>
<img src="/img/rock.png" width={200} height={200}/>
<img src="/img/scissors.png" width={200} height={200} />
</div>
</>
);
});
Por lo siguiente, incrustando dentro de choices
tres elementos span
con l clase choice
que será la opción de la jugada a realizar:
import { component$, useStyles$ } from '@builder.io/qwik';
import indexCss from './index.css?inline';
export default component$(() => {
useStyles$(indexCss);
return (
<>
<div class='title'>
<h1>¡¡Piedra, Papel ó Tijera!!</h1>
</div>
<div class='choices'>
<span class='choice'>
<img src='/img/paper.png' width={200} height={200} />
</span>
<span class='choice'>
<img src='/img/rock.png' width={200} height={200} />
</span>
<span class='choice'>
<img src='/img/scissors.png' width={200} height={200} />
</span>
</div>
</>
);
});
Con ello, conseguiremos el siguiente resultado:
Ahora que ya tenemos el título y las opciones correctamente añadidas con sus estilos, vamos a terminar con el tema de la apariencia del juego añadiendo un marcador donde se irán contabilizando las puntuaciones usuario-computadora
.
En este caso, para añadir el marcador, sin aplicar estilos, debemos de ir al fichero index.tsx
y añadir lo siguiente junto con lo anterior, entre el título y las opciones del juego:
<div class="score-board">
<div id="user-label" class="badge">user</div>
<div id="comp-label" class="badge">comp</div>
<span id="user-score">0</span>:<span id="comp-score">0</span>
</div>
Quedando en el código de la siguiente forma:
import { component$, useStyles$ } from '@builder.io/qwik';
import indexCss from './index.css?inline';
export default component$(() => {
useStyles$(indexCss);
return (
<>
<div class='title'>
<h1>¡¡Piedra, Papel ó Tijera!!</h1>
</div>
<div class='score-board'>
<div id='user-label' class='badge'>
Usu.
</div>
<div id='comp-label' class='badge'>
Comp.
</div>
<span id='user-score'>0</span>:<span id='comp-score'>0</span>
</div>
<div class='choices'>
<span class='choice'>
<img src='/img/paper.png' width={200} height={200} />
</span>
<span class='choice'>
<img src='/img/rock.png' width={200} height={200} />
</span>
<span class='choice'>
<img src='/img/scissors.png' width={200} height={200} />
</span>
</div>
</>
);
});
Lo que se verá en nuestra aplicación, será algo como lo siguiente:
Es bastante evidente que a este nuevo apartado le tenemos que dar algo más de cariño aplicando estilos.
Comenzamos a darle un estilo bonito para darle más sentido a todo el trabajo que hemos realizado anteriormente con el título y las opciones del juego.
Añadimos en el fichero index.css
lo siguiente, después de lo que ya tenemos añadido anteriormente:
... (sin cambios en lo anterior) ...
/* Marcador */
.score-board{
width: 200px;
border:2px solid white;
text-align: center;
margin:20px auto; /*top and bottom 20 px and centre it*/
padding: 20px 20px;
font-size: 40px;
position: relative;
transition: 0.3s ease-in;
}
.score-board:hover{
background-color: #323042;
}
.badge{
background-color: #3aa330;
padding: 2px 20px;
font-size: 20px;
}
#user-label{
position: absolute;
top:20px;
left: -30px;
}
#comp-label{
position: absolute;
top: 20px;
right: -45px;
}
.result{
font-size: 40px;
text-align: center;
color: white;
}
Con esto que acabamos de añadir, la apariencia ha mejorado bastante y ¡ya queda poco para terminar con la parte gráfica!
Ahora, para terminar con la parte visual y centrarnos en la lógica del juego, añadiremos debajo de las opciones un mensaje Selecciona tu jugada
y encima añadimos un apartado donde añadiremos el mensaje de la jugada realizada con la computadora y el resultado.
Vamos a index.tsx
y creamos una propiedad llamada game
usando useStore
donde almacenará los datos principales del juego con data
almacenando la jugada realizada y los puntos acumulados por parte del jugador (nosotros) y por parte de la computadora.
Añadimos de la siguiente forma:
import { ..., useStore, ... } from '@builder.io/qwik';
export default component$(() => {
useStyles$(indexCss);
const game = useStore({
data: {
result: 'Esperando la primera jugada...',
pointsUser: 0,
pointsComp: 0,
},
})
return (
...
);
});
Habiendo inicializado mediante useStore
los datos principales del juego, aplicamos las puntuaciones en el código JSX, cambiando lo que corresponde a lo siguiente:
<span id='user-score'>0</span>:<span id='comp-score'>0</span>
Por lo siguiente:
<span id='user-score'>{game.data.pointsUser}</span>:<span id='comp-score'>{game.data.pointsComp}</span>
Lo que nos quedaría es añadir la información correspondiente al valor game.data.result
que lo haremos añadiendo el elemento entre las opciones del marcador (score-board
) y las opciones del juego (choices
):
Y este es el resultado, donde se puede observar que el apartado de info-game
(Información del juego) no está alineado correctamente:
Aplicamos los siguientes estilos junto con lo ya definido en el index.css
para que tome un buen aspecto:
... (código ya definido anteriormente ) ...
.title, .info-game {
width: 100%;
background: white;
color: #110F26;
padding: 20px;
text-align: center;
}
.info-game {
background-color: transparent;
color: white;
font-size: 24px;
}
Al aplicar los estilos:
Ahora que ya hemos trabajado con la parte visual, nos vamos a centrar en que el juego funcione correctamente, para que podamos echarnos unas partidas, cuando estemos con ganas de entretenernos, por estar aburridos.
En este caso, vamos a definir la función que se encargará de realizar la selección de la opción de la jugada por parte de la computadora y esto lo realizaremos mediante una función aleatoria (random).
Debemos de ir dentro de src
, creamos un nuevo directorio llamado helpers
y creamos un fichero llamado random.ts
con el siguiente código:
/**
* Obtener un valor entero aleatorio desde 0 hasta el
* maxValueNoInclude - 1
* @param maxValueNoInclude Valor no incluido. Si tenemos 3, devuelve
* 0, 1 o 2
* @returns {number}
*/
export const getRandomInt = (maxValueNoInclude: number) => Math.floor(Math.random() * maxValueNoInclude);
Este código lo que hace es generar números aleatorios enteros, desde 0 hasta la longitud (no incluido) que le especifiquemos.
En este caso tenemos 3 opciones, pasándole un 3, obtenemos 0, 1 o 2.
Definimos la función getComputerChoice
para obtener la selección aleatoria de la computadora, para jugar contra nosotros mismos y poder tener una partida completa.
En src/routes/index.tsx
añadimos dentro de component$()
lo siguiente:
import { $, component$, useStore, useStyles$ } from '@builder.io/qwik';
import { getRandomInt } from '~/helpers/random';
import indexCss from './index.css?inline';
export default component$(() => {
...
const getComputerChoice = $(async () => {
const choices = ['r', 'p', 's']; // Roca, Papel, Tijeras
const randomChoice = getRandomInt(choices.length);
return choices[randomChoice];
});
return (
...
);
});
Por el momento, esta función no se usa, pero la empezaremos a usar a continuación, cuando ejecutemos nuestra jugada para empezar a jugar.
Ahora lo que vamos a definir es el resultado después de que el usuario haga su selección.
Cuando se haga la selección por parte del usuario, se pondrá en marcha la función juego, donde creamos un string que será una jugada (gamePlay
) que utilizaremos para determinar si el usuario ha ganado, ha perdido o ha empatado con la máquina.
Tenemos las siguientes combinaciones
Gana usuario:
rs
: Roca vs tijera.
sp
: Tijera vs papel.
pr
: Papel vs roca
Pierde usuario:
rp
: Roca vs papel.
ps
: Papel vs Tijera.
sr
: Tijera vs roca
Empatan los jugadores:
rr
: Roca vs Roca.
ss
: Tijera vs tijera.
pp
: Papel vs papel
Esto será traducido dentro de src/routes/index.tsx
de la siguiente forma:
// 1.- Lógica de la jugada
const gamePlay = $(
// 2.- Opción seleccionda por el usuario
async (userChoice: string): Promise<void> => {
// 3.- Jugada realizada con opciones del usuario + computadora
const playUserComp = userChoice + (await getComputerChoice());
console.log(`Jugada en progreso: ${playUserComp}`);
// 4.- Estado de la partida en esta jugada
let playStatus: {
message: string;
userAdd: number;
compAdd: number;
} = {
message: '',
userAdd: 0,
compAdd: 0,
};
switch (playUserComp) {
// 5.- Ganamos
case 'rs':
case 'sp':
case 'pr':
playStatus = {
message: 'Jugada ganada',
userAdd: 1,
compAdd: 0,
};
break;
// 6.- Gana la computadora
case 'rp':
case 'ps':
case 'sr':
playStatus = {
message: 'Gana la computadora',
userAdd: 0,
compAdd: 1,
};
break;
// 7.- Empatamos
case 'rr':
case 'pp':
case 'ss':
playStatus = {
message: 'Empate entre jugador-computadora',
userAdd: 0,
compAdd: 0,
};
break;
}
// 8.- Actualiza las puntuaciones globales y mensaje
game.data = {
...game.data,
result: playStatus.message,
pointsUser: game.data.pointsUser + playStatus.userAdd,
pointsComp: game.data.pointsComp + playStatus.compAdd,
};
}
);
Ahora que ya hemos definido el resultado de la selección del jugador contra la computadora, vamos a habilitar la opción para enviar lo que hemos elegido para realizar la jugada ya definida en el punto anterior.
Añadimos la función gamePlay
mediante el evento onClick$
en las diferentes opciones dentro de src/routes/index.tsx
, con un parámetro de tipo string que será la selección del usuario.
Por ejemplo, con la opción Papel
dejamos de la siguiente forma:
<span class='choice' onClick$={() => gamePlay('p')}>
<img src='/img/paper.png' width={200} height={200} />
</span>
Ahora implementando en todas las opciones:
<div class='choices'>
<span class='choice' onClick$={() => gamePlay('p')}>
<img src='/img/paper.png' width={200} height={200} />
</span>
<span class='choice' onClick$={() => gamePlay('r')}>
<img src='/img/rock.png' width={200} height={200} />
</span>
<span class='choice' onClick$={() => gamePlay('s')}>
<img src='/img/scissors.png' width={200} height={200} />
</span>
</div>
Guardamos y aunque visualmente no ha cambiado nada, ahora ya tenemos implementada la lógica del juego y por cada jugada, nos debería de ir dando el resultado de quien ha ganado esa jugada (o empate) y el estado actual del marcador.
Abrimos la consola en las Herramientas de desarrollador
de nuestro navegador favorito, para observar el mensaje de la jugada que se realiza con Jugada en progreso: <opción-jugador><opción-computadora>
.
Ejemplo 1: Seleccionando Papel
(p
)
Como se puede observar, la jugada lanzada por la computadora también ha sido la misma (p
) por lo que se puede apreciar en el mensaje del juego es que hemos empatado.
Ejemplo 2: Seleccionando de nuevo Papel
(p
)
En este caso, no hemos tenido suerte y hemos perdido la jugada, por lo que el contador de la computadora se incrementa y muestra el mensaje Gana la computadora
.
Ejemplo3: Seleccionando Piedra
(r
)
Otro empate
Y siguiendo este flujo de jugadas así quedaría el estado de la partida (podéis ir comprobando las combinaciones):
Después de ir haciendo todos los pasos, ¡el juego ya estará disponible en su versión más básica! Ahora lo que nos queda es disfrutar del juego y aplicarle mejoras, que espero que lo hagáis en el código que colgaré para contribuir y mejorar el juego.
Visualmente no va a cambiar nada, lo que vamos a hacer es aplicar mejorar de refactorizaciones principalmente estructurando el código en componentes, dinamizando las opciones del juego y mucho más.
Teniendo en cuenta la aplicación vamos a separar lo que corresponde la interfaz de usuario, en varios apartados, donde vamos a eliminar el apartado donde se muestra el mensaje Selecciona tu partida
ubicado en la parte inferior:
Title
(1): Apartado del títuloScoreBoard
(2): Panel de las puntuaciones de la partida con sus elementos como la etiqueta de usuario y computadora. Dentro de este componente añadiremos el componente Badge
.Badge
(5): Para añadir un label con un fondo bonito, en este caso para los elementos donde se muestra la información Usu.
y Comp.
haciendo referencia a los jugadores participantes.GameStatus
(3): Mensaje del estado del juego.Choices
(4): Opciones del juego donde tenemos los botones para elegir nuestra jugada. Aquí añadiremos dentro dinámicamente las diferentes jugadas.Esta distribución la podemos hacer como nosotros podamos pensar que es mejor.
Esta es mi propuesta y os animo a que lo implementéis con vuestra forma de trabajar.
Creamos un nuevo directorio llamado game
dentro de src/components
y dentro de este (src/components/game
) creamos los directorios correspondientes a cada uno de los componentes, junto con su fichero index.tsx
:
Title
: src/components/game/title
.ScoreBoard
: src/components/game/score-board
.Badge
: src/components/game/badge
.GameStatus
: src/components/game/game-status
.Choices
: src/components/game/choices
.Quedará estructurado de la siguiente forma:
├── src
│ ├── components
│ │ ├── game
│ │ │ ├── badge
│ │ │ │ └── index.tsx
│ │ │ ├── choices
│ │ │ │ └── index.tsx
│ │ │ ├── game-status
│ │ │ │ └── index.tsx
│ │ │ ├── score-board
│ │ │ │ └── index.tsx
│ │ │ └── title
│ │ │ └── index.tsx
│ │ ...
Title
: Implementamos este componente donde será un componente super sencillo y "tonto".export const Title = () => {
return (
<div class='title'>
<h1>¡¡Piedra, Papel ó Tijera!!</h1>
</div>
);
};
Ahora sustituimos el contenido dentro de src/routes/index.tsx
sustituyendo lo siguiente:
<div class='title'>
<h1>¡¡Piedra, Papel ó Tijera!!</h1>
</div>
Por lo siguiente:
...
// 1.- Importarlo
import { Title } from '~/components/game/title';
...
export default component$(() => {
....
// 2.- Añadir en el código JSX
return (
<>
<Title />
<div class='score-board'>
...
</>
);
});
Guardamos los cambios y en lo que respecta a la apariencia sigue igual, lo que cambia es en la estructura.
Abrimos el Inspector de elementos
y si os fijáis en el caso de este nuevo componente, está haciendo referencia a src/components/game/title/index.tsx
, por lo que ya lo está implementando correctamente:
GameStatus
: Implementamos este, el que dice el resultado de la jugada ya que es sencillo de hacerlo, como en Title
pero pasándole la información de game.data.result
.Antes de seguir, vamos a definir el modelo para pasar entre los diferentes componentes la información de la partida mediante props
. También existe la posibilidad que lo hagáis con el Context API
tal y como se ha definido en el Capítulo 8.
Lo que corresponde a la definición del modelo, lo realizamos, independientemente de trabajar con los props
o el Context API
.
Creamos el fichero game.ts
dentro de la carpeta models
que creamos en el directorio src
, quedando de la siguiente forma src/models/game.ts
cuyo código será el siguiente:
export interface GameStore {
// 1.- Datos de la partida principal
data: {
result: string;
pointsUser: number;
pointsComp: number;
};
// 2.- Datos de las opciones del juego
imageProperties: {
width: number;
height: number;
}
choices: {
rock: {
img: string;
alt: string;
},
paper: {
img: string;
alt: string;
},
scissors: {
img: string;
alt: string;
}
}
}
// 3.- Añadimos para poder pasarlo entre componentes la información de la partida
export interface GameProps {
game: GameStore;
}
Ahora que ya tenemos lo siguiente vamos a src/components/game-status/index.tsx
y añadimos lo siguente como base del componente, donde se pasa la información del estado del juego y se muestra el valor result
dentro de que se ha establecido como la parte del juego en data
.
En este caso sería suficiente con pasar un prop
con un string, pero vamos a pasarlo todo, ya que quiero enseñaros algo que ocurrirá enseguida:
import { component$ } from '@builder.io/qwik';
import { GameProps } from '~/models/game';
export default component$((store: GameProps) => {
const {
game: {
data: { result },
},
} = store;
return <p class='info-game'>{result}</p>;
});
Ahora, pasamos la información del juego desde src/routes/index.tsx
a este nuevo componente, eliminando el apartado:
<p class='info-game'>{game.data.result}</p>
Sustituyéndolo por:
import GameStatus from '~/components/game/game-status';
export default component$(() => {
...
return(
...
<GameStatus game={game} />
);
});
Y ahora nos da un error en ese apartado:
Si ponemos el cursor sobre el error, se muestra el mensaje descriptivo:
Property 'choices' is missing in type '{ data: { result: string; pointsUser: number; pointsComp: number; }; }' but required in type 'GameStore'.ts(2741)
game.ts(7, 5): 'choices' is declared here.
game.ts(28, 5): The expected type comes from property 'game' which is declared here on type 'IntrinsicAttributes & Omit<GameProps, `${string}$`> & _Only$<GameProps> & ComponentBaseProps & { ...; }'
(property) game: GameStore
¿Qué está ocurriendo? Como se puede leer, hay que definir choices
en ese valor que estamos pasando, cosa que no está ocurriendo.
En src/routes/index.tsx
, pasamos de este valor inicial definido:
const game = useStore({
data: {
result: 'Esperando la primera jugada...',
pointsUser: 0,
pointsComp: 0,
},
});
A definir todo la información requerida, quedando de la siguiente forma:
const game = useStore({
data: {
result: 'Esperando la primera jugada...',
pointsUser: 0,
pointsComp: 0,
},
imageProperties: {
width: 100,
height: 100,
},
choices: {
rock: {
img: '/img/rock.png',
alt: 'Rock Image',
},
paper: {
img: '/img/paper.png',
alt: 'Pape Image',
},
scissors: {
img: '/img/scissors.png',
alt: 'Scissors Image',
},
},
});
Si guardamos, ese error que se muestra, ya debe de desaparecer mostrándose correctamente el estado del juego.
Esta información choices
la usaremos posteriormente en el componente choices
que se encuentra en src/components/choices/index.tsx
.
El estado actual (sin error) del componente GameStatus
desde la referencia de src/components/game-status/index.tsx
:
Choices
: Para implementar las opciones de las jugadas a realizar.Dentro de Choices
tendremos los tres elementos. Por ejemplo, la Piedra
será un elemento dentro de las tres opciones que disponemos en este juego.
Antes de meternos de lleno en el componente mencionado, vamos a extraer la función gamePlay
y getComputerChoice
de dentro de src/routes/index.tsx
y las vamos a refactorizar dentro de src/helpers
.
Dentro de este nuevo directorio creamos un fichero llamado game.tsx
, añadiendo el código de la siguiente forma, donde vamos a añadir junto con userChoice
como parámetro de entrada el valor game
que trae los datos de la partida.
Esto todo quedará de la siguiente manera en src/helpers/game.tsx
:
export const gamePlay = $(
async (userChoice: string, game: GameStore): Promise<void> => {
const playUserComp = userChoice + (await getComputerChoice());
console.log(`Jugada en progreso: ${playUserComp}`);
let playStatus: {
message: string;
userAdd: number;
compAdd: number;
} = {
message: '',
userAdd: 0,
compAdd: 0,
};
switch (playUserComp) {
// Ganamos
case 'rs':
case 'sp':
case 'pr':
playStatus = {
message: 'Jugada ganada',
userAdd: 1,
compAdd: 0,
};
break;
// Gana la computadora
case 'rp':
case 'ps':
case 'sr':
playStatus = {
message: 'Gana la computadora',
userAdd: 0,
compAdd: 1,
};
break;
// Empatamos
case 'rr':
case 'pp':
case 'ss':
playStatus = {
message: 'Empate entre jugador-computadora',
userAdd: 0,
compAdd: 0,
};
break;
}
game.data = {
...game.data,
result: playStatus.message,
pointsUser: game.data.pointsUser + playStatus.userAdd,
pointsComp: game.data.pointsComp + playStatus.compAdd,
};
}
);
Y una vez implementados los cambios de la refactorización dentro de src/routes/index.tsx
debe de quedar de la siguiente forma, donde la llamada a gamePlay
habrá que pasarle como segundo argumento el objeto game
:
gamePlay(<JUGADA>, game)
Código completo:
import { component$, useStore, useStyles$ } from '@builder.io/qwik';
import { Title } from '~/components/game/title';
import indexCss from './index.css?inline';
import GameStatus from '~/components/game/game-status';
import { gamePlay } from '~/helpers/game';
export default component$(() => {
useStyles$(indexCss);
const game = useStore({
data: {
result: 'Esperando la primera jugada...',
pointsUser: 0,
pointsComp: 0,
},
imageProperties: {
width: 100,
height: 100,
},
choices: {
rock: {
img: '/img/rock.png',
alt: 'Rock Image',
},
paper: {
img: '/img/paper.png',
alt: 'Pape Image',
},
scissors: {
img: '/img/scissors.png',
alt: 'Scissors Image',
},
},
});
return (
<>
<Title />
<div class='score-board'>
<div id='user-label' class='badge'>
Usu.
</div>
<div id='comp-label' class='badge'>
Comp.
</div>
<span id='user-score'>{game.data.pointsUser}</span>:
<span id='comp-score'>{game.data.pointsComp}</span>
</div>
<GameStatus game={game} />
<div class='choices'>
<span class='choice' onClick$={() => gamePlay('p', game)}>
<img src='/img/paper.png' width={200} height={200} />
</span>
<span class='choice' onClick$={() => gamePlay('r', game)}>
<img src='/img/rock.png' width={200} height={200} />
</span>
<span class='choice' onClick$={() => gamePlay('s', game)}>
<img src='/img/scissors.png' width={200} height={200} />
</span>
</div>
</>
);
});
Una vez que se ha extraido la lógica de las jugadas a funciones helpers
, vamos a implementar el componente Choices
dentro de src/components/choices/index.tsx
.
Aquí implementaremos de manera dinámica la lógica individual de cada opción, teniendo en cuenta su imagen y el dato que se envía como jugada.
Por ejemplo, con el papel tenemos que tener en cuenta estos datos:
/img/paper.png
p
En estos momentos, en el objeto game
de tipo GameStore
no se almacena el valor de la jugada que se realizará con cada una de las opciones.
Por lo tanto, modificamos el modelo de GameStore
con la nueva propiedad option
en cada una de las opciones (choices
):
Ahora saltará un error por no definir los datos de option
en cada una de las opciones:
Añadimos a cada uno de ellos la jugada correspondiente:
p
: Papel.r
: Piedra.s
: Tijeras.Quedando así:
const game = useStore({
...
choices: {
rock: {
img: '/img/rock.png',
alt: 'Rock Image',
option: 'r'
},
paper: {
img: '/img/paper.png',
alt: 'Pape Image',
option: 'p'
},
scissors: {
img: '/img/scissors.png',
alt: 'Scissors Image',
option: 's'
},
},
});
Si guardamos, el juego debería de seguir funcionando igual, sin problemas.
Implementamos la lógica del componente Choices
, añadiendo el siguiente código:
import { component$ } from '@builder.io/qwik';
import { gamePlay } from '~/helpers/game';
import { GameProps } from '~/models/game';
export const Choices = component$<GameProps>((props) => {
const { game } = props;
const gameChoices = Object.keys(game.choices);
return (
<div class='choices'>
{gameChoices.map((optionKey) => {
const { img, alt, option } = (game.choices as any)[optionKey] as {
img: string;
alt: string;
option: string;
};
return (
<span key={'choice_game_' + index} class='choice' onClick$={() => gamePlay(option, game)}>
<img src={img} width={200} height={200} alt={alt} />
</span
);
})}
</div>
);
});
Una vez refactorizado el código al componente Choices
que lo encontramos en src/game/choices/index.tsx
lo añadimos en src/routes/index.tsx
eliminando el código correspondiente a la refactorización, quedando así:
import { component$, useStore, useStyles$ } from '@builder.io/qwik';
import { Title } from '~/components/game/title';
import GameStatus from '~/components/game/game-status';
import { Choices } from '~/components/game/choices';
import indexCss from './index.css?inline';
export default component$(() => {
useStyles$(indexCss);
const game = useStore({
data: {
result: 'Esperando la primera jugada...',
pointsUser: 0,
pointsComp: 0,
},
imageProperties: {
width: 100,
height: 100,
},
choices: {
rock: {
img: '/img/rock.png',
alt: 'Rock Image',
option: 'r'
},
paper: {
img: '/img/paper.png',
alt: 'Pape Image',
option: 'p'
},
scissors: {
img: '/img/scissors.png',
alt: 'Scissors Image',
option: 's'
},
},
});
return (
<>
<Title />
<div class='score-board'>
<div id='user-label' class='badge'>
Usu.
</div>
<div id='comp-label' class='badge'>
Comp.
</div>
<span id='user-score'>{game.data.pointsUser}</span>:
<span id='comp-score'>{game.data.pointsComp}</span>
</div>
<GameStatus game={game} />
<Choices game={game}/>
</>
);
});
Guardando los cambios, el juego debería de seguir funcionando de la misma forma.
Llegados a este punto hemos refactorizado el apartado del título (Title
), el estado del juego (GameStatus
) y el apartado de las opciones de selección de jugadas (Choices
).
Nos queda la parte del panel de puntuación, donde este componente se llamará ScoreBoard
que contendrá el componente Badge
para las etiquetas de los jugadores.
Se pide refactorizar lo siguiente:
En lo siguiente:
Y que el juego siga comportándose del mismo modo.
Aparte de esta refactorización, se podría extraer:
Resultado de lo trabajado en este capítulo
El código que encontráis es el resultado final de todo el proceso realizado durante el capítulo. Os recomiendo que vayáis haciendo los pasos poco a poco y paso a paso para ir interiorizando todos los conceptos y en este caso, pienso que os vendría bien realizar pruebas con más ejemplos, proponiendo alguna funcionalidad real y que sea útil.
https://shorten-up.vercel.app/aiAc9jXCbg
¿No os habéis dado cuenta que estamos pasando a todos los componentes hijos el
props
del juego llamadogame
?¿Por qué no usamos el
ContextAPI
? (Recordad que lo hemos aprendido en el capítulo 8)Este sería el resultado aplicando
ContextAPI
:
https://shorten-up.vercel.app/yWTIdlC7edY el
src/routes/index.tsx
quedará así de limpio:
![]()
Al finalizar este capítulo, hemos reforzado los siguientes conceptos:
Piedra, Papel o Tijera
.Aunque el juego sea simple, analizar los pasos dados nos permite reforzar conceptos y avanzar en nuestro conocimiento.
Llegados a este punto, hemos concluido este capítulo, donde aplicamos conceptos previamente vistos para crear un juego sencillo y divertido.
En conjunto, estos conceptos nos permiten desarrollar una experiencia de usuario interactiva y atractiva en el juego de Piedra, Papel o Tijera
, al tiempo que repasamos conceptos fundamentales.