Pull to refresh

Учим bash-скрипты, пишем Quadronix

Reading time4 min
Views11K
На хабре уже много статей об играх на bash, это Шахматы, Xonix, Sokoban, Морской бой и даже Шутер с псевдо-3D графикой. Но во всех этих играх управление происходит с помощью клавиатуры. Мы же пойдём дальше и напишем игру на bash, управление в которой будет осуществляться с помощью мыши. А заодно разберём как сделать игру устойчивой к изменению размера терминала. Итак, напишем игру Quadronix.





Игру Quadronix я в первый раз у видел у брата на телефоне несколько лет назад, и мне сразу понравилось в неё играть. Но так как брат свой телефон мне почти не давал, то я, не долго думая, реализовал клон игры в виде Java-апплета, тогда как раз изучал Java.

А сейчас, видя, что на bash уже делают эмуляторы процессоров и 3D-игры, понял, что моя реализация Xonix на bash уже прошлый век, и нужно двигаться дальше. И подумал, что реализовать Quadronix на bash будет неплохой разминкой для мозгов.

Правила игры просты. Нужно находить прямоугольники, все четыре вершины которых одного цвета. Для того чтобы удалить такой прямоугольник, нужно нажать на диагонально противоположные вершины. При этом количество очков, прибавляемое за прямоугольник, пропорционально его площади. А на месте уничтоженного прямоугольника появятся новые квадратики случайного цвета. Игра ведётся на время, и чтоб играть было интереснее, то чем больше очков, тем быстрее время убывает. Также если игрок ошибается с выбором прямоугольника, то из остатка времени вычитается штраф. Наконец, чтоб отменить выбор неправильно отмеченной вершины, по ней нужно кликнуть ещё раз.

