7

Simulando todas las manos del poker

Angel
Angrub
20092
<h3>Introducción</h3>

Como ya lo habrán podido deducir por el título, en este tutorial veremos cómo conseguir el aproximado de la probabilidad de cada mano del poker. Para efectos prácticos no comenzaremos desde cero, sino, apartir del código ya escrito por nuestro profesor David Aroesti. La intención de este tutorial es de poder expandir las posibilidades de la herramienta ya dada en clases anteriores, y explorar las formas que al menos a mí se me ocurrieron para construir cada componente de este software.

<h3>Modularizar</h3>

Si bien podriamos comenzar a escribir todas las nuevas características de nuestra pieza de software directamente en la función main(), no considero que sea lo más adecuado, fácilmente nuestro sencillo programa podría convertirse en un enorme armatoste de código, dificil y cansado de leer. Esto se puede evitar dando un sentido estructural al mismo proyecto. Pero ¡Oye! No hace falta inventarse algo ridiculamente innovador. Y más cuando nuestro profesor ya nos ha dicho implicítamente cómo hacerlo.

Modularizar el código en funciones es algo que hemos hecho a lo largo del curso, encapsula las funcionalidades de los elementos del programa haciendolas más fácil de modificar, y nos ayuda a organizar mejor nuestros pensamientos a la hora de programar. Y eso es lo que haremos en este caso, pero antes tenemos que preparar el terreno para ello; y ese terreno se llama main().

Veamos nuestra función main():

defmain(tamano_mano, intentos):
    baraja = crear_baraja()
    
    manos = []
    for _ in range(intentos):
        mano = obtener_mano(baraja, tamano_mano)
        manos.append(mano)

    pares = 0for mano in manos:
        valores = []
        for carta in mano:
            valores.append(carta[1])

        counter = dict(Counter(valores))
        for val in counter.values():
            if val == 2:
                pares += 1break
    probabilidad_par = pares / intentos
    print(f'la probabilidad de sacar un par en una mano de {tamano_mano} es de {probabilidad_par}')

¿Qué haremos con ella? Lo primero será abstraer el código que tiene la lógica para identificar los pares. Este se encuentra en el ciclo for que está anidado. Ahora que lo hemos identificado, es necesario encapsularlo en una función independiente. Le llamaremos par() y recibirá como parámetro counter.values(). Así quedaría main():

defmain(tamano_mano, intentos):
    baraja = crear_baraja()
    
    manos = []
    for _ in range(intentos):
        mano = obtener_mano(baraja, tamano_mano)
        manos.append(mano)

    # - pares = 0for mano in manos:
        valores = []
        for carta in mano:
            valores.append(carta[1])

        counter = dict(Counter(valores))
        # Aquí yacía el código

    probabilidad_par = pares / intentos
    print(f'la probabilidad de sacar un par en una mano de {tamano_mano} es de {probabilidad_par}')

Y así par():

defpar(valores_acumulados):for val in valores_acumulados:
        if val == 2:
            return1return0

Lo primero a observar, si es que todavía no te has dado cuenta, es que prácticamente sólo copié y pegué el for que retiramos de la función main(). La lógica para identificar el par es la misma, con la única diferencia de que ahora procede de otra forma si la condición se cumple. Observemos, si hay un par retornará un 1 y terminará la función, de lo contrario retornará un 0 después de acabar el ciclo. Lo segundo, es que en la función main() borramos la variable pares, que si no lo recuerdas, se encargaba de acumular la cantidad de pares por simulación, para posteriormente sacar el promedio. Esto lo arreglaremos después, por ahora no es relevante ya que la idea central es indicarnos si en la función están los valores buscados, y si es el caso, que retorne 1.

<h3>Promedios</h3>

Bien, es hora de arreglar el pequeño pero importante detalle que nos dejamos en la sección anterior. Ya que manejaremos la detección de jugadas en módulos independientes, delegaremos el trabajo de sacar promedios a nuestra queridísima función principal. Primero tenemos que tratar con el problema de acumular los valores. Para esto se me ocurrió crear un diccionario llamado jugadas que los almacene.

Función main():

jugadas = {
        'par': 0, 
        'doble par': 0, 
        'triada': 0, 
        'escalera': 0, 
        'color': 0, 
        'full': 0,
        'poker': 0,
        'escalera de color': 0,
        'escalerareal de color': 0 
        }

Dentro del segundo ciclo for; donde antes estaba el algoritmo para sacar pares, agregaremos la siguiente linea:

