Διαδρομές
/
Elm
Elm
/
Ασκήσεις
/
Κατασκευαστής λαβυρίνθων
Κατασκευαστής λαβυρίνθων

Κατασκευαστής λαβυρίνθων

Άσκηση εκμάθησης

Εισαγωγή

Τυχαιότητα

Σε μια καθαρά συναρτησιακή γλώσσα όπως η Elm, μια συνάρτηση που καλείται με τα ίδια ορίσματα πρέπει πάντα να επιστρέφει την ίδια τιμή. Επομένως μια συνάρτηση με υπογραφή τύπου rand : Int μπορεί να υλοποιηθεί μόνο ως rand = 4, κάτι που δεν προμηνύει τίποτα καλό για την παραγωγή τυχαίων ακεραίων.

Πώς λοιπόν παράγουμε τυχαίες τιμές στην Elm; Χωρίζουμε το πρόβλημα στα δύο: πρώτα περιγράφουμε την τιμή που θέλουμε να παράγουμε με ένα Random.Generator a και μετά παράγουμε μια τιμή.

Ο πρώτος τρόπος για να παράγεις μια τιμή είναι να δημιουργήσεις ένα Random.Seed μέσω του initialSeed : Int -> Seed και μετά να χρησιμοποιήσεις το step : Generator a -> Seed -> ( a, Seed ), που επιστρέφει μια τιμή και έναν νέο σπόρο. Σημείωσε ότι και οι δύο αυτές συναρτήσεις είναι καθαρές, οπότε καλώντας τες δύο φορές με τα ίδια ορίσματα θα παραχθούν οι ίδιες τιμές.

Ο δεύτερος τρόπος για να παράγεις μια τιμή είναι να χρησιμοποιήσεις το generate : (a -> msg) -> Generator a -> Cmd msg, αλλά αυτό μπορεί να γίνει μόνο μέσα σε μια εφαρμογή Elm. Σε αυτή την περίπτωση, το περιβάλλον εκτέλεσης της Elm μπορεί να χρησιμοποιήσει το step καθώς και εξωτερικούς, μη καθαρούς πόρους για να παράγει σπόρους.

Από εδώ και πέρα, θα επικεντρωθούμε στις γεννήτριες.

Ας υποθέσουμε, για χάρη των παραδειγμάτων, ότι έχουμε ορίσει με τα initialSeed και step μια συνάρτηση generate : Int -> Generator a -> List a που παράγει έναν αριθμό τιμών από μια γεννήτρια.

Η ενότητα Random παρέχει δύο βασικές γεννήτριες για την παραγωγή ακεραίων και δεκαδικών αριθμών μέσα σε ένα συγκεκριμένο εύρος:

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

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

Αυτές οι τιμές μπορούν να συνδυαστούν σε πλειάδες ή σε λίστες τιμών:

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)]

Το πακέτο elm-community/random-extra παρέχει πολλές περισσότερες γεννήτριες για διάφορες δομές δεδομένων: συμβολοσειρές, ημερομηνίες, λεξικά, πίνακες, σύνολα κ.λπ.

Μπορούμε να δημιουργήσουμε γεννήτριες που επιστρέφουν μόνο μία τιμή χρησιμοποιώντας το Random.constant:

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

Μπορούμε να επιλέγουμε τυχαία από δεδομένα στοιχεία με ίση πιθανότητα χρησιμοποιώντας το Random.uniform:

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

Το Random.uniform παίρνει δύο ορίσματα (Red και [Green, Blue]) για να εγγυηθεί ότι υπάρχει τουλάχιστον μία τιμή για να διαλέξεις, αφού μια μεμονωμένη λίστα θα μπορούσε να είναι κενή.

Μπορούμε επίσης να ρυθμίσουμε τις πιθανότητες χρησιμοποιώντας το Random.weighted:

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

Οι τιμές δεν χρειάζεται να αθροίζουν στο 100, θα επανακανονικοποιηθούν ούτως ή άλλως.

Μπορούμε να φτάσουμε στο εσωτερικό μιας γεννήτριας με το Random.map:

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

Μπορούμε επίσης να χρησιμοποιήσουμε το Random.map2 μέχρι και το Random.map5 για να συνδυάσουμε περισσότερες γεννήτριες:

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]

