مسیرها
/
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 از منابع بیرونی و ناخالص هم برای تولید بذرها استفاده کند.

از این پس، تمرکز ما روی «مولد» خواهد بود.

بیایید به بهانه‌ی نمایش مثال‌ها، فرض کنیم تابعی به اسم generate : Int -> Generator a -> List a را با initialSeed و step تعریف کرده‌ایم که تعدادی مقدار را از یک مولد تولید می‌کند.

ماژول 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]

لازم نیست این مقادیر به ۱۰۰ جمع شوند، چون در هر صورت دوباره نرمال‌سازی می‌شوند.

می‌توانیم با 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 صفر است، Next(Zero) یک است، Next(Next(Zero)) دو است و به همین ترتیب. ما peano را تعریف می‌کنیم تا یک عدد Peano تصادفی به ما بدهد.

پیش از هر چیز توجه کنید که Random.lazy چیزی به منطق اضافه نمی‌کند و تنها جلوی نوشتن بی‌نهایت peano درون peano توسط کامپایلر را می‌گیرد.

با Random.uniform بین صفر (با احتمال ۵۰٪) و یک عدد Peano دیگر به‌علاوه‌ی یک (با احتمال ۵۰٪) انتخاب می‌کنیم. اما برخلاف مثال پیشین، 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 چه نوع اعدادی تولید خواهد کرد؟ می‌دانیم که ۵۰٪ مواقع ۰ تولید می‌کند و ۵۰٪ مواقع یک تکرار دیگر از خودش به‌علاوه‌ی یک. یعنی اعداد ۰ (۵۰٪ مواقع)، ۱ (۲۵٪ مواقع)، ۲ (۱۲٪ مواقع)، ۳ (۶٪ مواقع) و به همین ترتیب خواهند بود. peano می‌تواند اعداد بسیار بزرگ تولید کند، اما با احتمالی که به‌صورت نمایی کاهش می‌یابد.

دستورالعمل‌ها

شما یک پژوهشگر هزارتو هستید و مشغول طراحی نسل تازه‌ای از سیاه‌چال‌ها و قلمروهای ماجراجویی هستید.

شما یک نظریه‌پرداز هستید، یعنی به چیدمان فیزیکی هزارتوها کاری ندارید (این کار مهندسان هزارتو است)، بلکه به پیچیدگی ریاضی آن‌ها اهمیت می‌دهید: ضریب انشعاب، بیشترین عمق و مواردی از این قبیل.

تصمیم می‌گیرید پروژه‌ی بعدی‌تان را با رویکرد تولید تصادفی پیش ببرید.

1. از پایان شروع کنید

هر هزارتوی خوبی عمدتاً از بن‌بست تشکیل شده است.

مولد deadend را طوری تعریف کنید که همیشه گونه‌ی DeadEnd را تولید کند.

2. با سرکه نمی‌توان مگس گرفت

بیایید به ماجراجویان دلیلی بدهیم تا به هزارتوهای شما قدم بگذارند.

مولد treasure را طوری تعریف کنید که با احتمالی برابر، Gold یا Diamond یا Friendship را تولید کند.

سپس مولد room را طوری تعریف کنید که مقادیری را که treasure تولید می‌کند، در گونه‌ی Room بپیچد.

3. شاخه زدن

مولد branch آرگومان Generator Maze را می‌گیرد، که مولدی است و می‌تواند هزارتویی دلخواه تولید کند.

مولد branch را طوری تعریف کنید که گونه‌ی Branch را تولید کند که شامل چندین شاخه است و هر شاخه به هزارتویی می‌رسد که آرگومانش تولید کرده است.

تعداد شاخه‌ها باید بین ۲ و ۴ باشد (بیشتر از این، مهندس هزارتو عصبانی می‌شود) و به‌صورت تصادفی با احتمالی برابر تعیین شود.

4. هزارتوهای شگفت‌انگیز

وقت کار است.

مولد maze را طوری تعریف کنید که ۶۰٪ مواقع بن‌بست، ۱۵٪ مواقع اتاق گنج و ۲۵٪ مواقع شاخه تولید کند. شاخه‌ها باید با استفاده از maze و به‌صورت بازگشتی هزارتوهای تازه تولید کنند. مطمئن شوید که در این مولد از deadend، room و branch استفاده می‌کنید.

هر شاخه به‌طور میانگین شامل ۳ هزارتو است، اما تنها ۲۵٪ از آن‌ها دوباره شاخه می‌زنند، که از یکی کمتر است، پس هزارتوهای تولیدشده‌ی شما احتمالاً در نهایت پایان می‌یابند.

5. باید عمیق‌تر برویم

مولد maze شما خوب است، اما چند مشکل دارد:

  1. ۷۵٪ از مقادیر تولیدشده هیچ شاخه‌ای ندارند و به‌سختی می‌توان آن‌ها را هزارتوی واقعی نامید
  2. ممکن است ماجراجویان زودتر از موعد در هزارتو به اتاق‌های گنج برسند و تصمیم بگیرند کاوش را متوقف کنند
  3. هرچند می‌توان هزارتوهای عمیق تولید کرد، احتمالشان زیاد نیست: احتمال تولید هزارتویی با عمق ۳ حدود ۳٪ است و هزارتویی با عمق ۱۰ حدود ۰٫۲٪ (عمق یک هزارتو با MazeMakerSupport.mazeDepth محاسبه می‌شود)

مولد mazeOfDepth n را طوری تعریف کنید که هزارتویی با عمق n تولید کند. تنها عمیق‌ترین سطح، یعنی 0، باید بن‌بست یا اتاق گنج داشته باشد (با احتمالی برابر)، و همه‌ی سطوح دیگر باید شاخه‌ای باشند شامل هزارتوهایی با عمق n-1 که به‌صورت بازگشتی با mazeOfDepth تولید شده‌اند. مطمئن شوید که در این مولد از deadend، room و branch استفاده می‌کنید.

ویرایش از طریق GitHub این لینک در پنجره یا زبانه‌ی جدیدی باز می‌شود
Elm Exercism

آماده‌اید سازنده‌ی هزارتو را شروع کنید؟

در Exercism ثبت‌نام کنید تا Elm را همراه با 28 مفهوم110 تمرین و مربی‌گری انسانی واقعی یاد بگیرید و در آن استاد شوید، همه‌ی این‌ها رایگان.