algorithms · go · data-structures · exercism

Resolviendo Blackjack con Go: guía paso a paso para el primer movimiento

7 min de lectura

¡Hola de nuevo, entusiastas de Go! ¿Cómo va el camino hacia ese trabajo soñado? Hoy vamos a resolver un ejercicio interesante que encontré en exercism.

Vamos a resolver el primer turno de una partida de BlackJack. En este ejercicio repasamos los fundamentos de Go y la subestimada sentencia switch, que no se usa tan seguido—ni siquiera en entrevistas técnicas.

Enunciado del problema

Simular el primer turno de una partida de BlackJack. Se reciben dos cartas y se ve la carta boca arriba del dealer. Todas las cartas están representadas como strings, por ejemplo “ace”, “king”, “two”, etc.

El valor de cada carta es:

Card Value
ace 11
two 2
three 3
four 4
five 5
six 6
seven 7
eight 8
nine 9
ten 10
jack 10
queen 10
king 10
other 0

Nota: normalmente, los ases pueden valer 1 u 11, pero para simplificar vamos a asumir que solo pueden valer 11.

Dependiendo de las dos cartas y de la carta del dealer, existe una estrategia para el primer turno del juego, en la cual se tienen las siguientes opciones:

  • Plantarse (S)

  • Pedir carta (H)

  • Dividir (P)

  • Ganar automáticamente (W)

Aunque todavía no es óptima, se va a seguir la estrategia que ha estado desarrollando un amigo, Alex, la cual es la siguiente:

  • Si se tiene un par de ases, siempre se deben dividir.

  • Si se tiene un Blackjack (dos cartas que suman 21) y el dealer no tiene un as, una figura (Jota/Reina/Rey) o un diez, entonces se gana automáticamente. Si el dealer sí tiene alguna de esas cartas, se tendrá que plantar y esperar a que revele la otra carta.

  • Si las cartas suman un valor dentro del rango [17, 20] siempre se debe plantar.

  • Si las cartas suman un valor dentro del rango [12, 16] siempre se debe plantar, a menos que el dealer tenga un 7 o más, en cuyo caso siempre se debe pedir carta.

  • Si las cartas suman 11 o menos siempre se debe pedir carta.

Descomponiendo el problema

A primera vista, el problema parece complejo porque hay demasiadas restricciones y escenarios. Pero si lo descomponemos como hemos estado haciendo a lo largo de esta serie, se descubre que no es tan difícil como parece.

Empecemos por identificar nuestra entrada. Vamos a recibir tres strings con representaciones de cartas: dos para nuestras cartas y uno para la del dealer.

Después, necesitamos determinar el valor de cada carta—su representación numérica—para poder sumarlos y decidir nuestro movimiento.

Finalmente, la salida deseada es el movimiento del primer turno según la estrategia de Alex del enunciado del problema.

Este algoritmo consiste en dos pasos cruciales: convertir los strings de entrada a su representación numérica, y determinar qué movimiento les corresponde.

Parsear las cartas de entrada

El primer paso es convertir los strings de las cartas de entrada a su representación numérica. Si bien el ejercicio de exercism se enfoca en aprender a trabajar con switches, nosotros vamos a usar mapas para simplificar esta solución. En la siguiente parte, vamos a usar la sentencia switch como estaba pensado en el ejercicio original.

Definamos la función ParseCard usando un mapa con claves de tipo string y valores de tipo int:

func ParseCard(card string) int {
    return map[string]int{
        "ace": 11, "two": 2, "three": 3, "four": 4, "five": 5,
        "six": 6, "seven": 7, "eight": 8, "nine": 9, "ten": 10,
        "jack": 10, "queen": 10, "king": 10,
    }[card]
}

¡Y listo! Se resolvió la primera parte en una sola línea. Existen otros enfoques para esta función, pero creo que esta es la forma más efectiva de demostrar el uso de mapas.

Por ejemplo, se pueden usar constantes para impresionar al entrevistador:

const (
    Ace   = "ace"
    Two   = "two"
    Three = "three"
    // ... another values
)

