Gra saper na LCDku - jak napisac od podstaw

Projekty użytkowników forum zarówno sprzętowe, jak i związane z programowaniem w dowolnym języku.
Awatar użytkownika
dambo
Expert
Expert
Posty: 645
Rejestracja: czwartek 17 mar 2016, 17:12

Gra saper na LCDku - jak napisac od podstaw

Postautor: dambo » sobota 05 sie 2017, 19:26

Witam wszystkich 
Dziś mnie natchnęło na zrobienie prostej gierki – sapera, ale nie o samą grę tu chodzi, a o to, ze przy okazji przedstawię krok po kroku z przykładami jak taką grę napisać pokazując swoją metodę podchodzenia do czegoś takiego.

Założenia projektu:
- kod na tyle „uniwersalny”, żeby można było go dostosować do innych rzeczy
- wynikiem ma być w pełni funkcjonalna gra
- wyświetlanie pozostałej ilości bomb
- randomowe plansze – to w późniejszej wersji – wjedzie pewnie szum z ADC jako mechanizm generowania liczb pseudolosowych (czy wtedy już będą losowe?)

Jest to dość spory projekt – potrzebujemy więc plan działania co i jak.
1. wybór platformy sprzętowej
2. pierwotny wygląd planszy – funkcja rysująca
3. funkcje umożliwiające „zapełnienie” poszczególnej kratki jaką chcemy
4. funkcje umożliwiające przechodzenie pomiędzy poszczególnymi kratkami
5. sposób zapisu danych odnośnie planszy + funkcje
6. sposoby wyświetlania danych na planszy + funkcje
7. interakcje - czyli ogólnie gra - zaznaczanie pól, odkrywanie, wygrana/przegrana + funkcje
7. sposób przedstawienia wyniku/ilości pozostałych bomb itp. + funkcje


Roboty jest sporo więc zaczynamy.
1. wybór platformy sprzętowej

Tutaj w moim przypadku najlepszym wyborem jest płytka z projektu SUDOKU STM32 – zawiera mikrokontrolerek STM32F030C8T6, kolorowy wyświetlacz ILI9341 + przyciski. Dzięki temu też podstawowe funkcje miałem już napisane – teraz je poprzerabiałem na bardziej uniwersalne – więc nawet jakby ktoś chciał zrobić szachy na tej podstawie – nie ma problemu!

2. Pierwotny wygląd planszy – funkcja rysująca

Wszyscy zapewne wiemy jak wygląda saper. Plansza z kwadratami, jednak jej rozmiar nie jest do końca zdefiniowany. Pamiętając o założeniu z uniwersalnością najpierw zadeklarujmy sobie kilka rzeczy z których najważniejsze to ile pikseli ma mieć bok kratki, ile kratek ma być w osi X i ile w osi Y. Czyli prosty sposób:

Kod: Zaznacz cały

#define BOK_KRATKI 25

#define PLANSZA_X 9
#define PLANSZA_Y 11

#define PLANSZA_START_X 6
#define PLANSZA_START_Y 6

#define PLANSZA_GRUBOSC_LINII 3

#define KOLOR_OBRAMOWANIA ILI9341_BLUE


Możemy teraz napisać funkcję, która rysuje obramowania:

Kod: Zaznacz cały

// funkcja rysujaca obramowanie gry
void rysuj_obramowanie(void)
{
   // linie poziome
   for(uint8_t i = 0; i < PLANSZA_Y + 1; i++)
   {
      lcd_zapelnij_prostokat(   PLANSZA_START_X,
                        PLANSZA_START_X + i * BOK_KRATKI,
                        BOK_KRATKI * PLANSZA_X + PLANSZA_GRUBOSC_LINII,
                        PLANSZA_GRUBOSC_LINII,
                        KOLOR_OBRAMOWANIA);
   }

   // linie pionowe
   for(uint8_t i = 0; i < PLANSZA_X + 1; i++)
   {
      lcd_zapelnij_prostokat(   PLANSZA_START_X + i * BOK_KRATKI,
                        PLANSZA_START_Y,
                        PLANSZA_GRUBOSC_LINII,
                        BOK_KRATKI * PLANSZA_Y + PLANSZA_GRUBOSC_LINII,
                        KOLOR_OBRAMOWANIA);
   }
}


Efekt widzimy na zdjęciu:
20170805_183920.jpg



Efekt dla ustawień:

Kod: Zaznacz cały

#define PLANSZA_X 6
#define PLANSZA_Y 6

#define PLANSZA_START_X 30
#define PLANSZA_START_Y 30

Jest taki:

20170805_184211.jpg


Czyli ten punkt już ogarnięty!

3. funkcje umożliwiające „zapełnienie” poszczególnej kratki jaką chcemy

Co chcemy uzyskać – funkcje, która po podaniu numeru kratki i zawartości – zadba nam o jej wypełnienie. Dla ułatwienia wpiszmy teraz do niej jakąś cyferkę.
Wjeżdzaja kolejne definy – potrzebne do umiejscowienia liteki w kratce – czyli przesunięcie w osi X i Y od piksela z „początkiem” kratki:

Kod: Zaznacz cały

#define PRZESUNIECIE_TEKSTU_X 4
#define PRZESUNIECIE_TEKSTU_Y 4

Najwygodniej będzie, jeśli nasza funkja będzie przyjmować numer kratki, jej zawartość i kolor tła. Wygląda ona następująco:

Kod: Zaznacz cały

