Prezentowane materiały są przeznaczone dla uczniów szkół ponadgimnazjalnych. 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