Tracks
/
Elm
Elm
/
Ejercicios
/
Creador de laberintos
Creador de laberintos

Creador de laberintos

Ejercicio de aprendizaje

Introducción

Random

En un lenguaje funcional puro como Elm, una función a la que se llama con los mismos argumentos siempre debe devolver el mismo valor. Por lo tanto, una función con la firma de tipo rand : Int solo puede implementarse como rand = 4, lo cual no augura nada bueno para generar números enteros aleatorios.

Entonces, ¿cómo generamos valores aleatorios en Elm? Dividimos el problema en dos: primero describimos el valor que queremos generar con un Random.Generator a, y luego generamos un valor.

La primera forma de generar un valor es crear una Random.Seed con initialSeed : Int -> Seed y luego usar step : Generator a -> Seed -> ( a, Seed ), que devuelve un valor y una nueva semilla. Ten en cuenta que ambas funciones son puras, así que llamarlas dos veces con los mismos argumentos producirá los mismos valores.

La segunda forma de generar un valor es usar generate : (a -> msg) -> Generator a -> Cmd msg, pero eso solo se puede hacer dentro de una aplicación de Elm. En ese caso, el entorno de ejecución de Elm puede usar step, además de recursos externos no puros, para generar semillas.

De ahora en adelante, nos centraremos en los generadores.

Imaginemos, para poder mostrar ejemplos, que definimos con initialSeed y step una función generate : Int -> Generator a -> List a que genera cierta cantidad de valores a partir de un generador.

El módulo Random proporciona dos generadores básicos para generar números enteros y números de punto flotante dentro de un rango específico:

generate 5 (Random.int -5 5)
    --> [0, 3, -5, 5, 0]

generate 3 (Random.float 0 5)
    --> [1.61803, 3.14159, 2.71828]

Esos valores se pueden combinar en tuplas o en listas de valores:

generate 2 (Random.list 3 (Random.int 0 3))
    --> [[0, 3, 3], [1, 3, 2]]

generate 2 (Random.pair (Random.int 0 3) (Random.float 10 10.3))
    --> [(0, 10.23412), (2, 10.17094)]

El paquete elm-community/random-extra proporciona muchos más generadores para diversas estructuras de datos: strings, fechas, diccionarios, arrays, conjuntos, etc.

Podemos crear generadores que solo devuelvan un único valor usando Random.constant:

generate 4 (Random.constant "hello")
    --> ["hello", "hello", "hello", "hello"]

Podemos elegir aleatoriamente entre elementos dados con la misma probabilidad usando Random.uniform:

generate 5 (Random.uniform Red [Green, Blue])
    --> [Red, Blue, Blue, Green, Red]

Random.uniform recibe dos argumentos (Red y [Green, Blue]) para garantizar que haya al menos un valor del que elegir, ya que una sola lista podría estar vacía.

También podemos ajustar las probabilidades usando Random.weighted:

generate 5 (Random.weighted (80, Red) [(15, Green), (5, Blue)])
    --> [Red, Red, Green, Red, Red]

Los valores no necesitan sumar 100, de todos modos se van a renormalizar.

Podemos acceder al interior de un generador con Random.map:

generate 3 (Random.int 1 6 |> Random.map (\n -> n * 10))
    --> [30, 60, 10]

También podemos usar Random.map2 hasta Random.map5 para combinar más generadores:

position =
    Random.map3
      (\x y z -> Position x y z)
      (Random.float -100 100)
      (Random.float -100 100)
      (Random.float -100 100)

generate 1 position
    --> [Position 33.788 -98.321 10.0845]

Para usos más complejos, tenemos Random.andThen : (a -> Generator b) -> Generator a -> Generator b, que puede usar el valor generado por un generador para crear otro:

bool = Random.uniform True [False]

failHalfOfTheTime : Generator a -> Generator (Maybe a)
failHalfOfTheTime generator =
    bool
        |> Random.andThen
            (\boolResult ->
                if boolResult then
                    Random.map Just generator

                else
                    Random.constant Nothing
            )

generate 6 (Random.int 0 1 |> failHalfOfTheTime)
    --> [Nothing, Just 1, Just 0, Nothing, Just 1, Nothing]

A veces es útil definir un generador de forma autorrecursiva. En esos casos, puede que necesites usar Random.lazy para evitar que el compilador expanda el generador de forma infinita.

type Peano
    = Zero
    | Next Peano


peano : Generator Peano
peano =
    Random.uniform (Random.constant Zero)
        [ Random.map Next (Random.lazy (\_ -> peano))
        ]
        |> Random.andThen identity

generate 12 peano
    --> [Zero, Next(Next(Zero)), Zero, Next(Zero), Zero, Zero, Next(Zero), Zero, Zero, Next(Zero), Next(Next(Next(Zero)))]

