Week 10

Viacrozmerné polia, knižnica Curses, argumenty príkazového riadku

Video (29.11.2018, Paralelka B)

Prezentácia

Zdrojový kód

Poznámky k prednáške

Deklarácia 2D poľa

char array[5][4];
  • Avšak pozor, toto pole nebude prázdne!

Inicializácia 2D poľa

  • 2D pole je možné inicializovať podobne, ako tomu bolo pri inicializácii 1D polí, teda priamym vymenovaním hodnôt poľa. To je možné dosiahnuť dvoma spôsobmi.
  • Prvý spôsob vyžaduje uviesť oba rozmery poľa. Inicializácia 2D poľa potom vyzerá nasledovne:
char array[5][4] = {
    {' ', '-', '-', ' '},
    {'|', ' ', ' ', '|'},
    {' ', '-', '-', ' '},
    {'|', ' ', ' ', '|'},
    {' ', '-', '-', ' '}
};
  • Druhý spôsob nevyžaduje uviesť prvý rozmer podobne, ako tomu bolo v prípade 1D polí. Ten bude zistený automaticky. Druhý rozmer je však povinný. Inicializácia 2D poľa potom vyzerá nasledovne:
char array[][4] = {
    {' ', '-', '-', ' '},
    {'|', ' ', ' ', '|'},
    {' ', '-', '-', ' '},
    {'|', ' ', ' ', '|'},
    {' ', '-', '-', ' '}
};

Prístup k prvkom poľa

  • Ako sme spomínali v prípade 1D polí, pole sa indexuje od 0. Podobne je tomu tak aj v 2D poliach, teda oba rozmery poľa sa indexujú od hodnoty 0.
  • Pri pristupovaní k prvkom poľa nezabudnite na to, že je dôležité nepomýliť si poradie súradníc!

Prechod 2D poľom

  • Vytvoríme si funkciu draw(), pomocou ktorej zabezpečíme vykreslenie 2D poľa na obrazovku.
  • Podobne ako v prípade odovzdávania poľa ako parameter funkcie, budeme postupovať aj v prípade 2D polí, teda okrem adresy samotného poľa odovzdáme v rámci parametrov aj jeho šírku a výšku.
  • Avšak podobne ako pri inicializácii poľa musí byť jedna jeho súradnica známa už v časti deklarácie funkcie. Od verzie jazyka C99 je možné použiť takýto zápis (záleží na poradí parametrov):
void draw(const int rows, const int cols, char array[][cols]);
  • Funkcia draw() bude potom vyzerať nasledovne:
void draw(const int rows, const int cols, char array[][cols]){
    for(int y = 0; y < rows; y++){
        for(int x = 0; x < cols; x++){
            printf("%c", array[y][x]);
        }
        printf("\n");
    }
}
  • Príklad vykreslí na obrazovku 7-segmentovku (seven-1.c):
 -- 
|  |
 -- 
|  |
 --

Hranice 2D polí

  • S hranicami 2D polí je to úplne rovnaké, ako v prípade 1D polí: Prekladač v dobe prekladu nevie odhaliť, či s nimi budete pracovať správne a počas behu programu môže dôjsť k indexovaniu poľa mimo jeho rozsah.
  • Pozor na Off-by-one Problem alebo Fence-post Problem, lebo je veľmi jednoduché pomýliť sa práve o hodnotu 1.
  • Sledovať, či náhodou nedochádza k zapisovaniu údajov mimo rozsahu polí, si musí sám programátor.

Reprezentácia 2D poľa v pamäti

  • Pamäť počítača je lineárna. Ako je v nej však uložené 2D pole, ktoré má 2 rozmery a pozeráme sa na neho ako na tabuľku?
  • Keď sme do pamäte ukladali 1D pole, jednotlivé jeho prvky boli uložené v pamäti za sebou od nižších adries k vyšším, pričom veľkosť jednej obsadenej "bunky" závisela od veľkosti uloženého údajového typu.
  • Aj 2D pole je nakoniec v pamäti reprezentované lineárne, a do pamäte sa riadky údajov ukladajú postupne za sebou.
  • Pohľad používateľa na 2D pole je tabuľkový (2D ako tabuľka), ale v skutočnosti je toto pole uložené v pamäti ako 1D pole (lineárne, sekvenčne).