Για πιο περίπλοκες χρήσεις, έχουμε το Random.andThen : (a -> Generator b) -> Generator a -> Generator b που μπορεί να χρησιμοποιήσει την τιμή που παράγει μια γεννήτρια για να δημιουργήσει μια άλλη:

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]

Μερικές φορές είναι χρήσιμο να ορίσεις μια γεννήτρια αυτοαναδρομικά. Σε αυτές τις περιπτώσεις, μπορεί να χρειαστεί να χρησιμοποιήσεις το Random.lazy για να εμποδίσεις τον μεταγλωττιστή να ξεδιπλώσει τη γεννήτρια επ' άπειρον.

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)))]

Αυτό το παράδειγμα είναι λίγο βαρύ, οπότε ας το αναλύσουμε.

Το Peano είναι ένας τύπος που αναπαριστά θετικούς ακέραιους: το Zero είναι 0, το Next(Zero) είναι 1, το Next(Next(Zero)) είναι 2, κ.λπ. Ορίζουμε το peano ώστε να μας δίνει έναν τυχαίο αριθμό Peano.

Πρώτα απ' όλα, σημείωσε ότι το Random.lazy δεν προσθέτει τίποτα στη λογική, απλώς εμποδίζει τον μεταγλωττιστή να γράψει το peano μέσα στο peano επ' άπειρον.

Χρησιμοποιούμε το Random.uniform για να διαλέξουμε ανάμεσα στο μηδέν (με πιθανότητα 50%) και σε έναν άλλο αριθμό Peano συν ένα (με πιθανότητα 50%). Ωστόσο, σε αντίθεση με το προηγούμενο παράδειγμα, το Random.uniform δεν διαλέγει από τιμές (όπως το Zero) αλλά από γεννήτριες (όπως το Random.constant Zero), αφού θέλουμε να χρησιμοποιήσουμε το ίδιο το peano. Αυτό σημαίνει ότι το Random.uniform θα επιστρέψει μια τιμή τύπου Generator (Generator Peano), που δεν είναι αυτό που χρειαζόμαστε.

Για να "ισιώσουμε" τη γεννήτρια, τη διοχετεύουμε στο Random.andThen : (a -> Generator b) -> Generator a -> Generator b, όπου θέλουμε το b να είναι Peano. Επειδή το Generator a είναι Generator (Generator Peano), το a πρέπει να είναι Generator Peano, και η συνάρτηση (a -> Generator b) πρέπει να έχει τύπο (Generator Peano -> Generator Peano). Δεν θέλουμε να τροποποιήσουμε τίποτα, οπότε το identity είναι η σωστή επιλογή.

Τέλος, τι λογής αριθμούς θα παράγει το peano; Ξέρουμε ότι θα παράγει 0 το 50% των φορών, ή μια ακόμη επανάληψη του εαυτού του συν ένα το άλλο 50%. Αυτό σημαίνει ότι οι αριθμοί θα είναι 0 (50% των φορών), 1 (25% των φορών), 2 (12% των φορών), 3 (6% των φορών), κ.λπ. Το peano μπορεί να παράγει οσοδήποτε μεγάλους αριθμούς, αλλά με εκθετικά φθίνουσα πιθανότητα.

Οδηγίες

Είσαι ερευνητής λαβυρίνθων και δουλεύεις πάνω στον σχεδιασμό μιας νέας γενιάς μπουντρουμιών και τόπων περιπέτειας.

Είσαι θεωρητικός, που σημαίνει ότι δεν σε απασχολεί η φυσική διάταξη των λαβυρίνθων (αυτό είναι δουλειά των μηχανικών λαβυρίνθων), αλλά η μαθηματική τους πολυπλοκότητα: ο παράγοντας διακλάδωσης, το μέγιστο βάθος και τα παρόμοια.

Αποφασίζεις να αντιμετωπίσεις το επόμενό σου έργο με μια προσέγγιση τυχαίας παραγωγής.

1. Ξεκίνα από το τέλος

Κάθε καλός λαβύρινθος αποτελείται κυρίως από αδιέξοδα.

Όρισε τη γεννήτρια deadend ώστε να παράγει πάντα την παραλλαγή DeadEnd.

2. Με το ξίδι δεν πιάνεις τις μύγες

Ας δώσουμε στους τυχοδιώκτες έναν λόγο να τολμήσουν να μπουν στους λαβυρίνθους σου.

