5. Go Fish!

portada

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:

  1. clubs

  2. diamonds

  3. hearts

  4. 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).

See also

Puedes encontrar más información sobre la función sorted aquí y sobre las funciones lambda aquí.

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í