9. týždeň

Bomber (Curses)

Jednoduché ASCII-grafické programy vytvárané pomocou knižnice ncurses

O čom je lab

Doteraz sme v pracovali len so štandardným vstupom a výstupom, ktorý neumožňoval robiť dostatočne interaktívne a pútavé výstupy. Na tomto domácom cvičení sa naučíte pracovať s knižnicou ncurses, ktorá Vám umožní vytvárať interaktívne aplikácie aj v prostredí textovej konzoly. Naprogramujete jednoduchú hru Bomber (bombardér).

Princípom hry Bomber je zničiť mesto jeho postupným bombardovaním. Lietadlo postupne prelieta ponad mesto a s každým novým okruhom letí nižšie. Lietadlo môže pustiť naraz len jednu bombu, ktorá po dopade zničí najviac 4 poschodia budovy. Lietadlo má možnosť aj vystreliť, ale najviac 5 pozícií pred seba.

Cvičenie by Vás malo pripraviť na zadanie Problem Set 6.

Ciele

  1. Zoznámiť sa s ASCII-grafickou knižnicou ncurses .
  2. Naprogramovať hru Bomber .
  3. Pripraviť sa na vypracovanie Problem Set-u 6 .

Postup

Krok 1: Curses Initialization

Programy pracujúce s knižnicou ncurses pracujú v špeciálnom režime. Ak s knižnicou teda chcete pracovať, musíte ju vo svojom programe najprv inicializovať. A predtým, ako sa Váš program skončí, musíte režim práce s touto knižnicou ukončiť. Rovnako je potrebné upraviť zoznam knižníc pre preklad.

Úloha 1.1

Najskôr si nastavte prostredie pre prácu s knižnicou ncurses.

V súbore ~/.bashrc upravte tento riadok:

export LDLIBS="-lm"

nasledovne:

export LDLIBS="-lm -lcurses"

Táto zmena zabezpečí prilinkovanie knižnice ncurses pri preklade Vášho programu.

Upozornenie

Aby sa Vaše nastavenia prejavili, bude potrebné sa najskôr odhlásiť a potom opäť prihlásiť. Alebo zadajte príkaz:

exec bash

Ak sa zmeny neprejavili, pravdepodobne pracujete na inej, vlastnej distribúcii Linuxu. V tom prípade overte, či Váš systém nepoužíva súbor ~/.profile.

V prípade, že pracujete v inom prostredí s OS Linux, nainštalujte si najskôr knižnicu ncurses (pokiaľ ju ešte nemáte nainštalovanú).

Distribúcie založené na Debiane:

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

Distribúcie založené na RedHat-e:

sudo dnf install ncurses-devel

Následne uskutočnite zmeny v súbore ~/.bashrc (viď vyššie).

Upozornenie

Ak sa zmeny nastavení neprejavili, overte, či Váš systém nepoužíva súbor ~/.profile namiesto ~/.bashrc.

V prípade, že pracujete priamo na OS Windows, musíte si poradiť sami. Pomôcť Vám môže táto stránka.

Úloha 1.2

Vytvote program bomber, vo funkcii main() inicializujte prácu s knižnicou ncurses a pred koncom funkcie ju zasa ukončite.

Použite volanie nasledujúcich funkcií:

  • WINDOW *initscr(void)
    • inicializuje program pre použitie knižnice
    • nastavuje hodnoty terminálu a alokuje miesto pre základné okná - obrazovky (curscr, stdscr)
    • musí byť volaná ako prvá z funkcií knižnice
  • int cbreak(void)
    • zabraňuje bufrovaniu
    • znefunkčňuje mazanie/zrušenie procesu (cez Ctrl-C), takže znaky sú okamžite k dispozícii programu
  • int noecho(void)
    • zabezpečí, aby sa znaky po stlačení a načítaní funkciou getch() nevypisovali na obrazovku
  • int keypad(WINDOW *win, bool bf)
    • ak je bf TRUE, potom používateľ smie stlačiť funkčné klávesy (napr. šípky) a funkcia wgetch() vráti jednu hodnotu reprezentujúcu klávesu, napr. KEY_LEFT
  • int curs_set(int visibility)
    • nastavuje viditeľnosť kurzora na základe hodnoty parametra (0 - neviditeľný, 1 - normálny a 2 - veľmi viditeľný
  • int nodelay(WINDOW *win, bool bf)
    • spôsobí, že funkcia getch() bude volaná bez blokovania (hneď)
    • ak nie je k dispozícii žiaden vstup, getch() vráti ERR
    • ak je táto funkcionalita blokovaná (bf je FALSE), getch() čaká, kým nie je stlačená klávesa
  • int endwin(void)
    • ukončuje prácu v režime knižnice ncurses a obnovuje pôvodné nastavenia terminálu

Poznámka

Pre prácu s knižnicou ncurses nezabudnite pripojiť hlavičkový súbor 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();
    return EXIT_SUCCESS;
}

