Si llegaron a este post directo, acá los links a los anteriores:

En la Parte 2 dejamos todo preparado para ya trabajar con redux. Incluso pasamos por todo el flujo básico de modelar una interacción que dispara la mutación del estado, es decir un action + creator, lógica en el reducer, y los connects necesarios para que un componente react dispare esa mutación, y para que otro componente este mostrando dicho estado y se actualice entonces automáticamente. Todo eso era para “iniciar un juego”.

Vamos a continuar entonces con la lógica de juego. Es decir con los actions requeridos para seguir jugando.

Conectando el estado al <Juego>

Lo primero que necesitamos es poder empezar a ver el estado real del juego, el de redux. Así que vamos a modificar Juego.jsx para sacarle los datos hardcodeados y en su lugar conectarlo con el store.

Modificamos Juego para eliminar los datos hardcodeados y en su lugar recibirlos como una prop “ronda”.

Nótese que ahora recibe una “ronda” como props, que tiene las cartas de ambos jugadores y las manos jugadas.

Hay un detalle acá que resolvimos de forma medio temporal. Cuando la aplicación carga inicialmente no hay juego, con lo cual tampoco hay ronda, hasta que el jugador clickea en el link. Eso despierta un nuevo problema/oportunidad en cuanto a diseñar como será la UX del juego al ingresar. Como preferimos no encarar ese detalle por ahora, lo que hicimos fue defaultear el valor, en caso de no haber una “ronda” creamos un esqueleto vacío de una de modo de que los componentes internos no se rompan. Decidimos hacerlo acá en el componente y no en el reducer ya que nos parece que es un tema de vista (cómo la app se va a mostrar cuando no hay un juego actual), y que el estado ya tiene todo lo necesario para expresar que “no hay un juego”. Introducir una ronda vacía en el state, sólo para que no se rompan los componentes de vista sería ensuciar el state con incumbencias o un detalle de implementación de la vista / componente. Entonces dejemos “la chanchada” en el lugar que corresponde: la vista.

Ahora sí, ya teníamos un container para conectarlo al action de iniciar juego, así que usamos ese.

Agregamos un mapStateToProps para conectar el Juego.jsx con la “ronda”

Fíjense un detalle interesante por ahora acá. Estamos conectando sólo la “ronda” del store al componente Juego. Sin embargo, si recordamos este componente no sólo muestra la ronda sino que también incluye el puntaje, que, en el store, está fuera de la ronda. Sin embargo no estamos conectando eso. Sin embargo el puntaje se va a ver bien.

Esto es porque en la “parte 2” ya conectamos el componente Puntaje al puntaje del store. Este detalle es muy importante, porque es una de las diferencias de manejar el estado en una aplicación “React pura” y una con “Redux”. Ya lo dijimos, pero vale la pena verlo ahora concreto en este ejemplo. Cuando un action modifique el puntaje del store, sólo el componente Puntaje se va a actualizar y no así todo el Juego.

Refactor: aparece <Ronda>

Aprovechamos para hacer un refactor en linea con esto mismo. Si ahora cambia algo de la “ronda” sí va a pasar que se renderiza Juego, y por consiguiente también Puntaje (Juego > Puntaje)

Miremos para qué usa los datos de la ronda en una version simplificada

function Juego({ ronda: { cartas, manos } }) {  ...
<div>
<Mano cartas={cartas.ellos} oponente />
<Mesa manos={manos} />
<Mano cartas={cartas.nosotros} />
</div>
...
}

Básicamente se usan para esa porción del html. Entonces, podríamos extraerla en un nuevo componente, de modo de poder partir el Juego y que la ronda y el puntaje se muestren con componentes “atómicos” chiquitos que podamos conectar y que no afecten a los otros.

touch src/components/Ronda.jsx
touch src/containers/Ronda.jsx

El nuevo componente Ronda.jsx

Nos trajimos las partes de Juego.jsx que mostraban la ronda (incluído el hackeo para la “no-ronda”)
Nos trajimos el connect de la ronda a un container propio de Ronda.

