Bomber, part I.

knižnica ncurses a práca s grafikou v konzole

Motivácia

Doteraz sme v rámci cvičení pracovali len so štandardným vstupom a výstupom, ktorý neumožňoval robiť dostatočne interaktívne a pútavé výstupy. Na tomto cvičení sa naučíte pracovať s knižnicou curses, ktorá vám umožní vytvárať interaktívne aplikácie aj v prostredí textovej konzoly. V rámci cvičenia 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.

Ciele

  1. Pracovať s knižnicou curses.

Postup

Krok #1: 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 praklad.

Úloha 1.1

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áves
  • int endwin(void) - ukončuje prácu v režime knižnice curses a obnovuje pôvodné nastavenia terminálu
Poznámka:  Pre prácu s knižnicou curses 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();
}

Úloha 1.2

Vytvorte si súbor Makefile s pravidlom all, pomocou ktorého zabezpečíte preklad programu.
Aby ste mohli prorgramy využívajúce knižnicu curses prekladať a následne spúšťať, do zoznamu knižníc pridajte -lcurses (premenná LDLIBS). Kompletná syntax pre prekladač gcc bude vyzerať nasledovne:
gcc -std=c11 -Wall -Werror main.c -lm -lcurses -o bomber
Poznámka:  Pokiaľ nepoužívate virtuálny stroj kurzu, ale máte nainštalovaný vlastnú linuxovú distribúciu, je potrebné knižnicu curses do systému doinštalovať. Ak používate niektorý derivát distribúcie Debian (Ubuntu), potrebné balíčky nainštalujete príkazom:
sudo apt-get install libncurses5 libncurses5-dev
Ak používate niektorý derivát distribúcie RedHat, napr. Fedora, potrebné balíčky nainštalujete príkazom:
sudo dnf install ncurses ncurses-devel

Ú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 curses. 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.

Úloha 2.1

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

Hodnoty poľa budú reprezentovať výšku jednotlivých budov.

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 curses. 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.2

Vykreslite mesto na obrazovku.

Pri práci s knižnicou curses 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 vykreslovaní 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.3

Overte správnosť svojej implementácie.

Ak ste postupovali správne, na obrazovke vášho terminálu sa zobrazia výškové budový 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

Overte správnosť svojej implementácie.

Doplnkové úlohy

    Diskusia

    Upozornenie: Do svojich príspevkov nevkladajte správne riešenia úloh a ani ich od ostatných nežiadajte! Nepoužívajte sprosté slová! Takéto príspevky budú zmazané! Riaďte sa podľa pravidiel etického kódexu.