// funkcja wpisujaca w podana kratke okreslona liczbe
void zapelnij_kratke(uint8_t numer, uint8_t liczba,uint16_t kolor)
{
   char buff[2] = "a";
   buff[0] = liczba + 48;

   // zamalowanie poprzedniej zawartosci kratki
   lcd_zapelnij_prostokat(   PLANSZA_START_X + PLANSZA_GRUBOSC_LINII + (numer%PLANSZA_X) * BOK_KRATKI,
                     PLANSZA_START_Y + PLANSZA_GRUBOSC_LINII + (numer/PLANSZA_X) * BOK_KRATKI,
                     BOK_KRATKI - PLANSZA_GRUBOSC_LINII,
                     BOK_KRATKI - PLANSZA_GRUBOSC_LINII,
                     kolor);

   // wypelnienie literka
   lcd_pisz_tekst_16(      PLANSZA_START_X + PLANSZA_GRUBOSC_LINII + (numer%PLANSZA_X) * BOK_KRATKI + PRZESUNIECIE_TEKSTU_X,
                     PLANSZA_START_Y + PLANSZA_GRUBOSC_LINII + (numer/PLANSZA_X) * BOK_KRATKI + PRZESUNIECIE_TEKSTU_Y,
                     buff,
                     ILI9341_BLACK,
                     kolor);
}

Komentarze z kodu mówią same za siebie.

Przetestujmy teraz jej działanie za pomocą funkcji testowej:

Kod: Zaznacz cały

// funkcja testowa zapelniajaca kolejne komorki planszy
void test_zapelnienia()
{
   uint8_t liczba = 0;
   uint8_t kratka = 0;
   while(1)
   {
      zapelnij_kratke(kratka,liczba,ILI9341_CYAN);
      liczba++;
      kratka++;

      if( liczba == 10 )
      {
         liczba = 0;
      }

      if(kratka == PLANSZA_X * PLANSZA_Y)
      {
         kratka = 0;
      }

      _delay_ms(200);

   }
}

Efekt – na filmiku:

https://youtu.be/M6DcSE_EoiE

Jak widać – wszystko działa ok! nawet dla różnych rozmiarów planszy.
4. funkcje umożliwiające przechodzenie pomiędzy poszczególnymi kratkami
Co chcemy uzyskać – możliwość przechodzenia pomiędzy kolejnymi kratkami za pomocą „strzałek na płytce”. Wiadomo strzałka w górę – do góry itp.
Wprowadźmy kilka zmiennych globalnych (globalnych dla wygody):

Kod: Zaznacz cały

// numer wybranej kostki - pozycja "kursora" na planszy - tam gdzie aktualnie się znajdujemy
uint8_t wybrana_kostka = 0;

// numer kostki z ktorej zrobilismy ruch
uint8_t poprzednia_kostka;

Zróbmy jedną funkcję ruchu, która jako parametr pobierze kierunek ruchu. Ułatwmy sobie sprawę enumem:

enum{ruch_lewo, ruch_prawo, ruch_gora,ruch_dol};

I tak wygląda nasza funkcja:

Kod: Zaznacz cały

// funkcja wyliczajaca nastepna pozycje kratki
void ruch_po_planszy( uint8_t kierunek )
{
   switch( kierunek )
   {
   case ruch_lewo:
   {
      poprzednia_kostka = wybrana_kostka;
      wybrana_kostka--;
      if(wybrana_kostka == 255)
      {
         wybrana_kostka = ( PLANSZA_X * PLANSZA_Y ) - 1;
      }
      break;
   }

   case ruch_prawo:
   {
      poprzednia_kostka = wybrana_kostka;
      wybrana_kostka++;
      if(wybrana_kostka == ( PLANSZA_X * PLANSZA_Y ))
      {
         wybrana_kostka = 0;
      }
      break;
   }

   case ruch_gora:
   {
      poprzednia_kostka = wybrana_kostka;
      wybrana_kostka -= PLANSZA_X;
      if(wybrana_kostka > 200)
      {
         wybrana_kostka = poprzednia_kostka + ( PLANSZA_X * ( PLANSZA_Y - 1 ) );
      }
      break;
   }

   case ruch_dol:
   {
      poprzednia_kostka = wybrana_kostka;
      wybrana_kostka += PLANSZA_X;
      if(wybrana_kostka > ( PLANSZA_X * PLANSZA_Y ) - 1)
      {
         wybrana_kostka = wybrana_kostka%PLANSZA_X;
      }
      break;
   }

   }
}

Teraz kwestia pożenienia tego ze sprzętem – akurat ja stosuję bibliotekę do obsługi przycisków, która wywołuje podpięte callbacki do eventów przycisków. Musimy więc dla każdego przycisku napisać osobną funkcję:

Kod: Zaznacz cały

void button_left()
{
   ruch_po_planszy( ruch_lewo );
   zamaluj_pola_po_ruchu();
}

void button_right()
{
   ruch_po_planszy( ruch_prawo );
   zamaluj_pola_po_ruchu();
}

void button_up()
{
   ruch_po_planszy( ruch_gora );
   zamaluj_pola_po_ruchu();
}

void button_down()
{
   ruch_po_planszy( ruch_dol );
   zamaluj_pola_po_ruchu();
}

I podpiąć je do przycisków:

Kod: Zaznacz cały

   // przypisanie funkcji do przyciskow
   button_ustaw(0,5,200,50,button_up,   NULL );
   button_ustaw(1,5,200,50,button_left,   NULL );
   button_ustaw(2,5,200,50,button_ok,   NULL );
   button_ustaw(3,5,200,50,button_right,NULL );
   button_ustaw(4,5,200,50,button_down,   NULL );


Wyżej użyliśmy funkcji “zamaluj_pola_po_ruchu();” – co ona robi – dla pozycji ze zmiennej „wybrana_kostka” wpisuje cyferkę „1”, natomiast dla poprzedniej wpisuje cyferkę „0” – wtedy ładnie przetestujemy działanie.
Tak ona wygląda:

Kod: Zaznacz cały

void zamaluj_pola_po_ruchu()
{
   zapelnij_kratke(poprzednia_kostka,0,ILI9341_WHITE);
   zapelnij_kratke(wybrana_kostka,1,ILI9341_YELLOW);
}

Uruchamiamy całość i oto efekt:

https://youtu.be/S8M40DbdZ1Y

W sumie mamy już nasz „silnik gry” dalej skupimy się bardziej na logice gry.

CDN... komentarze uwagi itp mile widziane :) kody udostępnie na samym końcu
Nie masz wymaganych uprawnień, aby zobaczyć pliki załączone do tego posta.
Nowy blog o tematyce embedded -> https://www.embedownik.pl/

SuperGość
Uber Geek
Uber Geek
Posty: 2346
Rejestracja: piątek 04 wrz 2015, 09:03

Re: Gra saper na LCDku - jak napisac od podstaw

Postautor: SuperGość » sobota 05 sie 2017, 19:42

no no - z zainteresowaniem czytam ....

Awatar użytkownika
dambo
Expert
Expert
Posty: 645
Rejestracja: czwartek 17 mar 2016, 17:12

Re: Gra saper na LCDku - jak napisac od podstaw

Postautor: dambo » niedziela 06 sie 2017, 01:54

No to kolejna część:
Jednocześnie działamy nad:
5. sposób zapisu danych odnośnie planszy + funkcje
6. sposoby wyświetlania danych na planszy + funkcje

Czas na przemyślenie zasady logiki gry, zaczynając od sposobu przechowania informacji.
W saperze najważniejsze są bomby – musimy jakoś ładnie przechować ich pozycję. Proponuję tu zwykłą tablicę o rozmiarze ilości pól. Jeśli jest tak „1” – to jest mina, „0” oznacza „czystą”. Oczywiście trochę to nadmiarowe, ale uprości inne funkcje, a RAMu mamy sporo.

Kod: Zaznacz cały

// tablica z pozycjami bomb
uint8_t tablica_bomb[ PLANSZA_Y][ PLANSZA_X ];

Zrobiłem ją jako dwuwymiarową tablicę – tu spoiler – dzięki temu łatwiejsza będzie funkcja licząca “bomby w sąsiedztwie”. Czasem przyda się też dostęp jednowymiarowy i mam do tego osobny wskaźnik:

Kod: Zaznacz cały

uint8_t *tablica_bomb_2d;

Zróbmy sobie kilka testowych funkcji, które zapełnią nam tablicę bomb:

Kod: Zaznacz cały

void test_uzupelnij_tablice_bomb()
{
   for(uint8_t i = 0; i < PLANSZA_X * PLANSZA_Y; i++)
   {
      tablica_bomb_2d[i] = rand() & ~(0xfe);
   }
}

void test_uzupelnij_tablice_bomb2()
{
   for(uint8_t i = 0; i < PLANSZA_X * PLANSZA_Y; i++)
   {
      tablica_bomb_2d[i] =( rand() >> 1) & ~(0xfe);
   }
}

void test_uzupelnij_tablice_bomb3()
{
   for(uint8_t i = 0; i < PLANSZA_X * PLANSZA_Y; i++)
   {
      tablica_bomb_2d[i] =( rand() >> 2) & ~(0xfe);
   }
}

void test_uzupelnij_tablice_bomb4()
{
   for(uint8_t i = 0; i < 20; i++)
   {
      tablica_bomb_2d[rand()% (PLANSZA_X * PLANSZA_Y)] = 1;
   }
}

Teraz czas na wyświetlenie tego, też w formie testowej:

Kod: Zaznacz cały

void test_wyswietl_tablice_bomb()
{
   for(uint8_t i = 0; i < PLANSZA_X * PLANSZA_Y; i++)
   {
      if( tablica_bomb_2d[i] )
      {
         zapelnij_kratke(i,1,ILI9341_RED);
      }
      else
      {
         zapelnij_kratke(i,0,ILI9341_GREEN);
      }
   }
}

Tak to wygląda:
20170806_013032.jpg

Dobrze. Mamy zapisane pozycje bomb, ale same w sobie nam nic nie dają. Pomyślmy jakie są możliwości zawartości pól w grze typu saper:
- zwykła kratka niekliknięta
- kratka z kursorem
- MINA
- kratka ze znakiem zapytania
- kratka z rozbrojona miną
- kratka z numerkiem oznaczającym ilość bomb w sąsiedztwie
Te informacje też warto przechować sobie w tablicy. Przyjmijmy taką metodę zapisu, że każda komórka ma 1 bajt danych.
- cztery najmłodsze bity – „numer bomb w sąsiedztwie” tu uwagi małe – „0” oznacza pustą kratkę, „f” – wykorzystamy jako oznaczenie bomby w tym miejscu
- następnie informacja, czy kratka jest odkryta, czy nie
- czy jest znakiem zapytania
- czy jest rozbrojona bomba
Został nam 1 bit – chciałem dać tu właściwość „czy dana kratka jest wybrana”, ale mamy to w zmiennej „wybrana_kostka” więc nie potrzebujemy redundancji.
Tablica z naszymi danymi oraz makra dla wyłuskania stanów pól:

Kod: Zaznacz cały

// tablica z zawartoscia komorki
uint8_t tablica_kratek[ PLANSZA_X * PLANSZA_Y ];
#define POLE_BOMBA 0x0f
#define POLE_SASIEDZI_MASKA 0x0f
#define POLE_ZASLONIETE ( 1 << 4 )
#define POLE_PYTAJNIK   ( 1 << 5 )
#define POLE_ROZBROJONA ( 1 << 6 )

Teraz musimy napisać funkcję, która utworzy nam tablicę_kratek z tablicy bomb, wypełniając ją np. liczbami bomb w sąsiedztwie…
Aaale jeszcze nie mamy wyznaczonych tych liczb – zajmie się tym osobna funkcja:

Kod: Zaznacz cały

