আমি প্রোগ্রামিং শেখানোর এক্সপার্ট নই, কিন্তু যখন আমি কোনো বিষয়ে দক্ষ হতে চাই, আমি সেটি মজার সাথে করার উপায় খুঁজি। উদাহরণস্বরূপ, যখন আমি শেল স্ক্রিপ্টিং-এ দক্ষ হতে চাইলাম, আমি বাশে মাইনসווিপার গেমের একটি ভার্সন প্রোগ্রাম করে প্র্যাকটিস করার সিদ্ধান্ত নিলাম।
আপনি যদি একজন অভিজ্ঞ বাশ প্রোগ্রামার হন এবং মজা করতেই আপনার স্কিল চমকাতে চান, তবে টার্মিনালে নিজের মাইনস്വিপার ভার্সন লিখতে ফলো করুন। সম্পূর্ণ সোর্স কোড এই GitHub রিপোজিটরিতে পাওয়া যাবে।
প্রস্তুতি
কোনো কোড লিখার আগে, আমি আমার গেম তৈরি করার জন্য যা যা দরকার তা আউटलাইন করলাম:
- একটি মাইনফিল্ড প্রিন্ট করুন
- গেমপ্লে লজিক তৈরি করুন
- উপলব্ধ মাইনফিল্ড নির্ধারণের লজিক তৈরি করুন
- উপলব্ধ ও আবিষ্কৃত (এক্সট্র্যাক্টেড) মাইন গণনা রাখুন
- এন্ডগেম লজিক তৈরি করুন
একটি মাইনফিল্ড প্রিন্ট করুন
মাইনস്വিপারে, গেম ওয়ার্ল্ড হলো একটি ২D অ্যারে (কলাম এবং রো) কনসيلد সেল। প্রতিটি সেলে বমবের মাইন থাকতে পারে বা নাও থাকতে পারে। প্লেয়ারের উদ্দেশ্য হলো মাইন রহিত সেলগুলো রিভিল করা, এবং কখনো মাইন রিভিল না করা। বাশ ভার্সন গেমে ১০x১০ ম্যাট্রিক্স ব্যবহার করা হয়েছে, সিম্পল বাশ অ্যারে দিয়ে ইমপ্লিমেন্ট করা।
প্রথমে, আমি কিছু র্যান্ডম ভেরিয়েবল অ্যাসাইন করি। এগুলো হলো লোকেশন যেখানে মাইন রাখা যাবে। লোকেশন সংখ্যা সীমাবদ্ধ রাখা হয়েছে যাতে এর ওপর বিল্ড করা সহজ হয়। লজিক ভালো হতে পারে, কিন্তু আমি গেমটি সিম্পল এবং একটু আধুরা দেখাতে চেয়েছিলাম। (আমি এটা মজার জন্য লিখলাম, কিন্তু আপনার কন্ট্রিবিউশন স্বাগতম যাতে এটি আরও ভালো দেখায়।)
নিচের ভেরিয়েবলগুলো কিছু ডিফল্ট ভেরিয়েবল, যেগুলো ফিল্ড প্লেসমেন্টের জন্য র্যান্ডমলি কল করা হবে, ভেরিয়েবল a-g-এর মত, আমরা সেগুলো ব্যবহার করব এক্সট্র্যাক্টেবল মাইন ক্যালকুলেট করার জন্য:
# variables score=0 # will be used to store the score of the game # variables below will be used to randomly get the extract-able cells/fields from our mine. a="1 10 -10 -1" b="-1 0 1" c="0 1" d="-1 0 1 -2 -3" e="1 2 20 21 10 0 -10 -20 -23 -2 -1" f="1 2 3 35 30 20 22 10 0 -10 -20 -25 -30 -35 -3 -2 -1" g="1 4 6 9 10 15 20 25 30 -30 -24 -11 -10 -9 -8 -7" # # declarations declare -a room # declare an array room, it will represent each cell/field of our mine.
পরবর্তী, আমি আমার বোর্ড প্রিন্ট করি কলাম (০-৯) এবং রো (a-j) দিয়ে, একটি ১০x১০ ম্যাট্রিক্স ফর্ম করে যা গেমের মাইনফিল্ড হিসেবে কাজ করবে। (M[10][10] হলো একটি ১০০-মানের অ্যারে যার ইন্ডেক্স ০-৯৯।) আপনি যদি বাশ অ্যারে সম্পর্কে আরও জানতে চান, You don't know Bash: An introduction to Bash arrays পড়ুন।
এটিকে একটি ফাংশন বলি, plough, আমরা হেডার প্রথম প্রিন্ট করি: দুটি ব্ল্যাঙ্ক লাইন, কলাম হেডিংস, এবং প্লেয়িং ফিল্ডের টপ আউटलাইন করার একটি লাইন:
printf '\n\n' printf '%s' " a b c d e f g h i j" printf '\n %s\n' "-----------------------------------------"
পরবর্তী, আমি একটি কাউন্টার ভেরিয়েবল স্থাপন করি, r নামে, যাতে কতগুলো হরিজন্টাল রো পপুলেটেড হয়েছে সেটি ট্র্যাক রাখা যায়। লক্ষ্য করুন, আমরা পরে গেম কোডে একই কাউন্টার ভেরিয়েবল 'r' আমাদের অ্যারে ইন্ডেক্স হিসেবে ব্যবহার করব। একটি বাশ for লুপে, seq কমান্ড ব্যবহার করে ০ থেকে ৯ পর্যন্ত ইনক্রিমেন্ট করি, আমি একটি ডিজিট (d%) প্রিন্ট করি রো নম্বর রিপ্রেজেন্ট করতে ($row, যা seq দ্বারা ডিফাইন করা):
r=0 # our counter for row in $(seq 0 9); do printf '%d ' "$row" # print the row numbers from 0-9
এখান থেকে আগে বড়ার আগে, আসুন চেক করি আমরা কী করেছি। আমরা সিকোয়েন্স [a-j] হরিজন্টালি প্রথম প্রিন্ট করলাম এবং তারপর রো নম্বর প্রিন্ট করলাম রেঞ্জ [0-9]-এ, আমরা এই দুটি রেঞ্জ ব্যবহার করব ইউজার ইনপুট কোঅরডিনেটস হিসেবে মাইন লোকেট করে এক্সট্র্যাক্ট করার জন্য।
পরবর্তী, প্রতিটি রোয়ের ভিতরে একটি কলাম ইন্টারসেকশন আছে, তাই একটি নতুন for লুপ খোলার সময়। এটি প্রতিটি কলাম ম্যানেজ করে, অর্থাৎ প্লেয়িং ফিল্ডের প্রতিটি সেল জেনারেট করে। আমি কিছু হেল্পার ফাংশন যোগ করেছি যা আপনি সোর্স কোডে পূর্ণ ডেফিনিশন দেখতে পারেন। প্রতিটি সেলের জন্য, আমাদের কিছু দরকার যাতে ফিল্ডটি মাইন जैसा দেখায়, তাই খালি সেগুলোকে ডট (.) দিয়ে ইনিশিয়ালাইজ করি, একটি কাস্টম ফাংশন is_null_field ব্যবহার করে। এছাড়া, আমাদের একটি অ্যারে ভেরিয়েবল দরকার প্রতিটি সেলের মান স্টোর করার জন্য, আমরা প্রিডিফাইন করা গ্লোবাল অ্যারে ভেরিয়েবল room ব্যবহার করব একটি ইন্ডেক্স ভেরিয়েবল r সহ। r ইনক্রিমেন্ট হওয়ার সাথে সাথে, আমরা সেলগুলোতে ইটারেট করি, মাইন ড্রপ করি পথে পথে।
for col in $(seq 0 9); do
((r+=1)) # increment the counter as we move forward in column sequence
is_null_field $r # assume a function which will check, if the field is empty, if so, initialize it with a dot(.)
printf '%s \e[33m%s\e[0m ' "|" "${room[$r]}" # finally print the separator, note that, the first value of ${room[$r]} will be '.', as it is just initialized.
#close col loop
done
শেষে, আমি বোর্ডকে ভালোভাবে ডিফাইন রাখি প্রতিটি রোর বটমকে একটি লাইন দিয়ে এনক্লোজ করে, এবং তারপর রো লুপ বন্ধ করি:
printf '%s\n' "|" # print the line end separator printf ' %s\n' "-----------------------------------------" # close row for loop done printf '\n\n'
পূর্ণ plough ফাংশন দেখায়:
plough()
{
r=0
printf '\n\n'
printf '%s' " a b c d e f g h i j"
printf '\n %s\n' "-----------------------------------------"
for row in $(seq 0 9); do
printf '%d ' "$row"
for col in $(seq 0 9); do
((r+=1))
is_null_field $r
printf '%s \e[33m%s\e[0m ' "|" "${room[$r]}"
done
printf '%s\n' "|"
printf ' %s\n' "-----------------------------------------"
done
printf '\n\n'
}
আমাকে কিছু সময় লেগেছিল is_null_field দরকার কিনা সেটি ডিসাইড করতে, তাই আসুন নজদিক থেকে দেখি এটি কী করে। গেমের শুরু থেকে আমাদের একটি নির্ভরযোগ্য স্টেট দরকার। সেই চয়েসটা আর্মিটারি—এটা একটি নম্বর বা যেকোনো ক্যারেক্টার হতে পারে। আমি ডট (.) ধরে রেখেছিলাম কারণ আমি মনে করি এটা গেমবোর্ডকে সুন্দর করে তোলে। এখানে সেটি কীভাবে দেখায়:
is_null_field()
{
local e=$1 # we used index 'r' for array room already, let's call it 'e'
if [[ -z "${room[$e]}" ]];then
room[$r]="." # this is where we put the dot(.) to initialize the cell/minefield
fi
}
এখন, আমার মাইনয়ের সব সেল ইনিশিয়ালাইজড, আমি একটি সিম্পল ফাংশন ডিক্লেয়ার করে এবং পরে কল করে সব উপলব্ধ মাইন গণনা করি নিচে দেখানো মতো:
get_free_fields()
{
free_fields=0 # initialize the variable
for n in $(seq 1 ${#room[@]}); do
if [[ "${room[$n]}" = "." ]]; then # check if the cells has initial value dot(.), then count it as a free field.
((free_fields+=1))
fi
done
}
এখানে প্রিন্ট করা মাইনফিল্ড, [a-j] হলো কলাম, এবং [0-9] হলো রো।

প্লেয়ার চালানোর লজিক তৈরি করুন
প্লেয়ার লজিক stdin থেকে একটি অপশন পড়ে কোঅরডিনেট হিসেবে মাইনগুলোতে এবং মাইনফিল্ডের সঠিক ফিল্ড এক্সট্র্যাক্ট করে। এটি বাশের প্যারামিটার এক্সপ্যানশন ব্যবহার করে কলাম এবং রো ইনপুট এক্সট্র্যাক্ট করে, তারপর কলামকে একটি সুইচে ফিড করে যা তার ইকুিভ্যালেন্ট ইন্টিজার নোটেশনকে পয়েন্ট করে বোর্ডে, এটি বুঝতে নিচের সুইচ কেস স্টেটমেন্টে ভেরিয়েবল 'o'-এ মান অ্যাসাইন করা হয়েছে। উদাহরণস্বরূপ, একটি প্লেয়ার c3 ইনপুট দিতে পারে, যা বাশ দুটি ক্যারেক্টরে ভাগ করে: c এবং 3। সিম্পলिसিটির জন্য, আমি ইনভ্যালিড এন্ট্রি হ্যান্ডলিং কীভাবে হয় সেটি স্কিপ করছি।
colm=${opt:0:1} # get the first char, the alphabet
ro=${opt:1:1} # get the second char, the digit
case $colm in
a ) o=1;; # finally, convert the alphabet to its equivalent integer notation.
b ) o=2;;
c ) o=3;;
d ) o=4;;
e ) o=5;;
f ) o=6;;
g ) o=7;;
h ) o=8;;
i ) o=9;;
j ) o=10;;
esac
তারপর এটি সঠিক ইন্ডেক্স ক্যালকুলেট করে এবং ইনপুট কোঅরডিনেটসের ইন্ডেক্স ওই ফিল্ডে অ্যাসাইন করে।
এখানেও shuf কমান্ডের অনেক ব্যবহার আছে, shuf হলো একটি লিনাক্স ইউটিলিটি যেটি ইনফরমেশনের একটি র্যান্ডম পারমুটেশন প্রদান করার জন্য ডিজাইন করা, যেখানে -i অপশন ইন্ডেক্স বা পসিবল রেঞ্জ ডিনোট করে শাফল করার জন্য এবং -n আউটপুট দেওয়ার ম্যাক্সিমাম নম্বর ডিনোট করে। ডাবল প্যারেনথেসিস বাশে ম্যাথমেটিক্যাল ইভ্যালুয়েশন আলাউ করে, এবং আমরা এখানে সেগুলো 헤ভিলি ব্যবহার করব।
আমাদের আগের উদাহরণ অনুমান করুন c3 stdin-এ পেয়েছে। তাহলে, ro=3 এবং o=3 উপরের সুইচ কেস স্টেটমেন্ট থেকে c-কে এর ইকুিভ্যালেন্ট ইন্টিজারে কনভার্ট করে, আমাদের ফাইনাল ইন্ডেক্স 'i' ক্যালকুলেট করার ফর্মুলায় দিয়েছে।
i=$(((ro*10)+o)) # Follow BODMAS rule, to calculate final index. is_free_field $i $(shuf -i 0-5 -n 1) # call a custom function that checks if the final index value points to a an empty/free cell/field.
এই ম্যাথের মাধ্যমে চলে যাই বুঝতে ফাইনাল ইন্ডেক্স 'i' কীভাবে ক্যালকুলেট হয়:
i=$(((ro*10)+o)) i=$(((3*10)+3))=$((30+3))=33
ফাইনাল ইন্ডেক্স মান ৩৩। আমাদের বোর্ডে, উপরে প্রিন্ট করা অনুযায়ী, ফাইনাল ইন্ডেক্স পয়েন্ট করে ৩৩তম সেলে এবং সেটি হয়ে উঠবে ৩য় (০ থেকে শুরু করলে, অন্যথায় ৪র্থ) রো এবং ৩য় (C) কলাম।
উপলব্ধ মাইনফিল্ড নির্ধারণের লজিক তৈরি করুন
একটি মাইন এক্সট্র্যাক্ট করতে, কোঅরডিনেট ডিকোড হওয়ার পর এবং ইন্ডেক্স খুঁজে পাওয়ার পর, প্রোগ্রাম চেক করে ওই ফিল্ডটি উপলব্ধ কিনা। যদি না হয়, প্রোগ্রাম একটি সতর্কতা দেখায়, এবং প্লেয়ার অন্য কোঅরডিনেট বেছে নেয়।
এই কোডে, একটি সেল উপলব্ধ যদি সেটি একটি ডট (.) ক্যারেক্টার ধারণ করে। অনুমান করুন এটি উপলব্ধ, তবে সেলের মান রিসেট হয় এবং স্কোর আপডেট হয়। যদি একটি সেল অনুপলব্ধ হয় কারণ সেটি ডট ধারণ করে না, তবে একটি ভেরিয়েবল not_allowed সেট হয়। সংক্ষিপ্ততার জন্য, আমি গেম লজিকের সতর্কতা স্টেটমেন্টের কন্টেন্ট সোর্স কোডে দেখতে ছেড়ে দিচ্ছি।
is_free_field()
{
local f=$1
local val=$2
not_allowed=0
if [[ "${room[$f]}" = "." ]]; then
room[$f]=$val
score=$((score+val))
else
not_allowed=1
fi
}

