Percursos
/
Elm
Elm
/
Exercícios
/
Criador de Labirintos
Criador de Labirintos

Criador de Labirintos

Exercício de aprendizagem

Introdução

Random

Numa linguagem puramente funcional como o Elm, uma função chamada com os mesmos argumentos tem de devolver sempre o mesmo valor. Por isso, uma função com a assinatura de tipo rand : Int só pode ser implementada como rand = 4, o que não augura nada de bom para gerar números inteiros aleatórios.

Então como é que geramos valores aleatórios em Elm? Dividimos o problema em dois: primeiro, descrevemos o valor que queremos gerar com um Random.Generator a; depois, geramos um valor.

A primeira forma de gerar um valor é criar uma Random.Seed através de initialSeed : Int -> Seed e depois usar step : Generator a -> Seed -> ( a, Seed ), que devolve um valor e uma nova semente. Repara que ambas as funções são puras, por isso chamá-las duas vezes com os mesmos argumentos produz os mesmos valores.

A segunda forma de gerar um valor é usar generate : (a -> msg) -> Generator a -> Cmd msg, mas isso só pode ser feito dentro de uma aplicação Elm. Nesse caso, o runtime do Elm pode usar step, bem como recursos externos não puros, para gerar sementes.

A partir de agora, vamos focar-nos nos geradores.

Vamos supor, para efeitos de exemplo, que definimos com initialSeed e step uma função generate : Int -> Generator a -> List a que gera uma série de valores a partir de um gerador.

O módulo Random fornece dois geradores básicos para gerar números inteiros e números de vírgula flutuante dentro de um intervalo 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]

Esses valores podem ser combinados em tuplos, ou em 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)]

O pacote elm-community/random-extra fornece muitos mais geradores para várias estruturas de dados: strings, datas, dicionários, arrays, conjuntos, etc.

Podemos criar geradores que devolvem apenas um único valor com Random.constant:

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

Podemos escolher aleatoriamente entre determinados elementos, com igual probabilidade, com Random.uniform:

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

Random.uniform recebe dois argumentos (Red e [Green, Blue]) para garantir que há pelo menos um valor de onde escolher, uma vez que uma única lista poderia estar vazia.

Também podemos ajustar as probabilidades com Random.weighted:

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

Os valores não precisam de somar 100; serão renormalizados de qualquer forma.

Podemos chegar ao interior de um gerador com Random.map:

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

Também podemos usar Random.map2 até Random.map5 para combinar mais geradores:

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 utilizações mais complexas, temos Random.andThen : (a -> Generator b) -> Generator a -> Generator b, que pode usar o valor gerado por um gerador para criar outro:

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]

Por vezes, é útil definir um gerador de forma autorrecursiva. Nesses casos, podes precisar de usar Random.lazy para impedir que o compilador expanda o gerador 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 exemplo é um pouco pesado, por isso vamos analisá-lo por partes.

Peano é um tipo que representa números inteiros positivos: Zero é 0, Next(Zero) é 1, Next(Next(Zero)) é 2, etc. Definimos peano para nos dar um número Peano aleatório.

Antes de mais, repara que Random.lazy não acrescenta nada à lógica: apenas impede que o compilador escreva peano dentro de peano indefinidamente.

Usamos Random.uniform para escolher entre zero (com 50% de probabilidade) e outro número Peano mais um (com 50% de probabilidade). No entanto, ao contrário do exemplo anterior, Random.uniform não está a escolher entre valores (como Zero), mas sim entre geradores (como Random.constant Zero), uma vez que queremos usar o próprio peano. Isto significa que Random.uniform vai devolver um valor do tipo Generator (Generator Peano), que não é o que precisamos.

Para «achatar» o gerador, encaminhamo-lo para Random.andThen : (a -> Generator b) -> Generator a -> Generator b, onde queremos que b seja Peano. Como Generator a é Generator (Generator Peano), a tem de ser Generator Peano, e a função (a -> Generator b) tem de ser do tipo (Generator Peano -> Generator Peano). Não queremos modificar nada, por isso identity é a escolha certa.

Por fim, que tipo de números vai o peano produzir? Sabemos que vai produzir 0 em 50% dos casos, ou outra iteração de si próprio mais um nos restantes 50%. Isso significa que os números serão 0 (50% das vezes), 1 (25% das vezes), 2 (12% das vezes), 3 (6% das vezes), etc. O peano pode produzir números arbitrariamente grandes, mas com probabilidade decrescente de forma exponencial.

Instruções

És um investigador de labirintos, a trabalhar no desenho de uma nova geração de masmorras e terrenos de aventura.

És um teórico, o que significa que não te preocupas com a disposição física dos labirintos (essa é uma tarefa para engenheiros de labirintos), mas sim com a sua complexidade matemática: fator de ramificação, profundidade máxima e coisas do género.

Decides abordar o teu próximo projeto com uma abordagem de geração aleatória.

1. Começa pelo fim

Qualquer bom labirinto é composto sobretudo por becos sem saída.

Define o gerador deadend de modo a gerar sempre a variante DeadEnd.

2. Não se apanham moscas com vinagre

Vamos dar aos aventureiros uma razão para se aventurarem nos teus labirintos.

Define o gerador treasure de modo a gerar Gold, Diamond ou Friendship com igual probabilidade.

Depois, define o gerador room de modo a envolver os valores gerados por treasure na variante Room.

3. Ramos

E agora a alma de um labirinto: os ramos.

O gerador branch recebe um argumento Generator Maze, que é um gerador capaz de gerar um labirinto arbitrário.

Define o gerador branch de modo a gerar uma variante Branch que contém vários ramos que conduzem a labirintos gerados pelo seu argumento.

O número de ramos deve estar entre 2 e 4 (mais do que isso e o engenheiro de labirintos ficaria irritado), determinado aleatoriamente com igual probabilidade.

4. Labirintos incríveis

Hora de tratar do assunto.

Define o gerador maze de modo a gerar um beco sem saída 60% das vezes, uma sala do tesouro 15% das vezes e um ramo 25% das vezes. Os ramos devem gerar novos labirintos usando maze recursivamente. Certifica-te de que usas deadend, room e branch no gerador.

Cada ramo contém, em média, 3 labirintos, mas apenas 25% destes voltarão a ramificar-se, o que é menos do que um; por isso, é provável que os teus labirintos gerados acabem por terminar.

5. Temos de ir mais fundo

O teu maze é bom, mas tem alguns problemas:

  1. 75% dos valores gerados não têm qualquer ramo, dificilmente são labirintos a sério
  2. Os aventureiros podem encontrar salas do tesouro logo no início do labirinto e decidir parar de explorar
  3. Embora seja possível gerar labirintos profundos, eles não são muito prováveis: um labirinto de profundidade 3 tem cerca de 3% de probabilidade de ser gerado, um labirinto de profundidade 10 tem 0,2% (a profundidade de um labirinto é calculada com MazeMakerSupport.mazeDepth)

Define o gerador mazeOfDepth n de modo a gerar um labirinto de profundidade n. Apenas o nível mais profundo 0 deve conter becos sem saída ou salas do tesouro (com igual probabilidade); todos os outros níveis devem ser um ramo que contém labirintos de profundidade n-1 gerados recursivamente com mazeOfDepth. Certifica-te de que usas deadend, room e branch no gerador.

Editar via GitHub A ligação abre numa nova janela ou separador
Elm Exercism

Estás pronto para começar Criador de Labirintos?

Inscreve-te no Exercism para aprenderes e dominares Elm com 28 conceitos110 exercícios, e mentoria humana real, tudo grátis.