Όρισε τη γεννήτρια treasure ώστε να παράγει είτε Gold είτε Diamond είτε Friendship με ίση πιθανότητα.

Έπειτα, όρισε τη γεννήτρια room ώστε να περικλείει τις τιμές που παράγει η treasure στην παραλλαγή Room.

3. Ας διακλαδωθούμε

Και τώρα η καρδιά και η ψυχή ενός λαβυρίνθου: οι διακλαδώσεις.

Η γεννήτρια branch δέχεται ένα όρισμα Generator Maze, το οποίο είναι μια γεννήτρια που μπορεί να παράγει έναν οποιονδήποτε λαβύρινθο.

Όρισε τη γεννήτρια branch ώστε να παράγει μια παραλλαγή Branch που περιέχει αρκετές διακλαδώσεις, οι οποίες οδηγούν σε λαβυρίνθους που παράγει το όρισμά της.

Ο αριθμός των διακλαδώσεων πρέπει να είναι μεταξύ 2 και 4 (παραπάνω από αυτό και ο μηχανικός λαβυρίνθων θα θύμωνε), καθορισμένος τυχαία με ίση πιθανότητα.

4. Καταπληκτικοί λαβύρινθοι

Ώρα για δουλειά.

Όρισε τη γεννήτρια maze ώστε να παράγει ένα αδιέξοδο στο 60% των περιπτώσεων, μια αίθουσα θησαυρού στο 15% των περιπτώσεων και μια διακλάδωση στο 25% των περιπτώσεων. Οι διακλαδώσεις πρέπει να παράγουν νέους λαβυρίνθους χρησιμοποιώντας αναδρομικά τη γεννήτρια maze. Φρόντισε να χρησιμοποιήσεις τις γεννήτριες deadend, room και branch μέσα στη γεννήτρια.

Κάθε διακλάδωση περιέχει κατά μέσο όρο 3 λαβυρίνθους, αλλά μόνο το 25% από αυτούς θα διακλαδωθεί ξανά, που είναι λιγότερο από έναν, οπότε οι λαβύρινθοι που παράγεις είναι πιθανό να τελειώσουν κάποια στιγμή.

5. Πρέπει να πάμε πιο βαθιά

Η γεννήτρια maze σου είναι ωραία, αλλά έχει κάποια προβλήματα:

  1. Το 75% των παραγόμενων τιμών δεν έχει καμία απολύτως διακλάδωση, δύσκολα πρόκειται για αληθινούς λαβυρίνθους
  2. Οι τυχοδιώκτες μπορεί να βρουν αίθουσες θησαυρού νωρίς μέσα στον λαβύρινθο και να αποφασίσουν να σταματήσουν την εξερεύνηση
  3. Αν και μπορούν να παραχθούν βαθείς λαβύρινθοι, δεν είναι πολύ πιθανοί: ένας λαβύρινθος βάθους 3 έχει περίπου 3% πιθανότητα να παραχθεί, ένας λαβύρινθος βάθους 10 έχει 0.2% πιθανότητα (το βάθος ενός λαβυρίνθου υπολογίζεται με το MazeMakerSupport.mazeDepth)

Όρισε τη γεννήτρια mazeOfDepth n ώστε να παράγει έναν λαβύρινθο βάθους n. Μόνο το βαθύτερο επίπεδο 0 πρέπει να περιέχει αδιέξοδα ή αίθουσες θησαυρού (με ίση πιθανότητα), ενώ όλα τα άλλα επίπεδα πρέπει να είναι μια διακλάδωση που περιέχει λαβυρίνθους βάθους n-1 που παράγονται αναδρομικά με το mazeOfDepth. Φρόντισε να χρησιμοποιήσεις τις γεννήτριες deadend, room και branch μέσα στη γεννήτρια.

Επεξεργασία μέσω GitHub Ο σύνδεσμος ανοίγει σε νέο παράθυρο ή καρτέλα
Elm Exercism

Έτοιμος να ξεκινήσεις την άσκηση Κατασκευαστής λαβυρίνθων;

Γράψου στο Exercism για να μάθεις και να κατακτήσεις Elm με 28 έννοιες110 ασκήσεις και πραγματική καθοδήγηση από ανθρώπους, όλα δωρεάν.