На хабре уже много статей об играх на bash, это Шахматы, Xonix, Sokoban, Морской бой и даже Шутер с псевдо-3D графикой. Но во всех этих играх управление происходит с помощью клавиатуры. Мы же пойдём дальше и напишем игру на bash, управление в которой будет осуществляться с помощью мыши. А заодно разберём как сделать игру устойчивой к изменению размера терминала. Итак, напишем игру Quadronix.
Игру Quadronix я в первый раз у видел у брата на телефоне несколько лет назад, и мне сразу понравилось в неё играть. Но так как брат свой телефон мне почти не давал, то я, не долго думая, реализовал клон игры в виде Java-апплета, тогда как раз изучал Java.
А сейчас, видя, что на bash уже делают эмуляторы процессоров и 3D-игры, понял, что моя реализация Xonix на bash уже прошлый век, и нужно двигаться дальше. И подумал, что реализовать Quadronix на bash будет неплохой разминкой для мозгов.
Правила игры просты. Нужно находить прямоугольники, все четыре вершины которых одного цвета. Для того чтобы удалить такой прямоугольник, нужно нажать на диагонально противоположные вершины. При этом количество очков, прибавляемое за прямоугольник, пропорционально его площади. А на месте уничтоженного прямоугольника появятся новые квадратики случайного цвета. Игра ведётся на время, и чтоб играть было интереснее, то чем больше очков, тем быстрее время убывает. Также если игрок ошибается с выбором прямоугольника, то из остатка времени вычитается штраф. Наконец, чтоб отменить выбор неправильно отмеченной вершины, по ней нужно кликнуть ещё раз.
Понятно, что без мыши в такую игру играть не интересно. Что ж реализуем мышь. Читаем
а управляющая последовательность
При включенном режиме отслеживания при изменении состояния мыши во входной поток консоли будут писаться управляющие последовательности, описывающие состояние мыши. Они имеют формат
Но не самой простой задачей на bash будет получение кода символа. Самой простой, на мой взгляд, командой будет следующая, полученная путём экспериментов:
Здесь следует обратить внимание на одинарную непарную кавычку перед знаком доллара. Это просто означает, что будет выводиться не следующий символ, а его код. А
Итак, напишем следующий скрипт.
Не сказать, что это совсем правильный способ, но это работает:
Если воспользоваться управляющей последовательностью
Теперь рассмотрим, как с помощью сигналов скрипт сделать менее хрупким от действий пользователя.
Первое, что хочется сделать, это вызов команды
Для того, чтобы при получении какого-либо сигнала выполнить некоторый код, используется команда
Возможные значения параметра
Второй случай, когда нам нужны обработчики сигналов — изменение размера окна. Если например распахнуть окно, или наоборот свернуть, вся наша красивая графика поползёт, а не очень хочется. Чтобы узнать, что размер окна терминала изменился можно воспользоваться сигналом
Отмечу, что если в качестве
Ну что ж, теперь ссылка на код скрипта: quadronix.sh.
И напоследок скажу, что
Игру 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.