Viacrozmerné polia

  • Ako si predstavujete viacrozmerné polia? Ako môže vyzerať 3D pole? (3D priestor) Napr. to môže byť číslo strany v knihe, číslo stĺpca a poradové číslo slova (Sherlock Holmes - The Valey of Fear)
  • Ako je to však s ďalšími rozmermi? Ako vyzerá 4D pole? Je štvrtým rozmerom čas? 5D pole? Čo sú tie 3 zvyšné "rozmery" v 5D kine? Viac D viac Adidas?
  • Príkladom viacrozmerných polí môže byť napríklad aj adresa (pseudokód):
entry["state"]["city"]["street"]["person"];
  • Upravme náš príklad tak, aby na obrazovku vykreslil konkrétnu číslicu 7-segmentovky. Poľu najskôr pridáme tretí rozmer, ktorý nám povie, či sa má daný znak vykresliť na obrazovku alebo nie. Keďže na začiatku nevieme, ktorú číslicu budeme vykresľovať, znaky, ktoré nie sú biele, nastavíme na 'n'.
  • Nezabudnime upraviť funkciu draw() tak, aby vedela pracovať s týmto tretím rozmerom.
  • Ďalej od používateľa načítame číslicu z obrazovky (v main()).
  • Pred vykreslením číslice vytvoríme novú funkciu set_segments(), ktorá upraví tretí rozmer poľa array podľa príslušných segmentov (na 'y').
  • Nezabudnime, že v hlavnej funkcii main() je potrebné zavolať funkciu set_segments() ešte pred volaním funkcie draw().
  • Príklad vykreslí na obrazovku 7-segmentovku (seven-2.c):
Enter number to display: 5
 -- 
|
 -- 
   |
 --

Piškvorky 2D a knižnica Curses

  • Najskôr si pripravíme program s 2D verziou hry Piškvorky z cvičenia č. 9. Túto hru postupne upravíme.

Inicializácia knižnice Curses

  • Nastavíme si prostredie pre prácu s knižnicou ncurses a v súbore ~/.bashrc upravíme tento riadok:
export LDLIBS="-lm"

nasledovne:

export LDLIBS="-lm -lcurses"

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

  • V programe s piškvorkami inicializujeme a ukončíme prácu s knižnicou ncurses:
    • Na začiatku programu pripojíme hlavičkový súbor curses.h.
    • Na začiatku hlavnej funkcie main() zavoláme funkciu initscr() pre inicializáciu a naopak na konci hlavnej funkcie main() zavoláme funkciu endwin() pre ukončenie práce s knižnicou ncurses:
// your includes are here
#include <curses.h>

int main(){
    // initialize the library
    initscr();

    // game code goes here

    // end curses
    endwin();
}

