El 23 de junio de 2020 hubo un temblor de 7.5 grados en la Ciudad de México (donde radico) y a manera de terapia para entretener al cerebro me planteé hacer una Simulación Montecarlo para modelar las probabilidades de cada resultado en el Póker de manos de 5 cartas. En esta solución aplico lo explicado por David Aroesti en el Curso de Programación Dinámica y Estadística con Python (y unas cosillas extras para allanar el camino).
Sin embargo, antes de comenzar aclaro que, aunque los resultados a los que llegaremos son una aproximación sólida a lo que se puede obtener mediante una solución analítica (matemática), la manera en que está escrito el código es lo más simple que se me ocurrió. Sé que el código se puede hacer mucho más eficiente y bonito (por ejemplo, empleando objetos para construir los jugadores), pero mi objetivo era encontrar una solución sólida y, después, mejorarla. Este tutorial solo llega hasta la parte de “sólida”, sin embargo, en caso de que alguien quiera tener certeza sobre la validez de este tutorial, les comparto aquí los resultados de mi algoritmo y una comparación con la solución analítica:
Al final de este tutorial proveeré links a videos con explicaciones matemáticas para validar las aproximaciones a las que llegaremos (para quien tenga curiosidad).
Pero prosigamos a la implementación.
Póker de manos de 5 cartas
El Póker es un juego de cartas ampliamente conocido pero que, en realidad, es un juego en el que existen muchas variantes. En la variante que será modelada en este tutorial solo se reparten 5 cartas a cada jugador de manera alternada. Además, implica lo siguiente:
La cantidad de jugadores debe ser entre 2 y 4
Se juega con el mazo de 52 cartas
El as puede ser considerado como 1 o como la figura de mayor valor
No se emplean comodines (wild cards)
Solución
Lo que haremos primero será explicar de manera general la estrategia. Para empezar, necesitamos crear una baraja de cartas y para tal fin se empleará lo explicado por el Profesor David Aroesti en el curso. En este caso, por una cuestión de practicidad, implementé todas las cartas con números, siendo que el Jack es igual a 11, la Reina es 12, el Rey es 13 y el As es el 14 (lamento si esto puede dificultar la lectura, pero, a priori, pensé que podría ser útil para extender la implementación a una expansión futura que modele la versión de Texas Hold’em de Póker). También tenemos que importar las librerías import y collections y crear las bases para construir nuestra baraja (similar a como ocurre en los vídeo).
Después procedemos a repartir las cartas entre los jugadores (esto es algo meramente cosmético, porque las probabilidades, si se reparten las cartas o no, son las mismas, pero la neta es que se ve más cool si simulamos la mesa como debería de ser). Aclaro que, esta implementación no es muy elegante que digamos, la verdad se podría hacer más linda, pero, por ahora, hace su trabajo:
La lista manos que regresa la función es un arreglo de listas que incluyen las cartas de cada jugador, es decir que, cada elemento de la lista manos contiene las 5 cartas de cada jugador expresadas como una tupla de palo y número (recordar que el Jack es 11, Reina es 12, Rey es 13 y As es 14).
Después de simular la creación de una baraja y de la repartición de cartas, lo siguiente sería simular el escenario en el que los ‘n’ jugadores se encontrarían después de la repartición de cartas (es decir, el escenario después de repartir 5 cartas a entre 2 y 4 jugadores). Este es el comienzo de la función central de todo el código:
Lo más importante de esta parte es el ciclo for, que, en palabras cotidianas, nos está diciendo que, por cada jugador en la mesa vamos a hacer un recorrido entre los cuatro jugadores (de 1 hasta [*range(numero_de_jugadores)] ). Ahora bien, dentro de la instrucción *for jugador in [range(numero_de_jugadores)]: la instrucción resultados.append(pares(manos[jugador])) lo que nos está diciendo es que en la variable ‘resultados’ vamos a guardar (adjuntar a la lista) el resultado de la función ‘pares’. Dicho de otra forma, si la variable ‘resultados’ fuera igual a resultados = ‘Un par’ esto se añadiría a la variable ‘resultados’ por cada jugador en la mesa. Un ejemplo de cómo quedaría la variable ‘resultados’ después de llamar a la función ‘juego’ sería: resultados = [‘Un par’,‘Un par’,‘None’,'None]. Por supuesto, ‘Un par’ o ‘Dos pares’ no son los únicos resultados posibles, de ahí que incluyamos lo siguiente:
La cuestión más importante de esta parte del código es que las líneas ‘resultados[jugador] =’ nos están diciendo que vamos a añadir esos resultados a la lista ‘resultados’. Por ejemplo, si el primer jugador [jugador 0] obtuvo un full_house, su casilla dirá ‘Full house’.
La parte “siguiente” sería incluir todo el código de las validaciones de cada tipo de jugada (par, dos pares, tercia, color, etcétera). Pero haré eso hasta el final. Dejaré aquí el código de la simulación montecarlo para el primer jugador (y ahí le dejo porque ya es muy tarde y tengo sueño). Aquí está el código del módulo simulación montecarlo (yo sé que es horrible, pero fue lo mejor que pude hacer en corto tiempo):
Aquí les dejo la implementación de las funciones necesarias para implementar el módulo anterior:
Módulo para simular un juego según la cantidad de jugadores:
Implementación del módulo pares para reconocer cuándo hay pares en la manos de los jugadores:
Aquí para encontrar tercias:
Para encontrar Color:
Para encontrar Escaleras:
Para encontrar full house:
Para encontrar Pókers:
Para encontrar Escalera de Color:
Para encontrar Flor Imperial:
Explicaciones para calcular analíticamente la probabilidad de obtener un par en el Póker de 5 hand cards:
Explicaciones para calcular analíticamente la probabilidad de obtener una escalera en el Póker de 5 hand cards: