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

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
و به همین سادگی بازی ما تموم شد. قطعا جای خیلی زیادی برای ارتقای این بازی وجود داره، اما با اطلاعاتی که توی این نوشته یاد گرفتید، می‌تونین خودتون بی‌نهایت این کد رو ارتفا بدین.