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

       Wyjaśnienie

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
            ^
pk
     

Drugi 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ę
rozmiar() - podaje aktualny rozmiar danych umieszczonych w kolejce
zapisz() - zapisuje współrzędne x i y na końcu kolejki
czytaj() - odczytuje współrzędne x i y z początku kolejki

} 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
kierunek: 1 - w górę, 2 - w prawo, 3 - w dół, 4 - w lewo
zjadl - określa ile zjadł wąż. Przy każdym obiegu pętli wąż zwiększa swą długość o jeden segment, jeśli coś zjadł
pokarm - określa, co jest do zjedzenia na ekranie: 1..9 lub 0, jeśli wąż zjadł cyferkę

  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ę.



List do administratora Serwisu Edukacyjnego Nauczycieli I LO

Twój email: (jeśli chcesz otrzymać odpowiedź)
Temat:
Uwaga: ← tutaj wpisz wyraz  ilo , inaczej list zostanie zignorowany

Poniżej wpisz swoje uwagi lub pytania dotyczące tego rozdziału (max. 2048 znaków).

Liczba znaków do wykorzystania: 2048

 

W związku z dużą liczbą listów do naszego serwisu edukacyjnego nie będziemy udzielać odpowiedzi na prośby rozwiązywania zadań, pisania programów zaliczeniowych, przesyłania materiałów czy też tłumaczenia zagadnień szeroko opisywanych w podręcznikach.



   I Liceum Ogólnokształcące   
im. Kazimierza Brodzińskiego
w Tarnowie

©2017 mgr Jerzy Wałaszek

Dokument ten rozpowszechniany jest zgodnie z zasadami licencji
GNU Free Documentation License.