Úloha 1.3

Preložte program a overte jeho funkčnosť.

Ak sa pri preklade objavili chyby, opravte ich. Ak ste po skončení činnosti programu dostali naspäť terminál bez kurzoru, zabudli ste ukončiť režim práce v knižnici ncurses. V tomto prípade stačí, ak zadáte (naslepo napíšete) príkaz reset, ktorý nastavenia terminálu obnoví.

Krok 2: The City

Bombardér bude prelietavať nad mestom, ktoré v tomto kroku vytvoríme.

Vytvorte jednorozmerné pole celých čisiel world, ktoré bude reprezentovať budovy v meste a inicializujte ho.

Aby ste dokázali dynamicky reagovať na veľkosť obrazovky a vygenerovali ste vždy dostatočný počet budov vhodne vysokých, s výhodou viete využiť premenné COLS a LINES, ktoré ponúka knižnica ncurses. Ich význam je nasledovný:

  • LINES - výška okna terminálu
  • COLS - šírka okna terminálu

Pri deklarácii poľa teda vytvorite pole také veľké, aby nezaberalo celú šírku obrazovky, ale zo začiatku aj z konca chýbalo práve 5 výškových budov.

Pri generovaní výšky jednotlivých budov zabezpečte, aby max. výška budovy nepresahovala polovicu výšky obrazovky.

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

Úloha 2.1

Vykreslite mesto na obrazovku.

Pri práci s knižnicou ncurses opäť platí, že pozícia [0,0] sa nachádza v ľavom hornom rohu obrazovky.

Na presun kurzora na konkrétnu pozíciu obrazovky a následný výpis na danú pozíciu môžete využiť kombináciu nasledujúcich funkcií:

  • move(int y, int x) - presunie kurzor na zadanú pozíciu
  • printw() - vypíše reťazec na aktuálnu pozíciu kurzora (použitie tejto funkcie je rovnaké, ako v prípade funkcie printf())
  • mvprintw(int y, int x, const char *fmt, ...) - kombinácia funkcií move() a printw(), presunie kurzor na konkrétnu pozíciu a vytlačí reťazec
  • int refresh(void) - táto funkcia musí byť zavolaná, inak zmeny nie sú zobrazené v termináli

Poznámka

Pre získanie dokumentácie k uvedeným funkciám môžete s výhodou využiť aj manuálové stránky. Napr. ak chcete získať dokumentáciu k funkcii printw(), zadajte do príkazového riadku príkaz:

man printw

Poznámka

Pri vykresľovaní môžete použiť aj spomaľovanie, kedy po vykreslení jedného poschodia jednej budovy vykonávanie Vášho programu na chvíľu zastavíte. Tým vznikne efekt, kedy celé mesto nebude vykreslené naraz, ale postupne. Pre spomalenie môžete využiť funkciu nanosleep() z knižnice time.h, ktorej použitie ilustruje nasledujúci fragment kódu:

// 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
    }
}

Úloha 2.2

Overte správnosť svojej implementácie.

Ak ste postupovali správne, na obrazovke Vášho terminálu sa zobrazia výškové budovy reprezentujúce mesto podobne, ako je tomu na nasledujúcom obrázku:

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

Krok 3: The Plane

Hlavným hrdinom dnešného scenára je bombardér, ktorý celý čas na mesto nalietava a pri každom prelete zníži výšku letu. V tomto kroku prelet lietadla implementujeme.

