Vješala igra (C tutorijal) Cover

Vješala igra (C tutorijal)

Matej Arlović – 28/06/2021

Ovaj tutorial je napravljen za apsolutne početnike u programiranju. Cijeli programski kod preuzmite ovdje: https://github.com/B-Matt/BalkanCoders/blob/master/01%20-%20Hangman/hangman.c

U ovome tutorijalu ćete naučiti:

  • Polja znakova (eng. strings)
  • Petlje
  • Osnovnu logiku iza video igrica

Što to svaka igra posjeduje?

Osim super specijalnih efekata (vizualnih i zvukovnih), zabavne igrivosti (eng. gameplay) i jako dobrog marketinga, svaka video igra posjeduje petlju koja se naziva igračka petlja. Ona se izvodi svakih nekoliko milisekundi i u njoj se nalazi logika igre za kretanje igrača, pucanje, odnosno svaki event unutar video igre. No što su to petlje?

Osnovne petlje unutar C programskog jezika su:

  • for — Izvodi se onoliko puta koliko vi odredite brojačem-Koristi se kada ZNATE koliko puta morate ponoviti neki dio koda.
  • while — Izvodi se sve dok uvjet ne bude zadovoljen — Ne mora se nikada izvesti jer se prvo ispituje uvjet pa se tek onda izvršavaju naredbe unutar petlje.
  • do-while — Izvodi se sve dok uvjet ne bude zadovoljen — Ona se izvodi minimalno JEDNOM jer se prvo izvršavaju naredbe unutar petlje pa tek onda provjerava uvjet.

Ljudi često pomiješaju uvjetno grananje s petljama pa kažu da je naredba if petlja što to NIJE. Grananje je razdvajanje koda na više grana prema nekakvom uvjetu. Uvjetno granjanje možete vizualizirati poput križanja (program može ići naprijed, lijevo ili desno ovisno o uvjetu), a petlje kao kružne tokove (unutra ste dok ne odlučite izaći van).

No vratimo se na igračku petlju. Ona se dakle ponavlja svakih nekoliko milisekundi i prikazuje informacije na igračev ekran (objekte, eventi, efekti, itd.). Tako ćemo i ovdje koristiti igračku petlju u kojoj ćemo stalno prikazivati vješalo i dijelove čovjeka (ako je igrač pogriješio slovo). Kako se to ne bi prikazivalo jedno ispod drugog konzolu ćemo svaku iteraciju (iteracija dolazi iz latinskog što znači ponavljanje) čistiti pa tek onda prikazati vješalo i čovjeka.

Prikaz video igre napravljene u Unity3D od strane Master Games tima.

O igri Vješala

Svako dijete iz 90-tih je barem jednom u svome životu igralo Vješala s svojim prijateljima. No, ako ju niste igrali cilj je otkriti riječ unutar 6 poteza. Igrač iterativno pogađa slova riječi. Ukoliko se pogodi slovo onda se ono zapisuje na crtu, a ako se ne pogodi onda se crta dio čovjeka na vješalima. Igra se zaustavlja na dva načina:

  1. Ako igrač pogodi riječ
  2. Ako igrač pogriješi slovo 6 puta (čovjek na vješalu je nacrtan do kraja)

Kako smisliti algoritam iz objašnjenja zadatka?

U ovome dijelu tutorijala ćete naučiti razliku između algoritma i programa i kako napisati algoritam iz zadanog zadatka. Algoritam je prikaz koraka operacija koje se moraju odraditi kako bi se riješio jedan ili skup problema. Računalni program je niz instrukcija napisanih u nekom programkom jeziku kako bi se izvela neka operacija na računalu. Kako biste napisali računalni program potrebno je implementirati algoritam unutar nekog programskog jezika.

Krenimo ispočetka! Ako je glavni cilj igre pogoditi zadanu riječ što trebate učiniti? Prvo trebate zahtijevati unos glavne riječi od igrača 1 i zatim ju spremiti unutar memorije. Nakon toga je potrebno nacrtati cijelo grafičko sučelje i započeti igračku petlju. Gdje ćete unutar svake iteracije od strane igrača 2 zahtijevati unos nekog znaka. Nakon čega ćete provjeriti dali se uneseni znak nalazi unutar spremljene riječi. Ako se ne nalazi unutar spremljene riječi povećava se broj pogrešaka i crtate dio čovjeka na vješalima, u protivnome prikazujete dio pogođene riječi na grafičko sučelje.

Osnove svakog C programa