Por último el Juego y “Juego container” también se modifican para usar este nuevo componente y para eliminar el mapStateToProps que ya no necesita (sí, lo hicimos 5 minutos antes y ya lo estamos borrando, así es la vida del programador :S)

El container vuelve exactamente a como estaba antes.. así que no lo incluímos acá :)

Lo que ganamos con esto es que ahora, si cambia la ronda, sólo se ve afectado este nuevo componente y no todo el Juego completo.

Diferentes “ramas” del state ahora actualizan sólo los componentes que realmente muestran esa data.

Barajando las cartas

Si clickean en iniciar juego ahora, va a explotar todo. Porque nuestro action de iniciar un juego espera recibir las cartas ya barajadas. Eso nos vino bien en su momento. Quizás ahora debamos cambiarlo para simplificar un poco la vista.

Pero primer necesitamos las cartas, así que vamos a trabajar un poco más en el comportamiento ahora. Hacemos una función que nos permita crear todas las cartas del truco. Si usamos las funciones de ramda entonces es bastante simple.

En actions/juego.js agregamos

import { always, range, pipe, without, xprod, map, zipObj } from 'ramda'export const cartas = pipe(
always(range(1, 13)),
without([8, 9]),
xprod(Object.values(Palo)),
map(zipObj(['palo', 'numero']))
)

Si no entienden qué hace eso, es un lindo mini-ejercicio para empezar a adaptar la cabeza a una forma mas “funcional” de programar con JS + ramda. Pueden ver la documentación de cada función en la doc de ramda

Luego un pequeño test en actions/juego.spec.js :

describe('cartas', () => {  it('crea una lista de objetos con palo y numero', () => {
const lasCartas = cartas()
expect(lasCartas.length).toEqual(40)
expect(lasCartas.every(o => o.palo && o.numero)).toBeTruthy()
expect(lasCartas.filter(o => o.numero === 1)).toEqual([
{ palo: 'bastos', numero: 1 },
{ palo: 'oros', numero: 1 },
{ palo: 'copas', numero: 1 },
{ palo: 'espadas', numero: 1 }
])
})
})

Ahora que tenemos las cartas podemos barajarlas. Para eso hacemos otra función que utiliza a la anterior en actions/juego.js

import shuffle from 'shuffle-array'export const barajar = pipe(
cartas,
tap(shuffle),
take(6),
splitEvery(3),
zipObj([Turno.NOSOTROS, Turno.ELLOS])
)

