Koło informatyczne 2012

Gra w Gomoku

Na dzisiejszych zajęciach zajmiemy się stworzeniem prostej aplikacji, która umożliwi dwóm osobom grę w Gomoku (jap. pięć kamieni). Jest to stara gra japońska, którą rozgrywa się na siatce 15x15 linii:

 

obrazek

 

Linie poziome są numerowane od 1 do 15. Linie pionowe oznacza się literami od a  do o. W grze uczestniczy dwóch graczy, którzy posiadają własne zestawy kamieni. Pierwszy gracz ma czarne kamienie - ten zawsze rozpoczyna grę, a drugi ma kamienie białe. Gracze stawiają swoje kamienie na przemian na przecięciu się linii:

 

obrazek

 

Wygrywa ten z graczy, któremu uda się ustawić dokładnie pięć swoich kamyków obok siebie w linii poziomej, pionowej lub po przekątnej. Remis jest wtedy, gdy zostaną wyczerpane wszystkie pola w grze, a żaden z graczy nie wygra.

 

obrazek

 

Zanim przystąpimy do tworzenia interfejsu gry, musimy dokładnie określić parametry pola gry oraz zasady jej działania.

 

Kolor tła ustalamy taki jak na powyższych rysunkach:

 

R = 250
G = 204
B = 113

 

Siatkę punktów rozpoczniemy od pozycji (32,32):

   obrazek

 

Linie będą w odstępie od siebie równym 32 piksele:

obrazek

 

Skoro tak, to każda linia będzie posiadała długość równą 14 x 32 = 448 pikseli.

 

Linie poziome:
Punkt początkowy: (32, (i+1)*32), gdzie i jest numerem linii liczonym od 0 do 14. Linia o numerze zero jest pierwszą od góry linią planszy.
punkt końcowy: (480, (i+1)*32)

Linie pionowe:
Punkt początkowy: ((i+1)*32,32)
Punkt końcowy: ((i+1)*32,480)

 

Rozmiar planszy ustalamy zatem na 512x512 pikseli. Do rysowania planszy wykorzystamy komponent Image, który posiada swoje płótno Canvas. Pozwoli nam to umieścić planszę w wybranym miejscu okienka, a następnie wypozycjonować względem niej pozostałe komponenty.

 

Kamyczki gracza będą rysowane jako białe lub czarne koła o środkach na przecięciu linii. Ustalamy średnicę tych kół na 24 piksele.

 

Będziemy używali następujących zmiennych globalnych:

 

Playfield
Definiuje zawartość pola gry. Jest to dwuwymiarowa tablica 15x15 komórek. Każda komórka określa zawartość odpowiadającego jej pola (przecięcia się linii) na planszy. Elementy będą posiadały następujące wartości:

0x0 – pole puste,
0x1 – pole zajęte przez czarny kamyczek
0x2 – pole zajęte przez biały kamyczek
0x5 – pole zajęte przez czarny kamyczek z czerwoną kropką. Takie kamyczki będą używane przy oznaczaniu 5 zwycięskich kamyczków.
0x6 – pole zajęte przez biały kamyczek z czerwoną kropką.

Jeśli spojrzysz na te wartości bitowo, to

b2 b1 b0

000 – pole puste
001 – gracz czarny
010 – gracz biały
101 – kropka czerwona na kamyczku czarnym
110 – kropka czerwona na kamyczku białym
 

 

GameIsOn
Wartość true oznacza, że gra się toczy i można stawiać kamyki na planszy.

Player
1 – gracz czarny, 2 – gracz biały

MoveCount
Zlicza ruchy.

LastMove
Zapamiętuje ostatni ruch:

0 - nie było ruchu
Player | (kolumna << 4) | (wiersz << 8)

Bitowo:

b11 b10 b9 b8 b7 b6 b5 b4 b3 b2 b1 b0
wiersz kolumna X X gracze

 

Po tych ustaleniach możemy przystąpić do projektowania interfejsu aplikacji. Na formie umieść komponent Image, 3 przyciski Button oraz jedną etykietę Label.

Form1

BorderIcons→biMaximize = false

BorderStyle = bsSingle

Caption = Gra w Gomoku

ClientHeight = 562

ClientWidths = 664

Name = frmGomoku

Position = poScreenCenter

Image1

Height = 512

Left = 16

Name = imgPlayfield