// przerobilem swoj algorytm napisany na potrzeby codefights.com
// w sumie to od niego wzial sie pomysl na napisanie tej gierki
uint8_t policz_miny( uint8_t numer_pola)
{
   // zmienne z wymiarami tablicy
    uint8_t wiersze = PLANSZA_Y;
    uint8_t kolumny = PLANSZA_X;

    // suma bomb w okolicy
    uint8_t wynik = 0;

    // pozycja w tablicy
    uint8_t i = numer_pola/PLANSZA_X;

    uint8_t j = numer_pola%PLANSZA_X;

   uint8_t suma = 0;

   // niestety - trzeba bedzie sprawdzic mnostwo warunkow brzegowych
   // nie bede ich laczyl tylko rozbije
   //
   // lewy gorny piksel
   if( i > 0 && j > 0 )
   {
      if(tablica_bomb[i-1][j-1])
      {
         suma++;
      }
   }

   // lewy piksel
   if( j > 0 )
   {
      if(tablica_bomb[i][j-1])
      {
         suma++;
      }
   }

   // lewy dolny piksel
   if( i < wiersze - 1 && j > 0 )
   {
      if(tablica_bomb[i+1][j-1])
      {
         suma++;
      }
   }

   // prawy gorny piksel
   if( i > 0 && j < kolumny - 1 )
   {
      if(tablica_bomb[i-1][j+1])
      {
         suma++;
      }
   }

   // prawy piksel
   if( j < kolumny - 1 )
   {
      if(tablica_bomb[i][j+1])
      {
         suma++;
      }
   }

   // prawy dolny piksel
   if( i < wiersze - 1 && j < kolumny - 1 )
   {
      if(tablica_bomb[i+1][j+1])
      {
         suma++;
      }
   }

   // gorny piksel
   if( i > 0 )
   {
      if(tablica_bomb[i-1][j])
      {
         suma++;
      }
   }

   // dolny piksel
   if( i < wiersze - 1 )
   {
      if(tablica_bomb[i+1][j])
      {
         suma++;
      }
   }

    return suma;
}

Jak w komentarzu – pisałem ja jako zadanie w codefights.com.
Czyli dopiero teraz – konwerter tablicy bomb na tablicę kratek:

Kod: Zaznacz cały

// funkcja tworzy tablice kratek na podstawie tablicy bomb
void utworz_tablice_kratek()
{
   for( uint8_t i = 0; i < PLANSZA_X * PLANSZA_Y; i++ )
   {
      if(tablica_bomb_2d[i] == 1)
      {
         tablica_kratek[i] = POLE_BOMBA | POLE_ZASLONIETE;
      }
      else
      {
         tablica_kratek[i] = policz_miny(i) | POLE_ZASLONIETE;
      }
   }
}

Przy okazji zasłaniamy wszystkie pola. Wyświetlmy sobie to i zobaczmy jak działa:

Kod: Zaznacz cały

void test_wyswietl_ilosci_bomb()
{
   for( uint8_t i = 0; i < PLANSZA_X * PLANSZA_Y; i++ )
   {
      if(tablica_bomb_2d[i] == 1)
      {
         zapelnij_kratke(i,9,ILI9341_RED);
      }
      else
      {
         zapelnij_kratke(i,tablica_kratek[i] & POLE_SASIEDZI_MASKA, ILI9341_GREEN );
      }
   }
}

Efekt:
20170806_013208.jpg


Miodek. Tutaj przyznam się szczerze nie miałem do końca pomysłu co najlepiej ruszyć dalej. Skupiłem się na funkcji, która rysuje poszczególne kratki. W jej środku sprawdzamy, czy jest to znak zapytania, numer, czy coś innego.
AAAA – kwestia grafik w grze – to zostawiamy na koniec! Teraz znak zapytania to znak zapytania, mina to „M”, rozbrojona to „R”.

Kod: Zaznacz cały

