Играючи BASH'им

Как я написал игру на bash'е.

image

Нетерпеливые могут посмотреть\поиграть, скачав игру тут.
Далее небольшой рассказ о процессе создания и разбор интересных (по моему мнению) мест.

гифка с геймплеем
image

Идея, как водится, пришла внезапно. Как-то раз я «красил» (вставлял управляющие коды в текстовые сообщения для изменения цвета текста/фона) очередной свой сценарий и подумал: «Хм, а ведь из этого может получиться игра!»

Кстати для раскрашивания я пользуюсь вот такой табличкой:

#----------------------------------------------------------------------+
#Color picker, usage: printf ${BLD}${CUR}${RED}${BBLU}"Some text"${DEF}|
#---------------------------+--------------------------------+---------+
#        Text color         |       Background color         |         |
#------------+--------------+--------------+-----------------+         |
#    Base    |Lighter\Darker|    Base      | Lighter\Darker  |         |
#------------+--------------+--------------+-----------------+         |
RED='\e[31m'; LRED='\e[91m'; BRED='\e[41m'; BLRED='\e[101m' #| Red     |
GRN='\e[32m'; LGRN='\e[92m'; BGRN='\e[42m'; BLGRN='\e[102m' #| Green   |
YLW='\e[33m'; LYLW='\e[93m'; BYLW='\e[43m'; BLYLW='\e[103m' #| Yellow  |
BLU='\e[34m'; LBLU='\e[94m'; BBLU='\e[44m'; BLBLU='\e[104m' #| Blue    |
MGN='\e[35m'; LMGN='\e[95m'; BMGN='\e[45m'; BLMGN='\e[105m' #| Magenta |
CYN='\e[36m'; LCYN='\e[96m'; BCYN='\e[46m'; BLCYN='\e[106m' #| Cyan    |
GRY='\e[37m'; DGRY='\e[90m'; BGRY='\e[47m'; BDGRY='\e[100m' #| Gray    |
#------------------------------------------------------------+---------+
# Effects                                                              |
#----------------------------------------------------------------------+
DEF='\e[0m'   # Default color and effects                              |
BLD='\e[1m'   # Bold\brighter                                          |
DIM='\e[2m'   # Dim\darker                                             |
CUR='\e[3m'   # Italic font                                            |
UND='\e[4m'   # Underline                                              |
INV='\e[7m'   # Inverted                                               |
COF='\e[?25l' # Cursor Off                                             |
CON='\e[?25h' # Cursor On                                              |
#----------------------------------------------------------------------+
# Text positioning, usage: XY 10 10 "Some text"                        |
XY   () { printf "\e[${2};${1}H${3}";   } #                            |
#----------------------------------------------------------------------+
# Line, usage: line - 10 | line -= 20 | line "word1 word2 " 20         |
line () { printf %.s"${1}" $(seq ${2}); } #                            |
#----------------------------------------------------------------------+

Вставляется в сценарий при помощи source или просто копипастится.
Обратите внимание на этот управляющий код — '\e${Y};${X}H'
Он позволяет перемещать позицию курсора, где Y — строка, X — столбец, собственно на этом и построена игра. Для удобства я завернул его в функцию XY.

Итак, вдохновившись такими консольными утилитами как: sl, cowsay, figlet и т.д. и т.п. я начал придумывать игру. Всегда питал теплые чувства к скроллерам-пулялкам, так что выбор пал именно на этот жанр. Кроме того, хотелось сделать action игру! А не какую-нибудь текстовую адвенчуру (ни чего не имею против текстовых адвенчур). Экшен в консоли, на bash'е? Вызов принят!
Первым делом я нарисовал «спрайт» самолетика (героя):

__
|★〵____ 
 \_| / °)-
   |/

«Окно» кабины пилота мешало сделать нижнюю часть, тут пригодился эфект подчеркивания.
Заодно добавил цвета:

__
|${RED}★${DEF}〵____
 \_| /${UND}${BLD} °${DEF})${DGRY}-${DEF}
   |/

Для отрисовки я решил использовать массив, разбиваем строки на элементы массива:

hero=("  "
      "__     "
      "|${RED}★${DEF}〵____ "
      " \_| /${UND}${BLD} °${DEF})${DGRY}-${DEF}"
      "   |/    "
      "     ")

И выводим вот такой командой:

for (( i=0; i<${#hero[@]}; i++ )); do XY ${X} $(($Y + $i)) " ${hero[$i]} "; done

Цикл по количеству элементов массива. Каждый элемент рисуется со смещением строки вниз.
Первый и последний элементы массива пустые для того, чтобы затирать остатки самолетика при вертикальном перемещении. А для удаления артефактов при горизонтальном перемещении строки рисуются в обрамлении пробелов — " ${hero[$i]} "

Заставим самолетик летать. Необходимо как-то обработать нажатие кнопок(WASD).

Воспользуемся утилитой — read:

while true; do

	read -t0.0001 -n1 input; case $input in
		"w") ((Y--));;
		"a") ((X--));;
		"s") ((Y++));;
		"d") ((X++));;
	esac

	for (( i=0; i<${#hero[@]}; i++ )); do
		XY ${X} $(($Y + $i)) " ${hero[$i]} "
	done

done

Бесконечный цикл while. В цикле read в переменную input. Параметры read:
-t0.0001 устанавливает таймаут — время ожидания ввода, я установил минимум, чтобы игра не вставала колом
-n1 количество символов, нам нужен только 1

Case проверяет что же прилетело в input и в зависимости от этого двигает самолетик (уменьшает/увеличивает координаты X и Y). Далее уже знакомый цикл for для отрисовки самолетика.

Получилось так:

image

Отлично, самолетик двигается, но мигает курсор и вылезаеет input.
Отключим курсор и спрячем ввод. Но это «испортит» терминал (с отключенным вводом работать несколько не комфортно), так что надо включить ввод после выхода из игры.
Создадим функцию bye:

function bye () {
	stty echo		# включает ввод
	printf "${CON}${DEF}"	# включает курсор, и цвета по умолчанию
	exit			# выход из скрипта
}

trap bye INT	# вызывает функцию bye при нажатии Ctrl+C
printf "${COF}"	# отключает курсор
stty -echo	# отключает ввод

Красота, но если самолетик «улетает» за край экрана, происходит факап, функция XY перестает работать.

image

Необходимо определить размер «экрана» и не пускать самолетик за края. Удобно поместить это в функцию, чтобы значения переменных менялись при растягивании «экрана»:

function get_dimensions {
	endx=$( tput cols  )		# кол-во столбцов(X)
	endy=$( tput lines )		# кол-во линий(Y)
	heroendx=$(( $endx - 12 ))	# ограничение для самолетика по коорд. X
	heroendy=$(( $endy - 7  ))	# ограничение для самолетика по коорд. Y
}

Модифицируем основной цикл while:

while true; do

	get_dimensions

	read -t0.0001 -n1 input; case $input in
		"w") ((Y--)); [ $Y -lt 1         ] && Y=1;;
		"a") ((X--)); [ $X -lt 1         ] && X=1;;
		"s") ((Y++)); [ $Y -gt $heroendy ] && Y=$heroendy;;
		"d") ((X++)); [ $X -gt $heroendx ] && X=$heroendx;;
	esac

	for (( i=0; i<${#hero[@]}; i++ )); do
		XY ${X} $(($Y + $i)) " ${hero[$i]} "
	done

done

Уже можно играть в двиганье самолетика по экрану. Поиграем немного. Хватит, пора добавить пулялку.

Для этого добавим опрос кнопки «P»(piu), она будет отвечать за выстрел:

while true; do
	HX=$(($X + 9)); HY=$(($Y + 3))
	get_dimensions

	read -t0.0001 -n1 input; case $input in
		"w") ((Y--)); [ $Y -lt 1         ] && Y=1;;
		"a") ((X--)); [ $X -lt 1         ] && X=1;;
		"s") ((Y++)); [ $Y -gt $heroendy ] && Y=$heroendy;;
		"d") ((X++)); [ $X -gt $heroendx ] && X=$heroendx;;
		"p") PIU+=("$HY $HX");;
	esac

	for (( i=0; i<${#hero[@]}; i++ )); do
		XY ${X} $(($Y + $i)) " ${hero[$i]} "
	done

done

Пулек может быть много, тут опять пригодится массив. При нажатии «P» в массив «PIU» добавляется запись "$HY $HX".

Это координаты новой пульки. А чтобы пульки появлялись у самолета из нужного места, а не из хвоста или крыла, эти координаты задаются со смещением от координат самолетика — HX=$(($X + 9)); HY=$(($Y + 3)).

Небольшое лирическое отступление
Массивы вообще очень полезы (О_о правда чтоле!?)

Небольшой пример: задача — взять с нескольких разномастных серверов postgresql дампы и скопировать себе.

Параметры подключения к серверам разные, базы называются по разному, дампы хранятся в разных местах…

Придется хардкодить, массив как нельзя лучше подходит для этого. Создаем массив:

dbases=(
#-------------------------------+---------------+------------+---------+
#           Ssh address         |  Dump folder  |   DB name  |  New db |
#-------------------------------+---------------+------------+---------+
	'-p123 user@192.168.0.1'   '/backup'        'test_db'   'db1'
	'-p321 looser@127.1'       '/tmp'           'main_db'   'db2'
	'someserver'               '/tmp/backup'    'a'         'db3'
); N=${#dbases[*]}; C=4

Значения выстраиваем столбиками, получается удобная табличка. Удобно добавлять/убавлять строки, можно вставлять комменты.

В переменной N подсчитывается общее количество значений, переменная C — количество столбцов (задается вручную).

Затем в цикле for ((i=0; i<${N}; i+=${C})); do используем вот такую конструкцию:

tmp=("${dbases[@]:$i:$C}")
srvadr="${tmp[0]}"
bkpath="${tmp[1]}"
dbname="${tmp[2]}"
dbtest="${tmp[3]}"

Можно использовать read:

read srvadr bkpath dbname dbtest <<< "${dbases[@]:$i:$C}"

Но read лажает с пробелами. Итого:

#!/bin/bash

dbases=(
#-------------------------------+---------------+------------+---------+
#           Ssh address         |  Dump folder  |   DB name  |  New db |
#-------------------------------+---------------+------------+---------+
	'-p123 user@192.168.0.1'   '/backup'        'test_db'   'db1'
	'-p321 looser@127.1'       '/tmp'           'main_db'   'db2'
	'someserver'               '/tmp/backup'    'a'         'db3'
); N=${#dbases[*]}; C=4

for ((i=0; i<${N}; i+=${C})); do tmp=("${dbases[@]:$i:$C}")

    srvadr="${tmp[0]}"
    bkpath="${tmp[1]}"
    dbname="${tmp[2]}"
    dbtest="${tmp[3]}"

    # copy dump
    scp ${srvadr}${bkpath}/${dbname}.gz .

    # test dump
    gunzip -c ${dbname}.gz | psql -v ON_ERROR_STOP=1 ${dbtest} \
		|| { printf "\nDB error!"; continue; }
done


Вуаля. Можно использовать массив для хардкодного парсинга, не прибегая к услугам grep, sed, awk etc:

df=($(df -h)); echo ${df[5]}

И т.д. и т.п., но вернемся к нашим пулькам.

Итак, пулька это запись вида «Y X» в массиве «PIU».
Необходим «спрайт» пульки, я решил сделать пульки не простые, а золотые анимированные.

У пульки будет анимация полета, как будто это мини ракета, получилось так:

shoot=(	" ->"
	"-=>"
	"=->"
	"- >")

И в цвете:

shoot=(	"${RED} -${DEF}${BLD}${GRN}>${DEF}"
	"${BLD}${LRED}-=${DEF}${GRN}>${DEF}"
	"${LRED}=-${DEF}${BLD}${GRN}>${DEF}"
	"${RED}- ${DEF}${GRN}>${DEF}")

В фунукию get_dimensions добавляем ограничения для пульки:

function get_dimensions {
	endx=$( tput cols  )	# кол-во столбцов(X)
	endy=$( tput lines )	# кол-во линий(Y)

heroendx=$(( $endx - 12 ))	# ограничение для самолетика по коорд. X
heroendy=$(( $endy - 7  ))	# ограничение для самолетика по коорд. Y
bullendx=$(( $endx - 4  ))	# ограничение для пульки по коорд. X
}

В основной цикл while помещаем вот такую конструкцию:

#-------------------------------{ Пульки }---------------------------------------
# переключатель спрайтов, почему L? исторически так сложилось
((L++)); [ $L -gt 3  ] && L=0

NP=${#PIU[@]} # вычисляем кол-во пулек

# циклом перебираем пульки
for (( t=0; t<${NP}; t++ )); do

	# преобразуем запись вида "Y X" в отдельные переменные
	# т.к. X должна изменяться(пулька летит)
	PI=(${PIU[$t]}); PY=${PI[0]}; PX=${PI[1]}

	# увеличиваем X координату пульки
	((PX++))

	[ $PX -ge $bullendx ] && {	# если пулька долетела до края экрана:
		XY ${PX} ${PY} "    "	# удаляем пульку(затираем пробелами)
		unset PIU[$t]		# удаляем пульку из массива
		PIU=("${PIU[@]}")	# массив необходимо переопределить
					# т.к. unset не меняет индексы
		((NP--))		# уменьшаем кол-во пулек
		continue		# переходим к след. пульке
	# если нет, записываем новые координаты пульки в массив "PIU"
	} || { PIU[$t]="$PY $PX"; }

	# рисуем пульку, пробел в начале затирает остатки старой пульки
	XY ${PX} ${PY} " ${shoot[$L]}"

done

Пиу, пиу, пиу!

image

Но стрелять пока не в кого. Добавим мишени. Мишенями будут извечные враги человечества, засланцы из космоса — инопланетяне в летающих тарелках.

Тарелки я тоже решил сделать анимированными, как будто тарелка крутится. Спрайт:

alien=(	" ___ "
	"( o ) "
	' `¯´ ')

Эфект кручения достигается анимацией внутренней части летающей тарелки:

small=(	'o  '
	'   '
	'  o'
	' o ')

В цвете:

small=(	${YLW}'o  '${DEF}
	${YLW}'   '${DEF}
	${YLW}'  o'${DEF}
	${YLW}' o '${DEF})

Чтобы не делать кучу фреймов летающей тарелки я решил сделать генератор спрайтов:

function sprites {
	# добавил тут эфекты чтобы в верхней части тарелки появилась "пупочка"
	alien=(	" _${UND}${BLD}_${DEF}_ "
		# каждый раз сюда вставляется следующий элемент массива small
		"(${small[$L]}${DEF}) "
		' `¯´ ')
}

Добавляем функцию sprites в цикл while, тайминг L подошел тут как нельзя кстати.

Добавляем ограничители для тарелок:

enmyendx=$(( $endx - 5  ))
enmyendy=$(( $endy - 7  ))

Количество врагов задается переменными: enumber (текущее количество) и enmax (максимальное количество).

Я сразу подумал о том, что буду добавлять еще какие-нибудь объекты кроме летающих тарелок. Поэтому создал массив «OBJ» и начал добавлять туда записи вида «X Y type».
Тарелки появляются у правого края экрана X=$enmyendx, а координата Y задается случайным образом Y=$(( (RANDOM % $enmyendy) + 3 )).
Добавляем в основной цикл while:

#-----------------------------{ Пришельцы }--------------------------------------
[ $enumber -lt $enmax ] && {
	OBJ+=("$enmyendx $(( (RANDOM % $enmyendy) + 3 )) alien")
	((enumber++))
}

Рисуем чужих:

NO=${#OBJ[@]}	# вычисляем кол-во объектов
er=${#alien[@]}	# вычисляем кол-во элементов спрайта

# циклом перебираем объекты
for (( i=0; i<$NO; i++ )); do

	# преобразуем запись вида "X Y type" в отдельные переменные
	OI=(${OBJ[$i]}); OX=${OI[0]}; OY=${OI[1]}; type=${OI[2]}

	# уменьшаем X координату(пришельцы летят к самолетику)
	((OX--))

	# если пришелец долетел до края экрана:
	[ $OX -lt 1 ] && {
		# удаляем пришельца, затирая пробелами
		# тут нужен цикл т.к. спрайт занимает несколько строк
		for (( k=0; k<$er; k++ )); do
			XY ${OX} $(($OY + $k)) "           " 
		done
		unset OBJ[$i]		# удаляем пришельца из массива
		OBJ=("${OBJ[@]}")	# массив необходимо переопределить
					# т.к. unset не меняет индексы
		((NO--))		# уменьшаем кол-во объектов
		((enumber--))		# уменьшаем текущее кол-во пришельцев
		continue		# переходим к след. объекту
	# если нет, записываем новые координаты в массив "OBJ"
	} || { OBJ[$i]="$OX $OY $type"; }

	# рисуем пришельца, также циклом
	for (( p=0; p<${er}; p++ )); do
		XY ${OX} $(($OY + $p)) "${alien[$p]}"
	done

done

Юху! Эм, пришельцы не умирают…

image

Реализуем проверку коллизий:

					# проверка коллизий
for (( p=0; p<${er}; p++ )); do		# цикл по елементам спрайта
	for (( t=0; t<${NP}; t++ )); do	# вложенный цикл по пулькам

		# преобразуем запись вида "Y X" в отдельные переменные
		# т.к. X должна изменяться(пулька летит)
		PI=(${PIU[$t]}); PY=${PI[0]}; PX=${PI[1]}

		# проверка условия попадания делается при помощи оператора
		# <i>case</i> а не <i>test([])</i> т.к. <i>case</i>
		# работает гораздо быстрей, скорость тут очень важна
		# задаем смещение координат, обозначающее попадание
		case "$(($OY + 1)) $(($OX + $p))" in
			# смещение координат должно совпасть с координатами пули
			"${PIU[$t]}")
			# удаляем пришельца, затирая пробелами
			# тут нужен цикл т.к. спрайт занимает несколько строк
			for (( k=0; k<$er; k++ )); do
				XY ${OX} $(($OY + $k)) "           " 
			done
			unset OBJ[$i]		# удаляем пришельца из массива
			OBJ=("${OBJ[@]}")	# переопределяем массив
			((NO--))		# уменьшаем кол-во объектов
			((enumber--))		# уменьшаем текущее кол-во
						# пришельцев

			# удаляем пульку(затираем пробелами)
			XY ${PX} ${PY} "    "
			unset PIU[$t]		# удаляем пульку из массива
			PIU=("${PIU[@]}")	# переопределяем массив
			((NP--))		# уменьшаем кол-во пулек
			break			# прерываем цикл
			;;
		esac

	done
done

Стрельба по тарелочкам.

image

Пришло время добавить немного смысла, чужие должны сталкиваться с самолетиком и отнимать жизни у игрока.

Для этого необходимо добавить проверку коллизий с самолетиком, но чтобы избежать копипасты желательно создать функции удаления объектов и пулек:

  frags=0	# очки
   life=3	# жизни

function remove_obj () {
	for (( k=0; k<$er; k++ )); do
		XY ${OX} $(($OY + $k)) "           "
	done
	unset OBJ[$1]; OBJ=("${OBJ[@]}"); ((NO--))
}

function remove_piu () {
	XY ${PX} ${PY} "    "
	unset PIU[$1]; PIU=("${PIU[@]}"); ((NP--))
}

Перепишем «пульки» и «пришельцы» с использованием функций и добавим проверку колизий с самолетиком:

#-------------------------------{ Пульки }---------------------------------------
# переключатель спрайтов, почему L? исторически так сложилось
((L++)); [ $L -gt 3  ] && L=0
NP=${#PIU[@]} # вычисляем кол-во пулек

# циклом перебираем пульки
for (( t=0; t<${NP}; t++ )); do

	# преобразуем запись вида "Y X" в отдельные переменные
	# т.к. X должна изменяться(пулька летит)
	PI=(${PIU[$t]}); PY=${PI[0]}; PX=${PI[1]}

	# увеличиваем X координату пульки
	((PX++))

	[ $PX -ge $bullendx ] && {	# если пулька долетела до края экрана:
		remove_piu ${t}		# удаляем пульку функцией, передав ей
					# индекс пульки в массиве "PIU"
		continue		# переходим к след. пульке
	# если нет, записываем новые координаты пульки в массив "PIU"
	} || { PIU[$t]="$PY $PX"; }

	# рисуем пульку, пробел в начале затирает остатки старой пульки
	XY ${PX} ${PY} " ${shoot[$L]}"

done

#------------------------------{ Пришельцы }-------------------------------------
[ $enumber -lt $enmax ] && {
	OBJ+=("$enmyendx $(( (RANDOM % $enmyendy) + 3 )) alien")
	((enumber++))
}

NO=${#OBJ[@]}	# вычисляем кол-во объектов
er=${#alien[@]}	# вычисляем кол-во элементов спрайта

# циклом перебираем объекты
for (( i=0; i<$NO; i++ )); do

	# преобразуем запись вида "X Y type" в отдельные переменные
	OI=(${OBJ[$i]}); OX=${OI[0]}; OY=${OI[1]}; type=${OI[2]}

	# уменьшаем X координату(пришельцы летят к самолетику)
	((OX--))

	[ $OX -lt 1 ] && {	# если пришелец долетел до края экрана:
		remove_obj ${i}	# удаляем пришельца, функцией, передав ей
				# индекс объекта в массиве "OBJ"
		((enumber--))	# уменьшаем текущее кол-во пришельцев
		continue	# переходим к след. объекту
	# если нет, записываем новые координаты в массив "OBJ"
	} || { OBJ[$i]="$OX $OY $type"; }

	# рисуем пришельца, также циклом
	for (( p=0; p<${er}; p++ )); do
		XY ${OX} $(($OY + $p)) "${alien[$p]}"
	done

	# проверка коллизий
	for (( p=0; p<${er}; p++ )); do		# цикл по елементам спрайта
		for (( t=0; t<${NP}; t++ )); do	# вложенный цикл по пулькам

			# преобразуем запись вида "Y X" в отдельные переменные
			# т.к. X должна изменяться(пулька летит)
			PI=(${PIU[$t]}); PY=${PI[0]}; PX=${PI[1]}

			# проверка условия попадания делается при помощи
			# <i>case</i> а не <i>test([])</i> т.к. <i>case</i>
			# работает гораздо быстрей, скорость тут очень важна
			# задаем смещение координат, обозначающее попадание
			case "$(($OY + 1)) $(($OX + $p))" in
				# смещение координат должно совпасть
				# с координатами пульки
				"${PIU[$t]}")
				remove_obj ${i}	# удаляем пришельца, функцией,
						# передав ей индекс объекта
						# в массиве "OBJ"
				((enumber--))	# уменьшаем текущее кол-во
						# пришельцев
				((frags++))	# увеличиваем очки
				remove_piu ${t}	# удаляем пульку функцией,
						# передав ей индекс пульки
						# в массиве "PIU"
				break		# прерываем цикл
				;;

			esac
		done

		# задаем смещение координат, обозначающее
		# столкновение с самолетиком
		case "$(($OY + 1)) $(($OX + $p))" in
			# смещение координат должно совпасть
			# с координатами самолетика
			"$HY $HX")
			remove_obj ${i}	# удаляем пришельца, функцией, передав ей
					# индекс объекта в массиве "OBJ"
			((enumber--))	# уменьшаем текущее кол-во пришельцев
			((life--))	# уменьшаем жизни игрока
			((frags++))	# увеличиваем очки
			break		# прерываем цикл
			;;
		esac

	done
done

Добавляем вывод игровой информации и проверку количества жизней:

#----------------------------{ Игровая информация }------------------------------
XY 0 0 "${BLD}killed aliens: ${DEF}${CYN}${frags}${DEF}  ${BLD}Life: ${DEF}${CYN}${life}${DEF} "

[ $life -le 0 ] && { clear; echo 'Game over man!'; bye; }

Игра закочена, дружище!

image

промежуточный итог
#!/bin/bash

#----------------------------------------------------------------------+
#Color picker, usage: printf ${BLD}${CUR}${RED}${BBLU}"Some text"${DEF}|
#---------------------------+--------------------------------+---------+
#        Text color         |       Background color         |         |
#------------+--------------+--------------+-----------------+         |
#    Base    |Lighter\Darker|    Base      | Lighter\Darker  |         |
#------------+--------------+--------------+-----------------+         |
RED='\e[31m'; LRED='\e[91m'; BRED='\e[41m'; BLRED='\e[101m' #| Red     |
GRN='\e[32m'; LGRN='\e[92m'; BGRN='\e[42m'; BLGRN='\e[102m' #| Green   |
YLW='\e[33m'; LYLW='\e[93m'; BYLW='\e[43m'; BLYLW='\e[103m' #| Yellow  |
BLU='\e[34m'; LBLU='\e[94m'; BBLU='\e[44m'; BLBLU='\e[104m' #| Blue    |
MGN='\e[35m'; LMGN='\e[95m'; BMGN='\e[45m'; BLMGN='\e[105m' #| Magenta |
CYN='\e[36m'; LCYN='\e[96m'; BCYN='\e[46m'; BLCYN='\e[106m' #| Cyan    |
GRY='\e[37m'; DGRY='\e[90m'; BGRY='\e[47m'; BDGRY='\e[100m' #| Gray    |
#------------------------------------------------------------+---------+
# Effects                                                              |
#----------------------------------------------------------------------+
DEF='\e[0m'   # Default color and effects                              |
BLD='\e[1m'   # Bold\brighter                                          |
DIM='\e[2m'   # Dim\darker                                             |
CUR='\e[3m'   # Italic font                                            |
UND='\e[4m'   # Underline                                              |
INV='\e[7m'   # Inverted                                               |
COF='\e[?25l' # Cursor Off                                             |
CON='\e[?25h' # Cursor On                                              |
#----------------------------------------------------------------------+
# Text positioning, usage: XY 10 10 "Some text"                        |
XY   () { printf "\e[${2};${1}H${3}";   } #                            |
#----------------------------------------------------------------------+
# Line, usage: line - 10 | line -= 20 | line "word1 word2 " 20         |
line () { printf %.s"${1}" $(seq ${2}); } #                            |
#----------------------------------------------------------------------+
small=(	${YLW}'o  '${DEF}
	${YLW}'   '${DEF}
	${YLW}'  o'${DEF}
	${YLW}' o '${DEF})

hero=("  "
      "__     "
      "|${RED}★${DEF}〵____ "
      " \_| /${UND}${BLD} °${DEF})${DGRY}-${DEF}"
      "   |/    "
      "     ")

shoot=(	"${RED} -${DEF}${BLD}${GRN}>${DEF}"
	"${BLD}${LRED}-=${DEF}${GRN}>${DEF}"
	"${LRED}=-${DEF}${BLD}${GRN}>${DEF}"
	"${RED}- ${DEF}${GRN}>${DEF}")

 X=1; Y=1	# начальные координаты самолетика
enumber=0	# текущее количество врагов
  enmax=10	# максимальное количество врагов
  frags=0	# очки
   life=3	# жизни
#-----------------------------{ функции }-------------------------------------
function sprites {

	alien=(	" _${UND}${BLD}_${DEF}_ "
		"(${small[$L]}${DEF}) "
		' `¯´ ')
}

function remove_obj () {
	for (( k=0; k<$er; k++ )); do
		XY ${OX} $(($OY + $k)) "           "
	done
	unset OBJ[$1]; OBJ=("${OBJ[@]}"); ((NO--))
}

function remove_piu () {
	XY ${PX} ${PY} "    "
	unset PIU[$1]; PIU=("${PIU[@]}"); ((NP--))
}

function bye () {
	stty echo
	printf "${CON}${DEF}"
	exit
}

function get_dimensions {
	endx=$( tput cols  )
	endy=$( tput lines )

heroendx=$(( $endx - 12 ))
heroendy=$(( $endy - 7  ))
bullendx=$(( $endx - 4  ))
enmyendx=$(( $endx - 5  ))
enmyendy=$(( $endy - 7  ))
}
#-----------------------------------------------------------------------------

trap bye INT
printf "${COF}"
stty -echo
clear

#---------------------------{ основной цикл }---------------------------------
while true; do

	HX=$(($X + 9)); HY=$(($Y + 3))
	get_dimensions; sprites

	read -t0.0001 -n1 input; case $input in
		"w") ((Y--)); [ $Y -lt 1         ] && Y=1;;
		"a") ((X--)); [ $X -lt 1         ] && X=1;;
		"s") ((Y++)); [ $Y -gt $heroendy ] && Y=$heroendy;;
		"d") ((X++)); [ $X -gt $heroendx ] && X=$heroendx;;
		"p") PIU+=("$HY $HX");;
	esac

	for (( i=0; i<${#hero[@]}; i++ )); do
		XY ${X} $(($Y + $i)) " ${hero[$i]} "
	done

	#--------------------------{ Пульки }---------------------------------
	((L++)); [ $L -gt 3  ] && L=0
	NP=${#PIU[@]}

	# перебираем пульки
	for (( t=0; t<${NP}; t++ )); do

		PI=(${PIU[$t]}); PY=${PI[0]}; PX=${PI[1]}; ((PX++))

		[ $PX -ge $bullendx ] && {
			remove_piu ${t}
			continue
		} || { PIU[$t]="$PY $PX"; }

		XY ${PX} ${PY} " ${shoot[$L]}"

	done

	#-------------------------{ Пришельцы }-------------------------------
	[ $enumber -lt $enmax ] && {
		OBJ+=("$enmyendx $(( (RANDOM % $enmyendy) + 3 )) alien")
		((enumber++))
	}

	NO=${#OBJ[@]}	# вычисляем кол-во объектов
	er=${#alien[@]}	# вычисляем кол-во элементов спрайта

	# перебираем объекты
	for (( i=0; i<$NO; i++ )); do

		OI=(${OBJ[$i]}); OX=${OI[0]}; OY=${OI[1]}; ((OX--))

		[ $OX -lt 1 ] && {
			remove_obj ${i}
			((enumber--))
			continue
		} || { OBJ[$i]="$OX $OY $type"; }

		# рисуем пришельца
		for (( p=0; p<${er}; p++ )); do
			XY ${OX} $(($OY + $p)) "${alien[$p]}"
		done

		# проверка коллизий
		for (( p=0; p<${er}; p++ )); do
			for (( t=0; t<${NP}; t++ )); do

				PI=(${PIU[$t]}); PY=${PI[0]}; PX=${PI[1]}

				case "$(($OY + 1)) $(($OX + $p))" in
	
					"${PIU[$t]}")
					remove_obj ${i}
					((enumber--))
					((frags++))
					remove_piu ${t}
					break
					;;

				esac
			done

			# столкновение с самолетиком
			case "$(($OY + 1)) $(($OX + $p))" in

				"$HY $HX")
				remove_obj ${i}
				((enumber--))
				((life--))
				((frags++))
				break
				;;
			esac

		done
	done

	#-----------------------{ Игровая информация }------------------------
	XY 0 0 "${BLD}killed aliens: ${DEF}${CYN}${frags}${DEF} ${BLD}Life: ${DEF}${CYN}${life}${DEF} "

	[ $life -le 0 ] && { clear; echo 'Game over man!'; bye; }

done

Основные моменты, из которых строится движуха, разобраны. Дальнейшее повествование пойдет в стиле «как нарисовать сову».

Я уже упоминал, что хотел добавить еще объекты кроме летающих тарелок. В итоге, я добавил бонусы, выпадающие из убитых врагов.

Жизнь — добавляет жизнь, патроны — добавляет патроны (да, патроны заканчиваются) и усилитель ствола — стрелялка х2 и х3, но и расход патронов соответствующий.

А также добавил элементы фона: деревья, облака, солнышко и взрывы уничтоженных врагов.
Деревья и облака разбиты на 3 «битплана» и появляются рандомно. Пришлось полностью переделать цикл объектов, переместив всю логику в функцию mover:

function mover () {

er=${#sprite[@]}

# двигайся
[ ${1} = 0 ] && {
	((OX--))
	[ $OX -lt 0 ] && OX=0
	OBJ[$i]="$OX $OY $type"
}

# край экрана
[ $OX -lt 1 ] && {
	remove_obj ${i}
	case ${type} in "alien") ((enumber--));; esac
	continue
}

# совместил рисование и проверку коллизий в одном цикле
for (( p=0; p<${er}; p++ )); do

	# рисуем
	XY ${OX} $(($OY + $p)) "${sprite[$p]}"

	# проверяем коллизии
	case ${type} in

		"life"  ) # подобрал жизнь
			case "$(($OY + $p)) $OX" in
				"$HY $HX")
					((life++))
					remove_obj ${i}
					break;;
			esac;;

		"ammo"  ) # подобрал пули
			case "$(($OY + $p)) $OX" in
				"$HY $HX")
					((ammo+=100))
					remove_obj ${i}
					break;;
			esac;;

		"gunup" ) # подобрал усилитель ствола
			case "$(($OY + $p)) $OX" in
				"$HY $HX")
					((G++))
					remove_obj ${i}
					break;;
			esac;;

		"bfire" ) # прилетело от босса
			case "$OY $OX" in
				"$HY $HX")
					((life--))
					remove_obj ${i}
					break;;
			esac;;

		"alien" ) for (( t=0; t<${NP}; t++ )); do

			# в чужого попала пуля
			case "$(($OY + 1)) $(($OX + $p))" in

				"${PIU[$t]}")
				# даст или не даст бонус
				[ $((RANDOM % $rnd)) -eq 0 ] && {
					OBJ+=("$OX $OY \
					${bonuses[$((RANDOM % ${#bonuses[@]}))]}")
				}
				((frags++))
				((enumber--))
				remove_obj ${i}
				remove_piu ${t}
				# добавляем взрыв
				OBJ+=("${OX} ${OY} boom")
				break;;
			esac
		done

		# столкнулся с самолетом
		case "$(($OY + 1)) $(($OX + $p))" in
			"$HY $HX")
				((life--))
				((frags++))
				((enumber--))
				remove_obj ${i}
				OBJ+=("${OX} ${OY} boom")
				break;;
		esac;;
	esac
done
}

А цикл объектов стал выглядеть так:

# двигаем\проверяем\рисуем все объекты, летящие к игроку |------------------
NO=${#OBJ[@]}
for (( i=0; i<$NO; i++ )); do
	OI=(${OBJ[$i]}); OX=${OI[0]}; OY=${OI[1]}; type=${OI[2]}
	case $type in

		# взрывы не летают просто рисуем 1 раз
		"boom"  )
			er=${boomC}
			for part in "${boom[@]:$B:$boomC}"; do
				XY ${OX} ${OY} " ${part}"
				((OY++))
			done
			[ ${E} = 0 ] && {
				((B+=${boomC}))
				[ $B -gt ${boomN} ] && {
					B=0
					remove_obj ${i}
				}
			};;

		# копируем нужный спрайт в массив sprite
		# и выполняем mover передав ему тайминг
		# за счет таймингов работают битпланы фоновых
		# объектов(деревья и облака)
		"alien" )	sprite=("${alien[@]}");  mover 0;;
		"bfire" )	sprite=("${bfire[@]}");  mover 0;;
		"ammo"  )	sprite=("${ammob[@]}");  mover 0;;
		"life"  )	sprite=("${lifep[@]}");  mover 0;;
		"gunup" )	sprite=("${gunup[@]}");  mover 0;;
		"tree1" )	sprite=("${tree1[@]}");  mover ${Q};;
		"tree2" )	sprite=("${tree2[@]}");  mover ${W};;
		"tree3" )	sprite=("${tree3[@]}");  mover ${E};;
		"cloud1")	sprite=("${cloud1[@]}"); mover ${Q};;
		"cloud2")	sprite=("${cloud2[@]}"); mover ${W};;
		"cloud3")	sprite=("${cloud3[@]}"); mover ${E};;
	esac
done

Ну и какая же стрелялка без босса? Как только число убитых пришельцев достигнет 100, появится злой папик. С появлением большой тарелки игрок понимает, откуда берутся эти бесконечные пришельцы, вот отсюда! Из этой большой тарелки, надо ее замочить!

Босс:

# БОСС |-----------------------------------------------------------------
if [ $frags -ge $tillboss ]; then
	# шкала жизни босса
	bar=; hp=$(( $bosshbar * $bhealth / 100 )); hm=$(( $endx - 10 ))
	for (( i=0  ; i<${hp}; i++ )); do bar="▒${bar}"; done
	for (( i=$hp; i<${hm}; i++ )); do bar="${bar} "; done
	XY 1 $(($endy - 1)) " ${BLD}BOSS: |${RED}${bar}${DEF}${BLD}|${DEF}"

	# двигай
	[ $BY -lt $Y ] && {
		((BY++))
	}

	[ $BY -gt $Y ] && {
		((BY--))
	}

	[ $BX -gt $(($endx / 2)) -a "$goback" == "false" ] && {
		((BX--))
	} || goback=true

	[ $BX -lt $bossendx      -a "$goback" == "true"  ] {
		&& ((BX++))
	} || goback=false

	# рисуй
	for (( i=0; i<${#boss[@]}; i++ )); do
		XY ${BX} $(($BY + $i)) " ${boss[$i]} "
	done

	# стреляй
	[ $BY -eq $Y -a $K -eq 0 ] && {
		OBJ+=("$(($BX - 4)) $(($BY + 3)) bfire")
	}

	# мелкие вылетают из босса
	[ $enumber -lt $enmax ] && {
		((enumber++))
		OBJ+=("$(($BX + 2)) $(($BY + 3)) alien")
	}
else
	[ $enumber -lt $enmax ] && {
		((enumber++))
		OBJ+=("$enmyendx $(( (RANDOM % $enmyendy) + 3 )) alien")
	}
fi

image

Вот такая получилась игра. Что хочется сделать еще: попробовать реализовать появление объектов посимвольно.

Тут придется либо отказываться от цвета/эфектов, либо серьезно приседать. Есть мысль добавить в массивы спрайтов дополнительные поля с управляющими кодами для каждого символа.
Возможно, придумаю какую-нибудь сюжетную линию и кто знает, что еще, но это уже совсем другая история.

Продолжение И. BASH'им дальше

Пиу, пиу, пиу!)
Поделиться публикацией
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама
Комментарии 33
  • 0
    Блин, у меня лагает(
    • 0
      Можно отключить фоновые объекты, будет побыстрей.
      • 0

        Не, мне кажется, оптимизировать терминальную игру можно только на калькуляторе. Просто при долгом нажатии на клавишу идут задержки. Это, конечно, проблема терминала, но может это как-то решается?

        • 0

          Понравилось солнышко! Я больше люблю солнышки рисовать. На канвасе правда.

      • +1
        Молоток автор!
      • +2

        У меня ошибка такая:
        Downloads/piu-piu: line 295: continue: only meaningful in a 'for', 'while', or 'until' loop


        bash --version            
        bash --version
        GNU bash, version 4.4.12(1)-release (x86_64-pc-linux-gnu)
        Copyright (C) 2016 Free Software Foundation, Inc.
        License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
        
        This is free software; you are free to change and redistribute it.
        There is NO WARRANTY, to the extent permitted by law.
        setLastCommandState;setGitPrompt
      • +1
        • +1
          ВАУ! Супер.
          • 0
            Шикарно выглядит.
            Я вот тоже немного увлекаюсь ASCII-играми: asciigames.tk Но я на плюсах пишу и для Андроида.
            • 0

              Библиотека виртуального терминала какая-то из уже готовых, или самописанная?

              • 0
                Рендерю литеры с помощью Qt.
            • 0
              Воспользуемся утилитой — read:

              read это не утилита:
              $ type read
              read is a shell builtin
              • 0
                Век живи, век учись. Спасибо, учту.
              • +1
                и запуск в фоне файла с имперским маршем в исполнении флоппи-дисководов:-)
              • +1
                А на MacOS почему-то не работает. Запускается, но на клавиши никак не рагирует(
                • 0
                  Ubuntu 16.04 LTS — тоже не работают стрелки, но стрелять можно. Проверял в yakuake и uxterm.
                  • 0
                    Мака нет, а на убунте проверю. Делал на убунте 14.04.
                    • 0
                      Попробовал на 16.04 сервер, полет нормальный.
                  • 0
                    Чтобы заработало на MacOS нужно установить bash v4:
                    brew update && brew install bash

                    он не перепишет ваш родной bash, а будет доступен по /usr/local/bin/bash.
                    И, соответственно, нужно поменять первую строку скрипта на:
                    #!/usr/local/bin/bash
                  • 0
                    Круто!
                    Но в маковой консоли не перехватываются нажатия клавиш(((

                    P.S. После отправки обновил страничку и увидел что меня опередили)
                    • 0
                      Клёвая игруха. Спасибо.
                      Поймал подряд две «тройные» пушки, и пушки не стало совсем… Самолётик, конечно, не расстроился, у него ведь 30000 жизней. Но теперь он не может убить босса, тарана нет.
                      • 0
                        Поправил баг с пушкой. Добавил возможность таранить босса. 30000 жизней? Читер!)
                      • 0
                        Для консоли тут когда-то показывали вот такой вот 3D-шутер: ссылка

                        image
                        • 0
                          На маке у функции read не получается установить -t 0.001
                          • 0
                            «Сер, вы знаете толк в извращениях» ©
                            Это комплимент.

                            Что за шрифт с красной звездой? Галочки-смайлики получить могу, звезду нет(красную).
                            • 0
                              Загуглил таблицу ascii символов и скопировал звезду.
                            • 0
                              Это прекрасно. Сам пару игрушек на баше написал, но сильно проще. Игру 2048 в 1107 байт (старался уместить в минимальное количество байт) и пятнашки.
                              • 0
                                FPS слабенький))))
                                А если серьёзно, то Браво автору!
                                • +1
                                  Хабра та! :)

                                  Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.