jugadas['par'] += par(counter.values())

Este será el remplazo de todo lo que quitamos en la sección de modularizar.

Por último pero no menos importante, vamos a agregar un par de líneas para imprimir el resultado. Como son 9 la cantidad de jugadas posibles, me parece poco práctico agregar 9 print() si todos van a decir lo mismo. Así que crearemos un ciclo for para recorrer el diccionario e imprimir sus valores.

for jugada in jugadas.keys():
        probabilidad = jugadas[jugada] / intentos
        print(f'la probabilidad de sacar {jugada} en una mano de {tamano_mano} es de {probabilidad}')

Nuestra función main() debe quedar así:

defmain(tamano_mano, intentos):
    baraja = crear_baraja()
    jugadas = {
        'par': 0, 
        'doble par': 0, 
        'triada': 0, 
        'escalera': 0, 
        'color': 0, 
        'full': 0,
        'poker': 0,
        'escalera de color': 0,
        'escalera real de color': 0 
        }
    
    manos = simular_manos(baraja, intentos, tamano_mano)
    
    for mano in manos:
        valores = []
        for carta in mano:
            valores.append(carta[1])

        counter = dict(Counter(valores))
        
        jugadas['par'] += par(counter.values())

    for jugada in jugadas.keys():
        probabilidad = jugadas[jugada] / intentos
        print(f'la probabilidad de sacar {jugada} en una mano de {tamano_mano} es de {probabilidad}')
<h3>Agregar nuevos modulos</h3>

Bien, ahora se viene lo divertido. He clasificado los módulos en 4 categorias:

  1. Los que necesitan counter.value(), son aquellos que requieren saber si algún valor se ha repetido en las cartas de esa mano.

    • par()
    • doble_par()
    • triada()
    • poker()
  2. Los que necesitan los palos que han salido en las cartas de esa mano.

    • color()
  3. Los que necesitan los valores que han salido en las cartas de esa mano.

    • escalera()
  4. Los que requieren de los tres incisos anteriores.

    • full()
    • escalera_de_color()
    • escalera_real_de_color()
<h5>Los que requieren counter.value()</h5>

Para estos será muy sencillo, ya que podemos usar el mismo algoritmo de la funcion par() para triada() y poker(). Basta con cambiar un número en una linea de código:

defpar(valores_acumulados):for val in valores_acumulados:
        if val == 3: # Antes era 2return1return0

Ahora sólo faltaría cambiarle el nombre a la función.

deftriada(valores_acumulados):for val in valores_acumulados:
        if val == 3: 
            return1return0

Felicidades, creaste el modulo triada(). Debemos hacer lo mismo para el poker.

Pero ¿Qué hay del doble par? Si lo que necesitamos es saber si hay más de un par, sólo tenemos que evitar que la función termine al encontrar uno, y agregar un contador que recuerde cuantas veces hubo un par.

defdoble_par(valores_acumulados):
    contador = 0# Contadorfor val in valores_acumulados:
        if val == 2:
            contador += 1# Ahora en vez de un return, cuenta las veces que hay parif contador == 2:     # Si hay dos pares, retorna 1return1else:
        return0

¡Bien! Con esto terminamos los primeros 4 módulos.

<h5>Los que requieren palos</h5>

Antes de proseguir, requeriremos de todos los palos de nuestra mano. Esto es tan sencillo como agregar una lista de palos a nuestra función main(), tal y como lo hicimos con la lista valores.

defmain(tamano_mano, intentos):
    baraja = crear_baraja()
    jugadas = {
        'par': 0, 
        'doble par': 0, 
        'triada': 0, 
        'escalera': 0, 
        'color': 0, 
        'full': 0,
        'poker': 0,
        'escalera de color': 0,
        'escalera real de color': 0 
        }
    
    manos = simular_manos(baraja, intentos, tamano_mano)
    
    for mano in manos:
        valores = []
        palos = []      # <-- Aquífor carta in mano:
            valores.append(carta[1])
            palos.append(carta[0])  # <-- Aquí
        counter = dict(Counter(valores))
        
        jugadas['par'] += par(counter.values())
        jugadas['doble par'] += doble_par(counter.values())
        jugadas['triada'] += triada(counter.values())
        jugadas['poker'] += poker(counter.values())
        jugadas['color'] += color(palos)    # <-- Aquífor jugada in jugadas.keys():
        probabilidad = jugadas[jugada] / intentos
        print(f'la probabilidad de sacar {jugada} en una mano de {tamano_mano} es de {probabilidad}')

