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

Criador de Labirintos

Exercício de aprendizagem

Introdução

Aleatoriedade

Em uma linguagem funcional pura como Elm, uma função chamada com os mesmos argumentos precisa sempre retornar o mesmo valor. Portanto, uma função com a assinatura de tipo rand : Int só pode ser implementada como rand = 4, o que não é nada promissor para gerar números inteiros aleatórios.

Então, como 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 um Random.Seed com initialSeed : Int -> Seed e depois usar step : Generator a -> Seed -> ( a, Seed ), que retorna um valor e uma nova semente. Repare que essas duas funções são puras, então chamá-las duas vezes com os mesmos argumentos produzirá 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 ambiente de execução do Elm pode usar step, assim como recursos externos não puros, para gerar sementes.

De agora em diante, vamos nos concentrar nos geradores.

Vamos supor, para fins de exemplo, que definimos com initialSeed e step uma função generate : Int -> Generator a -> List a que gera uma certa quantidade 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 ponto 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 tuplas 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 muito mais geradores para diversas estruturas de dados: strings, datas, dicionários, arrays, conjuntos etc.

Podemos criar geradores que retornam apenas um único valor usando Random.constant:

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

Podemos escolher aleatoriamente entre elementos dados com igual probabilidade usando 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 haja pelo menos um valor para escolher, já que uma única lista poderia estar vazia.

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

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

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

Podemos alcançar o 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 usos mais complexos, 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]

Às vezes é útil definir um gerador de forma autorrecursiva. Nesses casos, talvez você precise 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, então 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 tudo, repare que Random.lazy não adiciona nada à lógica; ele 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, diferentemente do exemplo anterior, Random.uniform não está escolhendo entre valores (como Zero), mas sim entre geradores (como Random.constant Zero), já que queremos usar o próprio peano. Isso significa que Random.uniform retornará um valor do tipo Generator (Generator Peano), que não é o que precisamos.

Para "achatar" o gerador, usamos o pipe para passá-lo a Random.andThen : (a -> Generator b) -> Generator a -> Generator b, em que queremos que b seja Peano. Como Generator a é Generator (Generator Peano), a deve ser Generator Peano, e a função (a -> Generator b) deve ter o tipo (Generator Peano -> Generator Peano). Não queremos modificar nada, então identity é a escolha certa.

Por fim, que tipo de números peano vai produzir? Sabemos que ele vai produzir 0 em 50% das vezes, ou outra iteração dele mesmo mais um em 50% das vezes. 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. peano pode produzir números arbitrariamente grandes, mas com probabilidade decrescente de forma exponencial.

Instruções

Você é um pesquisador de labirintos, trabalhando para projetar uma nova geração de masmorras e terrenos de aventura.

Você é um teórico, o que significa que não se preocupa com o layout físico dos labirintos (isso é tarefa dos engenheiros de labirintos), mas sim com a complexidade matemática deles: fator de ramificação, profundidade máxima e coisas do tipo.

Você decide abordar seu próximo projeto com uma abordagem de geração aleatória.

1. Comece pelo fim

Qualquer labirinto que se preze é feito principalmente de becos sem saída.

Defina o gerador deadend para que ele sempre gere a variante DeadEnd.

2. Não se pega mosca com vinagre

Vamos dar aos aventureiros um motivo para se aventurar nos seus labirintos.

Defina o gerador treasure para que ele gere Gold, Diamond ou Friendship com a mesma probabilidade.

Depois, defina o gerador room para que ele envolva os valores gerados por treasure na variante Room.

3. Ramificando

E agora, o coração e a alma de um labirinto: as ramificações.

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

Defina o gerador branch para que ele gere uma variante Branch contendo várias ramificações que levam a labirintos gerados pelo seu argumento.

O número de ramificações deve ficar entre 2 e 4 (mais do que isso e o engenheiro de labirintos ficaria irritado), determinado aleatoriamente com a mesma probabilidade.

4. Labirintos incríveis

Hora de botar a mão na massa.

Defina o gerador maze para que ele gere um beco sem saída em 60% das vezes, uma sala do tesouro em 15% das vezes e uma ramificação em 25% das vezes. As ramificações devem gerar novos labirintos usando maze de forma recursiva. Certifique-se de usar deadend, room e branch no gerador.

Cada ramificação contém, em média, 3 labirintos, mas apenas 25% deles vão se ramificar de novo, o que dá menos de um, então é provável que seus labirintos gerados acabem em algum momento.

5. Precisamos ir mais fundo

Seu maze é legal, mas tem alguns problemas:

  1. 75% dos valores gerados não têm nenhuma ramificação, o que dificilmente dá labirintos de verdade
  2. Os aventureiros podem encontrar salas do tesouro logo no início do labirinto e decidir parar de explorar
  3. Embora labirintos profundos possam ser gerados, eles não são muito prováveis: um labirinto de profundidade 3 tem cerca de 3% de chance de ser gerado, e um labirinto de profundidade 10 tem 0,2% de chance (a profundidade de um labirinto é calculada com MazeMakerSupport.mazeDepth)

Defina o gerador mazeOfDepth n para que ele gere um labirinto de profundidade n. Apenas o nível mais profundo, 0, deve conter becos sem saída ou salas do tesouro (com a mesma probabilidade); todos os outros níveis devem ser uma ramificação contendo labirintos de profundidade n-1 gerados recursivamente com mazeOfDepth. Certifique-se de usar deadend, room e branch no gerador.

Editar via GitHub O link abre em uma nova janela ou aba
Elm Exercism

Tudo pronto para começar Criador de Labirintos?

Crie sua conta no Exercism para aprender e dominar Elm com 28 conceitos110 exercícios e mentoria humana de verdade, tudo de graça.