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
- Zoznámiť sa s ASCII-grafickou knižnicou ncurses .
- Naprogramovať hru Bomber .
- 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
- zabezpečí, aby sa znaky po stlačení a načítaní funkciou
int keypad(WINDOW *win, bool bf)
- ak je
bf
TRUE, potom používateľ smie stlačiť funkčné klávesy (napr. šípky) a funkciawgetch()
vráti jednu hodnotu reprezentujúcu klávesu, napr. KEY_LEFT
- ak je
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
- spôsobí, že funkcia
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áluCOLS
- ší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íciuprintw()
- vypíše reťazec na aktuálnu pozíciu kurzora (použitie tejto funkcie je rovnaké, ako v prípade funkcieprintf()
)mvprintw(int y, int x, const char *fmt, ...)
- kombinácia funkciímove()
aprintw()
, presunie kurzor na konkrétnu pozíciu a vytlačí reťazecint 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
- Public Domain Curses (for Windows users)
- Curses Example
- Writing Programs with NCURSES
- Introduction to the Unix Curses Library
- What does
#define _POSIX_SOURCE
mean? - Funkcia
initscr()
: docs.oracle.com - Screen initialization function - Funkcia
cbreak()
: docs.oracle.com - Set input mode controls - Funkcia
noecho()
: docs.oracle.com - Disable terminal echo - Funkcia
keypad()
: docs.oracle.com - Enable/disable keypad handling - Funkcia
curs_set()
: docs.oracle.com - Set visibility of cursor - Funkcia
nodelay()
: docs.oracle.com - Set blocking or non-blocking read - Funkcia
endwin()
: docs.oracle.com - Restore initial terminal environment - Funkcia
wgetch()
: docs.oracle.com - Get a single-byte character from the terminal - Premenná
LINES
: docs.oracle.com - Number of lines on terminal screen - Premenná
COLS
: docs.oracle.com - Number of columns on terminal screen - Funkcia
move()
: docs.oracle.com - Move cursor in window - Funkcia
printw()
: docs.oracle.com - Print formatted output window - Funkcia
mvprintw()
: docs.oracle.com - Print formatted output window - Funkcia
refresh()
: docs.oracle.com - Refresh windows and lines - Funkcia
nanosleep()
: qnx.com - Suspend a thread until a timeout or signal occurs - Funkcia
getch()
: docs.oracle.com - Get a single-byte character from the terminal - Príkaz
switch
: cppreference.com - Transfers control to one of the several statements, depending on the value of a condition - Funkcia
start_color()
: docs.oracle.com - Manipulate color information - Funkcia
init_pair()
: docs.oracle.com - Manipulate color information - Funkcia
attron()
: docs.oracle.com - Change foreground window attributes - Funkcia
attroff()
: docs.oracle.com - Change foreground window attributes - Príkaz
reset
: tutorialspoint.com - Terminal initialization