ANSI Control Sequences

کدهای کنترل انسی

کدهای کنترل انسی برای تغییر عملکرد ترمینال، اعم از رنگ، استایل، شکل و مکان نشانگر و بسیاری دیگر استفاده می‌شوند.

تمام کدهای این صفحه با echo -e اجرا می‌شوند.

جدول زیر مشخصات رایج‌ترین کدها را نشان می‌دهد
رنگکد متن پیش‌زمینهکد پس‌زمینه
مشکی\033[0;30m\033[40m
قرمز\033[0;31m\033[41m
سبز\033[0;32m\033[42m
زرد\033[0;33m\033[43m
آبی\033[0;34m\033[44m
بنفش\033[0;35m\033[45m
فیروزه\033[0;36m\033[46m
سفید\033[0;37m\033[47m
ریست کردن رنگ‌ها\033[0m
مثال:
echo -e "\033[0;31mHello World\033[0m"
این دستور "درود جهان" را با رنگ قرمز چاپ می‌کند.

img

همانطور که می‌بینید در مثال بالا ما فقط رنگ پیش‌زمینه را تغییر دادیم اگر بخواهیم فقط رنگ پس‌زمینه را تغییر دهیم مانند زیر عمل می‌کنیم
echo -e "\033[0;41mHello World\033[0m"
این دستور "درود جهان" را با رنگ قرمز در پس‌زمینه چاپ می‌کند

img

و اگر بخواهیم هردو رنگ پیش و پس‌ زمینه را تغییر دهیم به شکل زیر عمل می کنیم
echo -e "\033[30;42mHello World\033[0m"
این دستور "درود جهان" را با رنگ مشکی در پس‌زمینه سبز چاپ می‌کند

img


تغییر رنگ تمام ترمینال

دستور زیر رنگ زمینه‌ی تمام ترمینال را به بنقش تغییر می‌دهد.
echo -ne "\033]11;#53186f\007"

img

دستور بعدی رنگ پیش‌زمینه‌ی تمام ترمینال را به سبز تغییر می‌دهد.
echo -ne "\033]10;#00ff44\007"

img



کارهای دیگری که می‌توان با کدهای انسی انجام داد

جابجایی نشانگر

می‌توانید موقعیت نشانگر را در پنجره ترمینال کنترل کنید

------
\033[Aحرکت نشانگر به بالا
\033[Bحرکت نشانگر به پایین
\033[Cحرکت نشانگر به جلو (راست)
\033[Dحرکت نشانگر به عقب (چپ)
\033[<row>;<column>Hتنظیم موقعیت نشانگر

پاک کردن متن

می‌توانید بخش‌هایی از صفحه یا کل صفحه را پاک کنید

------
\033[0Jپاک کردن صفحه از نشانگر تا انتها
\033[1Jپاک کردن صفحه از ابتدا تا نشانگر
\033[2Jپاک کردن کل صفحه
\033[0Kپاک کردن خط از نشانگر تا انتها
\033[1Kپاک کردن خط از ابتدا تا نشانگر
\033[2Kپاک کردن کل خط

اسکرول

می‌توانید اسکرول را درون ترمینال کنترل کنید*

------
\033[Sاسکرول به بالا یک خط
\033[Tاسکرول به پایین یک خط

نشانگر

ذخیره و بازیابی موقعیت نشانگر

می‌توانید موقعیت فعلی نشانگر را ذخیره کرده و بعدا بازیابی کنید

------
\033[sذخیره موقعیت نشانگر
\033[uبازیابی موقعیت نشانگر

مخفی کردن و نمایش نشانگر

ر می‌توانید نشانگر را مخفی یا نمایش دهید

------
\033[?25lمخفی کردن نشانگر
\033[?25hنمایش نشانگر

شکل نشانگر

می‌توانید نشانگر را به بلاک یا خط صاف تغییر دهید

------
\e[0 q(پیش‌فرض) شکل یک بلوک
\e[1 qشکل یک بلوک چشمک‌زن
\e[3 qشکل یک خط زیرین چشمک‌زن
\e[4 qشکل یک خط زیرین
\e[5 qشکل یک خط عمودی چشمک‌زن
\e[6 qشکل یک خط عمودی چشمک‌زن

افکت‌های متن

------
\e[1mBold
\e[3mItalic
\e[4mUnderline
\e[5mBlink
\e[7mInvert colors


echo -e "\033[30;42;3mHello World\033[0m"
این دستور "درود جهان" را با رنگ مشکی در پس‌زمینه سبز به‌صورت ایتالیک چاپ می‌کند




مطالب این سایت بصورت مداوم به‌روز‌رسانی می‌شوند. برای دنبال کردن مطالب این سایت را بوکمارک کنید.

جهت حمایت مالی از پروژه، در صورتی که در ایران هستید، روی دکمه‌ی برام قهوه بخر کلیک کنید.

جهت ارتباط با نگارنده از طریق تلگرام یا ایمیل اقدام کنید.

Telegram Protonmail



ساخت یک Hello World خلاقانه با BASH

move_hello.gif

همانطور که می‌بینید با کلید‌های جهت می‌توانیم عبارت Hello World را جا‌به‌جا کنیم.
#!/usr/bin/env bash 

shopt -s checkwinsize || return 1; (:;:)
خط دوم اسکریپت کلکی هوشمندانه برای چک کردن سایز ترمینال و ست کردن متغیر‌های COLUMNS و LINES است. این متغیرها هنگام تعامل کاربر با ترمینال ست می‌شوند و به همین خاطر استفاده از ‌آن‌ها در اسکریپت به کمی زیرکی نیاز دارد.

با استفاده از یک No Op ترمینال رو گول می‌زنیم تا توهم تعامل بزنه (😂) و متغیرها رو ست کنه. می‌تونستیم از

/usr/bin/true

هم استفاده کنیم ولی خیلی کار جالبی نیست!

عبارت (:;:) در اصل هیچ‌کاری انجام نمی‌دهد اما باعث می‌شود تا ترمینال دو متغیر مورد نیاز را به شل گزارش دهد. درباره‌ noop بیشتر بخوانید

در قسمت بعدی مختصات میانه‌ی ترمینال رو محاسبه می‌کنیم. این کار ضروری نیست. می‌توانستیم به سادگی x و y رو به COLUMNS و LINES اختصاص بدیم یا حتا مستقیما از COLUMNS و LINES استفاده کنیم. دقت کنید که در نحو ریاضی بش، یعنی در Arithmetic Syntax ،نیازی به استفاده از سیجیل $ برای متغیرها نداریم.

x=$(( COLUMNS / 2 ))
y=$(( LINES / 2 ))
با استفاده از کد انسی می‌توانیم عبارت خود را در مکان مشخصی در ترمینال چاپ کنیم.
printf "\e[10;20H Hello"
مختصات ۱۰ و ۲۰ مکان نشانگر هنگام چاپ را مشخص می‌کند. این کد را به شکل زیر نیز می‌توان نوشت:
printf "\e[%d;%dH Hello" 10 20
این نحو مرسوم نوشتار printf است. d% یک format specifier یا همان placeholder برای مقداری است که بعد از آن معین می‌شود. d% به معنی digit یا عدد صحیح است. برای اطلاعات بیشتر، `man printf` و `man 3 printf`را ببینید.

پس تا اینجای کار کد ما به شکل زیر است:

#!/usr/bin/env bash 

shopt -s checkwinsize || return 1; (:;:)

x=$(( COLUMNS / 2 ))
y=$(( LINES / 2 ))

printf "\e[%d;%dH Hello" $y $x

در قسمت بعدی کلید‌های جهت را تعریف می‌کنیم. برای این‌کار از `cat -v` استفاده می کنیم تا کد ASCII کلید‌ها را یاد بگیریم.

img

به ترتیب از چپ، بالا پایین چپ راست.

برای خواندن یک متغیر از STDIN طبق معمول از read استفاده می‌کنیم.

بنابراین برای دنبال کردن کلید‌هایی که توسط کاربر فشرده می‌شوند به شیوه‌ی زیر عمل می‌کنیم:

read -rsn3 key

فلگ‌های rsn3 به ترتیب برای خواندن بک‌اسلش‌ها، سکوت خروجی و خواندن مشخصا ۳ حرف از ورودی استفاده می‌شوند. برای اطلاعات بیشتر، man read را ببینید.

حالا با ترکیب یک لوپ و یک کیس مقادیر x و y را تغییر داده، ترمینال را پاک کرده و عبارت را مجدد چاپ می‌کنیم تا حرکت عبارت میسر شود.

while :; do

    read -rsn3 key
    clear

    case "$key" in
        $'\e[A') ((y--));;
        $'\e[B') ((y++));;
        $'\e[C') ((x++));;
        $'\e[D') ((x--));;
    esac

	printf "\e[%d;%dH Hello" $y $x
done

نحو خاص ;: به معنی true است.

تمام کد تا اینجا:

#!/usr/bin/env bash 

shopt -s checkwinsize || return 1; (:;:)

x=$(( COLUMNS / 2 ))
y=$(( LINES / 2 ))

while :; do

		printf "\e[%d;%dH Hello" $y $x
    
		read -rsn3 key
    clear

    case "$key" in
        $'\e[A') ((y--));;
        $'\e[B') ((y++));;
        $'\e[C') ((x++));;
        $'\e[D') ((x--));;
    esac
done

دقت کنید که در صورتی‌ که printf بعد از case قرار بگیرد، تا دریافت کلیدی از کاربر چیزی چاپ نخواهد شد.

move_hello_simple.gif

همانطور که در گیف می‌بینید، تفاوت‌هایی بین گیف اول و نتیجه‌ی فعلی کد ما وجود دارد.
  • نشانگر پنهان نشده است
  • ترمینال به موقع پاک نمی‌شود
  • محتوای پیشین ترمینال حفظ نمی‌شود
برای پنهان کردن نشانگر از یک کد انسی استفاده می‌کنیم:
printf "\e[?25l"

دستور انسی بالا به شکل echo -ne "\033[?25l" یا printf "\033[?25l"نیز می‌توانست نوشته شود. ( این قاعده بر باقی دستورات هم صدق می‌کند)

هردوی e\ و 033\ کد اسکی ۲۷ (ASCII 27) را به ترمینال دیکته می‌کنند. e\ یک escape sequence است که معنی ASCII 27 می‌دهد و 033\ نوشتار اوکتال همان است.

برای حفظ محتوای پیشین ترمینال از قابلیت Alt Screen استفاده می‌کنیم. این قابلیت که با یک کد انسی فعال می‌شود باعث می‌شود تا برنامه‌ی ما در یک بافر جداگانه اجرا شود. این همان ویژگی است که برنامه‌هایی همچون ویم از آن استفاده می‌کنند.

printf "\x1b[?1049h"
برای ظاهر کردن نشانگر و بازگشتن به بافر اصلی به ترتیب از دستورات انسی زیر استفاده می‌کنیم:
printf "\e[?25l"
printf "\x1b[?1049h"
بنابراین می توانیم دو تابع برای اعمال تنظیمات لازم پیش از اجرای قسمت اصلی کد، و برای بازگرداندن حالت پیشین ترمینال داشته باشیم.
setupTerm() {
    printf "\x1b[?1049h"
    printf "\e[?25l"
}

restoreTerm() {
    printf "\x1b[?1049l"
    printf "\x1B[?25h"
}
همچنین برای منظم کردن کد، برای چاپ عبارت نیز یک تابع می‌سازیم:
printHello() {
    printf '\e[%d;%dH Hello World' $y $x
}
حالا کافیست توابع را به نوبت و در جای درست صدا بزنیم تا کار خود را انجام دهند.
#!/usr/bin/env bash 

shopt -s checkwinsize || return 1; (:;:)

x=$(( COLUMNS / 2 ))
y=$(( LINES / 2 ))

setupTerm() {
    printf "\x1b[?1049h"
    printf "\e[?25l"
}

restoreTerm() {
    printf "\x1b[?1049l"
    printf "\x1B[?25h"
}

printHello() {
    printf '\e[%d;%dH Hello World' $y $x
}

setupTerm

trap 'restoreTerm;exit' SIGINT

printHello

while :; do

    read -rsn3 key
    clear

    case "$key" in
        $'\e[A') ((y--));;
        $'\e[B') ((y++));;
        $'\e[C') ((x++));;
        $'\e[D') ((x--));;
    esac

    printHello
done

برای اینکه تابع restoreTerm هنگام خروج و دریافت SIGINT یعنی Ctrl-c اجرا شود، از trap استفاده می‌کنیم. برای اطلاعات بیشتر man trap ببنید.




مطالب این سایت بصورت مداوم به‌روز‌رسانی می‌شوند. برای دنبال کردن مطالب این سایت را بوکمارک کنید.

جهت حمایت مالی از پروژه، در صورتی که در ایران هستید، روی دکمه‌ی برام قهوه بخر کلیک کنید.

جهت ارتباط با نگارنده از طریق تلگرام یا ایمیل اقدام کنید.

Telegram Protonmail



بیاین بازی ساده زیر رو بسازیم

demo.gif

در ابتدا به سایز ترمینال نیاز داریم
cols=$(tput cols)
lines=$(tput lines)

echo $cols
echo $lines
تی‌پات یک پکیجیه که با ncurses نصب می‌شه و روی اکثر سیستم‌ها هست

ec017c947f2f156f59e08bb68297aef7.png

دومین کارمون اینه که محیط یا همون فیلد بازی رو مشخص کنیم.
خب برای این کار باید بتونیم یک کاراکتری رو توی تمام خونه ‌های ترمینال چاپ کنیم.
cols=$(tput cols)
lines=$(tput lines)

for ((i=0; i < cols; i++)); do
    printf '-'
    for ((j=0; j < lines j++)); do
        printf '-'
    done
done

44dbb3c62a2171a4893f8b702c21398a.png

از اونجایی که لاین‌ها دارن یکم اسکرول می‌دن و ما این پایین ترمینالو می‌خوایم واسه اینکه بعدا میزان جون و تعداد حدس ‌های درست رو نشون بدیم، تعداد لاین‌ها رو کم می‌کنیم
cols=$(tput cols)
lines=$(tput lines)

for ((i=0; i < cols; i++)); do
    printf '-'
    for ((j=0; j < lines- 4; j++)); do <---- CHANGE HERE
        printf '-'
    done
done
در مرحله بعدی باید بتونیم یه سری ستاره جاهای رندم چاپ کنیم
یه فایل تست باز می کنیم
cols=$(tput cols)
lines=$(tput lines)

num_stars=20

declare -A star_positions

for ((n=0; n<num_stars; n++)); do
    rand_col=$((RANDOM % cols))
    rand_line=$((RANDOM % (lines - 4)))
    star_positions["$rand_line,$rand_col"]=1
done

for ((i=0; i < lines - 4; i++)); do
    for ((j=0; j < cols; j++)); do
        if [[ ${star_positions["$i,$j"]} ]]; then
            tput cup "$i" "$j"
            printf '*'
        fi
    done
done

tput cup "$lines" 0

b17844fb38b0388a70eaedbbeb2b1b6e.png

یک آرایه اسوشیتیو (آرایه کلید-مقدار) به نام `star_positions` ایجاد می‌کنیم.از این آرایه برای ذخیره کردن ستون و سطر رندم استفاده می‌کنیم.پس از آن در لوپ اول، برای هر ستاره، موقعیت تصادفن را در ترمینال تعیین می‌کنیم. موقعیت ستاره در آرایه `star_positions` با کلید `"$rand_line,$rand_col"` ذخیره می‌شود. بعدا یک تابع move cursor میسازیم ولی فعلا با تی‌پات کارمونو راه میندازیم
اگه بخوایم هم ستاره چاپ کنیم و هم خط فاصله میتونیم به سبک کد قبلی برگردیم.

حالا که دیدیم چطور می‌شه در جاهای رندم کاراکتری رو چاپ کرد،به شکل زیر عمل می‌کنیم.
for ((i=0; i < lines - 4; i++)); do
    for ((j=0; j < cols; j++)); do
        if [[ ${star_positions["$i,$j"]} ]]; then
            printf '*'
        else
            printf '-'
        fi
    done
    printf '\n'
done
پس تا اینجا کد ما به این شکل میشه
cols=$(tput cols)
lines=$(tput lines)

num_stars=20

declare -A star_positions

for ((n=0; n<num_stars; n++)); do
	rand_col=$((RANDOM % cols))
	rand_line=$((RANDOM % (lines - 4)))
	star_positions["$rand_line,$rand_col"]=1
done

for ((i=0; i < lines - 4; i++)); do
    for ((j=0; j < cols; j++)); do
        if [[ ${star_positions["$i,$j"]} ]]; then
            printf '*'
        else
            printf '-'
        fi
    done
    printf '\n'
done

45a129a9b27b87ffdde40b4563203638.png

حالا خوبه که مکان مین‌هارو پنهان کنیم و واسه این کار تمام ترمینال رو با کاراکتر یونیکد بلوک پر می‌کنیم
برای وارد کردن کاراکتر یونیکد توی ویم در حالت insert به این شکل عمل می‌کنیم`C-v u2588`
یعنی کنترل+v و بعد کد یونیکد رو وارد می‌کنیم.
cols=$(tput cols)
lines=$(tput lines)

num_stars=20

declare -A star_positions

for ((n=0; n<num_stars; n++)); do
	rand_col=$((RANDOM % cols))
	rand_line=$((RANDOM % (lines - 4)))
	star_positions["$rand_line,$rand_col"]=1
done

for ((i=0; i < lines - 4; i++)); do
    for ((j=0; j < cols; j++)); do
        if [[ ${star_positions["$i,$j"]} ]]; then
            printf '█'
        else
            printf '█'
        fi
    done
    printf '\n'
done
تا اینجای کار ما یک زمین بازی درست کردیم

cb5342614cf7b2fc15e9902819a5f642.png

حالا وقتشه که رنگ زمین بازی‌مون رو عوض کنیم

printf '\033[1;33m█\033[0m'

با استفاده از کد انسی بالا رنگ پیش زمینه ترمینال رو زرد می‌کنیم و بلوک هارو چاپ می‌کنیم

2ba36fd870fcc526c2cc394273263dbd.png

حالا باید راهی برای آزادانه حرکت دادن نشان‌گر پیدا کنیم. با جستجو در man 1 stty با قابلیت raw آشنا می‌شیم. همچنین با استفاده از دستور read می‌تونیم کلید های ورودی را بخونیم
# Enable raw input mode
stty -raw

# Clear the screen
clear
# Infinite loop to process key presses
while true; do
    # Read the input
    read -rsn1 key
    
    # Check for escape sequences
    if [[ $key == $'\x1b' ]]; then
        # Read the next two bytes
        read -rsn2 -t 0.1 key
        
        case "$key" in
            "[A") # Up arrow
                echo -ne "\033[A"
                ;;
            "[B") # Down arrow
                echo -ne "\033[B"
                ;;
            "[C") # Right arrow
                echo -ne "\033[C"
                ;;
            "[D") # Left arrow
                echo -ne "\033[D"
                ;;
        esac
    fi
done

# Reset terminal settings when done (not reached in this loop)
stty echo icanon
قطعه‌ی بالا به‌خوبی کامنت شده تا متوجه بشید هر قسمت چه کاری انجام می‌ده، اما به جزئیات بیشتری نمی‌پردازیم. اگه علاقه‌مند هستید، می‌تونید برای اطلاعات بیشتر به `man stty` و `man read` مراجعه کنید.

در سطح بعدی باید روشی پیدا کنیم تا بتونیم موقعیت ستاره‌های تصادفی رو ردیابی کنیم و بعد در صورتی که کاربر کلید Enter رو روی اون‌ها فشار داد، اون‌ها رو با چیزی جایگزین کنیم.

اما ابتدا کد خودمون رو سازماندهی می‌کنیم. تااینجا به یک تابع draw_grid، تابع replace_stars و یک حلقه‌ی اصلی بازی (game_loop) نیاز داریم، که توی اون حالت raw را فعال می‌کنیم. همچنین یک تابع move_cursor و متغیرهای cursor_row و cursor_col رو تعریف می‌کنیم تا حرکت مکان‌نما رو آسون‌تر مدیریت کنیم. این کار به ما اجازه می‌ده کدهای ANSI که مکان‌نما را در کد بالا جابجا می‌کردن، حذف کنیم.

cursor_row=$((lines - 5))
cursor_col=0

move_cursor() {
    echo -ne "\033[${1};${2}H"
}

draw_grid() {
	clear
	for ((i=0; i < lines - 4; i++)); do
		for ((j=0; j < cols; j++)); do
			if [[ ${star_positions["$i,$j"]} ]]; then
				printf '\033[1;33m█\033[0m'
			else
				printf '\033[1;33m█\033[0m'
			fi
		done
		printf '\n'
	done
}

make_stars() {
	for ((n=0; n<num_stars; n++)); do
		rand_col=$((RANDOM % cols))
		rand_line=$((RANDOM % (lines - 4)))
		star_positions["$rand_line,$rand_col"]=1
	done
}

game_loop() {
	while true; do
		
		move_cursor $((cursor_row+1)) $((cursor_col+1))

		read -rsn1 key

		if [[ $key == $'\x1b' ]]; then
			read -rsn2 -t 0.1 key

			case "$key" in
				"[A") # Up arrow
					((cursor_row--))
					((cursor_row < 0)) && cursor_row=0
					;;
				"[B") # Down arrow
					((cursor_row++))
					((cursor_row > lines - 5)) && cursor_row=$((lines - 5))
					;;
				"[C") # Right arrow
					((cursor_col++))
					((cursor_col >= cols)) && cursor_col=$((cols - 1))
					;;
				"[D") # Left arrow
					((cursor_col--))
					((cursor_col < 0)) && cursor_col=0
					;;
			esac
		fi
	done
}
جالا باید برخورد با ستاره‌ها را پیاده‌سازی کنیم. باید بتونیم وقتی کاربر کلید Enter رو روی ستاره‌ها فشار می‌ده، اون‌ها رو با کاراکتر دیگه‌ای جایگزین کنیم.
replace_star() {
    if [[ ${star_positions["$cursor_row,$cursor_col"]} ]]; then
        # Clear the star and replace it with '#'
        star_positions["$cursor_row,$cursor_col"]=0
        move_cursor $((cursor_row+1)) $((cursor_col+1))
        echo -n "#"
        move_cursor $((cursor_row+1)) $((cursor_col+1))
    fi
}
با هم
cols=$(tput cols)
lines=$(tput lines)
num_stars=10000
cursor_row=$((lines - 5))
cursor_col=0

move_cursor() {
	echo -ne "\033[${1};${2}H"
}

draw_grid() {
	clear
	for ((i=0; i < lines - 4; i++)); do
		for ((j=0; j < cols; j++)); do
			if [[ ${star_positions["$i,$j"]} ]]; then
				printf '\033[1;33m█\033[0m'
			else
				printf '\033[1;33m█\033[0m'
			fi
		done
		printf '\n'
	done
}

make_stars() {
	for ((n=0; n<num_stars; n++)); do
		rand_col=$((RANDOM % cols))
		rand_line=$((RANDOM % (lines - 4)))
		star_positions["$rand_line,$rand_col"]=1
	done
}

replace_star() {
	if [[ ${star_positions["$cursor_row,$cursor_col"]} ]]; then
		# Clear the star and replace it with '#'
		star_positions["$cursor_row,$cursor_col"]=0
		move_cursor $((cursor_row+1)) $((cursor_col+1))
		echo -n "#"
		move_cursor $((cursor_row+1)) $((cursor_col+1))
	fi
}

game_loop() {
	while true; do

		move_cursor $((cursor_row+1)) $((cursor_col+1))
		read -rsn1 key

		if [[ $key == "" ]]; then
			replace_star

		fi
		if [[ $key == $'\x1b' ]]; then
			read -rsn2 -t 0.1 key

			case "$key" in
				"[A") # Up arrow
					((cursor_row--))
					((cursor_row < 0)) && cursor_row=0
					;;
				"[B") # Down arrow
					((cursor_row++))
					((cursor_row > lines - 5)) && cursor_row=$((lines - 5))
					;;
				"[C") # Right arrow
					((cursor_col++))
					((cursor_col >= cols)) && cursor_col=$((cols - 1))
					;;
				"[D") # Left arrow
					((cursor_col--))
					((cursor_col < 0)) && cursor_col=0
					;;
			esac
		fi
	done
}

function main() {
	draw_grid
	make_stars
	game_loop
}
main
حالا وقتش رسیده که انفجارها رو بسازیم. برای این کار، تابع replace_star خودمون رو اصلاح می‌کنیم.
replace_star() {
	if [[ ${star_positions["$cursor_row,$cursor_col"]} ]]; then
		star_positions["$cursor_row,$cursor_col"]=0

		size=$((RANDOM % 5 + 3))

		local start_row=$((cursor_row - size / 2))
		local start_col=$((cursor_col - size / 2))

		for ((i=0; i<size; i++)); do
			move_cursor $((start_row + i)) $start_col
			printf '\033[1;31m'
			for ((j=0; j<size; j++)); do
				echo -n '█'
			done
			printf '\033[0m'
		done

		move_cursor $cursor_row $cursor_col
		echo -e '\033[1;31m#\033[0m'  # Center character in red
		move_cursor $((start_row + size + 1)) $start_col
	else
		move_cursor $((cursor_row+1)) $((cursor_col+1))
		printf '\033[1;36m█\033[0m'
	fi
}

286d1c0b7161dcf0edaa0572fa01a5f1.png

تقریباً کارمون تموم شده. تنها کاری که باید انجام بدیم اینه که متغیرهایی برای سلامتی و مرحله اضافه کنیم و منطق‌های کوچکی برای زمانی که کاربر برنده یا بازنده شده، اضافه کنیم. چاپ اطلاعات در پایین ترمینال مثل قبل آسونه و می‌تونیم از تابع move_cursor خودمون استفاده کنیم.
round=0;
hp=5;
win=0;
و مکان مشخصی برای پیام جون
health_msg=$((lines - 2))

move_cursor $health_msg 0 
printf "HP: $hp"

دو عملیات ریاضی بعد انفجار اضافه می‌کنیم
((round++))
((hp--))
به این شکل می‌تونیم`round` رو کم کنیم و`hp` زیاد کنیم
باقی کد هم توی یک لاجیک ساده wrap می‌کنیم
if [[ $round -ge 6 ]]; then
	tput reset
	figlet "GAME OVER"
	exit 0
else
	((win++))
fi
و کد زیر رو قبل` fi `بالا قرار می دیم
if [[ $win -eq 20 ]]; then
	tput reset
	echo "Round Cleared!"
	exit 0
fi
سپس به کاربر خوش‌امد می‌گیم
center_col=$((cols / 2 - 25))
center_line=$((lines / 2 - 2))

move_cursor $center_line $center_col 
printf "Survive 20 Guesses, or DIE after 5 explosions"
sleep 2;
و برای بار آخر کد رو ریفاکتور می‌کنیم
#!/usr/bin/env bash

cols=$(tput cols)
lines=$(tput lines)
center_col=$((cols / 2 - 25))
center_line=$((lines / 2 - 2))
cursor_row=$((lines - 5))
cursor_col=0
round=0
hp=5
win=0
health_msg=$((lines - 2))
num_stars=10000

move_cursor() {
    echo -ne "\033[${1};${2}H"
}

initialize_screen() {
    move_cursor $center_line $center_col
    printf "Survive 20 Guesses, or DIE after 5 explosions"
    sleep 2
    clear
    move_cursor $health_msg 0
    printf "HP: $hp"
}

generate_star_positions() {
    declare -gA star_positions
    for ((n=0; n<num_stars; n++)); do
        rand_col=$((RANDOM % cols))
        rand_line=$((RANDOM % (lines - 4)))
        star_positions["$rand_line,$rand_col"]=1
    done
}

draw_grid() {
    clear
    for ((i=0; i < lines - 4; i++)); do
        for ((j=0; j < cols; j++)); do
            if [[ ${star_positions["$i,$j"]} ]]; then
                printf '\033[1;33m█\033[0m'
            else
                printf '\033[1;33m█\033[0m'
            fi
        done
        printf '\n'
    done
}

replace_star() {
    if [[ ${star_positions["$cursor_row,$cursor_col"]} ]]; then
        star_positions["$cursor_row,$cursor_col"]=0
        size=$((RANDOM % 5 + 3))
        local start_row=$((cursor_row - size / 2))
        local start_col=$((cursor_col - size / 2))
        
        for ((i=0; i<size; i++)); do
            move_cursor $((start_row + i)) $start_col
            printf '\033[1;31m'
            for ((j=0; j<size; j++)); do
                echo -n '█'
            done
            printf '\033[0m'
        done
        
        ((round++))
        ((hp--))
        move_cursor $cursor_row $cursor_col
        echo -e '\033[1;31m#\033[0m'
        move_cursor $((start_row + size + 1)) $start_col

        if [[ $round -ge 5 ]]; then
            tput reset
            figlet "GAME OVER"
            exit 0
        else
            move_cursor 1 1
            printf '\033[1;37mYou Survive The Explosion!\033[0m'
            sleep 1
            move_cursor 1 1
            size=26
            printf '\033[1;33m'
            for ((i=0; i<size; i++)); do
                echo -n '█'
            done
            printf '\033[0m'
            round_msg=$((lines - 3))
            move_cursor "$round_msg" 0
            echo "Round: $round / 5"
            move_cursor $health_msg 0
            printf "HP: $hp"
        fi
    else
        move_cursor $((cursor_row+1)) $((cursor_col+1))
        printf '\033[1;36m█\033[0m'
        ((win++))
        win_msg=$((lines - 1))
        move_cursor $win_msg 0
        printf "Win: $win"

        if [[ $win -eq 20 ]]; then
            tput reset
            figlet "Round Cleared!"
            exit 0
        fi
    fi
}

game_loop() {
    stty -echo -icanon time 0 min 0
    while true; do
        move_cursor $((cursor_row+1)) $((cursor_col+1))
        read -rsn1 key
        if [[ $key == "" ]]; then
            replace_star
        fi
        if [[ $key == $'\x1b' ]]; then
            read -rsn2 -t 0.1 key
            case "$key" in
                "[A") ((cursor_row--)); ((cursor_row < 0)) && cursor_row=0 ;;
                "[B") ((cursor_row++)); ((cursor_row > lines - 5)) && cursor_row=$((lines - 5)) ;;
                "[C") ((cursor_col++)); ((cursor_col >= cols)) && cursor_col=$((cols - 1)) ;;
                "[D") ((cursor_col--)); ((cursor_col < 0)) && cursor_col=0 ;;
            esac
        fi
    done
    stty echo icanon
}

initialize_screen
generate_star_positions
draw_grid
game_loop

و به همین سادگی بازی ما تموم شد. قطعا جای خیلی زیادی برای ارتقای این بازی وجود داره، اما با اطلاعاتی که توی این نوشته یاد گرفتید، می‌تونین خودتون بی‌نهایت این کد رو ارتفا بدین.