যদি ইনপুট কোঅরডিনেট উপলব্ধ হয়, মাইন আবিষ্কৃত হয়, নিচে দেখানো মতো। যখন h6 ইনপুট হিসেবে দেওয়া হয়, কিছু র্যান্ডম মান আমাদের মাইনফিল্ডে পপুলেট হয়, এই মানগুলো ইউজার স্কোরে যোগ হয় মাইন এক্সট্র্যাক্ট হওয়ার পর।

এখন মনে রাখুন শুরুতে আমরা যেসব ভেরিয়েবল ডিক্লেয়ার করেছিলাম, [a-g], আমি এখন সেগুলো এখানে ব্যবহার করব র্যান্ডম মাইন এক্সট্র্যাক্ট করার জন্য তাদের মান ভেরিয়েবল m-এ অ্যাসাইন করে বাশ ইন্ডিরেকশন ব্যবহার করে। সুতরাং, ইনপুট কোঅরডিনেটের ওপর নির্ভর করে, প্রোগ্রাম একটি র্যান্ডম সেট অডিশনাল নম্বর (m) পিক করে আডিশনাল ফিল্ড পপুলেট করার জন্য (উপরে দেখানো মত) সেগুলো আসল ইনপুট কোঅরডিনেটস সাথে যোগ করে, যেটি এখানে i দ্বারা প্রতিনিধিত্ব করা হয়েছে (উপরে ক্যালকুলেট করা)।
লক্ষ্য করুন নিচের কোড স্নিপেটে ক্যারেক্টার X হলো আমাদের একমাত্র GAME-OVER ট্রিগার, আমরা এটিকে আমাদের শাফল লিস্টে যোগ করেছি যাতে র্যান্ডমলি আসে, shuf কমান্ডের সৌন্দর্য ইহা, এটি যেকোনো সংখ্যক চান্সের পর আসতে পারে বা আমাদের লাকি বিজয়ী ইউজারের জন্য আসতে পারে না।
m=$(shuf -e a b c d e f g X -n 1) # add an extra char X to the shuffle, when m=X, its GAMEOVER
if [[ "$m" != "X" ]]; then # X will be our explosive mine(GAME-OVER) trigger
for limit in ${!m}; do # !m represents the value of value of m
field=$(shuf -i 0-5 -n 1) # again get a random number and
index=$((i+limit)) # add values of m to our index and calculate a new index till m reaches its last element.
is_free_field $index $field
done
আমি চাই সকল রিভিল করা সেল প্লেয়ার দ্বারা সিলেক্ট করা সেলের সাথে কন্টিগুয়াস হয়।