void zapal_kratke_saper(uint8_t numer)
{
   char buff[2] = "a";
   buff[0] = 1 + 48;



   // teraz podejmujemy akcje w zaleznosci od typu zawartosci kratki

   // jesli kratka jest "wybrana" - tlo od "wybranej" komorki
   if( numer == wybrana_kostka )
   {
      // zamalowanie poprzedniej zawartosci kratki
      lcd_zapelnij_prostokat(   PLANSZA_START_X + PLANSZA_GRUBOSC_LINII + (numer%PLANSZA_X) * BOK_KRATKI,
                        PLANSZA_START_Y + PLANSZA_GRUBOSC_LINII + (numer/PLANSZA_X) * BOK_KRATKI,
                        BOK_KRATKI - PLANSZA_GRUBOSC_LINII,
                        BOK_KRATKI - PLANSZA_GRUBOSC_LINII,
                        ILI9341_CYAN);

      // jesli pole jest zasloniete:
      if( tablica_kratek[numer] & POLE_ZASLONIETE )
      {
         // zamalowanie wartosci kratki
         lcd_zapelnij_prostokat(   PLANSZA_START_X + PLANSZA_GRUBOSC_LINII + (numer%PLANSZA_X) * BOK_KRATKI,
                           PLANSZA_START_Y + PLANSZA_GRUBOSC_LINII + (numer/PLANSZA_X) * BOK_KRATKI,
                           BOK_KRATKI - PLANSZA_GRUBOSC_LINII,
                           BOK_KRATKI - PLANSZA_GRUBOSC_LINII,
                           ILI9341_CYAN);
      }
      else
      {
         // jesli kratka ma "cos w sobie"

         // znak zapytania
         if( tablica_kratek[numer] & POLE_PYTAJNIK )
         {
            lcd_pisz_tekst_16(      PLANSZA_START_X + PLANSZA_GRUBOSC_LINII + (numer%PLANSZA_X) * BOK_KRATKI + PRZESUNIECIE_TEKSTU_X,
                              PLANSZA_START_Y + PLANSZA_GRUBOSC_LINII + (numer/PLANSZA_X) * BOK_KRATKI + PRZESUNIECIE_TEKSTU_Y,
                              "?ds",
                              ILI9341_BLACK,
                              ILI9341_CYAN);
            return;
         }

         // znak bomby - jako rozbrojona
         if( tablica_kratek[numer] & POLE_ROZBROJONA )
         {
            lcd_pisz_tekst_16(      PLANSZA_START_X + PLANSZA_GRUBOSC_LINII + (numer%PLANSZA_X) * BOK_KRATKI + PRZESUNIECIE_TEKSTU_X,
                              PLANSZA_START_Y + PLANSZA_GRUBOSC_LINII + (numer/PLANSZA_X) * BOK_KRATKI + PRZESUNIECIE_TEKSTU_Y,
                              "R",
                              ILI9341_BLACK,
                              ILI9341_CYAN);
            return;
         }

         // mina:
         if( tablica_kratek[numer] == POLE_BOMBA )
         {
            lcd_pisz_tekst_16(      PLANSZA_START_X + PLANSZA_GRUBOSC_LINII + (numer%PLANSZA_X) * BOK_KRATKI + PRZESUNIECIE_TEKSTU_X,
                              PLANSZA_START_Y + PLANSZA_GRUBOSC_LINII + (numer/PLANSZA_X) * BOK_KRATKI + PRZESUNIECIE_TEKSTU_Y,
                              "M",
                              ILI9341_BLACK,
                              ILI9341_CYAN);
            return;
         }

         // ilosc sasiadow:
         buff[0] = (tablica_kratek[numer] & POLE_SASIEDZI_MASKA) + '0';
         lcd_pisz_tekst_16(      PLANSZA_START_X + PLANSZA_GRUBOSC_LINII + (numer%PLANSZA_X) * BOK_KRATKI + PRZESUNIECIE_TEKSTU_X,
                           PLANSZA_START_Y + PLANSZA_GRUBOSC_LINII + (numer/PLANSZA_X) * BOK_KRATKI + PRZESUNIECIE_TEKSTU_Y,
                           buff,
                           ILI9341_BLACK,
                           ILI9341_CYAN);
         return;
      }
   }
   else // jesli jest to jakas randomowa kostka - tlo "standardowe"
   {
      // zamalowanie poprzedniej zawartosci kratki
      lcd_zapelnij_prostokat(   PLANSZA_START_X + PLANSZA_GRUBOSC_LINII + (numer%PLANSZA_X) * BOK_KRATKI,
                        PLANSZA_START_Y + PLANSZA_GRUBOSC_LINII + (numer/PLANSZA_X) * BOK_KRATKI,
                        BOK_KRATKI - PLANSZA_GRUBOSC_LINII,
                        BOK_KRATKI - PLANSZA_GRUBOSC_LINII,
                        ILI9341_GREEN);

      // jesli pole jest zasloniete:
      if( tablica_kratek[numer] & POLE_ZASLONIETE )
      {
         // zamalowanie wartosci kratki
         lcd_zapelnij_prostokat(   PLANSZA_START_X + PLANSZA_GRUBOSC_LINII + (numer%PLANSZA_X) * BOK_KRATKI,
                           PLANSZA_START_Y + PLANSZA_GRUBOSC_LINII + (numer/PLANSZA_X) * BOK_KRATKI,
                           BOK_KRATKI - PLANSZA_GRUBOSC_LINII,
                           BOK_KRATKI - PLANSZA_GRUBOSC_LINII,
                           ILI9341_GREEN);
      }
      else
      {
         // jesli kratka ma "cos w sobie"

         // znak zapytania
         if( tablica_kratek[numer] & POLE_PYTAJNIK )
         {
            lcd_pisz_tekst_16(      PLANSZA_START_X + PLANSZA_GRUBOSC_LINII + (numer%PLANSZA_X) * BOK_KRATKI + PRZESUNIECIE_TEKSTU_X,
                              PLANSZA_START_Y + PLANSZA_GRUBOSC_LINII + (numer/PLANSZA_X) * BOK_KRATKI + PRZESUNIECIE_TEKSTU_Y,
                              "?",
                              ILI9341_BLACK,
                              ILI9341_GREEN);
            return;
         }

         // znak bomby - jako rozbrojona
         if( tablica_kratek[numer] & POLE_ROZBROJONA )
         {
            lcd_pisz_tekst_16(      PLANSZA_START_X + PLANSZA_GRUBOSC_LINII + (numer%PLANSZA_X) * BOK_KRATKI + PRZESUNIECIE_TEKSTU_X,
                              PLANSZA_START_Y + PLANSZA_GRUBOSC_LINII + (numer/PLANSZA_X) * BOK_KRATKI + PRZESUNIECIE_TEKSTU_Y,
                              "R",
                              ILI9341_BLACK,
                              ILI9341_GREEN);
            return;
         }

         // mina rozbrojona:
         if( tablica_kratek[numer] & POLE_ROZBROJONA )
         {
            lcd_pisz_tekst_16(      PLANSZA_START_X + PLANSZA_GRUBOSC_LINII + (numer%PLANSZA_X) * BOK_KRATKI + PRZESUNIECIE_TEKSTU_X,
                              PLANSZA_START_Y + PLANSZA_GRUBOSC_LINII + (numer/PLANSZA_X) * BOK_KRATKI + PRZESUNIECIE_TEKSTU_Y,
                              "M",
                              ILI9341_BLACK,
                              ILI9341_GREEN);
            return;
         }

         // mina:
         if( tablica_kratek[numer] == POLE_BOMBA )
         {
            lcd_pisz_tekst_16(      PLANSZA_START_X + PLANSZA_GRUBOSC_LINII + (numer%PLANSZA_X) * BOK_KRATKI + PRZESUNIECIE_TEKSTU_X,
                              PLANSZA_START_Y + PLANSZA_GRUBOSC_LINII + (numer/PLANSZA_X) * BOK_KRATKI + PRZESUNIECIE_TEKSTU_Y,
                              "M",
                              ILI9341_BLACK,
                              ILI9341_GREEN);
            return;
         }

         // ilosc sasiadow:

         buff[0] = (tablica_kratek[numer] & POLE_SASIEDZI_MASKA) + '0';
         lcd_pisz_tekst_16(      PLANSZA_START_X + PLANSZA_GRUBOSC_LINII + (numer%PLANSZA_X) * BOK_KRATKI + PRZESUNIECIE_TEKSTU_X,
                           PLANSZA_START_Y + PLANSZA_GRUBOSC_LINII + (numer/PLANSZA_X) * BOK_KRATKI + PRZESUNIECIE_TEKSTU_Y,
                           buff,
                           ILI9341_BLACK,
                           ILI9341_GREEN);
         return;

      }
   }
}

