![]() |
Autor artykułu: mgr Jerzy Wałaszek, wersja1.0 |
©2011 mgr
Jerzy Wałaszek
|
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:

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:

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.

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:
Siatkę punktów rozpoczniemy od pozycji (32,32):

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

Skoro tak, to każda linia będzie posiadała długość równą 14 x 32 = 448 pikseli.
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:
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:
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
BorderStyle = bsSingle
Caption = Gra w Gomoku
ClientHeight = 562
ClientWidths = 664
Name = frmGomoku
Position = poScreenCenter
Image1
Left = 16
Name = imgPlayfield
Top = 16
Width = 512
Button1
Left = 560
Name = btnNewGame
Top = 24
Button2
Left = 560
Name = btnMoveBack
Top = 56
Button3
Left = 560
Name = btnQuit
Top = 88
Label1
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.

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