Top = 16

Width = 512

Button1

Caption = Nowa Gra

Left = 560

Name = btnNewGame

Top = 24

Button2

Caption = Cofnij Ruch

Left = 560

Name = btnMoveBack

Top = 56

Button3

Caption = Zakończ

Left = 560

Name = btnQuit

Top = 88

Label1

Alignment = taCenter
Tekst w obszarze etykiety będzie centrowany.

Autosize = false
Rozmiar etykiety nie będzie automatycznie dopasowywany do szerokości tekstu.

Caption = ...

Left = 16

Name = lblMessage

Top = 536

Transparent = true

Width = 512

 

Po zaprojektowaniu interfejsu przechodzimy do wprowadzania kodu. Najpierw umieścimy w tekście programu zmienne globalne:

 

#pragma resource "*.dfm"
TfrmGomoku *frmGomoku;

// Zmienne globalne

int  Playfield[15][15];    // pole gry
bool GameIsOn;             // true - gra w toku
int  Player;               // 1 - gracz czarny, 2 - gracz biały
int  MoveCount;            // zlicza ruchy
int  LastMove;             // ostatni ruch

//---------------------------------------------------------------------------
__fastcall TfrmGomoku::TfrmGomoku(TComponent* Owner)

 

Teraz stworzymy kilka funkcji pomocniczych, funkcje te umieść na początku programu przed funkcjami obsługi zdarzeń, które stworzymy później.:

 

// Rysuje kamyczek zgodnie z zawartością tablicy Playfield
//--------------------------------------------------------
void PaintPiece(TCanvas * c, int w, int k)
{
  int t = Playfield[w][k];

  k = 32 * (k+1); // teraz k i w są współrzędnymi
  w = 32 * (w+1); // przecięcia siatki na planszy

  TRect r1(k-12,w-12,k+13,w+13); // prostokąt dla kamyczka
  TRect r2(k-2,w-2,k+3,w+3);     // prostokąt dla kropki

  if(!t) return;    // jeśli pole jest puste, kończymy

  if(t & 0x1)       // kamyczek czarny?
  {
    c->Pen->Color   = clBlack;
    c->Brush->Color = clBlack;
  }
  else              // kamyczek biały
  {
    c->Pen->Color   = clWhite;
    c->Brush->Color = clWhite;
  }

  c->Ellipse(r1);   // rysujemy kamyczek w odpowiednim kolorze

  if(t & 0x4)       // czerwona kropka?
  {
    c->Pen->Color   = clRed;
    c->Brush->Color = clRed;
    c->Ellipse(r2); // rysujemy czerwoną kropkę
  }
}

 

Zadaniem funkcji PaintPiece() jest narysowanie w odpowiednim miejscu płótna Canvas kamyczka białego lub czarnego. Rodzaj kamyczka jest odczytywany z tablicy Playfield. Funkcja otrzymuje w swoich parametrach wskaźnik do płótna Canvas oraz współrzędne w tablicy Playfield.

Gdy jeden z graczy postawi obok siebie pięć kamyczków, to gra się kończy i kamyczki te są rysowane na planszy z czerwoną kropką w środku. Dlatego funkcja po narysowaniu samego kamyczka sprawdza, czy nie należy na nim narysować jeszcze czerwonej kropki. O wszystkim decyduje wartość elementu tablicy Playfield, którą opisaliśmy wcześniej.

 

Funkcja PaintPlayfield() ma za zadanie narysować całą planszę gry wraz z kamyczkami. Ważne: umieść ją w programie pod funkcją PaintPiece().

 

// Rysuje całą planszę gry
//------------------------
void PaintPlayfield(TCanvas * c)
{
  AnsiString s;
  int x,y,t,i;

  // tło
  c->Brush->Color = RGB(250,204,113);
  c->FillRect(Rect(0,0,512,512));

  // linie siatki
  c->Pen->Color = clBlack;
  for(i = 0; i < 15; i++)
  {
     t = 32 * (i + 1);

     c->MoveTo(32,t);    // linie poziome
     c->LineTo(480,t);

     c->MoveTo(t,32);    // linie pionowe
     c->LineTo(t,480);

     // opisy wierszy
     s = IntToStr(15-i); // wiersze liczymy od dołu
     x = 16 - c->TextWidth(s);
     y = t - (c->TextHeight(s) / 2);
     c->TextOutA(x,y,s);

     // opisy kolumn
     s = char(97 + i);   // kolumny oznaczamy małymi literami
     x = t - (c->TextWidth(s) / 2);
     y = 495;
     c->TextOutA(x,y,s);
  }

  // na środku planszy rysujemy malutkie kółko
  c->Brush->Color = clBlack;
  c->Ellipse(Rect(254,254,259,259));

  // Rysujemy kamyczki graczy
  for(int i = 0; i < 15; i++)
    for(int j = 0; j < 15; j++) PaintPiece(c,i,j);
}

 

