Bomber (Curses-RU)

Простые ASCII-графические программы, созданный с помощью библиотеки _ncurses_

Тема лабораторной

До этого момента мы работали только со стандартным вводом\выводом, который не предоставлял возможности делать достаточно интерактивный и вразумительный вывод. На данном домашнем занятии вы научитесь работать с библиотекой ncurses, с помощью которой вы сможете создавать интерактивные приложения даже в среде текстовой консоли. Вы напишете простую игру Bomber.

Принципом игры Bomber является уничтожить ("разбомбить") город. Самолет летит над городом, снижаясь с каждым заходом вниз. Бомбардировщик может в одно время спустить только одну бомбу, которая в лучшем случае снесёт 4 этажа здания. Самолет может и стрелять перед собой, но максимально только на 5 позиций перед собой.

Благодаря данному задание вы будете готовы к выполнению задания Problem Set 8.

Objectives

  • Ознакомиться с ASCII-графической библиотекой ncurses.
  • Написать игру Bomber.
  • Подготовиться к выполнению Problem Set-а 8.

Postup

Step 1: Curses Initialization

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

Task 1.1

Сначала настроите среду для работы с библиотекой ncurses.

В файле ~/.bashrc измените следующий рядок:

export LDLIBS="-lm"

следующим образом:

export LDLIBS="-lm -lcurses"

Данное изменение обеспечит ссылание на библиотеку ncurses при сборке вашей программы.

Warning

Для того, чтобы изменения вступили в силу, нужно перезайти в систему или ввести команду:
exec bash
Если изменения не проявились, вам следует проверить, не работаете ли вы на другой дистрибуции Linux, а точнее, не использует ваша система файл ~/.profile.

В случае, если вы работаете в другой среде разработки с OS Linux, установите себе сначала саму библиотеку ncurses (в случае, если она не установлена).

Для дистрибутивов, основанных на Debian:

sudo apt-get install libncurses5-dev libncurses5 ncurses-doc

Для дистрибутивов, основанных на RedHat-e:

sudo dnf install ncurses-devel

Затем сделайте соответствующие изменения в файле ~/.bashrc (см. выше).

Warning

Если изменения не проявились, проверьте, не использует ли ваша система файла ~/.profile вместо ~/.bashrc.

Если вы до сих пор используете систему Windows, найдите решение сами. При в поиске вам может помочь данная страница.

Task 1.2

Напишите программу bomber, в функции main() инициализируйте начало работы с библиотекой ncurses и перед завершением работы функции закончите работу с библиотекой.