En este caso, sólo hay un módulo ¿Cómo podriamos hacerlo? Nuestra función debe retornar un 1 si todos los palos son iguales. Entonces si todos los palos son iguales, que retorne. Pero ¿Cómo podemos saber que eso se cumple? Podemos pensarlo de esta otra forma, cuando uno de los palos de las cartas no coincidan, entonces no hay color; de lo contrario sí. Programemos este razonamiento.

defcolor(palos):
    palo_carta_1 = palos[0] # Guarda el primer palofor palo in palos:
        if palo_carta_1 != palo:  # Lo compara con el resto de palos;return0return1

Como pueden ver, pregunta en cada iteración si el palo inicial es distinto al palo de ese ciclo. Si se llega a cumplir, retorna 0 y rompe el ciclo; De lo contrario termina el ciclo y retorna 1.

<h5>Los que requieren los valores</h5>

Al igual que en el módulo anterior, sólo es una la que lo requiere. La escalera es probablemente lo más complejo que programaremos en este tutorial, así que presta atención.

Primero analicemos el problema, ¿Qué necesitamos? Si no me equivoco, necesitamos los valores de la mano y queremos saber si se pueden ordenar de forma acendente respetando una diferencia de 1. Pero para poder ordenarlos los valores deben ser de tipo entero. Convirtamoslos a tipo entero.

defescalera(valores):
    valores_numericos = []
    for valor in valores: 
        try:    # Del 2 al 10
            valor_numerico = int(valor)
            valores_numericos.append(valor_numerico)

        except ValueError:  # jota, reina, rey y asif valor == 'jota':
                valores_numericos.append(11)
            elif valor == 'reina':
                valores_numericos.append(12)
            elif valor == 'rey':
                valores_numericos.append(13)
            elif valor == 'as':
                valores_numericos.append(1)
                valores_numericos.append(14)

En un ciclo intentaremos directamente convertir los strings en enteros con el método int() para posteriormente añadirlo a la lista valores_numericos(); funcionara con los valores 2-10. En el caso de que valor sea superior a 10 y menor o igual al as, compararemos valor con los 4 casos posibles para deducir y asignar su valor numérico. Al terminar el ciclo, ordenaremos la lista con el método sort().

valores_numericos.sort()

Lo útimo es saber si los valores de esta lista ordenada acendente tienen una diferencia de 1. Para eso haremos un recorrido de toda la lista comparando el valor actual con el valor próximo. Si el valor próximo menos 1 es distinto al valor actual, no tenemos escalera. Si el ciclo termina sin ninguna interrupción, sí es escalera.

defescalera(valores):
    valores_numericos = []
    for valor in valores:
        try:
            valor_numerico = int(valor)
            valores_numericos.append(valor_numerico)

        except ValueError:
            if valor == 'jota':
                valores_numericos.append(11)
            elif valor == 'reina':
                valores_numericos.append(12)
            elif valor == 'rey':
                valores_numericos.append(13)
            elif valor == 'as':
                valores_numericos.append(1)
                valores_numericos.append(14)
    
    valores_numericos.sort()
    n_valores = len(valores_numericos)

    for i in range(n_valores - 1):  # se resta 1 if valores_numericos[i] != valores_numericos[i + 1] - 1: # ¿valor actual es distinto a próximo?if valores_numericos[i] != 1:   # ???return0
        i += 1return1

Si ya te percataste, hay un par de cosas raras ahí ¿Por qué n_valores - 1? y ¿Qué hace el segundo if? Bueno escribimos n_valores - 1 ya que en la cuarta iteración ya comparamos los dos últimos valores [3, 4, 5, **6**, **7**], no hace falta hacer una quinta, esto sólo nos arrojaría un IndexError ya que no existe el indice 5.

¿El segundo if qué significa? Bien, recordemos que en el ciclo anterior donde generamos la lista numérica, agregamos 1 y 14 cuando el valor de la carta es 'as'. Esto significa que si llegara la posibilidad de salir una escalera real[1, 10, 11, 12, 13, 14], nuestro algoritmo no sería capaz de detectarla, ya que la diferencia entre 1 y 10 es de 9. Por eso mismo hacemos una segunda confirmación, con tal de asegurarnos de que el valor que nos dío verdadero en el primer if no sea el 1 de una escalera real.