Úloha 3.1

Vytvorte jeden prelet lietadlom po celej šírke obrazovky.

Lietadlo bude reprezentované ako reťazec, ktorý môže vyzerať napr. takto: " @=>-" alebo takto: " ^==-".

Jeden úspešný prelet nad mestom je zobrazený na nasledujúcom obrázku:

+-------------------------------------------------+
|                                             ^==-|
|                                                 |
|                                                 |
|                                                 |
|                                                 |
|                                                 |
|                                                 |
|         #         #     #         #   #         |
|      #  # #       ##  ###         #   #  #      |
|      ## # # #     ##  ### ##      #   ## #      |
|      ## # # # # # ##  #########   #   ## #      |
|     ####### ########  #############   #####     |
|     #######################################     |
+-------------------------------------------------+
// 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);
}

Úloha 3.2

Aktualizujte svoj kód tak, aby lietadlo preletelo od ľavého horného okraja, až po pravý dolný okraj, pred ktorým zastane (pristane).

Nezabudnite na to, že ľavý horný roh má súradnicu [0,0].

Pri pristáti musí lietadlo zastaviť v rohu obrazovky, a teda nesmie obrazovku opustiť (odrolovať do hangáru). Za vyriešenie úlohy je teda možné považovať len situáciu, že lietadlo zostane stáť v pravom dolnom rohu podobne, ako to ilustruje nasledovný obrázok:

+-------------------------------------------------+
|                                                 |
|                                                 |
|                                                 |
|                                                 |
|                                                 |
|                                                 |
|                                                 |
|                                                 |
|                                                 |
|                                                 |
|                                                 |
|                                                 |
|                                             ^==-|
+-------------------------------------------------+
// 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);
    }
}

Úloha 3.3

Úspešné pristátie oznámte hráčovi vypísaním textu Well done! do stredu obrazovky.

Oznam po úspešnom pristátí teda môže vyzerať napr. nasledovne:

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

Úloha 3.4

Preložte program a overte jeho funkcionalitu.

Ak ste postupovali správne, bombárder preletí cez celú mapu (v režime kosačka) a po úspešnom pristátí sa na obrazovke vypíše správa Well Done!:

Úloha 3.5

Aktualizujte implementáciu hernej slučky tak, aby sa hra ukončila s výpisom reťazca Game Over do stredu obrazovky vtedy, keď bombardér narazí do budovy.

Snažte sa identifikovať súradnice kabíny bombardéra a existenciu poschodia budovy v danej výške na danej pozícii.

Tento fragment kódu umiestnite do hernej slučky:

// 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;
}

Krok 4: Dropping the Bomb

V tomto kroku naprogramujete ovládanie z klávesnice.

Úloha 4.1

Zabezpečte, aby sa hra ukončila vtedy, keď hráč stlačí klávesu Q.

Načítavanie kláves v režime knižnice ncurses zabezpečíte volaním funkcie getch(). Funkcia vráti kód načítanej klávesy ako hodnotu typu int bez toho, aby program zastavila v jeho vykonávaní.

Keďže klávesa Q nebude jediná, ktorú pre ovládanie hry budeme používať, pre obsluhu stlačených kláves použite priamo príkaz switch.

Zoznam funkčných kláves a ich reprezentáciu v jazyku C ako aj možnosti nastavenia správania načítavania kláves si môžete zobraziť v manuálovej stránke k funkcii.

Poznámka

Ak Vám chýbajú manuálové stránky pre knižnicu ncurses vo Vašom virtuálnom stroji, môžete si ich doinštalovať príkazom:

sudo apt-get install ncurses-doc

Kód umiestnite do hernej slučky:

// 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;
    }
}

Úloha 4.2

Zabezpečte, aby pri stlačení kurzorovej šípky dolu bombardér vypustil bombu.

Pri lete bomby sa riaďte týmito pravidlami:

  • Bomba padá rovnakou rýchlosťou ako bombardér letí.
  • Bombardér vie naraz vypustiť len jednu bombu (na obrazovke sa nesmie nachádzať, resp. nesmie padať viac bômb ako jedna).
  • Pokiaľ pod bombou nie je žiadna budova, bomba letí dovtedy, kým nezmizne z obrazovky.
  • Pokiaľ pri padaní bomba natrafí na budovu, zničí max. 4 poschodia (každé v jednej iterácii cyklu hernej slučky - nie naraz)

