المسارات
/
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 تمارين، وإرشاد بشري حقيقي، وكل ذلك مجانًا.