ট্র্যাক
/
Elm
Elm
/
অনুশীলনী
/
গোলকধাঁধা নির্মাতা
গোলকধাঁধা নির্মাতা

গোলকধাঁধা নির্মাতা

লার্নিং অনুশীলনী

ভূমিকা

র‍্যান্ডম

Elm-এর মতো পিউর ফাংশনাল ভাষায়, একই আর্গুমেন্ট দিয়ে কল করা একটি ফাংশনকে সর্বদা একই মান রিটার্ন করতে হয়। তাই rand : Int টাইপ সিগনেচারযুক্ত একটি ফাংশন কেবল rand = 4 হিসেবেই ইমপ্লিমেন্ট করা যায়, যা র‍্যান্ডম ইন্টিজার জেনারেট করার জন্য ভালো কোনো ইঙ্গিত নয়।

তাহলে Elm-এ আমরা কীভাবে র‍্যান্ডম মান জেনারেট করি? আমরা সমস্যাটিকে দুই ভাগে ভাগ করি: প্রথমে আমরা Random.Generator a দিয়ে যে মানটি জেনারেট করতে চাই তা বর্ণনা করি, তারপর একটি মান জেনারেট করি।

মান জেনারেট করার প্রথম উপায় হলো initialSeed : Int -> Seed দিয়ে একটি Random.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]

মানগুলোর যোগফল ১০০ হতে হবে না, যাই হোক সেগুলো রিনরমালাইজ হয়ে যাবে।

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, ডেড এন্ড বা ধনভাণ্ডার কক্ষ ধারণ করবে (সমান সম্ভাবনায়); বাকি সব স্তর হবে একটি শাখা, যাতে mazeOfDepth দিয়ে রিকার্সিভভাবে তৈরি করা n-1 গভীরতার গোলকধাঁধা থাকে। জেনারেটরে deadend, room এবং branch ব্যবহার করতে ভুলবেন না।

GitHub-এর মাধ্যমে সম্পাদনা করুন লিংকটি একটি নতুন উইন্ডো বা ট্যাবে খোলে
Elm Exercism

গোলকধাঁধা নির্মাতা শুরু করতে প্রস্তুত?

Exercism-এ সাইন আপ করুন, Elm ট্র্যাকের 28টি কনসেপ্ট110টি অনুশীলনী আর সত্যিকারের মানুষের মেন্টরিং দিয়ে শিখুন ও দক্ষ হয়ে উঠুন, সম্পূর্ণ বিনামূল্যে।