Funkcja najpierw wypełnia całe płótno Canvas kolorem tła. Następnie w pętli są rysowane linie pionowe i poziome siatki. Po narysowaniu każdej linii zostaje również wypisane jej oznaczenie. Linie poziome są numerowane od 1 do 15. Numeracja biegnie od dołu w górę. Dlatego numer wiersza odejmujemy od 15. Otrzymamy w ten sposób odwrotne numery od 15 do 1. Wyznaczona zostaje pozycja tekstu w obrębie płótna – tekst jest wyrównany do prawej krawędzi. Podobnie są tworzone oznaczenia linii poziomych. Tutaj oznaczeniami są zamiast liczb literki od a do o. Literki centrujemy względem pozycji linii w poziomie.

Na koniec zostaje wyświetlona zawartość tablicy Playfield.

 

Funkcja PaintMessage() służy do wyświetlania krótkiego tekstu w etykiecie lblMessage, która jest umieszczona pod polem gry.

 

// Rysuje wiadomość pod planszą gry
//---------------------------------
void PaintMessage(TLabel * lbl)
{
  if(!GameIsOn) lbl->Caption = "Koniec gry";
  else
  {
    AnsiString s = "Ruch nr " + IntToStr(MoveCount) + " dla gracza ";
    if(Player == 1) s += "czarnego";
    else            s += "białego";
    lbl->Caption = s;
  }
}

 

Najbardziej skomplikowaną funkcją jest GameOver(). Zwraca ona wartość logiczną true, gdy:

// Sprawdza, czy gra się zakończyła
//---------------------------------
bool GameOver()
{
  int w,k,c,i;

  // szukamy 5 kamyczków w poziomie

  for(w = 0; w < 15; w++)
  {
    c = 1;
    for(k = 1; k < 15; k++)
    {
       if(Playfield[w][k-1] == Playfield[w][k]) c++;
       else c = 1;

       if((c == 5) && Playfield[w][k])
       {
         if((k < 14) && (Playfield[w][k] == Playfield[w][k+1])) continue;

         for(i = 0; i < 5; i++) Playfield[w][k-i] |= 0x4; // czerwona kropka

         return true;
       }
    }
  }

  // szukamy 5 kamyczków w pionie

  for(k = 0; k < 15; k++)
  {
    c = 1;
    for(w = 1; w < 15; w++)
    {
       if(Playfield[w-1][k] == Playfield[w][k]) c++;
       else c = 1;

       if((c == 5) && Playfield[w][k])
       {
         if((w < 14) && (Playfield[w][k] == Playfield[w+1][k])) continue;

         for(i = 0; i < 5; i++) Playfield[w-i][k] |= 0x4; // czerwona kropka

         return true;
       }
    }
  }

  // szukamy 5 kamyczków po przekątnej z lewa na prawo w dół

  for(k = 0; k < 11; k++)
    for(w = 0; w < 11; w++)
    {
       c = 1;
       for(i = 1; i < 5; i++)
         if(Playfield[w+i-1][k+i-1] == Playfield[w+i][k+i]) c++;
         else c = 1;

       if((c == 5) && Playfield[w][k])
       {
          if((w < 10) && (k < 10) && (Playfield[w][k] == Playfield[w+5][k+5]))
            continue;

          if((w) && (k) && (Playfield[w][k] == Playfield[w-1][k-1]))
            continue;

          for(i = 0; i < 5; i++) Playfield[w+i][k+i] |= 0x4; // czerwona kropka

          return true;
       }
    }

  // szukamy 5 kamyczków po przekątnej z prawa na lewo w dół

  for(k = 4; k < 15; k++)
    for(w = 0; w < 11; w++)
    {
       c = 1;
       for(i = 1; i < 5; i++)
         if(Playfield[w+i-1][k-i+1] == Playfield[w+i][k-i]) c++;
         else c = 1;

       if((c == 5) && Playfield[w][k])
       {
          if((w < 10) && (k > 4) && (Playfield[w][k] == Playfield[w+5][k-5]))
            continue;

          if((w) && (k<15) && (Playfield[w][k] == Playfield[w-1][k+1]))
            continue;

          for(i = 0; i < 5; i++) Playfield[w+i][k-i] |= 0x4; // czerwona kropka

          return true;
       }
    }

  if(MoveCount == 226) return true;

  return false;
}

 