Esto requiere importar un par de funciones extra de ramda como tap, take, y splitEvery. También requiere que instalemos una librería que provee la función shuffle (que es impura :( y por eso tuvimos que usar tap )

yarn add shuffle-array

Y, obviamente le hacemos al menos un test

describe('barajar()', () => {

it('retorna un objeto con "nosotros" y "ellos" y 3 cartas en cada uno', () => {
const barajado = barajar()

const expectMano = mano => {
expect(mano).toBeTruthy()
expect(mano.every(o => o.palo && o.numero)).toBeTruthy()
}

expectMano(barajado.ellos)
expectMano(barajado.nosotros)
})
})

Listo, entonces lo que nos queda es encontrar una forma de usar el action creator que ya teníamos, que era “predecible”, porque recibía las cartas ya barajadas y el turno del jugador inicial. Ya tenemos una forma de barajar “random”. Lo que nos falta es “randomizar” también el turno.

Fácil, una función (todo esto por ahora en actions/juego.jsx):

const turnoRandom = () => 
Math.random() > 0.5 ? Turno.NOSOTROS : Turno.ELLOS

Entonces tenemos ahora 4 cosas:

  • el action creator: iniciarJuego(cartas, turno)
  • función para crear “cartas”: barajar
  • función para crear el “turno”: turnoRandom
  • Componente conectado: Jugar, el container conecta la UI con una función, actualmente eso es iniciarJuego, pero podría ser otra (guiño, guiño)

Las 3 primeras funciones se podrían combinar en una nueva, que represente el “iniciar juego barajando y asignado un turno random”. Así de simple

export const iniciarJuegoRandom = () => 
iniciarJuego(barajar(), turnoRandom())

Esto ya es un action creator de redux, ya que simplemente delega en el que ya teníamos. Al final un action creator es cualquier función, no importan cuan simple o compleja sea, que retorna un objeto con type

Ahora simplemente “recableamos” el connect de containers/Juego.jsx para usar este nuevo actionCreator en lugar del anterior

import { iniciarJuegoRandom } from '../actions/juego'const mapActionsToProps = dispatch => ({
onIniciarJuego: () => dispatch(iniciarJuegoRandom())
})

Y simplemente con eso debería empezar a funcionar.

Conectamos el botón a una implementación real de “iniciarJuego” un action creator que baraja las cartas (random) y asigna el turno (también random). Luego el reducer hace su trabajo y el connect + React hacen el suyo y todo se “refresca” automáticamente (en tu cara MVC !)

Algo de vista: el turno

Nos queda un detalle pendiente: indicar a nivel de UX, de quién es el turno actual. Ahora vemos que es bastante molesto ya que no nos permite verificar visualmente lo aleatorio del iniciarJuego. Así que.. ya que estamos hagamos algo simple

Si repasamos la forma del state (puede ser útil mirar el código del reducer) nos damos cuenta que la info de quién es el turno está dentro de “ronda”. Y justamente <Ronda> ya está conectado con esa data, y muestra ambas manos y la mesa. Así que parece un buen lugar.

Lo que vamos a construir es esto:

Agregando feedback para el “turno actual”. Como la data ya estaba en redux e incluso conectada, sólo nos encargamos de la vista.

Editamos Ronda.jsx para introducir un nuevo componente que va a “envolver” a las <Mano> que teníamos, y que va a agregar el cursor cuando corresponda, y cuando no, dejar el espacio.

Nótese el import de ManoConTurno, y que ahora <Mano> están envueltas en ese <ManoConTurno> que recibe una property booleana que indica si es o no el turno.

touch src/components/ManoConTurno.jsx
touch src/components/ManoConTurno.css

El componente

Y acá hay algo interesante tal vez, si nunca habían hecho este tipo de componentes en React. Como el objetivo es que “envuelva” a otro componente (en nuestro caso la <Mano> pero podría ser cualquier elemento), estamos usando una prop reservada de React llamada children Este children representará a todo el contenido que escribió quien nos usó a nosotros componente. Así desde afuera nos están pasando ese contenido. Pueden leer más sobre esto en la doc de React

Sus estilos

Esto requiere que bajemos una imagen (con licencia que nos permite reutilizarla :P)

curl https://upload.wikimedia.org/wikipedia/commons/f/f6/Icon_Arrow_Right_256x256.png --output public/images/turno-flecha.png

Eso sería todo !

Jugar una Carta

Para esto tenemos que hacer varias cosas

  • la lógica de estado de redux: el action, el reducer, sus tests, etc.
  • asegurarnos que estamos “cableando” bien los callbacks entre los componentes: recuerden que lo que se clickea es una Carta pero eventualmente eso debe propagar un evento que termine dispatcheando el action.
  • condiciones de negocio: sólo se puede seleccionar una carta si es nuestro turno.

Cableado de callbacks

Lo más, molesto. Tenemos que tocar en varios lugares. bah.. molesto de documentar, para mí :P

Acá tomamos una decisión un poco arbitraria. Ya sabemos que redux nos permitiría “inyectarle” (conectarle), el action (la función posta que va a jugar la carta), a cualquier componente de react. Podríamos incluso inyectarla directo en cada <Carta>. Pero por un lado es un poco molesto hacer el container y por el otro tendríamos que modificar la Mano para que ahora siempre use el container de <Carta> con lo cual Mano dejaría de ser fácilmente testeable o utilizable desde storybooks (necesitaría tener redux). Con lo cual decidimos que el connect lo vamos a hacer reusando el container de Juego.jsx que ya tenemos y que ya inyecta el action de iniciarJuego(Random). Lo malo de este está solución es que tenemos que hacerle llegar la función hasta abajo, a la <Carta>. Que es todo el cableado qu edecimos.

Aclaramos esto, para contar que habría varias formas de hacerlo, y que ninguna es perfecta, y tienen sus ventajas y puntos flojos.

Entonces tocamos desde “afuera” hacia dentro. En Juego.jsx agregamos una prop que será la función a ejecutar al jugar una carta, y se la pasamos a la Ronda

export default function Juego({ onIniciarJuego, onJugarCarta }) {
...
<Ronda onJugarCarta={onJugarCarta} />
...
}

Lo mismo en Ronda que propaga a la Mano

const Ronda = ({ onJugarCarta, ronda: { cartas, manos, turno } = noRonda }) => (
...
<Mano cartas={cartas.nosotros} onClick={onJugarCarta} />
...
)

La Mano recibe ahora ese onClick y lo propaga a la Carta (ojo, sólo en caso de que sean las nuestras y no las de oponente, ni tampoco las que ya fueron jugadas)

export default function Mano({ cartas, seleccionable, oponente, onClick }) {
...
const crearCarta = oponente ?
(carta, i) => <CartaOponente key={i} jugada={carta.jugada}/>
: (carta, i) => carta.jugada ?
<CartaJugada key={i} />
: <Carta key={i} carta={carta} seleccionable onClick={onClick} />
...
}

Finalmente por ahora metemos un action “hardcodeado” en containers/Juego.jsx

const mapActionsToProps = dispatch => ({
onIniciarJuego: () => dispatch(iniciarJuegoRandom()),
onJugarCarta: carta => console.log('Carta jugada', carta)
})

Si ahora usamos la app vamos a ver ese console log

Obvio que le podríamos haber hecho un test al Juego pasando un spy callback de jest para checkear que se llame. E incluso tener testeados todos los casos, de que no se llame en cartas ya jugada o del oponente.

Lógica de Redux

Agregamos un nuevo action de redux que represente la orden de “jugar una carta X”.

export const JUGAR_CARTA = 'JUGAR_CARTA'export const jugarCarta = carta => ({
type: JUGAR_CARTA,
carta
})

Y algunos tests iniciales del reducer, sobre cómo deberá alterar las “manos” de la ronda y de quién será el siguiente turno

Testeamos el reducer que juegue la carta en la mano apropiada y que alterne el turno

Agregamos entonces un nuevo caso en el reducer

case JUGAR_CARTA: return {
...state,
ronda: {
...state.ronda,
turno: turnoContrario(state.ronda.turno),
manos: jugarCartaEnMano(state.ronda.manos, action.carta)
}
}

Esto es bastante común, que ante un action tengamos que modificar sólo cierta parte del state. Para eso nos viene bien usar el spread operator.

Nótese que en cada nivel tenemos que acordarnos de copiar el objeto del state actual :) En este caso necesitamos modificar state.ronda.manos y turno . Incluso dentro manos tenemos que cambiar una única mano. Por eso separamos esas mutaciones a dos funciones más chiquitas (puras)