Este ejemplo es un poco denso, así que analicémoslo por partes.

Peano es un tipo que representa números enteros positivos: Zero es 0, Next(Zero) es 1, Next(Next(Zero)) es 2, etc. Definimos peano para que nos dé un número Peano aleatorio.

En primer lugar, ten en cuenta que Random.lazy no aporta nada a la lógica; simplemente evita que el compilador escriba peano dentro de peano de forma indefinida.

Usamos Random.uniform para elegir entre cero (con un 50% de probabilidad) y otro número Peano más uno (con un 50% de probabilidad). Sin embargo, a diferencia del ejemplo anterior, Random.uniform no elige entre valores (como Zero), sino entre generadores (como Random.constant Zero), ya que queremos usar peano mismo. Esto significa que Random.uniform devolverá un valor de tipo Generator (Generator Peano), que no es lo que necesitamos.

Para «aplanar» el generador, lo canalizamos hacia Random.andThen : (a -> Generator b) -> Generator a -> Generator b, donde queremos que b sea Peano. Como Generator a es Generator (Generator Peano), a debe ser Generator Peano, y la función (a -> Generator b) debe ser de tipo (Generator Peano -> Generator Peano). No queremos modificar nada, así que identity es la opción correcta.

Por último, ¿qué tipo de números producirá peano? Sabemos que producirá 0 el 50% de las veces, o bien otra iteración de sí mismo más uno el otro 50% de las veces. Eso significa que los números serán 0 (el 50% de las veces), 1 (el 25% de las veces), 2 (el 12% de las veces), 3 (el 6% de las veces), etc. peano puede producir números arbitrariamente grandes, pero con una probabilidad que disminuye de forma exponencial.

Instrucciones

Eres un investigador de laberintos y trabajas en diseñar una nueva generación de mazmorras y terrenos de aventura.

Eres un teórico, lo que significa que no te interesa la disposición física de los laberintos (eso es tarea de los ingenieros de laberintos), sino su complejidad matemática: el factor de ramificación, la profundidad máxima y cosas por el estilo.

Decides abordar tu próximo proyecto con un enfoque de generación aleatoria.

1. Empieza por el final

Cualquier laberinto que se precie es, en su mayoría, callejones sin salida.

Define el generador deadend para que siempre genere la variante DeadEnd.

2. No se cazan moscas con vinagre

Vamos a darles a los aventureros una razón para adentrarse en tus laberintos.

Define el generador treasure para que genere Gold, Diamond o Friendship con la misma probabilidad.

Luego, define el generador room para que envuelva los valores generados por treasure en la variante Room.

3. Ramificarse

Y ahora, el corazón y el alma de un laberinto: las ramas.

El generador branch recibe un argumento Generator Maze, que es un generador capaz de generar un laberinto arbitrario.

Define el generador branch para que genere una variante Branch que contenga varias ramas que lleven a laberintos generados por su argumento.

La cantidad de ramas debe estar entre 2 y 4 (si hay más, el ingeniero de laberintos se enojaría), determinada al azar con la misma probabilidad.

4. Laberintos asombrosos

Es hora de ponerse manos a la obra.

Define el generador maze para que genere un callejón sin salida el 60% de las veces, una sala del tesoro el 15% de las veces y una rama el 25% de las veces. Las ramas deben generar nuevos laberintos usando maze de forma recursiva. Asegúrate de usar deadend, room y branch en el generador.

Cada rama contiene, en promedio, 3 laberintos, pero solo el 25% de estos se ramificará de nuevo, lo que da menos de uno, así que es probable que tus laberintos generados terminen en algún momento.

5. Hay que ir más profundo

Tu maze está bien, pero tiene algunos problemas:

  1. El 75% de los valores generados no tienen ninguna rama, así que difícilmente son laberintos de verdad
  2. Puede que los aventureros encuentren salas del tesoro al principio del laberinto y decidan dejar de explorar
  3. Aunque se pueden generar laberintos profundos, no son muy probables: un laberinto de profundidad 3 tiene aproximadamente un 3% de probabilidad de generarse, y uno de profundidad 10, un 0.2% (la profundidad de un laberinto se calcula con MazeMakerSupport.mazeDepth)

Define el generador mazeOfDepth n para que genere un laberinto de profundidad n. Únicamente el nivel más profundo, 0, debe contener callejones sin salida o salas del tesoro (con la misma probabilidad); todos los demás niveles deben ser una rama que contenga laberintos de profundidad n-1 generados de forma recursiva con mazeOfDepth. Asegúrate de usar deadend, room y branch en el generador.

Editar en GitHub El enlace se abre en una ventana o una pestaña nuevas
Elm Exercism

¿Todo listo para empezar Creador de laberintos?

Regístrate en Exercism para aprender y dominar Elm con 28 conceptos110 ejercicios y mentoría humana real, todo gratis.