Sprawdźmy, czy działa. Pamiętajmy, ze trzeba „odsłonić” wszystkie komórki + dla testu kilka dałem jako rozbrojone i znaki zapytania. Możemy napisać główna funkcję wyświetlającą planszę:

Kod: Zaznacz cały

// funkcja rysujaca plansze na podstawie tablicy kratek
void wyswietl_plansze()
{
   for( uint8_t i = 0; i < PLANSZA_X * PLANSZA_Y; i++)
   {
      zapal_kratke_saper(i);
   }
}

Po niej otrzymamy cały ekran zamalowany + zaznaczony kursor.
Żeby lepiej przetestować używamy czegoś takiego:

Kod: Zaznacz cały

void test_wyswietl_plansze()
{

   for( uint8_t i = 0; i < PLANSZA_X * PLANSZA_Y; i++)
   {
      // odslaniamy wszystkie pola
      tablica_kratek[i] = (tablica_kratek[i]) & ~(POLE_ZASLONIETE);
      zapal_kratke_saper(i);
   }
}

Efekt:
20170806_013417.jpg


Dodajmy kilka rozbrojonych i znaków zapytania:

Kod: Zaznacz cały

void test_wyswietl_plansze()
{
   tablica_kratek[0] |= POLE_BOMBA;
   tablica_kratek[1] |= POLE_BOMBA;
   tablica_kratek[2] |= POLE_PYTAJNIK;
   tablica_kratek[3] |= POLE_PYTAJNIK;

   for( uint8_t i = 0; i < PLANSZA_X * PLANSZA_Y; i++)
   {
      // odslaniamy wszystkie pola
      tablica_kratek[i] = (tablica_kratek[i]) & ~(POLE_ZASLONIETE);
      zapal_kratke_saper(i);
   }
}

Efekt:
20170806_013503.jpg

Względem poprzedniego obrazka - zmieniło się w pierwszych 4 kratkach - są 2 miny i 2 znaki zapytania.

Teraz możemy sobie stworzyć funkcje „nowa gra” która przygotowuje wszystko dla nas:

Kod: Zaznacz cały

void nowa_gra()
{
   // zerowanie pozycji
   wybrana_kostka = 0;
   
   rysuj_obramowanie();

   test_uzupelnij_tablice_bomb4();

   utworz_tablice_kratek();
   
   wyswietl_plansze();
}

Czyli dodaliśmy:
- system zapisywania informacji o bombach i o kratkach
- funkcje przeliczającą ilość sąsiednich bomb
- prototypowe wyświetlanie pobranych z komórek
- funkcje odpalającą naszą grę i wykonującą komplet niezbędnych funkcji - kod główny programu będzie "czystszy" :)

Trochę już zrobione i nie było wcale trudno - ale już wiem co będzie najtrudniejsze - motyw, gdy "klikniemy" na pole z numerkiem 0 czyli tzn "puste" i wtedy trzeba odsłonić wszystkie pola do niego przyległe z cyferkami. Będzie to zdecydowanie najtrudniejsze, ale nie jakieś ogólnie mega trudne :) pewnie jakaś ładna rekurencja wjedzie :)
Nie masz wymaganych uprawnień, aby zobaczyć pliki załączone do tego posta.
Nowy blog o tematyce embedded -> https://www.embedownik.pl/

Awatar użytkownika
dambo
Expert
Expert
Posty: 645
Rejestracja: czwartek 17 mar 2016, 17:12

Re: Gra saper na LCDku - jak napisac od podstaw

Postautor: dambo » niedziela 06 sie 2017, 20:17

No to lecimy dalej dziś robimy:

7. interakcje - czyli ogólnie gra - zaznaczanie pól, odkrywanie, wygrana/przegrana + funkcje
Temat mega do wcześniejszego przemyślenia, żeby potem nie poprawiać funkcji itp.
Chodzenie po planszy już mamy załatwione. Teraz czas na inne interakcje użytkownika. Wypiszmy jakie są możliwe:
- odsłonięcie komórki
- zaznaczenie jako mina „rozbrojona”
- zaznaczenie jako znak zapytania
- skoro jakoś można je zaznaczyć to też trzeba je odznaczyć

Co mamy do dyspozycji z hardware – przycisk OK – musimy go wykorzystać do kilku funkcji. Oczywiście można to zrobić na mnóstwo sposobów – moja propozycja jest taka:

Poruszamy się normalnie po planszy strzałkami, a za pomocą „OK” wchodzimy w „tryb edycji” danej komórki. Musimy ten tryb jakoś oznaczyć – jakiś inny kolorek wybranego pola + zmienna z informacją o tym. Gdy jesteśmy w trybie edycji przyciski strzałek będą miały inne funkcje:
- up – odsłonięcie komórki
- down – wybranie komórki jako „rozbrojona mina”
- left – znak zapytania
- prawo – wyjście z tego trybu edycji

