Rutas
/
Elm
Elm
/
Ejercicios
/
Creador de laberintos
Creador de laberintos

Creador de laberintos

Ejercicio de aprendizaje

Introducción

Aleatorio

En un lenguaje funcional puro como Elm, una función llamada con los mismos argumentos debe devolver siempre el mismo valor. Por lo tanto, una función con la firma de tipo rand : Int solo puede implementarse como rand = 4, lo que 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; después, generamos un valor.

La primera forma de generar un valor es crear una Random.Seed mediante 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 usando 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, así como recursos externos no puros, para generar semillas.

A partir de ahora, nos centraremos en los generadores.

Supongamos, para mostrar ejemplos, que hemos definido con initialSeed y step una función generate : Int -> Generator a -> List a que genera una serie de valores a partir de un generador.

El módulo Random proporciona dos generadores básicos para generar números enteros y decimales 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 varias 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 toma 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 tienen que sumar 100, se renormalizarán de todos modos.

Podemos llegar 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, es posible que necesites usar Random.lazy para evitar que el compilador desenrolle el generador infinitamente.

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 vamos a desglosarlo.

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, fíjate que Random.lazy no añade nada a la lógica, solo evita que el compilador escriba peano dentro de peano indefinidamente.

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 en sí 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 pasamos por 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, u otra iteración de sí mismo más uno el 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 exponencialmente.

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 preocupa 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 parte, callejones sin salida.

Define el generador deadend de modo que siempre genere la variante DeadEnd.

2. No se cazan moscas con vinagre

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

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

Después, define el generador room de modo 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 de modo que genere una variante Branch que contenga varias ramas que lleven a laberintos generados por su argumento.

El número de ramas debe estar entre 2 y 4 (más que eso y el ingeniero de laberintos se enfadaría), determinado aleatoriamente con la misma probabilidad.

4. Laberintos asombrosos

Hora de ponerse manos a la obra.

Define el generador maze de modo 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, de media, 3 laberintos, pero solo el 25% de estos volverá a ramificarse, lo que es menos de uno, así que es probable que tus laberintos generados terminen por acabar.

5. Tenemos que ir más profundo

Tu maze está bien, pero tiene algunos problemas:

  1. El 75% de los valores generados no tienen ninguna rama, apenas 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 probabilidades 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 de modo que genere un laberinto de profundidad n. Solo 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 pestaña nueva
Elm Exercism

¿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.