var cardValues = map[string]int{
    Ace:   11,
    Two:   2,
    Three: 3,
    // ...other values
}

func ParseCard(card string) int {
	return cardValues[card] // default value is 0 for a int in Go
}

Algoritmo para resolver el primer turno

Ahora que ya mapeamos cada nombre de carta a su valor correspondiente, necesitamos determinar nuestro primer movimiento.

Vamos a crear una sentencia switch sencilla con las distintas restricciones del enunciado del problema.

Primero, necesitamos parsear nuestras cartas de entrada.

parsedCard1 := ParseCard(card1)
parsedCard2 := ParseCard(card2)
parsedDealerCard := ParseCard(dealerCard) 

Ahora que sabemos cuánto vale cada carta, necesitamos sumar los valores de nuestras dos cartas para verificar si es un blackjack.

sumCards := parsedCard1 + parsedCard2

Ahora, con la elegancia y la magia de la sintaxis de Go, validemos cada escenario para determinar nuestro primer movimiento. La primera restricción es si se tiene un par de ases, se deben dividir—esto significa que si nuestras cartas suman 22 (cada as vale 11), entonces nuestro primer turno es una división y debemos devolver la letra "P".

switch{
	case sumCards == 22:
		return "P"
}

La segunda restricción: si se tiene un Blackjack (21) y el dealer no tiene un as, una figura (Jota/Reina/Rey) o un diez, se gana automáticamente. En otras palabras, si se tiene 21 y la carta del dealer es menor a 10, se gana.

case sumCards == 21 && parsedDealerCard < 10:
	return "W"

Sin embargo, si se tiene 21 y el dealer tiene alguna de esas cartas (un as o una figura), se debe plantar este turno.

case sumCards == 21 && parsedDealerCard >= 10:
	return "S"

Después, si las cartas suman un valor dentro del rango [17, 20], siempre se debe plantar.

case sumCards >= 17 && sumCards <= 20:
	return "S"

Si las cartas suman un valor entre 12 y 16, siempre se debe plantar—a menos que el dealer tenga un 7 o más, en cuyo caso se debe pedir carta.

La primera condición se ve así:

case sumCards >= 12 && sumCards <= 16 && parsedDealerCard >= 7: //dealers card equal or higher than 7
	return "H"

La siguiente condición maneja el caso en que la carta del dealer es menor a 7:

case sumCards >= 12 && sumCards <= 16 && parsedDealerCard < 7:
	return "S"

Si las cartas suman 11 o menos, siempre se debe pedir carta. Esto significa que cualquier escenario no cubierto equivale a pedir carta, ya que cubrimos todos los escenarios posibles. Así que nuestro caso por defecto es "H".

default:
	return "H"

¡Y listo! Se creó un algoritmo para calcular el primer turno en una partida de Blackjack. También se aprendió sobre mapas, sentencias switch y condicionales.

Reflexiones finales

Este ejercicio demuestra cómo descomponer problemas complejos en pasos más pequeños y manejables los hace mucho más fáciles de resolver. Lo que en un principio parecía un conjunto complicado de reglas se convirtió en un algoritmo sencillo al identificar los componentes clave: parsear los valores de las cartas y evaluar las condiciones de forma sistemática.

La sentencia switch en Go resulta increíblemente poderosa para manejar múltiples ramas condicionales de forma limpia y legible. Combinada con mapas para la búsqueda de valores, creamos una solución elegante.

Mientras se continúa el camino con Go, conviene recordar que la mayoría de los problemas del mundo real se pueden abordar de la misma manera—descomponerlos, identificar patrones y aprovechar las características simples pero poderosas de Go.

Vale la pena seguir practicando, seguir programando y, lo más importante, ¡disfrutar del proceso! Si este ejercicio resultó útil, hay más desafíos para explorar en exercism y seguir desarrollando las habilidades en Go. Como siempre, el código completo se puede encontrar en mi repo de GitHub: https://github.com/RubenOAlvarado/algorithms

¡Feliz programación y nos vemos en la próxima!