Понятно, что без мыши в такую игру играть не интересно. Что ж реализуем мышь. Читаем man console_codes и там находим, что управляющая последовательность ESC [ ? 9 h включает режим отслеживания мыши,
а управляющая последовательность ESC [ ? 9 l выключает этот режим.
При включенном режиме отслеживания при изменении состояния мыши во входной поток консоли будут писаться управляющие последовательности, описывающие состояние мыши. Они имеют формат ESC [ M b x y, где в b будет информация о нажатой кнопке и клавишах-модификаторах, а в x и y — информация о координатах мыши. Символ b нам не интересен. А чтоб получить координаты мыши, нужно и из x, и из y вычесть 32.

Но не самой простой задачей на bash будет получение кода символа. Самой простой, на мой взгляд, командой будет следующая, полученная путём экспериментов:
LC_ALL=C printf -v code '%d' "'$data"

Здесь следует обратить внимание на одинарную непарную кавычку перед знаком доллара. Это просто означает, что будет выводиться не следующий символ, а его код. А LC_ALL=C нужно, чтобы символ с кодом большим 127 интерпретировался сам по себе, а не как часть многобайтового символа.

Итак, напишем следующий скрипт.
#!/bin/bash

declare -i mouseX
declare -i mouseY
declare -i mouseButton
declare -r ESC_CODE=$'\e'
declare -r EXIT_CODE='x'
printMouseInfo() {
	echo button=$mouseButton column=$mouseX row=$mouseY
}

readMouse() {
	local mouseButtonData
	local mouseXData
	local mouseYData
	read -r -s -n 1 -t 1 mouseButtonData
	read -r -s -n 1 -t 1 mouseXData
	read -r -s -n 1 -t 1 mouseYData
	local -i mouseButtonCode
	local -i mouseXCode
	local -i mouseYCode
	LC_ALL=C printf -v mouseButtonCode '%d' "'$mouseButtonData"
	LC_ALL=C printf -v mouseXCode '%d' "'$mouseXData"
	LC_ALL=C printf -v mouseYCode '%d' "'$mouseYData"
    	((mouseButton = mouseButtonCode))
	((mouseX = mouseXCode - 32))
	((mouseY = mouseYCode - 32))
}

declare key
echo -ne "\e[?9h"
while true; do
	key=""
	read -r -s -t 1 -n 1 key
	case "$key" in
		$EXIT_CODE) 
			break;;
		$ESC_CODE) 
			read -r -s -t 1 -n 1 key
			if [[ "$key" == '[' ]]; then
				read -r -s -t 1 -n 1 key
				if [[ "$key" == "M" ]]; then
			 		readMouse
					printMouseInfo
				fi
			fi;;
	esac
done
echo -ne "\e[?9l"


Не сказать, что это совсем правильный способ, но это работает:
$ ./mouse.sh
button=0 column=46 row=17
button=0 column=61 row=19
button=0 column=64 row=15
button=0 column=59 row=11
button=0 column=43 row=9
button=0 column=36 row=10
button=0 column=42 row=17
button=0 column=63 row=23
button=0 column=75 row=22
button=0 column=91 row=19
$

Если воспользоваться управляющей последовательностью ESC [ ? 1000 h, то можно получать информацию о нажатии и об отпускании кнопок мыши.

Теперь рассмотрим, как с помощью сигналов скрипт сделать менее хрупким от действий пользователя.

Первое, что хочется сделать, это вызов команды reset, если во время работы скрипта нажать Ctrl+C. Это очень удобно при разработке, так как мы используем в игре цвета и подавляем ввод пользователя, и если скрипт прервать, то пока reset наугад не наберёшь, терминал будет в совершенно не приспособленном для работы состоянии.

Для того, чтобы при получении какого-либо сигнала выполнить некоторый код, используется команда trap:
trap КОМАНДА СИГНАЛ

Возможные значения параметра СИГНАЛ можно узнать, набрав trap -l. Если параметра КОМАНДА нет, то будет установлено действие по умолчанию. Если в качестве параметра СИГНАЛ указать EXIT, то заданная команда будет выполнена при завершении работы скрипта, а это то, что нам надо. Пишем:

function initApplication() {
        stty -echo
        echo -ne $HIDE_CURSOR_CODE
        trap finishApplication EXIT
        ...
}

function finishApplication() {
        trap EXIT
        reset
}

initApplication
runApplication
finishApplication


Второй случай, когда нам нужны обработчики сигналов — изменение размера окна. Если например распахнуть окно, или наоборот свернуть, вся наша красивая графика поползёт, а не очень хочется. Чтобы узнать, что размер окна терминала изменился можно воспользоваться сигналом SIGWINCH:
function repaint() {
        LINES=`tput lines`
        COLUMNS=`tput cols`
        mapXPosition=$(((COLUMNS - CELL_WIDTH * MAP_WIDTH) / 2 + 1))
        mapYPosition=$(((LINES - CELL_HEIGHT * MAP_HEIGHT) / 2 + 1))
        timerXPosition=$((MAP_WIDTH * CELL_WIDTH + mapXPosition + 6))
        timerYPosition=$((mapYPosition))
        echo -ne "\e[0m"
        clear
        drawMap
        drawHeader
        drawFooter
        ((isInvalidated = 0))
}

function initApplication() {
         ...
         trap "((isInvalidated = 1))" SIGWINCH
}

function runGame() {
        local key
        ...
        while true; do
		if ((isInvalidated)); then
			repaint
		fi
		...
		key=""
		...
		case "$key" in
			$NEW_GAME_CODE)
				continue 2;;
			$EXIT_CODE) 
				break 2;;
			$ESC_CODE) 		
				...
		esac		
		...
        done
}


Отмечу, что если в качестве СИГНАЛ указать DEBUG, то заданная команда будет выполняться после каждой команды скрипта, иногда бывает полезно при отладке.

Ну что ж, теперь ссылка на код скрипта: quadronix.sh.

И напоследок скажу, что man bash, man console_codes и ABS можно читать бесконечно, и каждый раз открывать для себя всё новые и новые грани программирования на bash.
Tags:
Hubs:
+80
Comments8

Articles

Change theme settings