<h5>Modulos compuestos</h5>

Ya estamos en la recta final, y detectar las jugadas restantes será de lo más fácil, así que démosle fin a esto.

El full, la escalera de color y la escalera real de color como ya lo había mencionado, son jugadas compuestas. Esto significa que podemos crear sus respectivos módulos con los ya existentes. Observa.

Un full se compone de un par y una triada. Así que sólo falta confirmar que ambos se presenten en la mano.

deffull(valores_acumulados):
    un_par = par(valores_acumulados)
    una_triada = triada(valores_acumulados)
    if un_par == 1and una_triada == 1: # ¿Hay par y triada?return1else:
        return0

Una escalera de color se compone de una escalera y un color. Sólo se debe confirmar su existencia en la mano.

defescalera_de_color(valores, palos):
    una_escalera = escalera(valores)
    un_color = color(palos)
    if una_escalera == 1and un_color == 1: # ¿Hay un color y una escalera?return1else:
        return0

Y por último la escalera real de color, esta es un poco más complicada, tenemos que confirmar la existencia de una escalera real y una escalera de color.

defescalera_real_de_color(valores, palos):
    una_escalera_de_color = escalera_de_color(valores, palos)
    if una_escalera_de_color == 1:      # ¿Hay una escalera de color?if'10'in valores and'as'in valores: # ¿existe 10 y as?return1else:
            return0return0

Se puede ver que para solucionar esto primero preguntamos si hay una escalera de color, si es el caso, preguntamos si en esa escalera de color cohexisten los valores 10 y 'as'. De cumplirse las dos condiciones, tenemos una escalera real de color.

¡Misión cumplida!

<h3>Últimos detalles</h3>

Ya tenemos todos nuestros módulos creados. Es hora de añadirlos a la función main().

función main():

defmain(tamano_mano, intentos):
    baraja = crear_baraja()
    jugadas = {
        'par': 0, 
        'doble par': 0, 
        'triada': 0, 
        'escalera': 0, 
        'color': 0, 
        'full': 0,
        'poker': 0,
        'escalera de color': 0,
        'escalera real de color': 0 
        }
    
    manos = simular_manos(baraja, intentos, tamano_mano)
    
    for mano in manos:
        valores = []
        palos = []
        for carta in mano:
            valores.append(carta[1])
            palos.append(carta[0])

        counter = dict(Counter(valores))
        
        jugadas['par'] += par(counter.values())
        jugadas['doble par'] += doble_par(counter.values())
        jugadas['triada'] += triada(counter.values())
        jugadas['escalera'] += escalera(valores)
        jugadas['color'] += color(palos)
        jugadas['full'] += full(counter.values())
        jugadas['poker'] += poker(counter.values())
        jugadas['escalera de color'] += escalera_de_color(valores, palos)
        jugadas['escalera real de color'] += escalera_real_de_color(valores, palos)

    for jugada in jugadas.keys():
        probabilidad = jugadas[jugada] / intentos
        print(f'la probabilidad de sacar {jugada} en una mano de {tamano_mano} es de {probabilidad}')

Llamemos a nuestra función.

if __name__ == '__main__':

    tamano_mano = int(input('ingrese el tamaño de la mano: '))
    intentos = int(input('ingrese el número de intentos: '))

    main(tamano_mano, intentos)

Estos serían nuestros resultados al pedir una mano de 5 cartas 10 millones de veces:

Texto alternativo
<h3>Conclusiones</h3>

Como ya aprendimos en este curso, la generación de simulaciones pueden ser muy útiles para aproximarse a los porcentajes reales de certidumbre de un problema estocástico. Mi recomendación para poder acercarse a esos valores de una forma más fiable, es ejecutando la simulación con un número de intentos mínimo a los 10 millones. Aunque esto sólo es una estimación perfectamente falible que saque a prueba y error, tiene mucho margen de mejora.

ADVERTENCIA:si tu pc no la consideras “rápida”, no ejecutes el programa con 10 millones de intentos.

Código del proyecto: https://github.com/Angrub/probabilidades_jugadas_poker.git

Escribe tu comentario
+ 2
Ordenar por:
4
22030Puntos

Muy buen tutorial solo que no se cargo la imagen de los resultados, si la puedes poner en un comentario estaría genial.

Saludos!

2
9391Puntos

Amigo muchas gracias! He podido entender un poco más de cómo va el funcionamiento de todo ese algoritmo gracias a tí!