Используйте вызовы следующих функций:

  • WINDOW *initscr(void)
    • инициализация работы библиотеки в программе
    • настраивает значения окна терминала, а также аллоцирует место основных окон (curscr, stdscr)
    • должна быть вызвана первой из функций библиотеки
  • int cbreak(void)
    • запрещает буферизацию
    • выключает возможность экстренного завершения программы (через Ctrl-C). Так, знаки становятся активными
  • int noecho(void)
    • обеспечивают, что знаки, считанные с помощью getch(), не будут выводиться на экран
  • int keypad(WINDOW *win, bool bf)
    • если bf TRUE, тогда пользователь может нажимать на работающие клавиши (например стрелочки) и функция wgetch() возвращает одно значение, представляющее клавишу, напр. KEY_LEFT
  • int curs_set(int visibility)
    • устанавливает видимость курсора (0 - невидимый, 1 - нормальный и 2 - "резковидимый"
  • int nodelay(WINDOW *win, bool bf)
    • способствует тому, что функция getch() будет вызываться без блокирования (сразу)
    • если нет ничего на входе, getch() возвращает ERR
    • если данный функционал заблокирован (bf FALSE), getch() будет ожидать, пока не будет нажата клавиша
  • int endwin(void)
    • заканчивает работу в режиме библиотеки ncurses и восстанавливает первоначальные настройки терминала

Comment

Для работы с библиотекой ncurses не забудьте подключить заголовочный файл curses.h!
#define _POSIX_C_SOURCE 200201L
#include <stdio.h>
#include <stdlib.h>
#include <curses.h>
#include <time.h>

int main(){
    srand(time(NULL));

    // initialize the library
    initscr();
    // set implicit modes
    cbreak();
    noecho();
    keypad(stdscr,TRUE);
    // invisible cursor, visibility of cursor (0,1,2)
    curs_set(FALSE);
    // getch() will be non-blocking
    nodelay(stdscr, TRUE);

    // your code goes here
    getchar();

    // end curses
    endwin();
}

Task 1.3

Скомпилируйте программу и проверьте её правильность.

Если во время компиляции возникли ошибки, исправьте их. Если после завершения работы программы вы получили терминал без курсора, это означает, что вы забыли закончить работу в библиотеке ncurses. В таком случае, написанный вслепую reset будет достаточным для того, чтобы обнулит конфигурацию терминала.

Step 2: The City

Бомбардировщик будет пролетать над городом, который мы создадим на этом этапе.

Создайте 1D массив world, наполненный целыми числами, которые будет представлять собой постройки, инициализируйте его.

Для того, чтобы вы смогли динамически реагировать на размер экрана и генерировать достаточное количество построек соответствующего размера, следует использовать специальные переменные COLS и LINES, которые предоставляет сама библиотека ncurses. Значение каждой из них следующее:

  • LINES - высота окна терминала
  • COLS - ширина окна терминала

При декларации массива в таком случае создайте массив столь большой, чтобы он не занимал весь экран, при этом с начала и с конца будет недоставать ровно 5 высотных значений.

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

int world[COLS - 2*5];
for(int i = 0; i < COLS - 2*5; i++){
    world[i] = rand() % LINES/2 + 1;
}

Task 2.1

Выведите город на экран.

При работе с библиотекой ncurses также правдиво, что позиция [0,0] указывает на верхний левый угол экрана.

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

  • move(int y, int x) - переместит курсор на соответствующую позицию
  • printw() - выводит рядок на настоящую позицию курсора (использование данной функции совпадает с использованием вами уже известной printf())
  • mvprintw(int y, int x, const char *fmt, ...) - комбинация функций move() и printw(), переместит курсор на конкретную позицию и выпишет рядок
  • int refresh(void) - функция должна быть вызвана, иначе изменения не будут изображены в терминале

Comment

Для прочтения документации к каждой из функций вы можете использовать manual pages. Например, если вы хотите прочесть документацию к функции printw(), выполните команду:
man printw

Comment

При выводе можете использовать и замедление, в таком случае при выводе одного этажа одного строения выполнение вашей программы на миг остановите. Таким образом, возникнет эффект, при котором город будет вырисовываться постепенно. Для искусственного замедления можно использовать функцию nanosleep() из библиотеки time.h, использование которой демонстрирует следующий фрагмент кода:
// time delay 0.001s for drawing city
struct timespec ts = {
    .tv_sec = 0,                    // nr of secs
    .tv_nsec = 0.001 * 1000000000L  // nr of nanosecs
};

nanosleep(&ts, NULL);
// time delay 0.001s for drawing city
struct timespec ts = {
    .tv_sec = 0,                    // nr of secs
    .tv_nsec = 0.001 * 1000000000L  // nr of nanosecs
};

// draw world
for(int x = 0; x < COLS - 10; x++){
    for(int y = 0; y < world[x]; y++){
        mvprintw(LINES-y-1, x + 5, "#");
        refresh();
        nanosleep(&ts, NULL); // provides simple effect
    }
}

Task 2.2

Проверьте правильность вашей реализации.

Если вы сделали всё верно, на экран терминала будут выведены здания, подобно следующему рисунку:

+-------------------------------------------------+
|                                                 |
|                                                 |
|                                                 |
|                                                 |
|                                                 |
|                                                 |
|                                                 |
|         #         #     #         #   #         |
|      #  # #       ##  ###         #   #  #      |
|      ## # # #     ##  ### ##      #   ## #      |
|      ## # # # # # ##  #########   #   ## #      |
|     ####### ########  #############   #####     |
|     #######################################     |
+-------------------------------------------------+

Step 3: The Plane

Главным героем сегодняшнего сценария является бомбардировщик, который всё время летит над городом, снижаясь с каждым поворотом. На данном этапе мы будем заниматься реализацией полёта.

Task 3.1

Реализуйте полёт самолёта по всей ширине экрана.

Самолет может быть представлен как строка вида: " @=>-" или: " ^==-".

Один успешный полёт над городом изображен на следующей картинке:

+-------------------------------------------------+
|                                             ^==-|
|                                                 |
|                                                 |
|                                                 |
|                                                 |
|                                                 |
|                                                 |
|         #         #     #         #   #         |
|      #  # #       ##  ###         #   #  #      |
|      ## # # #     ##  ### ##      #   ## #      |
|      ## # # # # # ##  #########   #   ## #      |
|     ####### ########  #############   #####     |
|     #######################################     |
+-------------------------------------------------+
// time delay 0.1s for plane
ts.tv_nsec = 0.1 * 1000000000L;

for(int x = 0; x < COLS; x++){
    mvprintw(0, x, " ^==-");
    refresh();
    nanosleep(&ts, NULL);
}

Task 3.2

Измените свой код так, чтобы самолет пролетел с левого верхнего угла до правого нижнего (остановится).

Не забудьте о том, что верхний левый край мира имеет координаты [0,0].

После посадки должен самолёт остановиться в углу экрана, таким образом, самолет не должен покинуть его (уехать в ангар например). Решением данной задачи можно считать ситуацию, когда самолет останется стоять в правом нижнем углу подобно тому, как это показано на следующей картинке:

+-------------------------------------------------+
|                                                 |
|                                                 |
|                                                 |
|                                                 |
|                                                 |
|                                                 |
|                                                 |
|                                                 |
|                                                 |
|                                                 |
|                                                 |
|                                                 |
|                                             ^==-|
+-------------------------------------------------+
// time delay 0.1s for plane
ts.tv_nsec = 0.1 * 1000000000L;

// game loop
for(int y = 0; y < LINES; y++){
    for(int x = 0; x < (y==LINES-1 ? COLS-4 : COLS); x++){
        mvprintw(y, x, " ^==-");
        refresh();
        nanosleep(&ts, NULL);
    }
}

Task 3.3

Сообщите игроку об успешной посадке надписью Well done!, помещённой в центр экрана.

Сообщение может выглядеть следующим образом:

+-------------------------------------------------+
|                                                 |
|                                                 |
|                                                 |
|                                                 |
|                                                 |
|                                                 |
|                   Well Done!                    |
|                                                 |
|                                                 |
|                                                 |
|                                                 |
|                                                 |
|                                             ^==-|
+-------------------------------------------------+
// well done
mvprintw(LINES/2, COLS/2 - 5, "Well done!");
refresh();

Task 3.4

Скомпилируйте программу и проверьте его правильность.

Если вы сделали всё правильно, бомбардировщик пролетит через весь экран, приземлится и на экран выведется сообщение Well Done!:

Task 3.5

Измените настоящую реализацию так, что в случае, если самолет столкнётся со строением, на экран будет выведено сообщение Game over, при этом игра завершится.

Старайтесь определить координаты кабины пилота и возможность строения на одной высоте, на одной позиции.

Данный фрагменты кода поместите в игровой цикл:

// check collision with building
if (x > 0 && x < COLS-5 && y == LINES-world[x-1]) {
    mvprintw(LINES/2, COLS/2 - 5, "Game over!");
    mvprintw(y, x, " ****");
    refresh();
    getchar();
    endwin();
    return EXIT_SUCCESS;
}

Step 4: Dropping the Bomb

На данном этапе мы займёмся программированием управлением с клавиатуры.

Task 4.1

Игра должна завершить свою работу, если была нажата клавиша Q.

Считывание с клавиатуры в режиме библиотеки ncurses обеспечите вызовом функции getch(). Функция возвращает код считанной клавиши как значение типа int без того, что игра будет приостановлена.

Поскольку клавиша Q не будет единственной, которую будем для управления в игре использовать, используйте конструкцию switch.

О списке работающих клавиш и их репрезентации в языке Си, ровно как и про возможность настройки считывания клавиш можно прочитать в мануальной странице к функции .

Comment

Если у вас не установлена документация локально для библиотеки ncurses, её можно установить, выполнив команду:
sudo apt-get install ncurses-doc

Поместите данный фрагмент в код программы:

// handle user input
int input = getch();
switch(input){
    case 'q': case 'Q':{
        mvprintw(LINES/2, COLS/2 - 5, "Quit Game");
        refresh();
        getchar();
        endwin();
        return EXIT_SUCCESS;
        break;
    }
}

Task 4.2

Сделайте так, чтобы при нажатии стрелочки вниз, самолёт выпустил бомбу.

При полёте самой бомбы руководствуйтесь следующим:

  • Бомба падает с той же скоростью, с которой летит бомбардировщик.
  • Бомбардировщик не может выпустить следующую бомбу, пока прошлая не достигнет цели.
  • В случае, что под бомбой не будет строения, бомба должна лететь до тех пор, пока не пропадёт с экрана.
  • В случае, что бомба попадает на строение, она должна разрушить максимум 4 этажа (по одному за каждую итерацию игрового цикла)

Код стрелочки вниз в библиотеке ncurses определён как KEY_DOWN.

Перед игровым циклом сначала инициализируйте переменные, касающиеся бомбы:

// bomb initialization
bool is_bomb_dropped = false;
int bomb_x;
int bomb_y;
int bomb_counter;

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

case KEY_DOWN:{
    if(is_bomb_dropped == false){
        is_bomb_dropped = true;
        bomb_x = x + 1;
        bomb_y = y;
        bomb_counter = 4;
    }
    break;
}

Наконец, измените игровой цикл та, чтобы в случае запуска бомбы она постепенно падала и уничтожала всё на своём пути (согласно правилам):

// update bomb
if(is_bomb_dropped == true){
    mvprintw(bomb_y, bomb_x, " ");
    bomb_y++;
    if(bomb_y == LINES || !bomb_counter){
        is_bomb_dropped = false;
    }
    else if(bomb_y == LINES - world[bomb_x-5]){
        world[bomb_x-5]--;
        mvprintw(bomb_y, bomb_x, "*");
        bomb_counter--;
    }
    else{
        mvprintw(bomb_y, bomb_x, "o");
    }
}

Task 4.3

Скомпилируйте программу и проверьте его правильность.

Если вы сделали всё верно, в результате получается функциональная игра, которую даже можно выиграть.

Step 5: Fire in the Hole!

На данном этапе реализуйте возможность стрельбы.

Task 5.1

В случае нажатия пробела (spacebar) должен быть открыт огонь с пулемёта впереди самолёта.

Игра приостановится. Пули будут долетать максимально на пять позиций перед самолётом в направлении его движения. Стрельба будет разносить все строения на расстоянии 5 знаков от бомбардировщика.

Step 6: Game Info

Добавьте очки (score) и количество пуль для бортового пулемёта.

Task 6.1

Создайте счётчик очков и пуль для пулемёта, актуальная количество которых будет выписываться в строке состояния игры.

Task 6.2

За каждый этаж, разрушенный бомбой добавляйте по 5 очков.

Task 6.3

За каждый этаж, разрушенный из пулемёта добавляется по 15 очков.

Task 6.4

Скомпилируйте программу и проверьте её правильность.

Step 7: Colors

На данном этапе вы инициализируете работу с цветами и на некоторых местах их используете.

Task 7.1

Добавьте в игру использование разных цветов.

Перед тем, как использовать разные цвета, их нужно инициализировать и настроить. Обычно настраиваются пары цветов (передний и задний планы). Каждая цветовая пара имеет свой номер, который вскоре будет использован в программе. Номер 0 не используется, начинайте с номером 1.

Инициализацию и настраивание цвета лучше поместить после инициализации библиотеки ncurses в главной функции main(), напр.:

int main(){
    // initialize the library
    initscr();
    // initialize colors
    start_color();
    // color sets
    init_pair(1, COLOR_YELLOW, COLOR_BLACK);
    init_pair(2, COLOR_GREEN, COLOR_BLACK);
    init_pair(3, COLOR_CYAN, COLOR_BLACK);

    // ...

    // end curses
    endwin();

    return 0;
}

После инициализации и настройки цвета его можно наконец использовать. Обычно сначала включается использование цветной пары и потом выключается (attron(), attroff()).

Использование цветов может выглядеть следующим образом:

attron(COLOR_PAIR(1));
printw("X");
attroff(COLOR_PAIR(1));

attron(COLOR_PAIR(2));
printw("O");
attroff(COLOR_PAIR(2));

refresh();

Step 8: Challenges

На данном этапе ознакомьтесь с другими вызовами библиотеки ncurses (упомянутыми в задании).

Task 8.1

Освойте иные возможности использования библиотеки ncurses.

Скачайте код программы curses-example.c и выучите использование другого функционала:

  • void roll_text() - Постепенная прокрутка снизу экрана (течение времени).
  • void draw_logo() - Постепенно выводится предопределённое logo (течение времени).
  • void screensaver() - Текст гуляет по экрану, отбиваясь от стен.
  • void moving_arrow() - "Игрок" двигается по экрану в зависимости о того, какие кнопки жмёт пользователь. Движение осуществляется с помощью клавиш стрелочек.

Step 9: Problem Set 8: Curses

С данного момента можете себя считать готовыми к Problem Set 8.

Additional Tasks

Task A.1

Добавьте в игру возможность приостановки (Pause), в случае, если пользователь нажмёт p или P.

Task A.2

Добавьте в игру возможность изображения справки с помощью клавиш h или H.

Task A.3

Добавьте в игру вводную страницу с меню.

Task A.4

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

Task A.5

После окончания игры выведите на экран контакты (credits). Эффект изображения можете выбрать сами (например в виде титров, как в фильме).

Additional Resources

  1. Public Domain Curses (for Windows users)
  2. Curses Example
  3. Writing Programs with NCURSES
  4. Introduction to the Unix Curses Library