উপলব্ধ এবং এক্সট্র্যাক্টেড মাইন গণনা রাখুন
প্রোগ্রামকে মাইনফিল্ডের উপলব্ধ সেলগুলো ট্র্যাক রাখতে হবে; অন্যথায়, সকল সেল রিভিল হওয়ার পরেও প্লেয়ার থেকে ইনপুট চাইতে থাকবে। এটি ইমপ্লিমেন্ট করতে, আমি একটি ভেরিয়েবল তৈরি করি free_fields নামে, প্রাথমিকভাবে ০ সেট করে। একটি for লুপে ডিফাইন করা হয় আমাদের মাইনফিল্ডে বাকি উপলব্ধ সেল/ফিল্ডের সংখ্যা দ্বারা। যদি একটি সেলে ডট (.) থাকে, তবে free_fields কাউন্ট ইনক্রিমেন্ট হয়।
get_free_fields()
{
free_fields=0
for n in $(seq 1 ${#room[@]}); do
if [[ "${room[$n]}" = "." ]]; then
((free_fields+=1))
fi
done
}
ওপস, কি হবে, যদি free_fields=0? এট বলছে, আমাদের ইউজার সব মাইন এক্সট্র্যাক্ট করে ফেলেছে। সঠিক কোডটি দেখতে উত্সাহিত করা হচ্ছে ভালো বুঝতে।
if [[ $free_fields -eq 0 ]]; then # well that means you extracted all the mines. printf '\n\n\t%s: %s %d\n\n' "You Win" "you scored" "$score" exit 0 fi
গেমওভার লজিক তৈরি করুন
গেমওভার সিচুয়েশনের জন্য, আমরা টার্মিনালের মাঝপথে প্রিন্ট করি কিছু নিফটি লজিক ব্যবহার করে যা আমি পাঠকের ওপর ছেড়ে দিচ্ছি কীভাবে কাজ করে সেটি এক্সপ্লোর করার জন্য।
if [[ "$m" = "X" ]]; then
g=0 # to use it in parameter expansion
room[$i]=X # override the index and print X
for j in {42..49}; do # in the middle of the minefields,
out="gameover"
k=${out:$g:1} # print one alphabet in each cell
room[$j]=${k^^}
((g+=1))
done
fi
শেষে, আমরা সে দুটি লাইন প্রিন্ট করি যেগুলো সবচেয়ে আকাঙ্ক্ষিত।
if [[ "$m" = "X" ]]; then printf '\n\n\t%s: %s %d\n' "GAMEOVER" "you scored" "$score" printf '\n\n\t%s\n\n' "You were just $free_fields mines away." exit 0 fi

এটিই সব, ফলক! আপনি যদি আরও জানতে চান, এই মাইনসווিপার গেম এবং বাশের অন্যান্য গেমের সোর্স কোড আমার GitHub রিপো থেকে এক্সেস করুন। আশা করি এটি আপনাকে আরও বাশ শিখতে এবং মজা করতেই শিখতে অনুপ্রাণিত করবে।