Go Fish!
Contents
5. Go Fish!¶
En este capítulos implementamos un juego de cartas conocido como Go Fish!. En el Go Fish pueden participar entre 2 y 6 jugadores. Normalmente se utiliza una baraja francesa a la que se le han retirado los comodines.
Las reglas son sencillas:
Se mezcla la baraja
Cada jugador recibe 5 cartas
Se elige aleatoriamente quién empieza
En el turno del jugador 1 pregunta al jugador 2:
"Tienes algún 3?" (el 3 es un ejemplo puede ser cualquier carta)
El jugador 2 tendrá que darle todas las cartas que tenga con ese número. Si acertó, el jugador 1 puede seguir pidiendo cartas hasta que el jugador 2 no disponga de esa carta. En ese momento, el jugador 2 dirá:
"Vete a pescar!"
En ese momento, el jugador 1 tendrá que coger la primera carta de la baraja
Si el jugador 1 coge precisamente una carta del valor que había solicitado, le vuelve a tocar y puede volver a pedir cartas al siguiente jugador (o de nuevo al jugador 2 si no hay nadie más)
Si el jugador 1 pesca una carta diferente, su turno se ha terminado y pasa al siguiente jugador
Cada vez que un jugador consigue los 4 palos de un número, las dejará en la mesa y sumará un punto a su contador
Si un jugador se queda sin cartas, coge 5 cartas de la baraja
Cuando la baraja se queda sin cartas, el juego continua hasta que todos los jugadores se hayan quedado sin cartas
El jugador que tenga más puntos gana
See also
Puedes encontrar más información sobre este juego en su página de Wikipedia
Para implementar el juego, vamos a simular la baraja como una lista de tuplas:
deck=[(suit, value),(suit, value),(suit, value),(suit, value),....]
En la que los palos son variables tipo STRING que pueden tomar los valores:
hearts
diamonds
spades
clubs
Y el valor de los naipes se encuentra entre el 1 y el 13 donde el 11 se corresponde con la Jota (J), el 12 con la Reina (Q) y el 13 con el rey (K).
Tip
Vamos a proponer la implementación de una serie de funciones que pueden ayudarte a codificar el juego. Dependiendo de como construyas el programa principal puede que no necesites utilizarlas todas, pero es un buen ejercicio de cara a implementar mejoras o incluso programar cualquier otro juego de cartas.
5.1. Genera una baraja completa¶
Vamos a empezar creando una función create_deck(values, suits) que reciba como parámetros:
values es una variable de tipo LIST con los valores admitidos
suits es una variable de tipo LIST con los palos de cartas
La función debe devolver una lista de tuplas con las 52 cartas de la baraja francesa (todas las combinaciones)
Solución:¶
def create_deck(values, suits):
out = []
for v in values:
for s in suits:
out.append((s,v))
return out
values = [1,2,3,4,5,6,7,8,9,10,11,12,13]
suits=['hearts','diamonds','clubs','spades']
deck = create_deck(values, suits)
deck[0:5]
[('hearts', 1), ('diamonds', 1), ('clubs', 1), ('spades', 1), ('hearts', 2)]
Note
Podemos utilizar esta función para generar una baraja española de 40 cartas sin más que modificar las variables con las que la invocamos.
values = [1,2,3,4,5,6,7,10,11,12]
suits=['oros','espadas','copas','bastos']
deck = create_deck(values, suits)
5.2. Ordenar la baraja¶
En algunas ocasiones puede resultar de utilidad disponer de una función que nos permita ordenar la baraja o la mano de un jugador. Vamos a crear una función sort_deck(tuples) que toma como argumento la variable tuples que es una lista de tuplas (de cualquier longitud). La variable tuples puede contener la baraja completa o un subconjunto de cartas que ha sido repartida a un jugador.
Dentro de un mismo palo el orden lo establecerá el valor de las cartas. Y entre los distintos palos, el orden será el alfabético. En el caso de la baraja francesa:
clubs
diamonds
hearts
spades
Solución:¶
def sort_deck(tuples):
return sorted(tuples, key=lambda tup: (tup[0],tup[1]))
sort_deck([("spades",1),("clubs",8),("clubs",5),("hearts",3),
("diamonds",3),("diamonds",7)])
[('clubs', 5),
('clubs', 8),
('diamonds', 3),
('diamonds', 7),
('hearts', 3),
('spades', 1)]
Note
La función sorted tiene una sintaxis muy versatil. Puede invocarse sobre una colección numérica y aplicar un criterio de ordenación de menor a mayor o sobre cualquier otro tipo de colecciones y definir un criterio de ordenación personalizado a través de una función lambda. En este caso estamos ordenando en primer lugar por el primer elemento de la tupla (el palo) y después por el segundo elemento de la tupla (el valor de la carta).
5.3. Traduce las figuras¶
De cara a que nuestro juego sea lo más real posible, vamos a cambiar el valor de las tuplas correspondientes a las figuras de la baraja francesa por la inicial de la figura.
Para ello vamos a programar la función get_figures(card) que recibe una tupla del tipo:
("hearts",11)
y devuelve otra tupla como la siguiente:
("hearts","J")
Solución:¶
def get_figures(card):
if card[1] == 11:
out = (card[0], 'J')
elif card[1] == 12:
out = (card[0], 'Q')
elif card[1] == 13:
out = (card[0], 'K')
else:
out = (card[0], card[1])
return out
Puedes comprobar que funciona correctamente pasándole una tupla para cada figura:
print(get_figures(("hearts",11)))
('hearts', 'J')
print(get_figures(("hearts",12)))
('hearts', 'Q')
print(get_figures(("hearts",13)))
('hearts', 'K')
5.4. Imprime la mano¶
Siempre resulta de ayuda que el programa lleve la cuenta de las cartas que tiene cada jugador y que se las muestren por pantalla. De esta manera, los jugadores sólo tienen que preocuparse de tomar la decisión del valor de la carta que quieren solicitar al resto. Para ello vamos a programar la función print_hand(hand). El argumento hand es una variable de tipo LIST que contiene una lista de tuplas con la mano del jugador.
Para hacerlo menos textual, vamos a intentar conseguir la siguiente salida:
print_hand([("spades",1),("clubs",8),("clubs",13),("hearts",3)])
1♠ 8♣ K♣ 3♥
Tip
Para conseguir el icono del palo, puedes utilizar este diccionario:
suits={"hearts":"♥","diamonds":"♦","clubs":"♣","spades":"♠"}
Solución:¶
def print_hand(hand):
suits={"hearts":"♥","diamonds":"♦","clubs":"♣","spades":"♠"}
for card in hand:
card=get_figures(card)
print("{}{}".format(card[1],suits[card[0]]), end=" ")
print()
5.5. Mezclar baraja¶
Una función clásica que requiere cualquier juego de cartas es el barajado. Vamos a implementar la función shuffle_deck(tuples) que recibe como único argumento la variable tuples que es una lista de tuplas de cartas y devuelve la misma lista pero aleatorizada.
Tip
En ocasiones perdemos mucho tiempo implementando funcionalidades que ya existen. Échale un vistazo a las funciones de la librería random y en concreto a la función shuffle
Solución:¶
import random
def shuffle_deck(tuples):
deck_shuffled=tuples.copy()
random.shuffle(deck_shuffled)
return deck_shuffled
Para verificar su funcionamiento, es suficiente con pasar una versión reducida de una baraja de 5 cartas:
shuffle_deck([('hearts', 1),('hearts', 2),('hearts', 3),('hearts', 4),('hearts', 5)])
[('hearts', 2), ('hearts', 4), ('hearts', 5), ('hearts', 1), ('hearts', 3)]
5.6. Coger una carta¶
Otra acción clásica es coger una carta de la baraja. Vamos a implementar la función get_card(deck). El único argumento de esta función es la variable deck que contiene una lista de tuplas de cartas. La función devuelve:
card la carta cogida
new_deck la baraja sin esa carta
Esta función devolverá siempre la primera carta del mazo.
Solución:¶
def get_card(deck):
card = deck[0]
new_deck = deck[1:]
return card, new_deck
Podemos validar su funcionamiento con la misma baraja reducida que hemos empleado en el ejemplo anterior:
deck = [('hearts', 1),('hearts', 2),('hearts', 3),('hearts', 4),('hearts', 5)]
card, new_deck = get_card(deck)
print("The card: ", card)
print("The deck: ", new_deck)
The card: ('hearts', 1)
The deck: [('hearts', 2), ('hearts', 3), ('hearts', 4), ('hearts', 5)]
Podemos comprobar como la función siempre coge la primera carta de la baraja y devuelve una lista con la baraja actualizada.
5.7. Coger una carta aleatoria¶
Esta acción es una versión aleatoria de la función anterior. Vamos a implementar la función get_random_card(deck) que recibe como argumento la variable deck, que representa una lista de tuplas de cartas y devuelve dos variables:
card la carta cogida
new_deck la baraja sin esa carta
Tip
Siempre que nos enfrentemos a alguna acción de tipo aleatorio, es importante tener presente la funciones de la librería random
Solución:¶
import random
def get_random_card(deck):
new_deck = deck.copy()
card = random.sample(deck,1)[0]
new_deck.remove(card)
return card, new_deck
De nuevo, puedes validar que has implementado la funcion correctamente con un código que simule una versión reducida de nuestro problema:
deck = [('hearts', 1),('hearts', 2),('hearts', 3),('hearts', 4),('hearts', 5)]
card, new_deck = get_random_card(deck)
print("The card: ", card)
print("The deck: ",new_deck)
The card: ('hearts', 2)
The deck: [('hearts', 1), ('hearts', 3), ('hearts', 4), ('hearts', 5)]
Vemos que la función devuelve una carta aleatoria y devuelve la barja original, en el mismo orden, pero sin la carta seleccionada.
5.8. Repartir cartas a los jugadores¶
En la mayoría de juegos se reparte un número de cartas a cada jugador. Vamos a implementar la función deal_hand(deck,number) que recibe como parámetros:
deck que es una lista de tuplas que simula la baraja
number que es una variable tipo INT que representa el número de cartas a repartir
y devuelve:
hand que contiene la lista de tuplas con el reparto de cartas para un jugador
new_deck con la nueva baraja sin las cartas que se han entregado al jugador
Tip
Esta función es muy similar a las funciones get_card(deck) y get_random_card(deck) que devolvían la primera carta o una carta aleatoria de la baraja. De hecho, se podría implementar llamando a las funciones anteriores.
Solución:¶
import random
def deal_hand(deck,number):
new_deck = deck.copy()
hand = random.sample(deck,number)
for card in hand:
new_deck.remove(card)
return hand, new_deck
Validamos su funcionamiento:
deck = [ ('hearts', 1), ('diamonds', 1), ('clubs', 1), ('spades', 1), ('hearts', 2)]
hand, new_deck = deal_hand(deck,number=2)
print("Hand: ",hand)
print("Remaining cards: ",new_deck)
Hand: [('hearts', 1), ('clubs', 1)]
Remaining cards: [('diamonds', 1), ('spades', 1), ('hearts', 2)]
Puedes comprobar que la función saca dos cartas aleatorias de la baraja y devuelve la lista de cartas original en el mismo orden pero sin las cartas elegidas.
5.9. Valores del mismo palo¶
Los jugadores de nuestro juego ganan puntos si consiguen todos los palos del mismo valor. Cuando se da esa circustancia, el jugador se deshace de esas cartas y aumenta en un punto su contador. Vamos implementar esta funcionalidad con un método same_suit(tuples) que recibe como argumento una lista de tuplas de cartas tuples y devuelve dos variables de tipo LIST:
all_suits es una variable de tipo LIST que devuelve los valores de las cartas recibidas en la variable tuples siempre y cuando tengamos los cuatro palos del mismo valor
remaining es una variable de tipo LIST que devuelve las cartas que no cumplan la condición anterior. Es decir, que no contengan los cuatro palos del mismo valor
Vamos a verlo más claro con un ejemplo:
tuples = [ ('hearts', 13), ('diamonds', 13), ('clubs', 13), ('spades', 13),
('spades', 2), ('clubs', 2), ('clubs', 12)]
all_suits, remaining = same_suit(tuples)
print("All suits: ",all_suits)
print("Remaining cards: ",remaining)
Congrats!! You collected all cards with value 13. Score +1
All suits: [13]
Remaining cards: [('spades', 2), ('clubs', 2), ('clubs', 12)]
En el ejemplo anterior, la función same_suit detecta que tenemos el rey (13) de los cuatro palos, además de tres cartas extra que se devuelven en la variable remaining.
Solución:¶
def same_suit(tuples):
cards={}
for suit, value in tuples:
if value not in cards:
cards[value]=[(suit,value)]
else:
cards[value].append((suit,value))
all_suits=[]
remaining=[]
for value, card_list in cards.items():
if len(card_list)==4:
all_suits.append(value)
print("Congrats!! You collected all cards with value {}. Score +1".format(value))
else:
remaining = remaining + card_list
return all_suits,remaining
Note
Esta función puede implementarse de muchas maneras. Nosotros hemos elegido calcular un diccionario cards en el que vamos almacenando todas las cartas que tenemos para cada uno de los valores que nos encontramos en la lista tuples. Así, para una lista como la siguiente:
tuples = [ ('hearts', 13), ('diamonds', 13), ('clubs', 13), ('spades', 13),
('spades', 2), ('clubs', 2), ('clubs', 12)]
Generamos el diccionario:
{13: [('hearts', 13), ('diamonds', 13), ('clubs', 13), ('spades', 13)],
2: [('spades', 2), ('clubs', 2)],
12: [('clubs', 12)]}
La lista all_suits la constituyen aquellas claves del diccionario cards cuyo campo valor contiene cuatro elementos (que serán los cuatro palos) mientras que el resto de las cartas las enviamos a la variable remaining.
5.10. Pedir carta al contrincante¶
Una de las acciones de nuestro juego consiste en que un jugador le solicita a otro jugador todas las cartas que tenga en su mano de un determinado valor. Para simular este comportamiento vamos a implementar la función ask_for_cards(hand, value) que recibe como argumentos:
hand que es una lista de tuplas con las cartas del jugador
values que es el número de la carta que un jugador le pide al otro jugador
y devuelve:
found las cartas del segundo jugador que tienen como valor el número indicado
new_hand la nueva colección de cartas del segundo jugador sin las cartas que ha cedido
Vamos a verlo con un ejemplo. Tenemos una variable hand que contiene la mano del jugador 2. El jugador 1 le solicita todas las cartas que tengan el valor 2:
hand = [ ('hearts', 1), ('diamonds', 1), ('clubs', 1), ('hearts', 2), ('diamonds', 2) ]
value = 2
found, new_hand = ask_for_cards(hand, value)
print("Found: ",found)
print("New hand: ",new_hand)
Found: [('hearts', 2), ('diamonds', 2)]
New hand: [('hearts', 1), ('diamonds', 1), ('clubs', 1)]
Vemos que la invocación a la función ask_for_cards ha devuelto la variable found con las dos cartas que tienen el valor 2 y la variable new_hand con la mano del jugador 2 actualizada.
Solución:¶
def ask_for_cards(hand, value):
new_hand = hand.copy()
found = []
for card in hand:
if card[1] == value:
found.append(card)
new_hand.remove(card)
return found, new_hand
5.11. Implementar Juego¶
Ahora es el momento de implementar el juego. Tienes muchas funciones, más de las necesarias, pero que definen prácticamente todas las operaciones clásicas de los juegos de cartas. Combínalas adecuadamente para implementar el juego.
Solución:¶
El primer paso es definir el tipo de baraja con el que vamos a jugar, crearla y barajarla. Todo esto podemos hacerlo con las funciones create_deck y shuffle_deck.
import random
values = [1,2,3,4,5,6,7,8,9,10,11,12,13]
suits=['hearts','diamonds','clubs','spades']
deck = create_deck(values, suits)
deck = shuffle_deck(deck)
Tip
De cara a probar versiones más sencillas del juego, podemos definir barajas más pequeñas, con menos valores o con menos palos.
Una vez que tenemos la baraja, vamos a inicializar las manos y los marcadores de los jugadores. Nosotros vamos a implementar una versión sencilla de dos jugadores en el que uno de ellos será el ordenador. Para ello vamos a definir una estructura de datos en la forma de un diccionario de Python de nombre who_plays. Este diccionario tiene una entrada de tipo “clave:valor” para cada participante. La clave es un valor numérico y el valor es un nuevo diccionario en el que se almacena el nombre, la mano y la puntuación de cada jugador.
player1, deck=deal_hand(deck,7)
player2, deck=deal_hand(deck,7)
who_plays={0:{"name": "Player", "hand":player1, "score":0},
1:{"name": "Computer","hand":player2, "score":0}}
Este es el contenido del diccionario para un posible reparto de cartas:
who_plays
{0: {'name': 'Player',
'hand': [('spades', 10),
('hearts', 13),
('hearts', 8),
('clubs', 4),
('spades', 2),
('diamonds', 8),
('diamonds', 10)],
'score': 0},
1: {'name': 'Computer',
'hand': [('spades', 3),
('diamonds', 7),
('clubs', 10),
('hearts', 1),
('hearts', 10),
('hearts', 4),
('diamonds', 12)],
'score': 0}}
Note
El propósito de utilizar claves numéricas es implementar un sistema de turnos de juego mediante un incremento “circular”. Por ejemplo, si tenemos 5 jugadores, las claves del diccionario who_plays tomarán los valores 0,1,2,3 y 4. De manera que podemos definir una variable player que indique en todo momento quién es el jugador, e ir pasando de uno a otro incrementando el valor de esa variable mediante el operador modulo (%):
player=(player+1)%5
Con este código, cuando la variable player toma el valor 4 sería el turno del jugador 5. Al incrementarla de nuevo, tomará el valor 0 volviendo a pasar el turno al jugador 1.
Podemos comprobar las manos iniciales utilizando la función print_hand:
print_hand(player1)
print_hand(player2)
10♠ K♥ 8♥ 4♣ 2♠ 8♦ 10♦
3♠ 7♦ 10♣ 1♥ 10♥ 4♥ Q♦
Y verificamos que las cartas repartidas ya no existen en la baraja:
print("Remaining cards in deck: ",len(deck))
Remaining cards in deck: 38
Vamos a inicializar el juego otorgando el turno al jugador 1. Para ello asignamos el valor 0 a la variable player y actualizamos el valor de las variables que usará el programa principal:
player=0
playing_name=who_plays[player]["name"]
playing_hand=who_plays[player]["hand"]
playing_score=who_plays[player]["score"]
adversary_name=who_plays[(player+1)%2]["name"]
adversary_hand=who_plays[(player+1)%2]["hand"]
adversary_score=who_plays[(player+1)%2]["score"]
Como vamos a invocar este código varias veces, lo más inteligente es meterlo dentro de una función:
def get_player(player,who_plays):
number_players=len(who_plays)
playing_name=who_plays[player]["name"]
playing_hand=who_plays[player]["hand"]
playing_score=who_plays[player]["score"]
adversary_name=who_plays[(player+1)%number_players]["name"]
adversary_hand=who_plays[(player+1)%number_players]["hand"]
adversary_score=who_plays[(player+1)%number_players]["score"]
return playing_name,playing_hand,playing_score,adversary_name,adversary_hand,adversary_score
A medida que el juego avanza, se modifica el valor de las variables:
playing_hand y playing_score
adversary_hand y adversary_score
Cuando el turno cambia, tenemos que salvar el contenido de esas variables en el diccionario who_plays. Esta tarea también la vamos a meter en una función para que el código del juego sea más legible:
def save_status(player,who_plays,playing_hand,playing_score,adversary_hand,adversary_score):
number_players=len(who_plays)
who_plays[player]["hand"]=playing_hand
who_plays[player]["score"]=playing_score
who_plays[(player+1)%number_players]["hand"]=adversary_hand
who_plays[(player+1)%number_players]["score"]=adversary_score
return who_plays
Antes de ponerte con la solución, recuerda que cuando se da la conción de “Go Fish”, si la carta obtenida de la baraja coincide con la carta que solicitó el jugador, entonces no hay cambio de turno. O lo que es lo mismo, volvemos al inicio del bucle while. Esto se puede forzar con el comando continue.
Tip
Puedes obtener más información sobre este comando aquí
while not(len(deck) ==0 and len(playing_hand) == 0 and len(adversary_hand) == 0):
print("Turn: ",playing_name)
print_hand(playing_hand)
print("Score: ",playing_score)
if playing_name!= "Computer" :
value = int(input(playing_name+" select card value: "))
else:
value = random.sample(values,1)[0]
print(playing_name+" selects card value: "+str(value))
found, adversary_hand = ask_for_cards(adversary_hand, value)
if len(found)!=0:
print(adversary_name+" gives:",end=" ")
print_hand(found)
playing_hand = playing_hand + found
all_suits, playing_hand = same_suit(playing_hand)
playing_score=playing_score+len(all_suits)
else:
print("No luck, "+playing_name+" go fish!")
card,deck = get_card(deck)
print(playing_name+" got:",end=" ")
print_hand([card])
playing_hand=playing_hand+[card]
all_suits, playing_hand = same_suit(playing_hand)
playing_score=playing_score+len(all_suits)
if card[1]==value:
print(playing_name+" keep on playing!")
continue
else:
# Turn change
who_plays = save_status(player,who_plays,playing_hand,playing_score,
adversary_hand,adversary_score)
player = (player+1)%2
playing_name,playing_hand,playing_score,adversary_name,adversary_hand,adversary_score=get_player(player,who_plays)
Note
Nota que la condición de salida del bucle está implementada como una condición negada. En ocasiones, dependiendo de la condición, resulta más sencillo implementar una condición y negarla que construir la versión positiva de la misma. Este es uno de esos casos. Tenemos claro que la condición de “fin de juego” es cuando se dan simultáneamente las tres condiciones siguientes:
Ninguno de los jugadores tiene cartas en su mano
No quedan cartas en la baraja
Si no quedan cartas en la barja pero si en las manos de los jugadores, hay que seguir jugando. O si uno de los jugadores se queda sin cartas, pero quedan cartas en la baraja, también hay que seguir jugando. El bucle while tiene que seguir ejecutándose precisamente en el caso contrario, es decir, cuando no se da la condición de “fin de juego”. Por eso, en este caso, la condición de salida del bucle while se puede implementar como:
not(len(deck) ==0 and len(playing_hand) == 0 and len(adversary_hand) == 0):
Intenta diseñar la condición inversa y descubrirás que efectivamente es más complicada de lo que parece.
Warning
Esta es una implementación mínima del juego. Una versión real no debería permitir al jugador 1 ver las cartas de la mano del ordenador. Tomar este tipo de licencias es algo muy recomendable sobre todo durante la etapa de desarrollo del juego para verificar que todo funciona como debe. Una vez que tenemos la seguidad de que el juego está bien programado, entonces podemos proceder a ocultar la mano del resto de jugadores.
Tip
Habrás notado que cuando es el turno del ordenador, el juego avanza demasiado rápido y hay que hacer “scroll up” para descubrir qué ha ocurrido. Este problema se puede resolver con la función sleep de la librería time.
import time
time.sleep(10)
Este comando permite meter tiempos de espera de segundos de manera que el jugador tiene la sensanción de que el ordenador está pensando y los eventos se suceden a un ritmo mucho más asequible.
5.12. Extensiones del juego¶
Si te ha sabido a poco, puedes complicar el juego con las siguientes propuestas:
Implementa un mensaje de salida que analice las puntuaciones de todos los jugadores y declarar si ha habido algún ganador
Implementar un juego para más jugadores
Introducir retrasos con la función time.sleep cuando el turno recae en jugadores impersonados por el ordenador
Intentar cambiar la manera de representar las cartas por iconos de las cartas reales. Echa un vistazo a los iconos de cartas en Unicode
Puedes utilizar la funcionalidad de Jupyter de limpiar la pantalla, para que la salida no implique hacer scroll. Esto se consigue con la función clear_output que se importa de la librería IPython.display. Puedes encontrar más información aquí