660 likes | 1.07k Views
Alejandro Pérez Roca Daniel Martín Prieto Jorge Tudela Glez de Riancho. Juegos de Azar y Programación Declarativa. Índice. Introducción. Reglas de los juegos. Recopilación y formateo de datos. Programas de obtención de premios. Apuestas múltiples. Diferencias entre los tipos de juegos.
E N D
Alejandro Pérez Roca Daniel Martín Prieto Jorge Tudela Glez de Riancho Juegos de AzaryProgramación Declarativa
Índice • Introducción. • Reglas de los juegos. • Recopilación y formateo de datos. • Programas de obtención de premios. • Apuestas múltiples. • Diferencias entre los tipos de juegos. • Programas de búsqueda de patrones. • Patrones numéricos. • Patrones visuales. • Resultados curiosos. • Ventajas de utilizar un lenguaje declarativo.
Introducción • Nuestro objetivo consistía en conseguir implementar los juegos del Euromillón, la Primitiva y la Quiniela basándonos en la historia de dichos juegos, aprovechando las ventajas tanto de eficiencia como de elegancia que nos proporciona un lenguaje declarativo. • Además, pretendíamos descubrir y analizar resultados curiosos aplicando patrones para conocer hasta qué punto es viable el uso de estos a la hora de apostar.
Reglas del Euromillón • Juego de lotería europeo organizado por diferentes instituciones de Loterías, entre ellas Loterías y Apuestas del Estado. • El sorteo tiene lugar los viernes y salvo excepción se juega a las 21.30 en París. • El juego consiste en elegir 5 números entre 1 y 50 y otros dos números comprendidos entre el 1 y el 9, conocidos como estrellas. • Las apuestas múltiples consisten en marcar más de 5 números hasta un máximo de 10. • No se pueden marcar más de dos estrellas.
Reglas del Euromillón El precio de cada apuesta es de 2 €, destinándose el 50% de la recaudación íntegra a los premios de la siguiente forma: • 16.00% para premios de la Primera Categoría (5 números y dos estrellas). • 3.70% para premios de la Segunda Categoría (5 números y una estrella). • 1.05% para premios de la Tercera Categoría (5 números y ninguna estrella). • 0.75% para premios de la Cuarta Categoría (4 números y dos estrellas). • 0.50% para premios de la Quinta Categoría (4 números y una estrella). • ...
Reglas de la Primitiva • Es un juego de azar cuyo primer sorteo data de 1985, se celebran unos 100 sorteos al año y, hasta hoy, más de 2300. • Consiste en elegir seis números diferentes entre el 1 y el 49. • Se pueden realizar apuestas múltiples eligiendo 5,7,8,9,10 u 11 números. • El sorteo se realiza extrayendo siete bolas al azar de un bombo de 49 bolas numeradas del 1 al 49. • Las seis primeras son la combinación ganadora. • La séptima es el complementario. • Por último se extrae el reintegro de un bombo de diez bolas numeradas del 0 al 9.
Reglas de la Primitiva El precio de cada apuesta es de 1 €, destinándose el 55% de la recaudación íntegra a los premios de la siguiente forma: • 52% para premios de la Primera Categoría (6 aciertos). • 8% para premios de la Segunda Categoría (5 aciertos + complementario). • 16% para premios de la Tercera Categoría (5 aciertos). • 24% para premios de la Cuarta Categoría (4 aciertos). • 8 € para premios de la Quinta Categoría (3 aciertos). • El reintegro es asignado aleatoriamente por el sistema a cada boleto.
Reglas de la Quiniela • Se basa en los resultados de la LFP. • El juego consiste en el sistema 1X2, donde sólo se pronostica si gana el equipo local (1), visitante (2), o si el partido queda empate (X). • La apuesta se realiza sobre una lista de 15 partidos, donde 'El Pleno al 15' sólo se tiene en cuenta en caso de haber acertado los 14 restantes. • Además de la apuesta sencilla, hay otras formas de apostar, pudiendo seleccionar para un mismo partido dos resultados posibles (doble) o incluso los tres (triple).
Reglas de la Quiniela El precio de cada apuesta es de 0.50 €, destinándose el 55% de la recaudación íntegra a los premios de la siguiente forma: • 10% para premios de la Categoría Especial (Primera Categoría + Pleno al 15). • 12% para premios de la Primera Categoría (14 aciertos)*. • 8% para premios de la Segunda Categoría (13 aciertos)*. • 8% para premios de la Tercera Categoría (12 aciertos)*. • 8% para premios de la Cuarta Categoría (11 aciertos)*. • 9% para premios de la Quinta Categoría (10 aciertos)*. (*) Sin contar el Pleno al 15.
Datos originales • Los datos de historia de los tres juegos han sido recopilados de www.onlae.es, la web oficial de Loterías y Apuestas del Estado. • Dichos datos, han sido copiados tal cual a un fichero de texto plano.
Datos originales • De esta forma quedaría el fichero original, el cual tenemos que formatear porque hay información que no nos interesa:
Datos formateados • Al fichero anterior, se le ha aplicado un filtro Lex para eliminar información que no queremos y adaptar el formato a las listas de Haskell, a través de las que se accederán a los datos para que los programas realicen los cálculos. • La primera lista es la apuesta ganadora. • La segunda la forman ventas, bote y acertantes con sus premios correspondientes.
Datos formateados • Mediante la función readFile iremos accediendo a la información de forma perezosa, según lo vayan necesitando las funciones correspondientes, de una forma simple y cómoda. quiniela :: String -> IO () quiniela apuesta = do x <- readFile historiaQuiniela let ganancias = calcularPremioHistoria ((traducir . words) apuesta) (lines x) ... ...
Datos formateados • Además, debido a que Haskell es un lenguaje de programación fuertemente tipado y para que haya coherencia de tipos, utilizamos la función read, que convierte un String al tipo inferido. calcularPremioHistoria :: [Int] -> [String] -> Float calcularPremioHistoria _ [] = 0.0 calcularPremioHistoria apuesta (ganadora:premios:restoHistoria) = calcularPremioDía apuesta (read ganadora) (read premios) + calcularPremioHistoria apuesta restoHistoria
Programas de obtención de premios • En esta sección se van a mostrar las funciones necesarias para implementar un programa que: • Reciba como entrada una combinación. • Nos devuelva las ganancias y pérdidas que obtendríamos si hubiésemos jugado con esta combinación durante toda la historia de la lotería.
Código del Euromillón • Lectura de ficheros y llamada al programa de cómputo. euromillón :: [Int] -> IO () euromillón apuesta = do x <- readFile historiaEuromillón let ganancias = calcularPremioHistoria apuesta (lines x) let pérdidas = fromIntegral ((length . lines) x)/2 * calcularPrecioApuesta apuesta print (ganancias, pérdidas, ganancias - pérdidas)
Código del Euromillón • Cálculo del precio de una apuesta. Apuestas múltiples. calcularPrecioApuesta :: [Int] -> Float calcularPrecioApuesta apuesta = 2.0 * fromIntegral (comb ((length apuesta)-2) 5) • Cálculo de las ganancias generadas por una apuesta durante toda la historia. calcularPremioHistoria :: [Int] -> [String] -> Float calcularPremioHistoria _ [] = 0.0 calcularPremioHistoria apuesta (ganadora:premios:restoHistoria) = calcularPremioDía apuesta (read ganadora) (read premios) + calcularPremioHistoria apuesta restoHistoria
Código del Euromillón • Cálculo de las ganancias generadas por una apuesta en un día. calcularPremioDía :: [Int] -> [Int] -> [Float] -> Float calcularPremioDía apuesta ganadora premios = calcularPremioCategoría (categoríaPremio apuesta ganadora) premios • Obtención de la categoría del acierto. categoríaPremio :: [Int] -> [Int] -> Int categoríaPremio apuesta ganadora = calcularCategoría (númeroAciertos ((init . init) apuesta) (take 5 ganadora)) (númeroAciertos (drop ((length apuesta)-2) apuesta) (drop 5 ganadora))
Código del Euromillón • Obtención del número de aciertos. númeroAciertos :: [Int] -> [Int] -> Int númeroAciertos [] _ = 0 númeroAciertos (x:xs) ys | elem x ys = 1 + númeroAciertos xs ys | otherwise = númeroAciertos xs ys • Cálculo de la categoría en función del número de aciertos. calcularCategoría :: Int -> Int -> Int calcularCategoría números estrellas = case (números,estrellas) of (5,_) -> 3 - estrellas (4,_) -> 6 - estrellas (3,0) -> 10 (3,_) -> 9 - estrellas (2,2) -> 9 (2,1) -> 12 (1,2) -> 11 (_,_) -> 0
Código del Euromillón • Obtención del premio obtenido según la categoría. calcularPremioCategoría :: Int -> [Float] -> Float calcularPremioCategoría categoría (ventas:bote:premios) = case categoría of 0 -> 0.0 1 -> bote/(premios!!0 + 1) 2 -> (ventas/2)*0.074/(premios!!2 + 1) 3 -> (ventas/2)*0.021/(premios!!4 + 1) 4 -> (ventas/2)*0.015/(premios!!6 + 1) 5 -> (ventas/2)*0.010/(premios!!8 + 1) 6 -> (ventas/2)*0.007/(premios!!10 + 1) 7 -> (ventas/2)*0.010/(premios!!12 + 1) 8 -> (ventas/2)*0.051/(premios!!14 + 1) 9 -> (ventas/2)*0.044/(premios!!16 + 1) 10 -> (ventas/2)*0.047/(premios!!18 + 1) 11 -> (ventas/2)*0.101/(premios!!20 + 1) 12 -> (ventas/2)*0.240/(premios!!22 + 1)
Código de la Primitiva • En la primitiva hay que tener en cuenta el reintegro. calcularPremioDia :: [Int] -> [Int] -> [Float] -> FloatcalcularPremioDia apuesta ganadora premios = calcularPremioCategoria (categoriaPremio apuesta ganadora) premios + reintegro where reintegro = if last apuesta == last ganadora then calcularPrecioApuesta apuesta else 0
Código de la Primitiva • Hay seis números en la combinación, no hay estrellas y hay que tener en cuenta el número complementario. categoriaPremio :: [Int] -> [Int] -> IntcategoriaPremio apuesta ganadora = calcularCategoria (numeroAciertos (init apuesta)(take 6 ganadora)) (numeroAciertos (init apuesta) comple) where comple = take 1 (drop 6 ganadora)
Código de la Primitiva • Hay cinco categorías en vez de doce, la categoría depende de la combinación ganadora y del complementario. calcularCategoria :: Int -> Int -> IntcalcularCategoria apuesta complementario = case (apuesta,complementario)of (6,_) -> 1 (5,1) -> 2 (5,_) -> 3 (4,_) -> 4 (3,_) -> 5 (_,_) -> 0
Código de la Quiniela • En nuestro programa de la Quiniela, la entrada es un String. • Por ejemplo: "1 1x 2 1 1 x 1x2 2 1 1 1 x 2 2 1x". • Para manejar la apuesta mejor, hacemos una traducción a lista de enteros. traducir :: [String] -> [Int] traducir [] = [] traducir (x:xs) | x=="1" = (1:traducir xs) | x=="2" = (2:traducir xs) | x=="x" = (3:traducir xs) | x=="12" = (4:traducir xs) | x=="1x" = (5:traducir xs) | x=="x2" = (6:traducir xs) | x=="1x2" = (7:traducir xs) | x=="?" = (8:traducir xs) -- Esto es para los patrones.
Código de la Quiniela • Para calcular el precio hay que saber cuántas apuestas se han hecho, en función de los dobles y triples que contenga. númeroApuestas :: [Int] -> Int númeroApuestas [] = 1 númeroApuestas (x:xs) | esDoble x = 2 * númeroApuestas xs | esTriple x = 3 * númeroApuestas xs | otherwise = númeroApuestas xs • Los dobles son 4, 5 y 6 (12, 1x, x2). • El triple es 7 (1x2).
Código de la Quiniela • También es diferente con respecto al Euromillón y la Primitiva el hecho de que el orden a la hora de hacer la apuesta importa. • No es lo mismo "1 2 x..." que "1 x 2...". númeroAciertos :: [Int] -> [Int] -> Int númeroAciertos apuesta ganadora = sum (zipWith (\x y -> if (acierta x y) then 1 else 0) apuesta ganadora) • La función acierta tiene en cuenta los dobles y triples. • Si apuesta = "x2 1..." y ganadora = "x x..." devolvería un acierto en el primer elemento de las combinaciones.
Código de la Quiniela • El número de aciertos nos sirve para conocer la categoría del premio al que hubiéramos optado. calcularCategoría :: Int -> Int -> Int calcularCategoría aciertos aciertoPleno = case (aciertos,aciertoPleno) of (14,1) -> 6 -- Pleno al 15. (14,0) -> 1 -- Categoría 1. (13,_) -> 2 -- Categoría 2. (12,_) -> 3 (11,_) -> 4 (10,_) -> 5 (_,_) -> 0 -- Categoría 0 == No premiado.
Búsqueda de Patrones • ¿Qué es un patrón? • Definición de la RAE: Modelo que sirve de muestra para sacar otra cosa igual. • Nuestra definición: Conjunto de apuestas que tienen algo en común.
Búsqueda de Patrones • Diferenciamos dos tipos de patrones: • Patron numérico: Conjunto de apuestas cuyos números guardan algún tipo de relación matemática. • Ejemplos: • Apuesta con números primos. • Apuesta con números menores que diez. • Apuesta de números pares. • Patrón visual: Conjunto de apuestas que al ser marcadas sobre el boleto, representan una figura.
Búsqueda de Patrones • Ejemplo: patrones visuales en el boleto de la Quiniela.
Búsqueda de Patrones • Nuestro objetivo es conocer si la gente juega de forma aleatoria o sigue algún patrón. • ¿Cómo podemos saber a qué juega la gente si el número de apuestas no es público? • Sólo tenemos información sobre las ventas de cada sorteo y el número de acertantes de cada categoría. Ratio ventas/acertantes: • La ratio de las ventas entre la suma de los acertantes de N categorías, es un indicador de la cantidad de gente que ha ganado apostando con un determinado patrón.
Búsqueda de Patrones • Necesitamos poder comparar la ratio de cada combinación con alguna referencia. • Nuestra referencia será la media de esa ratio en todos los sorteos de los que tenemos información. imprimirMedia :: [String] -> Float imprimirMedia [] = 0.0 imprimirMedia (combinacion:premios:historia) = (head . calcularRatio . read) premios + imprimirMedia historia
Búsqueda de Patrones • Esto nos permite conocer cuánto ha sido apostada una combinación o un conjunto de combinaciones (patrón). • Cuanto mayor sea nuestro indicador para una combinación, menos acertantes se habrán producido y viceversa. • Los programas que hemos implementado utilizan este indicador de forma que, dado un patrón y un predicado, nos devuelven el conjunto de combinaciones que lo cumplen, las ventas, los acertantes y la ratio ventas entre acertantes, para cada combinación seleccionada. • El predicado servirá para filtrar las combinaciones en función del número de coincidencias.
Patrones numéricos: Euromillón • Lectura de ficheros y llamada al programa de cómputo. • patrón :: (Int -> Bool) -> (Float -> Bool) -> Int -> IO () • patrón f g n = do • x <- readFile historiaEuromillón • imprimirPatron (lines x) f g n • Selección de combinaciones a imprimir. imprimirPatron :: [String] -> (Int -> Bool) -> (Float -> Bool) -> Int -> IO () imprimirPatron (combinacion:premios:historia) f g n | (length (filter f (take 5 (read combinacion))) >= n) && ((g . head . calcularRatio . read) premios) = do imprimirPatron historia f g n print (combinacion ++ " --> " ++ show((calcularRatio . read) premios))) | otherwise = imprimirPatron historia f g n imprimirPatron [] _ _ _ = putStr ""
Patrones numéricos: Euromillón • Calculamos la ratio de la siguiente forma: Las ventas que se produjeron para ese sorteo entre el número de acertantes totales. calcularRatio :: [Float] -> [Float] calcularRatio (ventas:bote:acertantes) = [ventas/(sumarAcertantes acertantes 8),ventas,sumarAcertantes acertantes 8] • Para que el valor obtenido sea más representativo no hemos tenido en cuenta los acertantes de las últimas categorías ya que siempre hay muchos y no nos dan una información significativa.
Patrones visuales: Primitiva • Todas las apuestas de un patrón cumplen que: • La distancia de todos los números respecto al primero siempre es la misma. • La distancia en columnas de todos los números respecto al primero siempre es la misma.
Patrones visuales: Primitiva • Ejemplo: Las dos apuestas pertenecen al mismo patrón. • [1,11,12,21,22,32] == [17,27,28,37,38,48]
Patrones visuales: Primitiva • La función patron recibe como parámetro la primera combinación que pertenece a un patrón y un predicado p que sirve para filtrar las combinaciones en función del número de coincidencias con el patrón. • Muestra la combinación, la ratio, las ventas, el nº de acertantes y dibuja la combinación. patron :: [Int]-> (Int -> Bool) -> IO[()]patron pat p = do x <- readFile historiaPrimitiva dibujaBoleto pat mapM (sequence_ . dibujaResultados)(buscaPatrones (sort pat) (lines x) p)
Patrones visuales: Primitiva • Selección de combinaciones que pertenecen al patrón. buscaPatrones :: [Int] -> [String] -> (Int -> Bool) -> [([Int],[Float])] buscaPatrones _ [] _ = [] buscaPatrones [] _ _ = [] buscaPatrones patron (ganadora:premios:restoHistoria) p | encajaPatron ((take 7 . read) ganadora) (creaTodosPatrones patron) p = (((sort . take 7 . read)ganadora),calcularRatio (read premios)):buscaPatrones patron restoHistoria p |otherwise = buscaPatrones patron restoHistoria p
Patrones visuales: Primitiva • La función creaTodosPatrones recibe la menor apuesta que es cubierta por el patrón, y devuelve lista con todas las apuestas cubiertas por el patrón. creaTodosPatrones :: [Int] -> [[Int]] creaTodosPatrones patron = creaPatrones (sort patron) (creaDesp (sort patron)) • encajaPatron comprueba que una combinación pertenece a un patrón, satisfaciendo el predicado p. encajaPatron :: [Int] -> [[Int]] -> (Int -> Bool) -> Bool encajaPatron apuesta patron p = p (foldr (\x y -> max (numeroAciertos x apuesta) y) 0 patron)
Patrones visuales: Primitiva • Hemos definido una función constante que devuelve la estructura del boleto. _boleto = " 00 10 20 30 40\n 01 11 21 31 41\n 02 12 22 32 42\n 03 13 23 33 43" ++ "\n 04 14 24 34 44\n 05 15 25 35 45\n 06 16 26 36 46\n 07 17 27 37 47" ++ "\n 08 18 28 38 48\n 09 19 29 39 49" • Sobre_boleto, sustituimos los números de una combinación por la cadena "__", y los mostramos por pantalla.
Patrones visuales: Primitiva • Funciones para crear un boleto y mostrarlo por pantalla. dibujaBoleto :: [Int] -> IO() dibujaBoleto = do putStrLn . ("\n" ++) . creaBoletoVisual creaBoletoVisual :: [Int] -> String creaBoletoVisual combinacion = unlines (map unwords (sustituyeBoleto combinacion "__" (map words (lines _boleto))))
Patrones visuales: Quiniela • Hemos implementado la función pintar, que dado un patrón o una apuesta, la muestra por pantalla como si fuera el boleto. pintar :: String -> IO() pintar apuesta = (putStrLn . ("\n 1 X 2\n " ++) . unwords) (zipWith (++) pintaColumna (map traducirGráfico (words apuesta))) pintaColumna :: [String] pintaColumna = map (\x -> if (x<10) then " "++show(x) else show(x)) [1..15] traducirGráfico :: String -> String traducirGráfico simb = case simb of "?" -> " \n" "1" -> " * \n" "x" -> " * \n" "2" -> " * \n" "1x" -> " * * \n" "12" -> " * * \n" "x2" -> " * * \n" "1x2" -> " * * * \n"
Patrones visuales: Quiniela • La función patrón, a la que se le pasa un patrón (puede tener filas no marcadas, señaladas con '?') y un número mínimo de aciertos que debe proporcionarnos dicho patrón, nos lo muestra por pantalla y nos da una relación de combinaciones ganadoras que cumplen ese mínimo y los aciertos y la ratio anteriormente explicado. patrón :: String -> Int -> IO() patrón p error = do x <- readFile historiaQuiniela pintar p imprimirPatrón (lines x) (traducir (words p)) error
Patrones visuales: Quiniela imprimirPatrón :: [String] -> [Int] -> Int -> IO() imprimirPatrón (combinación:premios:historia) patrón error | (númeroAciertos patrón (read combinación)) >= error = do imprimirPatrón historia patrón error print ((bonita (read combinación)) ++ " --> " ++ show (númeroAciertos patrón (read combinación)) ++ " --> " ++ show (calcularRatio (read premios))) | otherwise = imprimirPatrón historia patrón error imprimirPatrón [] _ _ = putStr ""