Kreslenie

  • Ďalej upravíme výstupy hry tak, aby sa zobrazili pomocou knižnice ncurses.
  • Upravíme vykresľovanie hracieho poľa tak, aby sa celé vykreslilo iba raz. Aktualizáciu hry upravíme tak, aby sa aktualizovali iba konkrétne časti obrazovky (aby sa zakaždým nevykreslilo celé hracie pole).
  • Po inicializácii práce s knižnicou ncurses už neuvidíme standardný výstup na obrazovke, na aký sme zvyknutí. Výstup je potrebné realizovať do nového "okna" knižnice. To umožňuje funkcia printw(), ktorej správanie je v zásade rovnaké ako správanie funkcie printf().
  • Po inicializácii práce s knižnicou sa kurzor nachádza na začiatku okna (vľavo hore). Súradnica tohto miesta je (0,0). Avšak každé použitie funkcie s výstupom posunie kurzor ďalej (doprava resp. dole).
  • Ak potrebujeme premiestniť kurzor na iné miesto, používame funkciu move(). Funkcia má dva parametre: Súradnica y, súradnica x.
  • Presun kurzora môžeme uskutočniť súčasne s výpisom na obrazovku pomocou funkcie mvprintw(). Funkcia mvprintw() má oproti funkcii printw() o dva parametre navyše: Prvý - súradnica y, druhý - súradnica x.
  • Rozmery okna sú dané makrami knižnice LINES a COLS. To znamená, že maximálna súradnica y je LINES - 1 a maximálna súradnica x je COLS - 1.
  • V našom prípade uskutočníme vykreslenie hracieho poľa na obrazovku len raz - na začiatku hry, ešte pred hlavnou slučkou hry (ešte pred hlavým cyklom programu):
    • Premenujeme funkciu draw() na first_draw(). Parametre funkcie zostanú zachované. Nezabudnime funkciu premenovať v mieste jej deklarácie aj definície.
    • Upravíme program tak, aby sme funkciu first_draw() volali iba raz: Pred hlavnou slučkou hry, ale za inicializáciou knižnice ncurses:
int main(){
    // initialize the library
    initscr();

    // size and field[][size] creation

    first_draw(size, field);

    // game cycle goes here

    // end curses
    endwin();

    return EXIT_SUCCESS;
}
  • Upravíme funkciu first_draw() tak, aby namiesto funkcie printf() používala všade funkciu printw(). Keďže ide o prvé vykreslenie, nie je potrebné meniť polohu kurzora. Nachádza sa na súradnici (0,0).
  • Po vykreslení hracieho poľa vypíšeme na obrazovku výzvu pre hráča A (ešte v rámci funkcie first_draw()).
  • Aby sa vykonané zmeny prejavili, na konci funkcie first_draw() je potrebné zavolať funkciu refresh():
void first_draw(const int size, char field[][size]){
    //drawing code goes here

    printw("\n\nPlayer A: ");
    refresh();
}
  • Prvotné vykreslenie hry môže byť nasledovné:
  +-+-+-+-+-+-+-+
7 | | | | | | | |
  +-+-+-+-+-+-+-+
6 | | | | | | | |
  +-+-+-+-+-+-+-+
5 | | | | | | | |
  +-+-+-+-+-+-+-+
4 | | | | | | | |
  +-+-+-+-+-+-+-+
3 | | | | | | | |
  +-+-+-+-+-+-+-+
2 | | | | | | | |
  +-+-+-+-+-+-+-+
1 | | | | | | | |
  +-+-+-+-+-+-+-+
   1 2 3 4 5 6 7

Player A: █
  • Ďalej uskutočníme aktualizáciu zobrazeného okna počas priebežnej aktualizácie hry:
    • S každou novou slučkou hry je na rade iný hráč (A alebo B). Vo výpise, ktorý sa nachádza pod hracím poľom, upravíme zobrazený znak (A alebo B) na nový znak podľa toho, ktorý hráč je na rade. Nezabudnime, že najskôr je potrebné nastaviť kurzor na správne miesto.
    • Hráč, ktorý je na rade, zapisuje na obrazovku ťah (súradnice x, y). Zmažeme ťah predchádzajúceho hráča (medzerami) a nastavíme kurzor tak, aby bolo možné načítať nový ťah hráča. Nezabudnime aktualizovať obrazovku.
    • Načítame nový ťah hráča pomocou funkcie scanw(), ktorej správanie je v zásade rovnaké ako správanie funkcie scanf(). Výsledok môže vyzerať nasledovne (vo funkcii main()):
    while( !is_solved(size, field) ){
        player = (player == 'A') ? 'B' : 'A';
        // change player on screen
        mvprintw(3+size*2, 7, "%c", player);
        // remove previous player move
        mvprintw(3+size*2, 10, "   ");
        // set cursor to scan new player move from right place
        move(3+size*2, 10);
        refresh();
        int x, y;
        scanw("%d %d", &x, &y);

        // game logic comes here, change printf()'s to printw()'s
    }
  • Vytvoríme novú funkciu draw(), ktorá zrealizuje aktualizáciu obrazovky po aktualizácii hry. Aktualizácia bude závisieť od veľkosti poľa, súradníc nového ťahu (X alebo O) a hráča, ktorý je na rade. Pozíciu kurzora bude potrebné matematicky prepočítať. Nezabudnime aktualizovať obrazovku. Výsledok môže vyzerať nasledovne:
void draw(const int size, const int x, const int y, const char player){
    // set cursor to right position
    move( (size - y) * 2 + 1, (x - 1) * 2 + 3 );
    // draw X or O
    if(player == 'A'){
        printw("X");
    }
    else{
        printw("O");
    }
    refresh();
}
  • Vytvorenú funkciu draw() zavoláme v hlavnej slučke hry:
    while( !is_solved(size, field) ){
        // some screen update is here
        // game logic comes here, change printf()'s to printw()'s
        draw(size, x, y, player);
    }
  • V prípade, že hráč zadal nesprávnu pozíciu (Wrong position), tento oznam sa objaví o riadok nižšie, ako bol zadaný ťah hráča. Nie je teda potrebné meniť polohu kurzora. Avšak tento oznam sa môže striedať s druhým oznamom o existencii X/O na danej pozícii, ktorý je dlhší. Preto oznam o nesprávnej pozícii upravíme tak, aby bol o čosi dlhší (napr. medzerami). Nezabudnime na aktualizáciu obrazovky.
  • V prípade, že hráč zadal pozíciu, ktorá je už obsadená (X/O is already there), tento oznam sa objaví tiež o riadok nižšie, ako bol zadaný ťah hráča. Nie je teda potrebné meniť polohu kurzora. Nezabudnime na aktualizáciu obrazovky.
  • V obidvoch prípadoch chceme, aby sa oznam zobrazil len počas 1 nasledujúceho ťahu. Ak by niektorý z týchto oznamov zostal na obrazovke dlhšie, mohol by zmiasť hráčov. Upravíme program tak, aby sa oznamy o chybách zobrazovali len 1 nasledujúci ťah. Výsledok môže vyzerať nasledovne (vo funkcii main()):
    int error_message = 0;
    while( !is_solved(size, field) ){
        // some screen update is here

        int cross = add_cross(size, field, x, y, player);
        if( cross == -1 ){
            error_message = 1;
            printw("Wrong position!    \n");
            refresh();
            // skip draw() in the end of while
            continue;
        }
        else if( cross == 0 ){
            error_message = 1;
            printw("%c is already there!\n", (field[size-y][x-1] == 'X') ? 'X' : 'O');
            refresh();
            // skip draw() in the end of while
            continue;
        }
        if(error_message){
            error_message = 0;
            // remove error message after 1 step
            mvprintw(4+size*2, 0, "                   ");
            // refresh will come with draw() call
        }
        draw(size, x, y, player);
    }
  • Ďalej aktualizujeme oznam o výhercovi hry.
  • Za hernou slučkou v hlavnej funkcii main() sa nachádza oznam o víťazovi hry. Nastavíme jeho umiestnenie napr. za hracie pole. Nezabudnime aktualizovať obrazovku. Pozor, ak ihneď zavoláme funkciu endwin(), zobrazenie celej hry zmizne. Preto ešte predtým zavoláme funkciu, ktorá bude očakávať napr. stlačenie ľubovoľnej klávesy (getch()). Výsledok môže vyzerať nasledovne:
int main(){
    // some initialization
    // game logic comes here
    mvprintw(3+size*2, 0, "Player %c wins!\n", player);
    refresh();
    getch();

    // end curses
    endwin();

    return EXIT_SUCCESS;
}

Farby

  • Ďalej inicializujeme prácu s farbami a na niekoľkých miestach ich použijeme.
  • Najskôr teda doplníme do hry používanie farieb.
  • Predtým, než začneme 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();
    // set colors
    init_pair(1, COLOR_YELLOW, COLOR_BLACK);
    init_pair(2, COLOR_GREEN, COLOR_BLACK);
    init_pair(3, COLOR_CYAN, COLOR_BLACK);

    // size and field[][size] creation

    first_draw(size, field);

    // game cycle goes here

    // end curses
    endwin();

    return EXIT_SUCCESS;
}
  • 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žívanie farieb vo funkcii draw() môže vyzerať nasledovne:
void draw(const int size, const int x, const int y, const char player){
    move( (size - y) * 2 + 1, (x - 1) * 2 + 3 );
    if(player == 'A'){
        attron(COLOR_PAIR(1));
        printw("X");
        attroff(COLOR_PAIR(1));
    }
    else{
        attron(COLOR_PAIR(2));
        printw("O");
        attroff(COLOR_PAIR(2));
    }
    refresh();
}
  • Farebný oznam o víťazovi v hlavnej funkcii main() môže vyzerať nasledovne:
int main(){
    // some initialization

    // game cycle goes here

    attron(COLOR_PAIR(3));
    mvprintw(3+size*2, 0, "Player %c wins!\n", player);
    refresh();
    getch();

    // end curses
    endwin();

    return EXIT_SUCCESS;
}
  • Aktuálne by naše piškvorky už mali byť použiteľné.

Argumenty príkazového riadku

  • Program s piškvorkami by už mal fungovať pomocou knižnice ncurses. V tomto kroku ho vylepšíme o používanie argumentov príkazového riadku.
  • Upravíme deklaráciu hlavnej funkcie main() tak, aby používala argumenty príkazového riadku:
    • Parameter argc uchováva počet argumentov príkazového riadku. Argument je vždy minimálne jeden (názov programu).
    • Parameter argv uchováva samotné argumenty v 2D poli (pole reťazcov).
    • Prvý argument príkazového riadku je vždy názov programu (./tictactoe). Ostatné argumenty sú tie, ktoré za ním v riadku nasledujú (oddelené medzerou).
    • V poli argv sa názov programu nachádza na indexe 0, ostatné argumenty sa nachádzajú na indexoch 1, 2....
int main(int argc, char *argv[]){
    // some code is here
}
  • V našom prípade budeme používať iba jeden argument: Rozmer hracieho poľa (v programe sa používa ako size).
  • Na začiatku hlavnej funkcie main() zistíme, či bol program spustený s parametrom navyše.
  • Zaujíma nás, či program má aspoň 2 argumenty príkazového riadku. V prípade, že to tak nie je, program sa ukončí:
int main(int argc, char *argv[]){
    if(argc < 2){
        printf("Not enough arguments!\n");
        return 1;
    }
    // some code is here
}
  • Ďalej upravíme program tak, aby namiesto načítania veľkosti hracieho poľa size získal túto hodnotu z argumentu príkazového riadku.
  • Argument je uchovaný ako reťazec. My však potrebujeme číslo (veľkosť hracieho poľa). Používanie funkcie scanf() teda nahradíme príslušným výpočtom. Výsledok môže vyzerať nasledovne:
int main(int argc, char *argv[]){
    if(argc < 2){
        printf("Not enough arguments!\n");
        return 1;
    }
    int size = 0, len = strlen(argv[1]);
    do{
        size *= 10;
        size += argv[1][len-1] - '0';
        len--;
    } while(len > 0);

    if(size < 4 || size > 9){
        printf("Wrong argument! Should be 4-9.\n");
        return EXIT_FAILURE;
    }

    char field[size][size];
    for(int y = 0; y < size; y++){
        for(int x = 0; x < size; x++){
            field[y][x] = ' ';
        }
    }
    // some code is here
    return EXIT_SUCCESS;
}

Poznámka

Keďže výpočet používa funkciu strlen(), nezabudnime pridať na začiatok programu #include <string.h>.
  • Teraz môžeme otestovať našu hru.

Curses: Ďalšie príklady

  • V rámci domáceho cvičenia Bomber máte k dispozícii program curses-example.c, kde sú úkážky ďalších možností knižnice ncurses:
    • 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.

Doplňujúce zdroje

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