export const turnoContrario = turno => 
turno === Turno.NOSOTROS ? Turno.ELLOS : Turno.NOSOTROS
const jugarCartaEnMano = (manos, carta) =>
adjust(
m => ({ ...m, nosotros: carta }),
manos.findIndex(m => !m.nosotros)
)(manos)

Estamos usando adjust de ramda. Pueden ver la doc acá. Basicamente generamos un nuevo array igual al original pero con un elemento “mapeado” (como en .map())

Finalmente tenemos que cablear este nuevo action a la vista. Editamos containers/juego.js

import { iniciarJuegoRandom, jugarCarta } from '../actions/juego'const mapActionsToProps = dispatch => ({
onIniciarJuego: () => dispatch(iniciarJuegoRandom()),
onJugarCarta: carta => dispatch(jugarCarta(carta))
})

Y deberíamos ver algo así

Conectamos la selección de una carta con la acción de redux que muta el estado. La mesa se renderiza de nuevo automáticamente.

Aclaramos que tuvimos que modificar además la lógica del reducer de INICIAR_JUEGO de modo de que inicialice las manos con objetos medio vacíos

case INICIAR_JUEGO: return {
...
manos: range(0, 3).map(() => (
{ nosotros: undefined, ellos: undefined, resultado: undefined }
))
...
}

Marcar Jugada