Svaki program pisan u C programskom jeziku se sastoji od 6 odjeljaka:

  1. Dokumentacija - Sastoji se od komentara u kojem pišu osnovne informacije o programu (ime i prezime autora, datum nastanka programa i slično)
  2. Uključivanje biblioteka - Ovdje se uključuju vanjske biblioteke pomoću #include naredbe
  3. Definicije - Sastoji se od simboličkih definicija deklariranih pomoću #define naredbe 
  4. Deklaracija globalnih varijabli - Sastoji se od deklaracije i inicijalizacije globalnih varijabli koje možete koristiti unutar svih funkcija unutar programa, ali i deklaracija funkcija unutar programa
  5. Glavna funkcija - Svaki C program MORA imati glavnu funkciju
  6. Subprogrami - Sastoji se od korisničkih funkcija koje su deklarirane iznad glavne funkcije

Slika je preuzeta sa https://www.cybotech.xyz/2018/05/basic-structure-of-c-program.html

Nešto o stringovima

Stringovi su nizovi znakova. U dolje prikazanoj slici možete vidjeti deklaraciju i inicijalizaciju stringova.

Prilikom deklaracije polja potrebno je definirati veličinu tog polja. No ako deklarirate i inicijalizirate polje onda ne morate definirati veličinu tog polja već će kompajler automatski prebrojati elemente i zapisati veličinu polja. Svaki znak se unutar varijabli tipa char definira pomoću cijelog broja iz ASCII/Unicode tablice (primjer ASCII tablice http://www.asciitable.com). Tim varijablama možete pridružiti znakove ili cijeli broj.

char testVar = 'a'; // Spremljena vrijednost je a
testVar = 67; // Spremljena vrijednost je C

Da remiziramo sve napisano u ovom odjeljku. Varijable su mjesta u memoriji u koja spremate neke podatke. Stvaranje varijable se izvodi putem dva dijela: deklaracija (definirate tip i ime varijable) i inicijalizacija (nakon deklaracije postavljate vrijednost varijable pomoću operatora za pridjeljivanje vrijednosti (=)). Vrijednost varijabli tipa char se unutar memorije prikazuje pomoću cijelih brojeva, tako da u njih možete spremati cijele brojeve ili znakove. Također string je niz znakova.

Pisanje koda za igru!

Još jedna stvar vezana za deklariranje varijabli. UVIJEK davajte opisna imena koja će i drugi programeri razumjeti. Loša je navika imenovati varijable s jednim ili dva znaka jer ćete tako samo vi znati značenje tih varijabli i nitko drugi! Drugi programeri bi trebali pretražiti cijeli programski kod kako bi shvatili značenje nekih varijabli. Osim toga prilikom pisanja SVAKOG programa, morate znati što i gdje (u lokalnoj ili globalnoj varijabli, polju ili klasi/strukturi) spremiti.

Prvo što trebate učiniti je deklarirati varijable koje ćete koristiti u programu. U algoritmu se spominje dva podatka. Riječ korištena za igru i broj igračevih pogrešaka. No morate računati i na male "komplikacije" u kodu. U ovome tutorijalu koristim tri niza jer je on pisan za apsolutne početnike osim toga kod izgleda čišće.

char inputWord[8];
char helpWord[8];
char fullWord[8];
    
char inputLetter;
    
int playerHits = 0;
int playerErrors = 0;
    
int isGameOver = 0;
int isGameWon = 0;

U ovoj verziji, igra NE prikazuje sve pogođene znakove već samo prvi u nizu. Ako je tražena riječ "test" i igrač unese slovo "t". Program će prikazati samo prvo "t" kao riješeno, stoga igrač mora ponovno unijeti slovo t kako bi pronašao i ovo drugo slovo u riječi. Za viježbu napišite programski kod koji će pretražiti CIJELU riječ za unešeni znak.

Nakon deklariranja svih varijabli potrebno je napisati programski kod za unos riječi čija veličina mora biti između 2 i 7 znakova. Za to ponašanje će se koristiti do-while petlja jer će se ona izvesti barem jednom i ponavljati će se sve dok se uvjet ne zadovolji. Važno je napomenuti da unutar zagrada while dijela petlje treba unijeti OBRNUTO od onoga što želite postići. Znači ako mi tražimo riječ između 2 i 7 unutar while petlje ćete upisati ako je unos manji od 2 ili veći od 7 (ako je to istina onda se unesena riječ nalazi unutar tog raspona brojeva).

do 
{
	printf("Input game word (max. 8 chars): ");
	scanf("%s", inputWord);
} 
while(strlen(inputWord) < 2 || strlen(inputWord) > 7);

Funkcija printf() se koristi za ispis oblikovanog teksta, a funkcija scanf() služi za spremanje korisničkog unosa u memoriju. Prvi argument u funkciji scanf() je format podatka koji želite spremiti u memoriju. Drugi argument je memorijska adresa na koju treba spremiti unos podatka. Da biste dobili adresu u memoriji od neke varijable, morate koristiti adresni operator (&). Na primjer: &testVar će vratiti memorijsku adresu varijable u heksadekadskom zapisu. No ako koristimo polja onda ne moramo koristiti adresni operator već je dovoljno predati samo naziv polja BEZ adresnog operatora i operatora za pristup elementu na primjer: testVar.

Tablica preuzeta sa https://alvinalexander.com/programming/printf-format-cheat-sheet

Ako znate maksimalan broj iteracija koje će petlja izvesti onda možete koristiti for petlju. U ovome tutorijalu sam ograničio veličinu stringa na 8 (7 znakova + null znak) stoga i je korištena for petlja. No, kako želimo ispisati helpWord na igračev ekran (taj string će se sastojati od # znaka i pogođenih znakova) moramo prvo provjeriti pristupamo li stvarno znaku ili samo null znaku. Prema zadanim postavkama kada deklarirate string, on će biti ispunjen null znakovima (\0) koji također označava kraj niza. Osim što popunjavamo helpWord sa # znakovima, također trebamo i kopirati inputWord u fullWord kako bi igra znala koja je zadana riječ!

for(int i = 0; i < 8; i++)
{
	if(inputWord[i] != '\0')
	{
		helpWord[i] = '#';
		fullWord[i] = inputWord[i];
	}
}

Također možete tu for petlju pojednostaviti tako da ju napišete ovako i izbacite if provjeru prije kopiranja/postavljanja:

for(int i = 0; inputWord[i] != '\0'; i++)

Igračka petlja

Za igračku petlju ćemo koristiti do-while petlju jer želimo pokrenuti igru barem jednom, a zatim provjeriti je li igra završila. Igračka petlja ćemo odraditi sljedeće:

  • Prikazati vješala i čovjeka
  • Ispisati pomoćnu riječ
  • Zahtijevati unos znaka od igrača
  • Pretražiti cijelu riječ prema tom znaku
  • Odlučiti je li igrač pogodio ili nije
  • Odlučiti je li igra završila ili nije i tko je pobjednik
do
{
	system("cls");                                                        // system("clear") if you use Linux!
	PrintHangmanTree(playerErrors);
	printf("HELP: %s\n", helpWord);
	
	printf("Input character: ");
	inputLetter = getchar();	    
	if(inputLetter == 10)
	{
		continue;
	}
	
	int letterIndex = SearchGameWord(inputLetter, inputWord, strlen(inputWord));		    
	if(letterIndex != -1)
	{
		helpWord[letterIndex] = inputWord[letterIndex];
		inputWord[letterIndex] = '#';
		playerHits++;
		
		if(playerHits >= strlen(fullWord))
		{
			isGameWon = 1;
			isGameOver = 1;
		}
	}
	else
	{
		playerErrors++;
		
		if(playerErrors >= 6)
		{
			isGameOver = 1;
		}
	}
}
while(isGameOver == 0);

Uz scanf() funkciju možete koristiti i getchar() funkciju. getchar() funkcija uzima samo JEDAN znak prilikom korisničkog unosa. Za pretraživanje riječi za dano slovo napisat ćemo funkciju koja će se zvati SearchGameWord(). Ona će pomoću for petlje proći kroz cijeli string i provjeriti nalazi li se dano slovo unutar te riječi. Ako se zadano slovo nalazi unutar stringa ono će vratiti njegovu poziciju, u protivnome će vratiti -1 što označava da zadanog slova nema unutar riječi. Obično se koriste nula ili vrijednosti u minusu kako bi se označilo da nešto nije dobro završilo.

int SearchGameWord(char inputChar, char gameWord[], int wordSize)
{
	for(int i = 0; i < wordSize; i++)
	{
		if(gameWord[i] == inputChar)
		{
			return i;
		}
	}
	return -1;
}

Ako SearchGameWord() funkcija vrati broj veći od -1 program će zamijeniti '#' znak iz pomoćne riječi u pogođeno slovo na poziciji koja je vraćena. Zatim bi trebali ažurirati dvije glavne varijable isGameOver i isGameWon. isGameWon će biti 1 (inače se koriste booleani, no u C-u su booleani dodani tek u C99 standardu) samo ako igrač pogodi cijelu riječ! isGameOver s druge strane će biti 1 ako igrač pobjedi ili izgubi. Ako je rezultat SearchGameWord() funkcije -1 to znači da je igrač pogriješio i da se treba povećati playerErrors varijabla. Nakon što se završi s provjerom pogotka ili pogreške prikazuje se ažurirano vješalo i čovjek (morate nacrtati onoliko dijelova tijela koliko je pogrešaka). Igrač ima najviše 6 pokušaja (glava, 2x ruke, tijelo i 2x noge) da pogodi riječ. Kada se dosegne maksimalan broj pogrešaka isGameOver varijabla se postavlja na 1 i automatski se završava igračka petlja.

int letterIndex = SearchGameWord(inputLetter, inputWord, strlen(inputWord));		    
if(letterIndex != -1)
{
	helpWord[letterIndex] = inputWord[letterIndex];
	inputWord[letterIndex] = '#';
	playerHits++;
	
	if(playerHits >= strlen(fullWord))
	{
		isGameWon = 1;
		isGameOver = 1;
	}
}
else
{
	playerErrors++;
	
	if(playerErrors >= 6)
	{
		isGameOver = 1;
	}
}

Također svaku iteraciju morate očistiti konzolu i nanovo prikazati vješalo i čovjeka (kao u pravim video igrama)! Za to ćemo napisati jednostavnu PrintHangman() funkciju koja prikazuje vješalo i dijelove čovjeka ovisno o broju pogrešaka. Za to će se koristiti switch-case grananje kako bi se na najlakši način prikazao čovjek. U ovome slučaju ste mogli koristiti i if-else if-else no želio sam pokazati kako switch-case radi. Nakon svakog slučaja unutar switch-case grananja morate dodati i break naredbu što signalizira programu završetak slučaja, u protivnome će se izvesti i sljedeći slučajevi sve dok programski pokazivač ne dođe do kraja switch-case bloka ili break naredbe.

void PrintHangmanTree(int errors)
{
	printf("__________________\n");
	printf("|\t\t |\n");
	printf("|\t\t |\n");
	
	switch(errors)
	{
	    case 0:
	    {
        	printf("|\n|\n|\n|\n|\n|\n");
        	break;
	    }
	    case 1:
		{
			printf("|\t\t O\n");
			printf("|\n|\n|\n|\n|\n\n");
			break;
		}
		case 2:
		{
			printf("|\t\t O\n");
			printf("|\t\t/\n");
			printf("|\n|\n|\n|\n");
			break;
		}
		case 3:
		{
			printf("|\t\t O\n");
			printf("|\t\t/X\n");
			printf("|\n|\n|\n|\n");
			break;
		}
		case 4:
		{
			printf("|\t\t O\n");
			printf("|\t\t/X\\\n");
			printf("|\n|\n|\n|\n");
			break;
		}
		case 5:
		{
			printf("|\t\t O\n");
			printf("|\t\t/X\\\n");
			printf("|\t\t/\n");
			printf("|\n|\n|\n");
			break;
		}
		case 6:
		{
			printf("|\t\t O\n");
			printf("|\t\t/X\\\n");
			printf("|\t\t/ \\\n");
			printf("|\n|\n|\n");
			break;
		}
	}
}

Kada je igra završena konzola će se ponovno očistiti i ispisati informativnu poruku u kojoj će pisati je li igrač pobijedio ili izgubio i koliko je pokušaja potrošio.

system("cls");
if(isGameWon == 1)
{
	printf("You won!\n");
}	
PrintHangmanTree(playerErrors);
printf("WHOLE WORD: %s\n", fullWord);

I to je to! Nadam se da ste nešto naučili, ako imate neka pitanja postavite ih na službenim profilima Master Games studija. Što se tiče napisanog koda nemojte ga mrziti jer je on napisan prije desetak godina kada sam ja učio programirati i kada je Master Games bio san. Nisam želio puno toga mijenjati jer je on pravi primjer kako početnički kod izgleda. U redu je osjećati se "glupo" jer ne razumijete kako nešto radi (barem sam se ja tako osjećao), no ako se redovno trudite i učite nove stvari doći ćete jako daleko. Do sljedećeg čitanja

Podjeli post:
Matej Arlović's Avatar
Napisao:

Matej Arlović

Matej Arlović je osnivač Master Games studija. Matej je osnovao Master Games tijekom srednjoškolskih dana. Voli programiranje, video igre i glazbu. Test