Funkcja sprawdza kolejno planszę w poziomie, w pionie, w dół na prawo i w dół na lewo. Jeśli natrafi na dokładnie 5 kamyczków obok siebie, to oznacza je kropką i zwraca true. Na końcu sprawdza jeszcze, czy bieżący ruch ma numer 226. Takiego ruchu nie daje się wykonać, ponieważ plansza gry posiada jedynie 225 pól. Zatem gra kończy się remisem.

Zasada wyszukiwania 5 kamyczków jest zawsze taka sama. Tworzymy licznik c. Nadajemy mu wartość 1, Przeglądanie rozpoczynamy zawsze od następnej pozycji. W każdym obiegu pętli sprawdzamy, czy element na bieżącej pozycji jest taki sam jak na poprzedniej. Jeśli tak, to zwiększamy licznik o 1. Gdy licznik osiągnie wartość 5, sprawdzamy co zliczył. Jeśli są to puste miejsca, to ignorujemy stan licznika i kontynuujemy przeglądanie. Inaczej sprawdzamy, czy zliczone kamyczki nie kontynuują się dalej wzdłuż wybranej linii, tzn. czy nie jest ich więcej niż 5. Jeśli tak, to ignorujemy licznik i kontynuujemy przeglądanie. W przeciwnym razie natrafiliśmy na 5 kolejnych kamyków. W tablicy Playfield wyposażamy je w czerwone kropki ustawiając bit b2 na 1 i kończymy funkcję zwracając true.

 

Teraz tworzymy funkcje obsługi zdarzeń. Najpierw utwórz funkcję dla zdarzenie onCreate dla frmGomoku i wpisz poniższy kod:

 

void __fastcall TfrmGomoku::FormCreate(TObject *Sender)
{
  for(int i = 0; i < 15; i++)
    for(int j = 0; j < 15; j++) Playfield[i][j] = 0;

  GameIsOn = true;
  Player = 1;
  MoveCount = 1;
  LastMove = 0;

  PaintPlayfield(imgPlayfield->Canvas);

  PaintMessage(lblMessage);

  DoubleBuffered = true;
}

 

Funkcja zeruje tablicę Playfield oraz inicjuje wszystkie zmienne globalne. Na koniec zostaje utworzona plansza gry w imgPlayfield oraz zainicjowana etykieta lblMessage. Aby uniknąć migotania elementów okna, włączamy podwójne buforowanie.

 

Skojarz funkcję obsługi onCreate ze zdarzeniem onClick dla przycisku btnNewGame: Wybierz ten komponent i na zakładce Events obok zdarzenie onClick kliknij strzałkę listy rozwijanej, po czym wybierz z niej FormCreate.

 

Utwórz funkcję obsługi zdarzenia onMouseDown dla imgPlayfield. Zdarzenie powstaje po kliknięciu przyciskiem myszki w obszarze komponentu. Do funkcji wpisz kod:

 

