9. týždeň

Curses & Command-line Args

Knižnica Curses, argumenty príkazového riadku

Video (11'2022)

Prezentácia

Zdrojový kód

Poznámky k prednáške

Piškvorky 2D a knižnica Curses

  • Najskôr si pripravíme program s 2D verziou hry Piškvorky z cvičenia. 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;
}
  • No... a teraz sa na tento výpočet pozrite. Páči sa vám?
  • Existuje iný, jednoduchší spôsob ako získať hodnotu (číslo) z reťazca?
  • Vyskúšajme sscanf():
int main(int argc, char *argv[]){
    if(argc < 2){
        printf("Not enough arguments!\n");
        return 1;
    }

    sscanf(argv[1], "%d", &size); // <-- String to Nr magic :)

    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

Ak ste sa rozhodli použiť vyššie uvedený výpočet, všimnite si, že používa funkciu strlen(). Nezabudnime preto 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. Public Domain Curses (for Windows users)
  2. Curses Example
  3. Writing Programs with NCURSES
  4. Introduction to the Unix Curses Library
  5. C - Command Line Arguments
  6. Command line arguments in C and C++

Video