Kód kurzorovej šípky dolu je v knižnici ncurses definovaný ako KEY_DOWN.

Pred hernou slučkou najskôr inicializujte premenné týkajúce sa bomby:

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

Následne pri stlačení kurzorovej šípky dolu inicializujte vlastnosti bomby, ak už táto náhodou neletí:

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

Nakoniec aktualizujte hernú slučku tak, aby v prípade, že bomba je vypustená, táto postupne padala a ničila všetko, čo jej stojí v ceste (v zmysle uvedených pravidiel):

// 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");
    }
}

Úloha 4.3

Preložte program a overte jeho funkcionalitu.

Ak ste postupovali správne, máte pred sebou hru, ktorá sa dá hrať, a ktorá sa dá aj vyhrať.

Krok 5: Fire in the Hole!

V tomto kroku naprogramujete strieľanie.

Úloha 5.1

Naprogramujte dávku z predného guľometu, keď sa stlačí medzerník (spacebar).

Hra sa zastaví. Dávku je možné vystreliť do vzdialenosti max. 5 znakov od lietadla v smere letu. Dávkou je možné rozstrieľať všetky budovy do vzdialenosti 5 znakov (ubudne im 1 poschodie).

case ' ':{
    for(int i = 0; i < 5 && x+5+i < COLS-5; i++){
        // check collision with building
        if(y == LINES - world[x+i]){
            // this is collision
            mvprintw(y, x+i+5, "*");
            world[x+i]--;
        }
        else{
            // no collision here
            mvprintw(y, x+i+5, "~");
        }
        if(i > 0){
            // clear previous shot print (optional)
            mvprintw(y, x+i+4, " ");
        }
        refresh();
        nanosleep(&ts, NULL);
    }
    // clear last shot print (optional)
    if(x+5+4 < COLS-5){
        mvprintw(y, x+5+4, " ");
        refresh();
        nanosleep(&ts, NULL);
    }
    break;
}

Krok 6: Game Info

Naprogramujte skóre a obmedzený počet striel v guľomete.

Úloha 6.1

Vytvorte počítadlo skóre a počtu dávok do guľometu bombardéru, a ich aktuálny stav vypíšte do stavového riadku hry.

// state line initialization
int score = 0;
int shots = 8;

// draw state line
mvprintw(0, 5, "SCORE     SHOTS");
mvprintw(1, 6, "%d", score);
mvprintw(1, 17, "%d", shots);
refresh();
nanosleep(&ts, NULL);

// update state line
mvprintw(1, 6, "%d", score);
mvprintw(1, 17, "%d", shots);

Úloha 6.2

Za každé poschodie zasiahnuté bombou pripočítajte 5 bodov.

// update bomb
score += 5;

Úloha 6.3

Za každé poschodie zasiahnuté dávkou z guľometu pripočítajte 15 bodov.

// in case of ' '
score += 15;

Úloha 6.4

Strely sa postupne míňajú. Ak sa hráčovi minú strely, nemôže viac strieľať.

// in case of ' '
if (!shots) {
    break;
}
else {
    shots--;
}

Úloha 6.5

Preložte program a overte jeho funkčnosť.

Krok 7: Colors

V tomto kroku inicializujete prácu s farbami a na niekoľkých miestach ich použijete.

Úloha 7.1

Doplňte do hry používanie farieb.

Predtým, než začnete používať farby, je potrebné ich inicializovať a nastaviť. Spravidla sa nastavujú farebné páry (popredie a pozadie). Každý farebný pár má svoje číslo, ktoré sa neskôr použije v programe. Číslo 0 sa nenastavuje, začíname číslom 1.

Inicializáciu a nastavenie farieb je vhodné umiestniť za inicializáciu knižnice ncurses v hlavnej funkcii main(), napr.:

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;
}