Dodatkowo jeśli wjedziemy na komórkę ze znakiem zapytania no to możemy ją zmienić na „rozbrojoną minę” albo odsłonić. Nie będę tego jakoś blokował – użytkownik musi sam się pilnować w tej kwestii.

Generalnie oznaczanie jako znak zapytania i rozbrojenie to trywialne kwestie. Chociaż – zaznaczanie min jako rozbrojonych powinno zmniejszać licznik min – trzeba go zaimplementować jak również jego dekrementację i wyświetlanie.

Pomyślmy co się może stać po odsłonięciu kratki:
- jeśli jest tam cyfra 1-8 – wyświetlamy ją ładnie
- jeśli jest tam mina – wyświetlamy ją i kończymy grę – potem możemy zrobić jakąś ładną animację na koniec itp.
- teraz najbardziej problematyczna sprawa – odsłaniamy pole z numerem 0 – wtedy w saperze odsłaniają się wszystkie przyległe do tego pola kratki z tą samą liczbą.

W sumie teraz „tylko” przemyśleliśmy działanie, ale jest to bardzo ważne. W następnym wpisie przystąpimy do okodzenia tego - czyli podpunkt "+ funkcje"

Jakieś sugestie odnośnie działania?
Nowy blog o tematyce embedded -> https://www.embedownik.pl/

Awatar użytkownika
dambo
Expert
Expert
Posty: 645
Rejestracja: czwartek 17 mar 2016, 17:12

Re: Gra saper na LCDku - jak napisac od podstaw

Postautor: dambo » niedziela 06 sie 2017, 23:33

No to lecimy dalej dziś robimy:
7. interakcje - czyli ogólnie gra - zaznaczanie pól, odkrywanie, wygrana/przegrana + funkcje
Temat mega do wcześniejszego przemyślenia, żeby potem nie poprawiać funkcji itp.
Chodzenie po planszy już mamy załatwione. Teraz czas na inne interakcje użytkownika. Wypiszmy jakie są możliwe:
- odsłonięcie komórki
- zaznaczenie jako mina „rozbrojona”
- zaznaczenie jako znak zapytania
- skoro jakoś można je zaznaczyć to też trzeba je odznaczyć

Co mamy do dyspozycji z hardware – przycisk OK – musimy go wykorzystać do kilku funkcji. Oczywiście można to zrobić na mnóstwo sposobów – moja propozycja jest taka:
Poruszamy się normalnie po planszy strzałkami, a za pomocą „OK” wchodzimy w „tryb edycji” danej komórki. Musimy ten tryb jakoś oznaczyć – jakiś inny kolorek wybranego pola + zmienna z informacją o tym. Gdy jesteśmy w trybie edycji przyciski strzałek będą miały inne funkcje:
- up – odsłonięcie komórki
- down – wybranie komórki jako „rozbrojona mina”
- left – znak zapytania
- prawo – wyjście z tego trybu edycji
Dodatkowo jeśli wjedziemy na komórkę ze znakiem zapytania no to możemy ją zmienić na „rozbrojoną minę” albo odsłonić. Nie będę tego jakoś blokował – użytkownik musi sam się pilnować w tej kwestii.
Generalnie oznaczanie jako znak zapytania i rozbrojenie to trywialne kwestie. Chociaż – zaznaczanie min jako rozbrojonych powinno zmniejszać licznik min – trzeba go zaimplementować jak również jego dekrementację i wyświetlanie.
Pomyślmy co się może stać po odsłonięciu kratki:
- jeśli jest tam cyfra 1-8 – wyświetlamy ją ładnie
- jeśli jest tam mina – wyświetlamy ją i kończymy grę – potem możemy zrobić jakąś ładną animację na koniec itp.
- teraz najbardziej problematyczna sprawa – odsłaniamy pole z numerem 0 – wtedy w saperze odsłaniają się wszystkie przyległe do tego pola kratki z tą samą liczbą.
W sumie teraz „tylko” przemyśleliśmy działanie, ale jest to bardzo ważne. W następnym wpisie przystąpimy do okodzenia tego

Czyli działamy:
Ustawiliśmy, ze po kliknięciu „ok” na komórce mamy przejść do menu edycji – zróbmy sobie zmienną z trybem w jakim jesteśmy:

Kod: Zaznacz cały

enum {tryb_gry_poruszanie, tryb_gry_edycja};

uint8_t tryb_gry = tryb_gry_poruszanie;

teraz funkcja dla przycisku OK:

Kod: Zaznacz cały

//obsluga przycisku "ok"
void button_ok()
{
   if( tryb_gry == tryb_gry_poruszanie )
   {
      tryb_gry = tryb_gry_edycja;
   }
}

Czyli zwykłe przełączenie trybu, powrót zrobimy trochę inaczej. Na logikę teraz – dla funkcji przycisków sterujących powinniśmy w środku też zrobić takie rozróżnienie, czyli w praktyce poprzerabiać ich funkcje, ale one już działają, nie chcemy tam grzebać… Wykorzystajmy callbacki!
Więc – mieliśmy funkcje typu:

Kod: Zaznacz cały

void button_left()
{
   ruch_po_planszy( ruch_lewo );
   zamaluj_pola_po_ruchu();
}

Dodajmy teraz takie:

Kod: Zaznacz cały

void button_left_edycja()
{
   // tu dodamy nasz kod
}

A gdzie będziemy podmieniać te callbacki? W kodzie od przycisku OK:

Kod: Zaznacz cały

