P017 - Prosta gra zręcznościowa - WĄŻ |
---|
Programy uruchomiono w środowisku Bloodshed Dev-C++ 4.9.9.2 |
Uwaga, program p017 wykorzystuje bibliotekę newconio, którą stworzyliśmy na wcześniejszych zajęciach koła. Do projektu należy dołączyć plik newconio.cpp oraz plik nagłówkowy newconio.h. Bez tych plików program nie uruchomi się.
// I Liceum Ogólnokształcące // im. K. Brodzińskiego // w Tarnowie //-------------------------- // Koło informatyczne 2006/7 //-------------------------- // Program: P017-01 //-------------------------- #include "newconio.h" #include <iostream> using namespace std; const int KMAX = 80 * 50; // maksymalny rozmiar kolejki // Definicja klasy obsługującej kolejkę //------------------------------------- class kolejka { private: int x[KMAX], y[KMAX]; // przechowuje współrzędne węża; int pk, kk; // początek i koniec kolejki public: void zeruj(); // zeruje kolejkę int rozmiar(); // podaje aktualny rozmiar kolejki void zapisz(int wx, int wy); // zapisuje współrzędne void czytaj(int &wx, int &wy); // czyta wpis z początku kolejki } k; // na podstawie definicji klasy deklarujemy zmienną k // Definicje metod klasy kolejka //------------------------------ // Zeruje kolejkę //--------------- void kolejka::zeruj() { pk = kk = 0; } // Zwraca aktualny rozmiar kolejki. Rozmiar jest zawsze // równy liczbie nieodczytanych wpisów do kolejki //----------------------------------------------------- int kolejka::rozmiar() { if(kk >= pk) return kk - pk; else return kk + KMAX - pk; } // Wpisuje na koniec kolejki współrzędne //-------------------------------------- void kolejka::zapisz(int wx, int wy) { x[kk] = wx; y[kk] = wy; kk = (kk + 1) % KMAX; } // Odczytuje współrzędne z początku kolejki //----------------------------------------- void kolejka::czytaj(int &wx, int &wy) { wx = x[pk]; wy = y[pk]; pk = (pk + 1) % KMAX; } // Wyświetla ekran tytułowy gry //----------------------------- void ekran_tytulowy() { textattr(7); clrscr(); textcolor(15); center(10, _pl("KOŁO INFORMATYCZNE '2007")); textcolor(14); center(12, "I LO w Tarnowie"); textcolor(12); center(14, _pl("W Ą Ż")); textcolor(7); center(49, _pl("--- Naciśnij dowolny klawisz ---")); while(!getch()) ; } // Wyświetla planszę gry //---------------------- void plansza() { textattr(0x17); clrscr(); frame(FRAME_DOUBLE,0x1e,0,0,79,48); fillrectattr(0x70,0,49,79,49); } // Główna pętla zdarzeń gry //------------------------- void jedz_i_rosnij() { int wx = 37, wy = 21, kierunek = 2, zjadl = 5, pokarm = 0; k.zeruj(); do { k.zapisz(wx,wy); putxy('@',0x1e,wx,wy); // losujemy cyferke do zjedzenia if(!pokarm) { int x,y; do { x = 1 + rand() % 78; y = 1 + rand() % 47; } while(getchxy(x,y) != ' '); pokarm = 1 + rand() % 9; putxy(TCHAR(pokarm + 48),0x1b,x,y); } delay(75); // wpływa na szybkość ruchu węża // obsługujemy sterowanie z klawiatury if(kbhit() && !getch()) { switch(getch()) { case 72 : kierunek = 1; break; // strzałka w górę case 77 : kierunek = 2; break; // strzałka w prawo case 80 : kierunek = 3; break; // strzałka w dół case 75 : kierunek = 4; break; // strzałka w lewo } } // wykonujemy ruch głową węża w zadanym kierunku putxy('O',0x1f,wx,wy); switch(kierunek) { case 1 : wy--; break; case 2 : wx++; break; case 3 : wy++; break; case 4 : wx--; break; } // sprawdzamy, czy głowa zjada cyferkę char znak = getchxy(wx,wy); if((znak >= '1') && (znak <= '9')) { putxy(' ',0x10,wx,wy); // zjadamy cyferkę zjadl += pokarm; pokarm = 0; } // jeśli wąż nie rośnie, usuwamy z planszy jego koniec if(zjadl) zjadl--; else { int x,y; k.czytaj(x,y); putxy(' ',0x10,x,y); } // na dole planszy wyświetlamy aktualną długość węża textattr(0x70); gotoxy(35,49); cout << _pl("DŁUGOŚĆ : ") << k.rozmiar(); } while(getchxy(wx,wy) == ' '); // kasujemy węża while(k.rozmiar()) { k.czytaj(wx,wy); putxy('X',0x10,wx,wy); } // czyścimy bufor klawiatury delay(500); while(kbhit()) while(!getch()); } // Funkcja sprawdza, czy gracz chce kontynuować rozgrywkę //------------------------------------------------------- bool dalsza_gra() { char klawisz; textattr(0xf4); center(48, _pl(" Jeśli chcesz zagrać jeszcze raz, naciśnij klawisz [T]. ")); while(!(klawisz = getch())) ; return (klawisz == 't') || (klawisz == 'T'); } // Program główny //--------------- main() { _cinit(); srand((unsigned)time(NULL)); fullscreen(true); cursoroff(); ekran_tytulowy(); do { plansza(); jedz_i_rosnij(); } while(dalsza_gra()); cursoron(); fullscreen(false); } |
Widok okienka konsoli
w uruchomionym programie
Gra Wąż (ang. Snake) pochodzi z końca lat 70-tych XX wieku. Gracz steruje wężem poruszającym się po planszy. Wąż zjada pojawiające się na planszy cyferki. Po zjedzeniu każdej cyferki wąż rośnie o tyle segmentów, ile wynosiła wartość zjedzonej cyferki. Wąż ginie, jeśli uderzy głową w barierkę lub w samego siebie - taki rachityczny gad...
Podstawowym problemem w tej grze jest uzyskanie poruszającego się węża. Zrobimy drobne oszustwo. Otóż ruch węża będzie realizowany w ten sposób, iż segment głowy zostanie dorysowany na początku węża, natomiast na końcu usuniemy segment końcowy. Pozostałe segmenty nie będą zmieniały swojego położenia - od głowy wąż przyrasta, a od ogona ubywa. Efekt jest dosyć zadowalający.
Skoro musimy kasować segmenty (zastępując je spacją), to należy zapamiętywać ich współrzędne x i y. Do tego celu wykorzystamy prostą strukturę danych, zwaną buforem lub kolejką cykliczną, którą zrealizujemy w formie klasy.
Klasa
Klasa w języku C++ jest typem danych zawierającym również definicje funkcji składowych, które operują na danych zdefiniowanych wewnątrz klasy. Jeśli chcemy utworzyć zmienną typu klasa, to najpierw należy zdefiniować typ klasy, czyli jakby określić plan, wg którego zmienna zostanie zbudowana. W języku C++ robimy to następująco:
... class nazwa_typu_klasy { private: definicje danych i funkcji prywatnych; public: definicje danych i funkcji publicznych; }; ...Klasa jest obiektem. Idea programowania obiektowego opiera się na ukrywaniu szczegółów działania obiektu przed resztą programu (np. chcąc słuchać radia nie muszę znać jego wewnętrznej budowy). Zatem obiekt w C++ może posiadać elementy prywatne, które deklarujemy w obrębie sekcji private. Dostęp do zadeklarowanych tutaj elementów posiadają tylko funkcje składowe tej klasy.
Na zewnątrz klasa udostępnia tzw. interfejs, który deklaruje się w sekcji public. Zdefiniowane tutaj dane i funkcje są dostępne dla reszty programu - są publiczne. Przy ich pomocy program komunikuje się z klasą (np. z radiem komunikuję się za pomocą dostępnych na zewnątrz przycisków i pokręteł). Takie podejście znacznie ułatwia symulację obiektów ze świata rzeczywistego. Programista nie musi posiadać szczegółowej wiedzy o wszystkich elementach klasy - musi jedynie wiedzieć, jak komunikować się z udostępnionymi publicznie elementami.
Klasa - kolejka
Kolejka buforuje wprowadzone dane. Składa się z tablicy, w której będziemy umieszczać wprowadzane elementy oraz dwa wskaźniki:
pk - wskaźnik początku kolejki - zawsze wskazuje element tablicy, do którego zapisano pierwszy element.
kk - wskaźnik końca kolejki - wskazuje element tablicy tuż za ostatnim elementem kolejki.W naszym przypadku zastosujemy dwie tablice x[ ] i y[ ] dla współrzędnych segmentów węża:
0 | 1 | 2 | . | . | . | . | . | . | . | . | . | n-3 | n-2 | n-1 | |
x[ ] : | x | x | x | x | x | x | |||||||||
y[ ] : | y | y | y | y | y | y | |||||||||
^ pk |
^ kk |
Dane zapisujemy na pozycji kk. Po zapisie kk jest zwiększane o 1. Jednak jeśli kk = n, to kk ustawiamy na 0. Dzięki temu kolejka zawsze przyjmie dane i nie zabraknie dla nich miejsca.
Odczyt zawsze wykonujemy z pozycji pk. Po odczycie pozycja pk, podobnie jak kk, musi być zwiększona o 1 i ustawiona na 0, jeśli osiągnie n. Odczyt jakby "goni" zapis.
Dodatkowo będziemy potrzebowali informacji o liczbie elementów w kolejce. Mogą być dwa przypadki:
pk ≤ kk 0 1 2 . . . . . . . . . n-3 n-2 n-1 rozmiar = kk - pk x[ ] : x x x x x x y[ ] : y y y y y y ^
pk^
kk
pk > kk 0 1 2 . . . . . . . . . n-3 n-2 n-1 rozmiar = kk + n - pk x[ ] : x x x x x x x x y[ ] : y y y y y y y y ^
kk^
pkDrugi przypadek występuje, gdy wskaźnik kk został przewinięty na początek tablicy. Dodanie do kk długości tablicy n spowoduje przejście do przypadku pierwszego - kk + n znów trafi przed pk.
Deklaracja typu klasy jest następująca:
class kolejka { |
Rozpoczynamy od słówka kluczowego class, za którym umieszczamy nazwę typu - u nas będzie to kolejka. |
private: int x[KMAX], y[KMAX]; int pk, kk; |
Tworzymy wewnętrzną sekcję private, w której umieszczamy dwie tablice dla współrzędnych x i y oraz wskaźniki pk i kk. Do tych danych będą miały dostęp tylko funkcje składowe klasy kolejka. Poza nimi pola prywatne nie są widoczne. W ten sposób ukrywamy wewnętrzną strukturę danych w klasie, gdyż informacja ta nie jest potrzebna programiście. |
public: void zeruj(); int rozmiar(); void zapisz(int wx, int wy); void czytaj(int &wx, int &wy); |
W sekcji public tworzymy interfejs klasy. Deklarujemy kilka funkcji,
poprzez które program będzie się komunikował ze strukturą danych
umieszczoną w klasie - to jakby przyciski i pokrętła we wspomnianym
radioodbiorniku. Użytkownik musi jedynie wiedzieć do czego służą, a nie
jak wewnątrz radioodbiornika są wykorzystywane. zeruj() - zeruje
kolejkę |
} k;
|
Na podstawie definicji klasy kolejka tworzymy zmienną obiektową k. |
Po zadeklarowaniu typu klasy musimy podać definicję wszystkich jej funkcji składowych. Funkcję składową klasy definiujemy jak zwykłą funkcję, lecz poprzedzamy jej nazwę nazwą klasy i operatorem zakresu ::. Funkcje składowe posiadają bezpośredni dostęp do wszystkich pól danej klasy.
void kolejka::zeruj() { pk = kk = 0; } |
Zerowanie kolejki polega na ustawieniu wskaźników pk i kk na 0 - wtedy oba wskazują pierwsze elementy obu tablic x[ ] i y[ ]. |
int kolejka::rozmiar() { if(kk >= pk) return kk - pk; else return kk + KMAX - pk; } |
Ilość elementów umieszczonych w kolejce obliczamy wg zasad opisanych powyżej rozważając dwa możliwe przypadki dla pk i kk. |
void kolejka::zapisz(int wx, int wy) { x[kk] = wx; y[kk] = wy; kk = (kk + 1) % KMAX; } |
Podane współrzędne umieszczamy na pozycji kk w odpowiednich tablicach, czyli dopisujemy na koniec kolejki. Następnie wskaźnik kk jest zwiększany o 1. Wynik jest poddawany operacji modulo KMAX (maksymalna długość kolejki, czyli rozmiar tablic x[ ] i y[ ]). Dzięki temu, po wyjściu poza ostatni element tablic, kk jest przewijane na początek - przyjmuje wartość 0. |
void kolejka::czytaj(int &wx, int &wy) { wx = x[pk]; wy = y[pk]; pk = (pk + 1) % KMAX; } |
Ta metoda odczytuje współrzędne z początku kolejki. Wskaźnik pk jest przesuwany o 1 pozycję dalej w kolejce. Jeśli wyjdzie poza ostatni element, to operacja modulo KMAX sprowadzi go z powrotem na początek tablic x[ ] i y[ ]. Zwróć uwagę, iż argumenty funkcji są przekazywane przez referencję - wskazanie. |
Główna pętla gry
Zadaniem głównej pętli gry jest obsługa wszystkich zdarzeń, które mogą się pojawić w trakcie gry. Użytkownik steruje kierunkiem ruchu węża, ponieważ wąż porusza się samodzielnie. Pętla kończy się, jeśli głowa węża uderzy w jakąkolwiek przeszkodę za wyjątkiem pokarmu, czyli cyferek 1...9.
void jedz_i_rosnij() { int wx = 37, wy = 21, kierunek = 2, zjadl = 5, pokarm = 0; |
Na początku funkcji deklarujemy kilka zmiennych, które będą używane
wewnątrz pętli. Jednocześnie inicjujemy wartości początkowe tych
zmiennych: wx,wy -
współrzędne głowy węża |
k.zeruj();
|
Przed wejściem do pętli zerujemy kolejkę. |
do
{
|
Rozpoczynamy pętlę obsługującą zdarzenia w grze. Pętla ta kończy się w momencie, gdy na pozycji głowy węża jest znak inny od spacji. |
k.zapisz(wx,wy); putxy('@',0x1e,wx,wy); |
Zapisujemy w kolejce pozycję głowy węża. Na pozycji tej umieszczamy znak głowy. |
if(!pokarm) { int x,y; do { x = 1 + rand() % 78; y = 1 + rand() % 47; } while(getchxy(x,y) != ' '); pokarm = 1 + rand() % 9; putxy(TCHAR(pokarm + 48),0x1b,x,y); } |
Sprawdzamy, czy na planszy gry znajduje się jakiś pokarm. Jeśli nie (pokarm=0), to wyznaczamy na planszy niezajętą przez węża pozycję x,y - losujemy x i y w obszarze planszy dotąd, aż pozycja x,y zawiera spację. Następnie losujemy pokarm w zakresie od 1 do 9 i na pozycji x,y umieszczamy wylosowaną cyferkę. |
delay(75);
|
Wprowadzamy opóźnienie. Wartość ta wpływa na szybkość węża. |
if(kbhit() && !getch()) switch(getch()) { case 72 : kierunek = 1; break; // w górę case 77 : kierunek = 2; break; // w prawo case 80 : kierunek = 3; break; // w dół case 75 : kierunek = 4; break; // w lewo } |
Sprawdzamy, czy naciśnięto klawisz, a jeśli tak, to czy jest to klawisz
kontrolny (pierwszy kod zwracany przez funkcję getch()
jest równy 0). Jeśli oba warunki są spełnione, odczytujemy kod
matrycowy klawisza kontrolnego (drugi kod zwracany
przez funkcję getch()) i w zależności od tego kodu modyfikujemy
odpowiednio zmienną kierunek. Widoczne w instrukcji switch kody matrycowe są kodami klawiszy kursora. |
putxy('O',0x1f,wx,wy); |
Ponieważ głowa węża się przesunie, na jej pozycji umieszczamy segment węża. |
switch(kierunek) { case 1 : wy--; break; case 2 : wx++; break; case 3 : wy++; break; case 4 : wx--; break; } |
W zależności od kierunku ruchu węża modyfikujemy odpowiednio współrzędne jego głowy. Po tej operacji współrzędne wx i wy wskazują nowe położenie głowy węża. |
char znak = getchxy(wx,wy); if((znak >= '1') && (znak <= '9')) { putxy(' ',0x10,wx,wy); // zjadamy cyferkę zjadl += pokarm; pokarm = 0; } |
Odczytujemy znak z planszy na pozycji głowy węża. Następnie sprawdzamy,
czy jest to cyferka 1...9. Jeśli tak, wymazujemy ją z planszy i dodajemy
jej wartość do zmiennej zjadl, po czym ją
zerujemy - spowoduje to wygenerowanie następnej cyferki w kolejnym
obiegu pętli. Wymazanie cyferki z ekranu jest konieczne, ponieważ pętla zdarzeń sprawdza, czy na pozycji głowy jest spacja. Jeśli nie, to gra się kończy. |
if(zjadl) zjadl--; else { int x,y; k.czytaj(x,y); putxy(' ',0x10,x,y); } |
Sprawdzamy, czy wąż coś zjadł. Jeśli tak, to zmienną
zjadł tylko zmniejszamy o 1. Ponieważ głowa węża się przesunęła,
to spowoduje to zwiększenie długości węża o 1. Jeśli zjadl jest równe 0, to odczytujemy z kolejki (z jej początku) współrzędne ostatniego segmentu węża i na tej pozycji umieszczamy spację. Da to efekt przesuwania się węża po planszy. |
textattr(0x70); gotoxy(35,49); cout << _pl("DŁUGOŚĆ : ") << k.rozmiar(); |
Na spodzie ekranu, pod planszą gry wypisujemy aktualną długość węża, czyli ilość elementów wprowadzonych do kolejki. |
} while(getchxy(wx,wy) == ' '); |
Warunkiem kontynuacji pętli jest, aby na pozycji głowy węża była spacja. |
while(k.rozmiar()) { k.czytaj(wx,wy); putxy('X',0x10,wx,wy); } |
Po wyjściu z pętli głównej odczytujemy z kolejki cyklicznej wszystkie współrzędne pozostałych segmentów węża i na ich pozycjach wypisujemy znaki X. |
delay(500);
|
Odczekujemy pół sekundy, aby gracz zwolnił klawiaturę. |
while(kbhit()) while(!getch()); } |
Jeśli w buforze klawiatury są jakieś znaki, usuwamy je i kończymy funkcję. |
![]() | I Liceum Ogólnokształcące |
Pytania proszę przesyłać na adres email: i-lo@eduinf.waw.pl
W artykułach serwisu są używane cookies. Jeśli nie chcesz ich otrzymywać,
zablokuj je w swojej przeglądarce.
Informacje dodatkowe