Podemos ver que nos falta contemplar una cosa. La carta, una vez jugada no debería permitir volver a jugarse. Incluso, no se debería mostrar en la mano. Eso ya lo tenemos preparado en la vista: <Carta>, <Mano> sabían mostrar una carta como jugada si tenía el atributo carta.jugada.

Así que podríamos modificar nuestro reducer para que, además marque la carta como jugada en la mano del usuario.

Acá un tests (habría que hacer más)

Y el código del reducer, que además de modificar las manos jugadas y el turno, ahora también modifica las cartas en mano de los jugadores, para macar la apropiada como “jugada”

case JUGAR_CARTA: return {
...state,
ronda: {
...state.ronda,
cartas: marcarJugada(state.ronda.cartas, action.carta),
turno: turnoContrario(state.ronda.turno),
manos: jugarCartaEnMano(state.ronda.manos, action.carta)
}
}

donde:

const marcarJugada = (cartas, carta) => mapObjIndexed(
listaCartas =>
listaCartas.map(
c => esCarta(c, carta) ? { ...c, jugada: true } : c
)
)(cartas)
const esCarta = (carta, otra) =>
otra.numero === carta.numero && otra.palo === carta.palo

Esta función soporta actualizar la carta tanto si está en nosotros como en ellos. Con lo cual también nos servirá cuando recibamos eventos de que el contrario jugó una carta.

Ahora sí quedará así

Jugar una carta ahora la marca como jugada y eso la saca de la mano

Nota sobre esta solución:

Existen al menos 2 otras soluciones que podríamos haber elegido para modelar la idea de que una carta fue jugada.

Esta que implementamos nos facilita la vista, ya que el componente Mano asume que en la carta misma ya se indica si fue jugada o no. El problema que introduce es que ahora la carta estará duplicada en el store, ya que aparece una vez en la mano del usuario y otra en las manos jugadas. En general esto se intenta evitar, ya que luego si ese objeto se modifica nos tenemos que asegurar de actualizarlo en ambos lados.

En nuestro caso un carta no cambia más, y luego vamos a descartar toda la ronda, así que no es un problema. Y por eso usamos esta solución. Pero dejamos esta nota para otros casos.

Brevemente otras opciones son

  1. nunca tener el objeto duplicado, y al jugar una carta no sólo introducirle en la mano jugada, sino que al mismo tiempo “sacarla” de la mano del usuario, dejando algún objeto “nulo” a fin de que la vista sepa que en ese slot la carta no está más.
  2. utilizar selectores (que quizás veamos en un post más adelante)

Que Juegue el Oponente

Para poder continuar con el juego ahora que ya podemos jugar cartas necesitamos que el oponente juegue en su turno. Por ahora vamos a simular esto, con una especie de “bot” si se quiere, medio tontín, que juegue la primer carta que encuentre. Obvio que uno podría tener otra lógica, como jugar una carta random o bien cierta inteligencia como para decidir cuándo jugar una carta que gane, etc.. pero lo más simple es lo mejor por ahora.

De nuevo acá hay vaarias formas de hacerlo, en cuanto a que podríamos poner el comportamiento en varios lugares. Ej.. en el reducer podríamos intantáneamente ya mutar el estado como si el oponente hubiera jugado cada vez que le toque. Pero eso nos va a ensuciar el reducer con código que después tendremos que remover. Y al ser un elemento “core” del sistema que queremos tener testeado, no queremos que esta funcionalidad temporal nos afecte a sus tests.. etc.

En conclusión vamos a elegir la solución que nos permita desarrollar de la forma más transparente, para luego poder “sacar” este bot y meter funcionalidad que reciba las jugadas desde un servidor.

Por eso vamos a hacer en un componente de React. Pero uno particular.. que no va a generar ningún HTML. Su única función es estar atento a los cambios de estado. Saber cuándo le toca jugar al oponente (“ellos”) y cuando detecte este disparar un action de jugarCarta, con un pequeño delay random, de modo de que simule al oponente “pensando”.

touch src/componentes/Oponente.jsx
touch src/containers/Oponente.jsx

Donde el componente es:

Notamos que tanto luego de que se monta, como luego de que actualiza (sus props) ejecuta simularJugada(). Este método usa la prop turno para ver si le toca jugar a ELLOS. En ese caso encola una función a ejecutar entre 1 y 5 segundos después (random). Esa función toma la primer carta de entre las cartas (otra prop que espera), y ejecuta una función que también es una prop: jugarCarta con la carta elegida.

O sea, como siempre nos queda 100% desacoplado de redux en sí mismo.

Con el container entonces vamos a popular esas props.

  • turno: como viene del store, de quién es el turno
  • cartas: las cartas de “ellos”
  • jugarCarta: la misma action que ya usábamos, ya que el reducer se banca (hasta ahí, ahora vemos) jugadas de ambos jugadores

Ahora vamos a incluir este componente en algún lado, digamos.. moe.. digo Juego.jsx

Incluímos el Oponente (container) en Juego de modo de que esté colgado en el árbol de componentes y puede “jugar”

Si juegan ahora van a ver un error. Que las cartas del oponente se posicionan en la mesa como si fueran jugadas por nosotros. Eso es porque el reducer no se bancaba del todo cartas jugadas por ELLOS.

Arreglemos ese. jugarCartaEnMano tiene hardcodeado el “nosotros”

const jugarCartaEnMano = (manos, carta) =>
adjust(
m => ({ ...m, nosotros: carta }),
manos.findIndex(m => !m.nosotros)
)
(manos)

Se usa como atributo a popular en la mano, pero también para buscar la primer mano que no tenga ya jugada. Necesitamos hacer dinámico esto, ya que podría ser ellos o nosotros, dependiendo de quién es el “turno actual”.

const jugarCartaEnMano = (manos, carta, turno) =>
adjust(
m => ({ ...m, [turno]: carta }),
manos.findIndex(m => !m[turno])
)
(manos)

Si no están familiarizados con esta sintaxis puede ver acá. Específicamente la sección de “Computed property keys”.

Ahora modificamos al case del reducer que usaba esta función

case JUGAR_CARTA: return {
...state,
ronda: {
...state.ronda,
cartas: marcarJugada(state.ronda.cartas, action.carta),
turno: turnoContrario(state.ronda.turno),
manos: jugarCartaEnMano(
state.ronda.manos,
action.carta,
state.ronda.turno
)
}
}

Nótese que trabajamos todo en el reducer. Uno podría haber pensado en agregar un dato más al action de jugar carta para indicar carta + quien . No hubiera estado mal. Sin embargo, como en el truco siempre sólo puede jugar el jugador del turno actual y ya tenemos en el store esa información, entonces podemos derivarlo solitos.

Ahora sí:

El oponente ahora juega automáticamente sin ensuciar demasiado nuestro código. Preparado para luego tener “multiusuario”

Evaluando manos

Nop, no es “leer el destino” sino implementar la lógica para que, al jugarse una carta se verifique si ya está completa esa mano (ambos jugaron) y en dicho caso calcular el resultado (ganador, perdedor, empate).

Obviamente esto es una mutación de estado, resultado del action de jugar carta. Así que nuevamente vamos a trabajar todo desde el reducer.

Pero, de nuevo acá hay dos formas, dependiendo de qué problemática queremos encarar primero. Podríamos primero encarar la lógica de mutación del reducer independiente del “cálculo de los valores de las cartas del truco”. Y luego hacer eso. O al revés.

Esto porque, calcular la carta ganadora entre dos no es taan simple en el truco.

Vayamos primero por la lógica del truco esta vez.

Evaluar 2 cartas

Es útil para eso recordar los valores de las cartas con esta imagen

Tabla de valoración de las cartas del truco. De izq. a der. las de mayor valor. Cartas en la misma columna valen lo mismo. Fuente: https://www.taringa.net/posts/juegos/13530217/Como-jugar-al-truco-juego-de-cartas.html

Vamos a trabajar esto como una función pura fuera de redux, JS plano. Vemos primero el test. Creamos un archivo model/constants.spec.js ya que vamos a poner la función en constants.js (seeh.. el nombre no quedó muy lindo, conceptualmente es lógica bien de core del truco/modelo/negocio)