//obsluga przycisku "ok"
void button_ok()
{
   if( tryb_gry == tryb_gry_poruszanie )
   {
      tryb_gry = tryb_gry_edycja;
      
      // przypisanie funkcji do przyciskow
      button_ustaw(0,5,200,50,button_up_edycja,   NULL );
      button_ustaw(1,5,200,50,button_left_edycja,   NULL );
      button_ustaw(2,5,200,50,button_ok_edycja,   NULL );
      button_ustaw(3,5,200,50,button_right_edycja,NULL );
      button_ustaw(4,5,200,50,button_down_edycja,   NULL );
   }   
}

Zróbmy to jeszcze ładniej:

Kod: Zaznacz cały

void ustaw_przyciski_tryb_edycji()
{
   button_ustaw(0,5,200,50,button_up_edycja,   NULL );
   button_ustaw(1,5,200,50,button_left_edycja,   NULL );
   button_ustaw(2,5,200,50,button_ok_edycja,   NULL );
   button_ustaw(3,5,200,50,button_right_edycja,NULL );
   button_ustaw(4,5,200,50,button_down_edycja,   NULL );
}

void ustaw_przyciski_tryb_poruszania()
{
   button_ustaw(0,5,200,50,button_up,   NULL );
   button_ustaw(1,5,200,50,button_left,   NULL );
   button_ustaw(2,5,200,50,button_ok,   NULL );
   button_ustaw(3,5,200,50,button_right,   NULL );
   button_ustaw(4,5,200,50,button_down,   NULL );
}

//obsluga przycisku "ok"
void button_ok()
{
   if( tryb_gry == tryb_gry_poruszanie )
   {
      tryb_gry = tryb_gry_edycja;
      ustaw_przyciski_tryb_edycji();
   }
}

Takie moje TODO na potem – przerobić bibliotekę do przycisków tak, żeby można było podać też samą funkcję, bez ustawień dotyczących dobouncingu.
Czyli teraz napiszmy sobie zestawik funkcji do odsłaniania kratek, znakowania znakiem zapytanie i oznaczania jako mine (te funkcje są typowo „saperowe” więc maja taki przedrostek w nazwach, wcześniejsze są bardziej „ogólne”):

Kod: Zaznacz cały

void saper_zaznacz_rozbrojona_mine( uint8_t numer )
{
   // jesli pole jest zasloniete, jest znakiem zapytania lub rozbrojona mina
   if( tablica_kratek[numer] & POLE_ZASLONIETE  || tablica_kratek[numer] & POLE_ROZBROJONA || tablica_kratek[numer] & POLE_PYTAJNIK)
   {
      tablica_kratek[numer] = tablica_kratek[numer] & ~(POLE_ZASLONIETE | POLE_PYTAJNIK | POLE_ROZBROJONA);
      tablica_kratek[numer] = tablica_kratek[numer] | POLE_ROZBROJONA;
   }
   
   // odswiezenie kratki
   zapal_kratke_saper(numer);
}

void saper_zaznacz_pytajnik( uint8_t numer )
{
   // jesli pole jest zasloniete, jest znakiem zapytania lub rozbrojona mina
   if( tablica_kratek[numer] & POLE_ZASLONIETE  || tablica_kratek[numer] & POLE_ROZBROJONA || tablica_kratek[numer] & POLE_PYTAJNIK)
   {
      tablica_kratek[numer] = tablica_kratek[numer] & ~(POLE_ZASLONIETE | POLE_PYTAJNIK | POLE_ROZBROJONA);
      tablica_kratek[numer] = tablica_kratek[numer] | POLE_PYTAJNIK;
   }
   
   // odswiezenie kratki
   zapal_kratke_saper(numer);
}

void saper_zaznacz_odkryj( uint8_t numer )
{
   // jesli pole jest zasloniete, jest znakiem zapytania lub rozbrojona mina
   if( tablica_kratek[numer] & POLE_ZASLONIETE  || tablica_kratek[numer] & POLE_ROZBROJONA || tablica_kratek[numer] & POLE_PYTAJNIK)
   {
      tablica_kratek[numer] = tablica_kratek[numer] & ~(POLE_ZASLONIETE | POLE_PYTAJNIK | POLE_ROZBROJONA);
   }
   
   // odswiezenie kratki
   zapal_kratke_saper(numer);
}

Teraz funkcje przyciskow:

Kod: Zaznacz cały

void button_left_edycja()
{
   saper_zaznacz_pytajnik(wybrana_kostka);
   saper_ustaw_tryb_poruszania();
}

void button_right_edycja()
{
   saper_ustaw_tryb_poruszania();
}

void button_up_edycja()
{
   saper_zaznacz_odkryj(wybrana_kostka);
   saper_ustaw_tryb_poruszania();
}

void button_down_edycja()
{
   saper_zaznacz_rozbrojona_mine(wybrana_kostka);
   saper_ustaw_tryb_poruszania();
}

Do przejścia w tryb poruszania wykorzystałem taką prostą funkcję:

Kod: Zaznacz cały

void saper_ustaw_tryb_poruszania()
{
   tryb_gry = tryb_gry_poruszanie;
   ustaw_przyciski_tryb_poruszania();
}

Ok więc co teraz mamy – dodaliśmy możliwość zaznaczania/odkrywania poszczególnych kratek obecnie jeszcze bez żadnego dodatkowego efektu tzn. przegranej itp. Sprawdźmy, czy to działa tak jak chcieliśmy:
// filmik dodam potem :p
Co widzimy: działa nam ustawianie rozbrojeń, i znaków zapytania, odkrywanie oczywiście też i to liczb i to min. Widać jedną wadę – gry jesteśmy w trybie edycji – przydałaby się jakaś informacja o tym – ustalmy, że będzie to kolor kratki zaznaczenia inny.
Nowy blog o tematyce embedded -> https://www.embedownik.pl/


Wróć do „DIY”

Kto jest online

Użytkownicy przeglądający to forum: Obecnie na forum nie ma żadnego zarejestrowanego użytkownika i 4 gości