Video (12.11.2024)
Prezentácia
Zdrojový kód
- tictactoe.c (bez knižnice ncurses)
- tictactoe-curses.c (s knižnicou ncurses)
- curses-example.c
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 funkciuinitscr()
pre inicializáciu a naopak na konci hlavnej funkciemain()
zavoláme funkciuendwin()
pre ukončenie práce s knižnicou ncurses:
- Na začiatku programu pripojíme hlavičkový súbor
// 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 funkcieprintf()
. - 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()
. Funkciamvprintw()
má oproti funkciiprintw()
o dva parametre navyše: Prvý - súradnica y, druhý - súradnica x. - Rozmery okna sú dané makrami knižnice
LINES
aCOLS
. To znamená, že maximálna súradnica y jeLINES - 1
a maximálna súradnica x jeCOLS - 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()
nafirst_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:
- Premenujeme funkciu
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 funkcieprintf()
používala všade funkciuprintw()
. 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ť funkciurefresh()
:
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 funkciescanf()
. Výsledok môže vyzerať nasledovne (vo funkciimain()
):
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! ");
refresh();
// skip draw() in the end of while
continue;
}
else if( cross == 0 ){
error_message = 1;
printw("%c is already there!", (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 funkciuendwin()
, 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!", 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!", 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 indexe0
, ostatné argumenty sa nachádzajú na indexoch1, 2...
.
- Parameter
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 EXIT_FAILURE;
}
// 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 EXIT_FAILURE;
}
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
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>
.
- 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 EXIT_FAILURE;
}
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;
}
- 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.