Po inicializácii a nastavení farieb je môžné ich použiť. Spravidla sa najskôr zapne používanie farebného páru a potom vypne (attron(), attroff()).

Použitie farieb môže vyzerať nasledovne:

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

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

refresh();

Krok 8: Challenges

V tomto kroku sa pozriete na ďalšie výzvy knižnice ncurses (spomínané sú v znení zadania).

Úloha 8.1

Osvojte si ďalšie možnosti používania knižnice ncurses.

Stiahnite si program curses-example.c a naštudujte si používanie ďalších funkcionalít:

  • void roll_text() - Postupne roluje text v dolnej časti obrazovky (pohyb v čase).
  • void draw_logo() - Postupne zobrazí vopred definované logo (pohyb v čase).
  • void screensaver() - Text putuje po obrazovke a odráža sa od stien, čím pripomína šetrič obrazovky. Pohyb je vykonaný stanovený počet krát (pohyb v čase).
  • void moving_arrow() - "Hráč" sa pohybuje po obrazovke podľa toho, aké klávesy stláča používateľ. Pohyb je ovládaný pomocou kláves: šípka hore, dole, doprava a doľava.

Krok 9: Problem Set 6: Curses

Teraz by ste už mali byť pripravení na vypracovanie zadania Problem Set 6.

Doplňujúce úlohy

Úloha A.1

Pridajte do hry možnosť prerušenia hry (Pause), ak hráč stlačí p alebo P.

Úloha A.2

Pridajte do hry možnosť zobrazenia nápovedy (samostatná obrazovka), ak hráč stlačí h alebo H.

Úloha A.3

Pridajte do hry úvodnú obrazovku s menu.

Úloha A.4

Pridajte do hry rolujúci text (horizontálny, v smere sprava doľava), ktorý sa bude bežať počas celej hry v najspodnejšom alebo najvrchnejšom riadku hry.

Úloha A.5

Po ukončení hry zobrazte na obrazovke záverečné kredity. Efekt zobrazovania si môžete zvoliť sami (napr. vo forme filmových tituliek zdola nahor alebo postupným zobrazovaním jednotlivých správ alebo ľubovoľný iný).

Doplňujúce zdroje

  1. Public Domain Curses (for Windows users)
  2. Curses Example
  3. Writing Programs with NCURSES
  4. Introduction to the Unix Curses Library
  5. What does #define _POSIX_SOURCE mean?
  6. Funkcia initscr(): docs.oracle.com - Screen initialization function
  7. Funkcia cbreak(): docs.oracle.com - Set input mode controls
  8. Funkcia noecho(): docs.oracle.com - Disable terminal echo
  9. Funkcia keypad(): docs.oracle.com - Enable/disable keypad handling
  10. Funkcia curs_set(): docs.oracle.com - Set visibility of cursor
  11. Funkcia nodelay(): docs.oracle.com - Set blocking or non-blocking read
  12. Funkcia endwin(): docs.oracle.com - Restore initial terminal environment
  13. Funkcia wgetch(): docs.oracle.com - Get a single-byte character from the terminal
  14. Premenná LINES: docs.oracle.com - Number of lines on terminal screen
  15. Premenná COLS: docs.oracle.com - Number of columns on terminal screen
  16. Funkcia move(): docs.oracle.com - Move cursor in window
  17. Funkcia printw(): docs.oracle.com - Print formatted output window
  18. Funkcia mvprintw(): docs.oracle.com - Print formatted output window
  19. Funkcia refresh(): docs.oracle.com - Refresh windows and lines
  20. Funkcia nanosleep(): qnx.com - Suspend a thread until a timeout or signal occurs
  21. Funkcia getch(): docs.oracle.com - Get a single-byte character from the terminal
  22. Príkaz switch: cppreference.com - Transfers control to one of the several statements, depending on the value of a condition
  23. Funkcia start_color(): docs.oracle.com - Manipulate color information
  24. Funkcia init_pair(): docs.oracle.com - Manipulate color information
  25. Funkcia attron(): docs.oracle.com - Change foreground window attributes
  26. Funkcia attroff(): docs.oracle.com - Change foreground window attributes
  27. Príkaz reset: tutorialspoint.com - Terminal initialization

Video