Testeamos el resultado de enfrentar 2 cartas. Nótese que aprovechamos el poder de los closures de javascript para generar tests sin tanta burocracia (no escribimos el “it” por cada uno)

Y ahora sí la solución en constants.js

La función principal es resultadoDeMano, que delega en funciones más chiquitas. La idea es modelar una tabla de valores exactamente igual a la de la foto. Luego obtener el valor de en qué “columna” de la foto aparece cada carta. Y finalmente comparar esos valores para ver si nuestra carta es menor (GANADOR), mayor (PERDEDOR), o bien el mismo (EMPATE).

Para eso como verán creamos un par de funciones extra para poder armar la tabla de forma más concisa y expresiva.

Modificar el reducer

Ahora sí podemos tocar el reducer para que use esta función. Lo que debe hacer es no sólo actualizar la mano con la carta que se está jugando sino también setearle un “resultado” si es que ya se puede calcular.

La vista lo va a saber mostrar automáticamente porque ya le dimos soporte cuando trabajamos con los storybooks

Básicamente tenemos que actualizar sólo la función jugarCartaEnMano(), para que haga ese paso extra de mutar el resultado

Lo que hicimos fue extra a otra función la lógica que modificaba la mano (primer parámetro de adjust()), para no contaminar demasiado. Luego convertimos esa función en la composición de 2 funciones, como para separar ambas mutaciones: 1) se actualiza la carta, 2) se calcula resultado.

Eso lo hicimos con pipe() de ramda. El primer paso, también lo cambiamos entonces a usar mergeDeepLeft()(doc), que va a mergear la mano que vendrá, con ese mini objetito que sólo tiene el “delta”, los cambios que queremos aplicarle (la carta jugada). El segundo paso casi que llama a la función que hicimos en la sección anterior. Simplemente controla sólo hacerlo si ya están jugadas ambas cartas.

Y listo. Con esto vemos que ya se calculan bien los resultados parciales de cada mano.

El juego ya evalúa y muestra correctamente qué carta ganó cada mano

Lo que sigue

Vamos a cortar acá por ahora. Lo que nos resta para el siguiente post es:

  • el paso natural, evaluar cuándo se termina la ronda. Esto involucra: dar algún tipo de feedback de modo de que no sea muy “brusco” actualizar los puntos, y generar una nueva mano.
  • evaluar cuando se termina el juego: y de nuevo dar algún tipo de feedback
  • Implementar una cosita que nos quedó adeudada: que el usuario no pueda jugar si no es su turno :P
  • tener una forma de volver a iniciar un nuevo juego
  • soporte para multiples usuarios: usando algún tipo de comunicación en “tiempo real” como websockets. También dispara nuevos requerimientos de vista/aplicación/UX para poder arrancar un nuevo juego con alguien (requiere tener usuarios conectados ? mandar una invitación ? abrir un juego y esperar a que alguien entre ? el que entra simplemente pide un juego random y se lo asigna ?, etc). Es decir muchas preguntas para definir antes de codear :)

Como prometimos en el post anterior, creo que no incorporamos conceptos nuevos, sino que ejercitamos bastante como usar redux y cómo conectarlo con react.

Por otro lado es interesante entender que el valor mismo está en dos cosas: 1) que justamente no tuvimos que incorporar conceptos nuevos para modelar el comportamiento de la aplicación (estado) 2) que salvo quizás por ínfimos cambios, o bien por una “optimización” que hicimos, o porque nos dimos cuenta que faltaba una cosita (el cursor del turno actual), casi casi que no tuvimos que tocar todo el trabajo que hicimos de vista con react en los primeros posts. El trabajo es verdaderamente “lineal”, o incremental. Y esto es muy poderoso, porque indica que hay una muy buena separación de concerns, pero que además luego la integración es muy buena, al momento de conectar ambos mundos (vista + estado). Y que cada parte se puede trabajar y testear independientemente. En la programación de interfaces de usuario eso es muy pero muy bueno :)

Hasta la próxima (?)

--

--

Javier Fernandes

Software Engineer@SCVSoft. Professor@UNQ (Public National University, Argentina). Creator of Wollok Programming Language. Always learning & creating things :)