void __fastcall TfrmGomoku::imgPlayfieldMouseDown(TObject *Sender,
      TMouseButton Button, TShiftState Shift, int X, int Y)
{
  if(GameIsOn)
  {
    int k,w,kp,wp;

    // wyznaczamy kolumnę planszy

    k = X / 32;
    if(X - 32*k > 12)
    {
      k++;
      if(32*k - X > 12) return;
    }

    // wyznaczamy wiersz planszy
    w = Y / 32;
    if(Y - 32*w > 12)
    {
      w++;
      if(32*w - Y > 12) return;
    }

    if((k < 1) || (k > 15) || (w < 1) || (w > 15)) return;

    // zmniejszamy wiersz i kolumnę, aby odpowiadały
    // indeksom w tablicy Playfield

    k--; w--;

    if(!Playfield[w][k]) // pole musi być puste
    {
      // kasujemy czerwoną kropkę na poprzednim kamyczku
      if(LastMove)
      {
        kp = (LastMove >> 4) & 0xf;
        wp = (LastMove >> 8) & 0xf;
        PaintPiece(imgPlayfield->Canvas,wp,kp);
      }

      // w polu planszy umieszczamy kamyczek gracza z czerwoną kropką
      Playfield[w][k] = Player | 0x4;

      // zwiększamy numer ruchu
      MoveCount++;

      // wykonany ruch zapamiętujemy do cofnięcia i do usunięcia kropki
      LastMove = Player | (k << 4) | (w << 8);

      // rysujemy wstawiony kamyczek
      PaintPiece(imgPlayfield->Canvas,w,k);

      // usuwamy kropkę z tablicy Playfield
      Playfield[w][k] &= 0x3;

      // sprawdzamy, czy po wstawieniu kamyczka gra się wciąż toczy
      if(GameOver())
      {
        // jesli nie, rysujemy całą planszę, ponieważ w przypadku
        // wygranej pojawiły się kamyczki z kropkami
        PaintPlayfield(imgPlayfield->Canvas);

        // ustawiamy koniec gry
        GameIsOn = false;
      }
      else
      {
        // zmieniamy gracza
        Player ^= 3;
      }

      // wyświetlamy nową wiadomość
      PaintMessage(lblMessage);
    }
  }
}

 

Na początku funkcji najpierw przeliczamy współrzędne myszki na numer kolumny i wiersza planszy. Akceptujemy kliknięcie w odległości do co najwyżej 12 pikseli od punktu przecięcia się linii siatki. Jeśli gracz kliknie dalej, to ruch zostanie zignorowany. Również ruch będzie zignorowany, jeśli kliknięte pole nie jest puste.

Po wyznaczeniu pozycji funkcja umieszcza w tablicy Playfield kamyczek gracza, po czym sprawdza, czy gra się zakończyła. Jeśli tak, to zostaje wyświetlona cała plansza wraz ze wszystkimi kamyczkami. Inaczej zostaje zmieniony gracz i gra toczy się dalej.

 

Wybierz przycisk btnQuit i utwórz dla niego funkcję obsługi zdarzenia onClick, po czym wpisz do niej kod:

 

void __fastcall TfrmGomoku::btnQuitClick(TObject *Sender)
{
  Close();        
}

 

I została nam ostatnia funkcja obsługi zdarzenia onClick dla przycisku btnMoveBack. Utwórz ją, po czym wpisz kod:

 

void __fastcall TfrmGomoku::btnMoveBackClick(TObject *Sender)
{
  if((MoveCount > 1) && LastMove)
  {
    int p,k,w,i,j;

    p = LastMove & 0xf;        // gracz
    k = (LastMove >> 4) & 0xf; // kolumna
    w = (LastMove >> 8) & 0xf; // wiersz

    LastMove = 0;              // zerujemy ostatni ruch

    GameIsOn = true;           // przywracamy grę
    Player = p;                // ustawiamy gracza
    MoveCount--;               // zmniejszamy numer ruchu
    Playfield[w][k] = 0;       // usuwamy kamyk

    for(i = 0; i < 15; i++)    // zerujemy kamyczki z kropką
      for(j = 0; j < 15; j++) Playfield[i][j] &= 3;

    PaintPlayfield(imgPlayfield->Canvas);
    PaintMessage(lblMessage);
  }
}

 

Funkcja ta cofa ostatni ruch. Dane odtwarzamy ze zmiennej LastMove przez proste operacje logiczne. Zmienną zerujemy, aby nie można już było cofać kolejnego ruchu, który przecież nie był zapamiętany. Odczytane dane umieszczamy w zmiennych globalnych.

 

To wszystko. Uruchom aplikację. Miłej zabawy.

 

obrazek

 

Program możesz rozbudować. Na przykład, można go wyposażyć w zapis listy wykonanych ruchów i możliwość cofania i przywracania więcej niż jednego ruchu. Kamyczki mogą być przygotowane w programie graficznym i rysowane na planszy funkcją Canvas->Draw(). Sama plansza również może być przygotowana wcześniej i szybko wyrysowana za pomocą tej samej funkcji. Pozostawiam to jednak ambitnym czytelnikom.

 


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

©2024 mgr Jerzy Wałaszek

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

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