[C++] Tic Tac Toe mit GUI (Borland C++-Builder 5 VCL) [Teil 2 von 4]
Im letzten Teil (Teil 1) dieser Artikelreihe habe ich euch gezeigt, was das Spiel „Tic Tac Toe“ alles kann, wie es aussieht und wo ihr es euch herunterladen könnt.
In diesem Teil geht es um die Programmierung des Spiels, genauer gesagt um den Spielcontroller.
Kurze Einführung ins MVC-Prinzip
Beim MVC-Prinzip (Model View Controller), auch als 3-Schichtenmodell bekannt, werden die Daten von der Verarbeitung und der Ausgabe getrennt. Der Controller ist dabei die zentrale Schnittstelle zwischen Model und View. Er holt sich die Daten vom Model, verarbeitet sie und gibt sie an den View weiter. Bekommt er vom View eine Benachrichtugung (Event), verarbeitet er die Anfrage entsprechend. Das kann z.B. ein Klick auf einen Button sein.
Bei meinem Spiel habe ich das MVC angewandt, da es eine schöne Trennung der einzelnen Schichten und damit eine hohe Wiederverwendbarkeit bietet.
Der Spielcontroller
Der Spielcontroller ist mit seinen stolzen 22kb Code der größte und wichtigste Teil des Spiels. Er wird hier als erstes erklärt. Im Controller enthalten sind diese Methoden:
- öffentlich
- Controller(TForm1*);
- Spieler* getSpieler(int);
- TForm1* getGUI();
- SpielfeldModel* getModel();
- void reset();
- void setPlayerType(int id, AnsiString type);
- void refreshStatus();
- void refreshTimer();
- void resetTimer();
- privat
- bool isCombination(Spieler*, int, int, int, int, unsigned int);
- void __fastcall onClick(TObject *Sender);
- void __fastcall adjust(TObject *Sender);
- void setFinished(bool);
- bool isFinished();
- void zugPC();
- void pcEasy();
- void pcDifficult();
- bool isUnentschieden();
- int getCurrentPlayerID();
- Spieler* getCurrentPlayer();
- int getNextPlayerID();
- Spieler* getNextPlayer();
- void searchWinner();
- void incrementCounter();
- void decrementCounter();
- int getCounter();
- void resetCounter();
- Feld* findCombinations(Spieler*, unsigned int);
- Spieler* checkField();
Außerdem gibt es diese Attribute:
- privat:
- TForm1 *gui;
- Spieler *spieler[2];
- int counter;
- SpielfeldModel* model;
- unsigned int timer_min;
- unsigned int timer_sec;
- bool finished;
Auf den ersten Blick ist das ganz schön viel und man hätte für vieles auch keine Methoden gebraucht. Aber da ich kein Fan von Redundanz bin, habe ich alles, was mehrmals benötigt wird, in Methoden ausgelagert.
Kompletter Code
Am besten Zeite ich auch hier mal den kompletten Code. Danach beschreibe und erkläre ich noch wichtige Stellen im Code.
/*
Der Controller regelt den Spielablauf
*/
class Controller //Kontrolliert den Spielablauf
{
public:
Controller(TForm1*); //Konstruktor
Spieler* getSpieler(int); //Liefert einen Spieler
TForm1* getGUI(); //GUI zurückgeben
SpielfeldModel* getModel(); //Liefert das Model (Speicher aller Felder)
void reset(); //Spiel zurücksetzten (Neustart)
void setPlayerType(int id, AnsiString type); //Typ setzen
void refreshStatus(); //Aktualisiert die Statusanzeige
void refreshTimer(); //Aktualisiert die Zeitanzeige
void resetTimer(); //Uhr auf 0 setzen
private:
TForm1 *gui; //Die GUI
Spieler *spieler[2]; //Das Array der Spieler
int counter; //Zählt die Spielzüge
SpielfeldModel* model; //Model
unsigned int timer_min; //Minutenanzeige
unsigned int timer_sec; //Sekundenanzeige
bool finished; //Spiel beendet?
bool isCombination(Spieler*, int, int, int,
int, unsigned int); //Kombination?
void __fastcall onClick(TObject *Sender); //Behandelt einen Klick
void __fastcall adjust(TObject *Sender); //Passt das Spielfeld an
void setFinished(bool); //Spiel beenden / starten
bool isFinished(); //Spiel beendet?
void zugPC(); //Zug des PC-Spielers
void pcEasy(); //PC-Schwierigkeit Einfach
void pcDifficult(); //PC-Schwierigkeit Schwer
bool isUnentschieden(); //Ist das Spiel unentschieden?
int getCurrentPlayerID(); //Die ID des aktuellen Spielers zurückgeben
Spieler* getCurrentPlayer(); //Den aktuellen Spieler zurückgeben
int getNextPlayerID(); //Die nächste Spieler-ID
Spieler* getNextPlayer(); //Nächstes Spieler-Objekt
void searchWinner(); //Sucht gewinner / Unentschieden
void incrementCounter(); //1 Spielzug mehr
void decrementCounter(); //1 Spielzug weniger
int getCounter(); //gibt Anzahl der Spielzüge zurück
void resetCounter(); //Setzt die Spielzüge auf 0 zurück
Feld* findCombinations(Spieler*, unsigned int); //Findet Kombinationen
Spieler* checkField(); //Ermittelt einen Gewinner
};
//Konstruktor initialisiert Variablen und erstellt Objekte
Controller::Controller(TForm1* Form1)
{
this->gui = Form1; //Das erste Formular als GUI verwenden
//Die Breite der GUI fetlegen
this->gui->Width = (CONST__anzX+1)* (CONST__breite_label + CONST__abstand) + CONST__abstand;
//Die Höhe der GUI festlegen
this->gui->Height = (CONST__anzY+1)* (CONST__hoehe_label + CONST__abstand) + CONST__abstand + 10;
//Eventhandler des Feldcontainers beim Resize (auch beim Hinzufügen von Elementen)
this->gui->panel_feldcontainer->OnResize = adjust;
//Eventhandler der GUI beim Resitze = OnResize von Feldcontainer
this->gui->OnResize = this->gui->panel_feldcontainer->OnResize;
//Das Model, das das Spielfeld enthält, wird erstellt.
this->model = new SpielfeldModel(
this->gui, this->gui->panel_feldcontainer,
CONST__breite_label,
CONST__hoehe_label,
(CONST__hoehe_label + CONST__abstand),
(CONST__breite_label + CONST__abstand)
);
//Jetzt muss noch jedem Feld die onClick-Methode zugewiesen werden.
for(int y = 0; y < CONST__anzY; y++)
{
for(int x = 0; x < CONST__anzX; x++) { model->getFeld(x,y)->OnClick = onClick;
}
}
this->spieler[0] = new Mensch; //Ersten Spieler erzeugen
this->spieler[0]->setColor(CONST__default_spieler1_color); //Farbe setzen
this->spieler[1] = new Computer(2); //Zweiten Spieler erzeugen
this->spieler[1]->setColor(CONST__default_spieler2_color); //Farbe setzen
this->counter = 0; //Es wurde noch nicht gespielt
this->finished = false; //Das Spiel ist noch nicht beendet
this->timer_min = 0; //Timer zurücksetzen
this->timer_sec = 0; //Timer zurücksetzen
this->refreshStatus(); //Statusanzeige aktualisieren (bzw. initialisieren)
}
// Diese Methode regelt den Spielablauf. Mit dem ersten Klick eines Spielers beginnt das Spiel.
// Ist der zweite Spieler der PC, wird automatisch die entsprechende Methode aufgerufen.
void __fastcall Controller::onClick(TObject *Sender)
{
Feld *feld = NULL; //Temporärer Speicher für Felder
feld = dynamic_cast(Sender); //Sender-Objekt wird zu Feld-Objekt gecastet
if(this->isFinished()) //Falls das Spiel beendet wurde, wird abgebrochen
{
return; //Abbrechen
}
if(this->getCurrentPlayer()->getType() == "Mensch" //Falls der Spieler ein Mensch ist
&& (Sender == NULL || feld == NULL)) //Und kein Feld angeklickt wurde
{
return; //Abbrechen
}
if(this->getCurrentPlayer()->getType() == "Mensch"
&& !feld->isEmpty()) //Falls der Spieler ein Mensch ist und das Feld belegt ist
{
return; //Wird die Methode abgebrochen, da nur leere
} //Felder angeklickt werden dürfen
//Sollte der aktuelle Spieler der PC sein, muss der Zug des PCs mit der Methode zugPC ausgeführt werden
if(this->getCurrentPlayer()->getType() == "Computer" && !this->isFinished())
{
Sleep(300); //PC setzt nicht sofort, sondern wartet 300ms.
this->zugPC(); //Der PC führt den Zug aus
}
//Der aktuelle Spieler ist kein PC, sondern ein Mensch. Klick wird ausgewertet
else if(this->getCurrentPlayer()->getType() == "Mensch" && !this->isFinished())
{
if(feld->isEmpty()) //Man darf nur leere Felder anklicken
{
feld->setColor(this->getCurrentPlayer()->getColor()); //Angeklicktes Feld einfärben
feld->setPlayer(this->getCurrentPlayer()); //Besitzer des Felds eintragen
}
}
else
{
ShowMessage("Der Spielertyp des aktuellen Spielers ist undefiniert. Spielabbruch");
this->setFinished(true); //Spiel beenden, da Fehler auftrat.
}
this->searchWinner(); //Schauen, ob es einen Gewinner gibt / Unentschieden
//Nur weitermachen, wenn das Spiel nicht beendet wurde.
if(!this->isFinished())
{
//Spielzähler erhöhen. Der aktuelle Spieler ist jetzt der nächste Spieler
this->incrementCounter();
//Status aktualisieren. Es wird jetzt der aktuelle (also der nächste) Spieler
//im Status angezeigt.
this->refreshStatus();
//Diese Stelle ist sehr wichtig für den Fall, dass ein Computerspieler an der Reihe ist.
//Der PC löst nämlich kein onClick-Event aus, deshalb machen wir das hier manuell.
if(this->getCurrentPlayer()->getType() == "Computer")
{
//Hier wird das onClick-Event manuell ausgelöst (als ob der PC geklickt hätte)
this->onClick(NULL);
}
}
else
{
this->incrementCounter(); //Wenn Spiel beendet wurde, muss der Spielzähler
//noch um 1 erhöht werden, weil er sonst den
//beendenden Zug nicht enthält.
}
}
//Diese Methode wird aufgereufen, wenn der PC dran ist.
void Controller::zugPC()
{
//Aktueller Spieler ist PC?
if(this->getCurrentPlayer()->getType() != "Computer")
{
ShowMessage("Es ist ein schwerwiegender Fehler aufgetreten");
this->setFinished(true); //Spiel beenden
}
//getCurrentPlayer gibt einen Pointer auf ein Spieler-Objekt zurück.
//Die Klasse Spieler kennt aber die Methoden von Computer nicht.
//Deshalb müssen wir einen Type-Cast zu einem Computer*-Pointer durchführen
//Danach sind alle Methoden und Attribute des Objekts ansprechbar.
Computer* spieler = dynamic_cast(this->getCurrentPlayer());
//Je nach Schwierigkeit andere Methode ausführen
switch(spieler->getSchwierigkeit())
{
case 1: this->pcEasy(); //PC spielt mit pcEasy()
break;
case 2: this->pcDifficult(); //PC spielt mit pcDifficult()
break;
default: ShowMessage("Undefinierte Schwierigkeitsstufe");
break;
}
}
/*
Die einfache Spielmethode setzt an einer zufälligen Stelle im Spielfeld
einen Stein, falls das Feld frei ist.
*/
void Controller::pcEasy()
{
Feld *feld = NULL; //Temporärer Speicher für Feld
int x = 0, y = 0; //Koordinaten des PC-Zugs
randomize(); //Zufallsgenerator starten
do
{
x = random(CONST__anzX); //zufällige x-Koordinate
y = random(CONST__anzY); //zufällige y-Koordinate
if(this->model->isValidCoord(x,y)) //Prüfen, ob Koordinate erlaubt ist
{
feld = this->model->getFeld(x,y); //Das Feld (x|y) holen
}
}
//solange das Spiel nicht unentschieden, beendet oder das Feld voll ist.
while(!this->isUnentschieden() && feld != NULL && !feld->isEmpty() && !this->isFinished());
feld->setColor(this->getCurrentPlayer()->getColor()); //Dem gefundenen Feld die Farbe zuweisen
feld->setPlayer(this->getCurrentPlayer()); //Dem gefundenen Feld den Besitzer zuweisen
}
/*
Die schwere Spielmethode versucht, eigene Kombinationen zu erstellen und zu
vervollständigen.
Außerdem findet diese Methode die "Gewinnversuche" des Gegners und versucht,
diese zu verhindern.
*/
void Controller::pcDifficult()
{
Feld *feld = NULL; //Temporärer Speicher für Feld
//Die ersten beiden Züge sollen nicht mit der KI gemacht werden,
//weil der PC sonst am Anfang immer den gleichen Zug macht, wenn der Mensch
//das auch macht.
if(this->getCounter() <= 1) { this->pcEasy();
return; //Abbruch, da Zug gemacht wurde.
}
//Als erstes wird versucht, eigene 3er-Kombinationen zu erzeugen
feld = this->findCombinations(this->getCurrentPlayer(), 2);
if(feld == NULL)
{ //Dann wird versucht, gegnerische 2er zu blockieren, so dass keine 3er entstehen
feld = this->findCombinations(this->getNextPlayer(), 2);
}
if(feld == NULL)
{ //Andernfalls werden aus eigenen 1ern 2er gemacht
feld = this->findCombinations(this->getCurrentPlayer(), 1);
}
if(feld == NULL)
{ //Sonst gegnerische 1er blockieren, so dass keine 2er entstehen
feld = this->findCombinations(this->getNextPlayer(), 1);
}
if(feld == NULL)
{ //Sollte nichts geklappt haben, wird die Zufallsmethode versucht
this->pcEasy();
}
else
{ //Wenn die KI zugeschlagen hat, wird das Feld belegt
feld->setColor(this->getCurrentPlayer()->getColor());
feld->setPlayer(this->getCurrentPlayer());
}
}
/*
Diese Methode durchsucht das Spielfeld nach Kombinationen mit der Länge "length"
Anschließend gibt sie das Feld, das "vor" der Kombination kommt und leer ist,
zurück.
*/
Feld* Controller::findCombinations(Spieler* player, unsigned int length)
{
for(int y = 0; y < CONST__anzY; y++)
{
for(int x = 0; x < CONST__anzX; x++) { if(this->model->isValidCoord(x,y) && //Koordinate gültig?
this->model->getFeld(x,y)->isEmpty() //Feld leer?
)
{
if(this->isCombination(player, x, y+1, 0, 1, length) //Senkrecht
|| this->isCombination(player, x+1, y, 1, 0, length) //Waagrecht
|| this->isCombination(player, x, y-1, 0, -1, length) //Senkrecht
|| this->isCombination(player, x-1, y, -1, 0, length) //Waagrecht
|| this->isCombination(player, x+1, y+1, 1, 1, length) //Diagonal
|| this->isCombination(player, x+1, y-1, 1, -1, length) //Diagonal
|| this->isCombination(player, x-1, y+1, -1, 1, length) //Diagonal
|| this->isCombination(player, x-1, y-1, -1, -1, length) //Diagonal
)
{
return this->model->getFeld(x,y); //Feld zurückgeben
}
}
}
}
return NULL; //Kein Feld gefunden
}
/**
Hier wird geprüft, ob einer der Spieler das Spiel gewonnen hat oder ob das
Spiel unentschieden ist.
*/
void Controller::searchWinner()
{
Spieler *winner = NULL; //Temporärer Speicher für den Gewinner-Spieler
winner = this->checkField(); //Das Spielfeld nach dem Gewinner durchsuchen
if(winner != NULL) //Wenn winner != NULL ist, gibt es einen Gewinner.
{
this->gui->setStatusMessage(winner->getName()+" hat gewonnen!"); //Gewinner ausgeben
this->gui->setSpielerfarbeVorschau(winner->getColor()); //Farbe des Gewinners anzeigen
this->setFinished(true); //Spiel beenden
}
else
{
//Auf unentschieden prüfen
if(this->isUnentschieden()) //Prüfen, ob unentschieden ist
{
this->gui->setStatusMessage("Untentschieden"); //Unentschieden ausgeben
this->gui->resetSpielerfarbeVorschau(); //Farbe zurücksetzen
this->setFinished(true); //Spiel beenden
}
}
}
/**
Gibt die ID (index im spieler-Array) des aktuellen Spielers zurück
*/
int Controller::getCurrentPlayerID()
{
return this->counter % 2 == 0 ? 0 : 1;
}
/*
gibt den aktuellen Spieler zurück (Pointer auf Spieler-Objekt)
*/
Spieler* Controller::getCurrentPlayer()
{
//Das Spieler-Objekt mit dem ermittelten Index wird returned
return this->spieler[this->getCurrentPlayerID()];
}
/**
Gibt die ID (index im spieler-Array) des nächsten Spielers zurück
*/
int Controller::getNextPlayerID()
{
return (this->getCurrentPlayerID() == 1) ? 0 : 1; //"toggle"
}
/*
gibt den nächsten Spieler zurück (Pointer auf Spieler-Objekt)
*/
Spieler* Controller::getNextPlayer()
{
//Das Spieler-Objekt mit dem ermittelten Index wird returned
return this->spieler[this->getNextPlayerID()];
}
/*
Prüft, ob alle Felder belegt sind und das Spiel somit beendet ist.
*/
bool Controller::isUnentschieden()
{
int volle_felder = 0; //Wieviele Felder sind bereits belegt?
//In dieser verschachtelten for-Schleife wird ermittelt, wieviele Felder belegt sind.
for(int y = 0; y < CONST__anzY; y++)
{
for(int x = 0; x < CONST__anzX; x++) { if(this->model->isValidCoord(x,y) && //Gültige Koordinate &&
!this->model->getFeld(x,y)->isEmpty() //Feld belegt?
)
{
volle_felder++; //Ein belegtes Feld mehr
}
}
}
//Entspricht die Anzahl der vollen Felder der Anzahl der vorhandenen Spielfelder?
return (volle_felder >= (CONST__anzX * CONST__anzY));
}
//Diese Methode leitet den Aufruf direkt an die GUI weiter (Delegation)
void __fastcall Controller::adjust(TObject *Sender)
{
this->gui->placeElements();
}
/*
isCombination überprüft, ob es ab der Stelle (startX|startY) eine
Feldkombination des Spielers player gibt.
Mit dirX und dirY kann man steuern, ob waagrechte, senkrechte oder diagonale
Kombinationen gesucht werden sollen.
length gibt die Mindestlänge der Kombinationen an.
*/
bool Controller::isCombination(Spieler* player, int startX, int startY, int dirX, int dirY, unsigned int length)
{
//Teste, ob Startkoordinaten gültig sind
if(!this->model->isValidCoord(startX, startY))
{
return false; //Ungültige Koordinaten
}
//Teste, ob Endkoordinaten gültig sind
if(!this->model->isValidCoord(startX + (dirX*(length-1)), startY + (dirY*(length-1))))
{
return false; //Ungültige Koordinaten
}
//jetzt wird eine Schleife length mal wiederholt.
for(unsigned int i = 0; i < length; i++) { //Es wird geprüft, ob das Feld NICHT vom Spieler ist. if(this->model->getFeld(startX + (dirX*i), startY + (dirY*i))->getPlayer() != player)
{
return false; //Wenn es nicht vom Spieler ist, wird false zurückgegeben,
//da dann keine Kombination existiert.
}
}
return true; //Eine Kombination existiert!
}
/*
Überprüft, ob es im Spielfeld einen Gewinner gibt, der length
Felder in einer Kombination hat.
*/
Spieler* Controller::checkField()
{
unsigned int length = 3; //TicTacToe erfordert 3er-Kombinationen
Spieler* player = NULL; //Pointer auf spieler-Objekt
//Spielfeld durchlaufen
for(unsigned int y = 0; y < CONST__anzY; y++)
{
for(unsigned int x = 0; x < CONST__anzX; x++) { if(this->model->isValidCoord(x,y)) //Ist die Koordinate gültig?
{
player = this->model->getFeld(x, y)->getPlayer(); //Besitzer des Felds holen
if(player != NULL) //Wenn es einen Besitzer gibt
{
if( this->isCombination(player, x, y, 0, 1, length) //Senkrecht
|| this->isCombination(player, x, y, 1, 0, length) //Waagrecht
|| this->isCombination(player, x, y, 1, 1, length) //Diagonal
|| this->isCombination(player, x, y, 1, -1, length) //Diagonal
|| this->isCombination(player, x, y, -1, 1, length) //Diagonal
|| this->isCombination(player, x, y, -1, -1, length) //Diagonal
)
{
return player; //Gewinner zurückgeben
}
}
}
}
}
return NULL; //Kein Gewinner
}
/*
Gibt entweder Spieler 1 oder Spieler 2 zurück.
*/
Spieler* Controller::getSpieler(int nr)
{
if(nr != 1 && nr != 2) // nr muss 1 oder 2 sein, weil es nur 2 Spieler gibt
{ throw "Dieser Spieler existiert nicht"; }
return this->spieler[nr-1]; //Array beginnt bei 0 --> -1
}
/*
Gibt Pointer auf Model-Objekt zurück
*/
SpielfeldModel* Controller::getModel()
{
return this->model;
}
/*
Erhöht den Counter, der die Spielzüge zählt.
*/
void Controller::incrementCounter()
{
this->counter++;
this->gui->showSpielzuege(this->counter); //Neue Anzahl anzeigen
}
/*
Verringert den Counter, der die Spielzüge zählt
*/
void Controller::decrementCounter()
{
this->counter--;
this->gui->showSpielzuege(this->counter); //Neue Anzahl anzeigen
}
/*
Counterstand zurückgeben
*/
int Controller::getCounter()
{
return this->counter;
}
/**
Counter zurückstetzen
*/
void Controller::resetCounter()
{
this->counter = 0;
this->gui->showSpielzuege(this->counter); //Neue Anzahl anzeigen
}
/*
Controller zurücksetzten und neues Spiel starten
*/
void Controller::reset()
{
this->resetCounter(); //Counter zurücksetzen
this->model->reset(); //Spielfeld leeren
this->refreshStatus(); //Status aktualisieren
this->gui->RefreshField(); //Feld neu anzeigen
this->resetTimer();
this->setFinished(false); //Spiel starten
}
/*
Spiel beenden / starten
*/
void Controller::setFinished(bool finished)
{
this->finished = finished;
}
/**
Wurde das Spiel beendet?
*/
bool Controller::isFinished()
{
return this->finished;
}
/*
Status aktualisieren
Status enthält den aktuellen Spielernamen und seine Farbe
*/
void Controller::refreshStatus()
{
//Spielername anzeigen
this->gui->setStatusMessage("Spieler '" + this->getCurrentPlayer()->getName() + "' ist dran!");
//Farbe anzeigen
this->gui->setSpielerfarbeVorschau(this->getCurrentPlayer()->getColor());
//Änderungen von der GUI verarbeiten lassen
Application->ProcessMessages();
}
/*
Mit dieser Methode kann man den Typ (Computer / Mensch)
eines Spielers ändern.
Die ID entspricht hier nicht der ID im array, sondern ist 1 höher.
*/
void Controller::setPlayerType(int id, AnsiString type)
{
//Spieler 1 oder SPieler 2 NICHT?
if(id != 1 && id != 2)
{
throw "Dieser Spieler existiert nicht!";
}
id--; //ID entspricht jetzt Array-Index von this->spieler
if(type == "Mensch")
{
this->spieler[id] = new Mensch; //MEnsch-Objekt hineinschreiben
}
else if(type == "Computer")
{
this->spieler[id] = new Computer; //Computer-Objekt hineinschreiben
}
else
{
throw "Dieser Spielertyp existiert nicht";
}
}
//Gibt einen Zeiger auf die GUI (Form1) zurück
TForm1* Controller::getGUI()
{
return this->gui;
}
//Diese Methode wird aufgerufen, wenn der Timer aktiv wird.
void Controller::refreshTimer()
{
if(!this->isFinished()) //Zeit nur zählen, wenn Spiel nicht beendet ist
{
this->timer_sec++; //Bei jedem Aufruf soll der Sekundenzähler erhöht werden.
if(this->timer_sec >= 60) //Prüfen, ob eine Minute vorbei ist
{
this->timer_sec = 0; //Wenn ja, Sekunden auf 0 setzen
this->timer_min++; //und Minuten erhöhen
}
}
this->gui->showTimerData(this->timer_min, this->timer_sec); //Die GUI soll die Daten jetzt ausgeben
}
/*
Setzt die Spieldauer auf 0:00 min zurück
*/
void Controller::resetTimer()
{
this->timer_sec = 0; //0 Sekunden
this->timer_min = 0; //0 Minuten
this->gui->showTimerData(this->timer_min, this->timer_sec); //Anzeige neu beschreiben lassen
}
Ein wenig Klarheit
Das ist jetzt ganz schön viel Code auf einem Haufen. Aber dank den Kommentaren sollte er relativ verständlich sein. Auf die wichtigsten Stellen gehe ich jetzt nochmal gesondert ein.
Konstruktor
//Konstruktor initialisiert Variablen und erstellt Objekte
Controller::Controller(TForm1* Form1)
{
this->gui = Form1; //Das erste Formular als GUI verwenden
//Die Breite der GUI fetlegen
this->gui->Width = (CONST__anzX+1)* (CONST__breite_label + CONST__abstand) + CONST__abstand;
//Die Höhe der GUI festlegen
this->gui->Height = (CONST__anzY+1)* (CONST__hoehe_label + CONST__abstand) + CONST__abstand + 10;
//Eventhandler des Feldcontainers beim Resize (auch beim Hinzufügen von Elementen)
this->gui->panel_feldcontainer->OnResize = adjust;
//Eventhandler der GUI beim Resitze = OnResize von Feldcontainer
this->gui->OnResize = this->gui->panel_feldcontainer->OnResize;
//Das Model, das das Spielfeld enthält, wird erstellt.
this->model = new SpielfeldModel(
this->gui, this->gui->panel_feldcontainer,
CONST__breite_label,
CONST__hoehe_label,
(CONST__hoehe_label + CONST__abstand),
(CONST__breite_label + CONST__abstand)
);
//Jetzt muss noch jedem Feld die onClick-Methode zugewiesen werden.
for(int y = 0; y < CONST__anzY; y++)
{
for(int x = 0; x < CONST__anzX; x++) { model->getFeld(x,y)->OnClick = onClick;
}
}
this->spieler[0] = new Mensch; //Ersten Spieler erzeugen
this->spieler[0]->setColor(CONST__default_spieler1_color); //Farbe setzen
this->spieler[1] = new Computer(2); //Zweiten Spieler erzeugen
this->spieler[1]->setColor(CONST__default_spieler2_color); //Farbe setzen
this->counter = 0; //Es wurde noch nicht gespielt
this->finished = false; //Das Spiel ist noch nicht beendet
this->timer_min = 0; //Timer zurücksetzen
this->timer_sec = 0; //Timer zurücksetzen
this->refreshStatus(); //Statusanzeige aktualisieren (bzw. initialisieren)
}
Der Konstruktor hat hier folgende Aufgaben:
- Die GUI dem Controller zuweisen
- Maße der GUI festlegen
- onResize-Eventhandler zuweisen
- das Model erstellen und das onClick-Event des Spielfelds zuweisen
- Die 2 Spieler erstellen und konfigurieren
- Status und andere Variablen zurücksetzen
Aber um den Teil mit dem Model zu verstehen, sollte man erstmal den Aufbau des Models kennen!
Das Model
/*
Das SpielfeldModel ist eine Klasse, die zur Speicherung des Spielfelds dient.
Sie hat alle Felder in einem Array und bietet Methoden zum Ansprechen dieser
Felder.
*/
class SpielfeldModel
{
private:
Feld* array[CONST__anzX][CONST__anzY]; //Array mit allen Feldern
TColor emptyFieldColor; //Farbe eines leeren Felds
TColor backgroundColor; //Hintergrundfarbe
public:
//Konstruktor
SpielfeldModel(
TComponent* Owner,
TWinControl* parent,
unsigned int width,
unsigned int height,
unsigned int top,
unsigned int left)
{
//Zuweisung der Standartfarben
this->emptyFieldColor = CONST__default_emptyField_color;
this->backgroundColor = CONST__default_background_color;
//Das Array durchlaufen
for(int y = 0; y < CONST__anzY; y++)
{
for(int x = 0; x < CONST__anzX; x++) { //Jedem Arrayelement ein Feld zuweisen. this->array[x][y] = new Feld("x" + IntToStr(x) + "_y" + IntToStr(y), //Feldname
Owner, parent, //Besitzer und Vater
width, height, //Maße des Felds
y*top, x*left, //Koordinaten des Felds
this->emptyFieldColor //Farbe des Felds
);
}
}
//Die Hintergrundfarbe des Elternobjekts wird gesetzt.
dynamic_cast(parent)->Color = this->backgroundColor;
}
/*
Liefert ein Feld des Spielfelds
*/
Feld* getFeld(int x, int y)
{
//Prüfen, ob gültige Koordinaten übergeben wurden
if(this->isValidCoord(x,y))
{
return this->array[x][y]; //Zurückgeben
}
else
{
throw "Dieses Feld besitzt ungueltige Koordinaten";
}
}
//Farbe der leeren Felder setzen
void setEmptyFieldColor(TColor cl)
{
this->emptyFieldColor = cl;
}
//Farbe der leeren Felder holen
TColor getEmptyFieldColor()
{
return this->emptyFieldColor;
}
//Hintergrundfarbe setzen
void setBackgroundColor(TColor cl)
{
this->backgroundColor = cl;
}
//Hintergrundfarbe holen
TColor getBackgroundColor()
{
return this->backgroundColor;
}
//Prüfen, ob eine Koordinate gültig ist
bool isValidCoord(int x, int y)
{
//Prüft die x- und y-Koordinate seperat
return (this->isValidCoordX(x) && this->isValidCoordY(y));
}
//x-Koordinate erlaubt?
bool isValidCoordX(int x)
{
return (x >= 0 && x < CONST__anzX); } //y-Koordinate erlaubt? bool isValidCoordY(int y) { return (y >= 0 && y < CONST__anzY);
}
//Spielfeld zurücksetzen auf Anfangszustand
void reset()
{
//Spielfeld durchlaufen
for(int y = 0; y < CONST__anzY; y++)
{
for(int x = 0; x < CONST__anzX; x++) { //Feld leeren this->getFeld(x,y)->setEmpty();
}
}
}
};
Im Grunde werden hier nur ein paar getter und setter angeboten, der Rest ist selbsterklärend.
Aber was hat es mit diesen Konstanten auf sich?
Konstanten wie CONST__anzX werden bei mir überall im gesamten Projekt verwendet, wo etwas steht, was einfach austauschbar sein muss. z.B. die Breite des Spielfelds (–> CONST__anzX)
Definiert werden alle Konstanten in der __headers.h
/*
In dieser Datei werden alle wichtigen Konstanten gesetzt.
Außerdem werden alle Header-Dateien, die zum Projekt gehören,
eingebunden.
*/
static const unsigned short int CONST__anzX = 3; //Spielfeld-Breite
static const unsigned short int CONST__anzY = 3; //Spielfeld-Höhe
static const unsigned int CONST__breite_label = 80; //Breite eines Felds
static const unsigned int CONST__hoehe_label = 80; //Höhe eines Felds
static const unsigned int CONST__abstand = 1; //Abstand zw. Feldern
static const TColor CONST__default_spieler1_color = clRed; //Standartfarbe Spieler1
static const TColor CONST__default_spieler2_color = clLime; //Standartfarbe Spieler2
static const TColor CONST__default_background_color = clWhite; //Standarthintergrund
static const TColor CONST__default_emptyField_color = clBtnFace; //Standartfarbe Leeres Feld
static const AnsiString CONST__default_spieler1_name = "Spieler 1"; //Standartname Spieler1
static const AnsiString CONST__default_spieler2_name = "Spieler 2"; //Standartname Spieler2
#include <stdio.h>
#include <IniFiles.hpp>
#include "anleitung.h"
#include "tictactoe.h"
#include "spieler.h"
#include "model_feld.h"
#include "computer.h"
#include "mensch.h"
#include "model_spielfeld.h"
#include "controller.h"
#include "model_options.h"
#include "optionen.h"
#include "controller_options.h"
Diese Datei ist in allen Dateien meines Projekts die einzige Header-Datei, die eingebunden wird. In dieser h-Datei werden dann wiederum alle anderen Header in der richtigen Reihenfolge eingebunden. Gefällt mir persönlich sehr gut, da es die Wiederverwendbarkeit fördert.
Noch ein Model: Feld
Das Model Feld ist eine Klasse, die anklickbare Felder auf dem Spielfeld repräsentiert. Feld wird von TLabel abgeleitet und so modifiziert:
//Die Klasse Feld wird genutzt, um Feld-Objekte zu erzeugen.
//Objekte der Klasse Feld sind von TLabel abgeleitet.
//Beim Anklicken verändern sie ihr Aussehen.
class Feld : public TLabel
{
private:
Spieler* belongsToPlayer; //Besitzer des Spielfelds
public:
__fastcall Feld(AnsiString name, //Name des Felds
TComponent* Owner, //Objektbesitzer
TWinControl* parent, //Elternelement
unsigned int width, //Breite
unsigned int height, //Höhe
unsigned int top, //y-Koordinate
unsigned int left, //x-Koordinate
TColor cl) //Feldfarbe
: TLabel(Owner) //Konstrukturaufruf von TLabel
{
this->Color = cl;
this->AutoSize = false; //Das Feld soll sich nicht dem Inhalt anpassen
this->Font->Color = clWhite;
this->Parent = parent;
this->Caption = " "; //Auf dem Feld soll nichts stehen
this->ShowHint = true; //Hinweis beim Überfahren mit der Maus: ja
this->Hint = "Leeres Feld"; //Standarttext des Hinweises
this->Top = top;
this->Left = left;
this->Width = width;
this->Height = height;
this->Name = name;
this->belongsToPlayer = NULL; //Anfangs besitzerlos
this->Layout = tlCenter; //in y-Richtung zentrieren
this->Alignment = taCenter; //in x-Richtung zentrieren
this->Font->Size = 10;
this->Font->Style = TFontStyles()<< fsBold; //Fette Schrift } void setColor(TColor farbe) { this->Color = farbe; //Feldfarbe ändern
}
TColor getColor()
{
return this->Color; //Feldfarbe zurückgeben
}
void setPlayer(Spieler* pl) //Besitzer setzen
{
this->belongsToPlayer = pl;
this->Caption = this->belongsToPlayer->getName(); //Name als Beschriftung
this->Hint = "Feld belegt von "+this->Caption; //Hint setzen
}
Spieler* getPlayer()
{
return this->belongsToPlayer; //Spielerzeiger zurückgeben
}
bool isEmpty()
{
return this->belongsToPlayer == NULL; //Hat das Feld einen Besitzer?
}
void setEmpty()
{
this->belongsToPlayer = NULL; //Besitzer löschen
this->Caption = " "; //Beschriftung leeren
this->Hint = "Leeres Feld"; //Hinweistext zurücksetzen
}
};
Im Konstruktor werden hier alle wichtigen Attribute gesetzt, wie z.B. Owner, Parent, die Maße, die Koordinaten oder das Aussehen. Wenn später im Spielverlauf ein Spieler ein Feld belegt, wird durch die Methode setPlayer(Spieler* pl) der Besitzer in das Attribut belongsToPlayer eingetragen. Dabei wird auch die Beschriftung des Felds in den Namen des Spielers umgeändert.
Zurück zum eigentlichen Thema: Controller
Ihr wisst jetzt also, wie genau das Ganze aufgebaut ist. Das (Spielfeld-)Model macht nichts als die einzelnen Felder anzulegen und richtig anzuordnen. Dann bietet es noch Methoden zum Zurücksetzen des Spielfelds und zum Prüfen von Koordinaten auf Gültigkeit.
Die Methoden zum Prüfen der Koordinaten braucht man, um im Controller entscheiden zu können, ob man sich noch innerhalb des Spielfelds befindet oder in einen undefinierten Speicherbereich schreiben möchte, was nicht so gut wäre (EAccessViolation lässt grüßen).
Die onClick-Methode des Controllers
Diese Methode hat mir meinen letzten Nerv geraubt. Irgendwo hatte ich einen Logikfehler drin, den ich irgendwann zwar behoben habe, aber immer noch nicht weiß, was denn der Fehler eigentlich war. Jedenfalls funktioniert es jetzt und das ist das, was zählt 🙂
// Diese Methode regelt den Spielablauf. Mit dem ersten Klick eines Spielers beginnt das Spiel.
// Ist der zweite Spieler der PC, wird automatisch die entsprechende Methode aufgerufen.
void __fastcall Controller::onClick(TObject *Sender)
{
Feld *feld = NULL; //Temporärer Speicher für Felder
feld = dynamic_cast(Sender); //Sender-Objekt wird zu Feld-Objekt gecastet
if(this->isFinished()) //Falls das Spiel beendet wurde, wird abgebrochen
{
return; //Abbrechen
}
if(this->getCurrentPlayer()->getType() == "Mensch" //Falls der Spieler ein Mensch ist
&& (Sender == NULL || feld == NULL)) //Und kein Feld angeklickt wurde
{
return; //Abbrechen
}
if(this->getCurrentPlayer()->getType() == "Mensch"
&& !feld->isEmpty()) //Falls der Spieler ein Mensch ist und das Feld belegt ist
{
return; //Wird die Methode abgebrochen, da nur leere
} //Felder angeklickt werden dürfen
//Sollte der aktuelle Spieler der PC sein, muss der Zug des PCs mit der Methode zugPC ausgeführt werden
if(this->getCurrentPlayer()->getType() == "Computer" && !this->isFinished())
{
Sleep(300); //PC setzt nicht sofort, sondern wartet 300ms.
this->zugPC(); //Der PC führt den Zug aus
}
//Der aktuelle Spieler ist kein PC, sondern ein Mensch. Klick wird ausgewertet
else if(this->getCurrentPlayer()->getType() == "Mensch" && !this->isFinished())
{
if(feld->isEmpty()) //Man darf nur leere Felder anklicken
{
feld->setColor(this->getCurrentPlayer()->getColor()); //Angeklicktes Feld einfärben
feld->setPlayer(this->getCurrentPlayer()); //Besitzer des Felds eintragen
}
}
else
{
ShowMessage("Der Spielertyp des aktuellen Spielers ist undefiniert. Spielabbruch");
this->setFinished(true); //Spiel beenden, da Fehler auftrat.
}
this->searchWinner(); //Schauen, ob es einen Gewinner gibt / Unentschieden
//Nur weitermachen, wenn das Spiel nicht beendet wurde.
if(!this->isFinished())
{
//Spielzähler erhöhen. Der aktuelle Spieler ist jetzt der nächste Spieler
this->incrementCounter();
//Status aktualisieren. Es wird jetzt der aktuelle (also der nächste) Spieler
//im Status angezeigt.
this->refreshStatus();
//Diese Stelle ist sehr wichtig für den Fall, dass ein Computerspieler an der Reihe ist.
//Der PC löst nämlich kein onClick-Event aus, deshalb machen wir das hier manuell.
if(this->getCurrentPlayer()->getType() == "Computer")
{
//Hier wird das onClick-Event manuell ausgelöst (als ob der PC geklickt hätte)
this->onClick(NULL);
}
}
else
{
this->incrementCounter(); //Wenn Spiel beendet wurde, muss der Spielzähler
//noch um 1 erhöht werden, weil er sonst den
//beendenden Zug nicht enthält.
}
}
Ein Problem bei meinem Konzept war, dass man immer einen Klick tätigen musste, obwohl der Mensch dran war. Das habe ich jetzt teilweise gelöst, indem ich am Ende der Methode prüfe, ob der nächste Spieler ein PC ist. Ist der nächste Spieler der PC, rufe ich die onClick-Methode erneut auf, allerdings mit NULL als Sender*. So spare ich mir den Klick, denn der PC „klickt“ sozusagen selbst.
Aber es ist trotzdem noch immer nötig, dass ein Klick das ganze Spiel zum Laufen bringt. Denn letztendlich steuert die onClick-Methode den kompletten Ablauf, was ich etwas störend finde. Ich wüsste aber nicht, wie man das besser machen könnte.
Der Zug des PCs
Der PC kennt 2 Schwierigkeitsstufen: Einfach und Schwierig.
Die einfache Stufe ist eigentlich nur ein random, siehe hier:
/*
Die einfache Spielmethode setzt an einer zufälligen Stelle im Spielfeld
einen Stein, falls das Feld frei ist.
*/
void Controller::pcEasy()
{
Feld *feld = NULL; //Temporärer Speicher für Feld
int x = 0, y = 0; //Koordinaten des PC-Zugs
randomize(); //Zufallsgenerator starten
do
{
x = random(CONST__anzX); //zufällige x-Koordinate
y = random(CONST__anzY); //zufällige y-Koordinate
if(this->model->isValidCoord(x,y)) //Prüfen, ob Koordinate erlaubt ist
{
feld = this->model->getFeld(x,y); //Das Feld (x|y) holen
}
}
//solange das Spiel nicht unentschieden, beendet oder das Feld voll ist.
while(!this->isUnentschieden() && feld != NULL && !feld->isEmpty() && !this->isFinished());
feld->setColor(this->getCurrentPlayer()->getColor()); //Dem gefundenen Feld die Farbe zuweisen
feld->setPlayer(this->getCurrentPlayer()); //Dem gefundenen Feld den Besitzer zuweisen
}
Es wird solange eine zufällige Koordinate ermittelt, bis sie frei ist oder das Spiel unentschieden bzw. beendet ist.
Wird eine freie Koordinate gefunden, wird das Feld eingefärbt und der Besitzer (PC) eingetragen.
Die schwere Stufe und die Sache mit der Pseudo-KI
Bei der schweren Stufe wollte ich ursprünglich eine richtige KI entwickeln, die vorrausschauend spielt und den Gegner niemals gewinnen lässt. Dass das aber viel zu schwer ist, merkte ich sehr schnell. Ich bin dann auf eine Pseudo-KI umgestiegen, die einfach nur nach 2er-Kombinationen des Gegners sucht und verhindert, dass er sie zu 3ern macht. Genauso versucht der PC, eigene 2er zu 3ern zu machen.
/*
Die schwere Spielmethode versucht, eigene Kombinationen zu erstellen und zu
vervollständigen.
Außerdem findet diese Methode die "Gewinnversuche" des Gegners und versucht,
diese zu verhindern.
*/
void Controller::pcDifficult()
{
Feld *feld = NULL; //Temporärer Speicher für Feld
//Die ersten beiden Züge sollen nicht mit der KI gemacht werden,
//weil der PC sonst am Anfang immer den gleichen Zug macht, wenn der Mensch
//das auch macht.
if(this->getCounter() <= 1) { this->pcEasy();
return; //Abbruch, da Zug gemacht wurde.
}
//Als erstes wird versucht, eigene 3er-Kombinationen zu erzeugen
feld = this->findCombinations(this->getCurrentPlayer(), 2);
if(feld == NULL)
{ //Dann wird versucht, gegnerische 2er zu blockieren, so dass keine 3er entstehen
feld = this->findCombinations(this->getNextPlayer(), 2);
}
if(feld == NULL)
{ //Andernfalls werden aus eigenen 1ern 2er gemacht
feld = this->findCombinations(this->getCurrentPlayer(), 1);
}
if(feld == NULL)
{ //Sonst gegnerische 1er blockieren, so dass keine 2er entstehen
feld = this->findCombinations(this->getNextPlayer(), 1);
}
if(feld == NULL)
{ //Sollte nichts geklappt haben, wird die Zufallsmethode versucht
this->pcEasy();
}
else
{ //Wenn die KI zugeschlagen hat, wird das Feld belegt
feld->setColor(this->getCurrentPlayer()->getColor());
feld->setPlayer(this->getCurrentPlayer());
}
}
Da das mal wieder viel zu viel Code für eine Methode war und ich Wiederverwendbarkeit ganz groß schreibe, habe ich eine Methode zum Finden von Kombinationen geschrieben. Diese kommt übrigens auch bei der Gewinnerermittlung zum Einsatz.
/*
Diese Methode durchsucht das Spielfeld nach Kombinationen mit der Länge "length"
Anschließend gibt sie das Feld, das "vor" der Kombination kommt und leer ist,
zurück.
*/
Feld* Controller::findCombinations(Spieler* player, unsigned int length)
{
for(int y = 0; y < CONST__anzY; y++)
{
for(int x = 0; x < CONST__anzX; x++) { if(this->model->isValidCoord(x,y) && //Koordinate gültig?
this->model->getFeld(x,y)->isEmpty() //Feld leer?
)
{
if(this->isCombination(player, x, y+1, 0, 1, length) //Senkrecht
|| this->isCombination(player, x+1, y, 1, 0, length) //Waagrecht
|| this->isCombination(player, x, y-1, 0, -1, length) //Senkrecht
|| this->isCombination(player, x-1, y, -1, 0, length) //Waagrecht
|| this->isCombination(player, x+1, y+1, 1, 1, length) //Diagonal
|| this->isCombination(player, x+1, y-1, 1, -1, length) //Diagonal
|| this->isCombination(player, x-1, y+1, -1, 1, length) //Diagonal
|| this->isCombination(player, x-1, y-1, -1, -1, length) //Diagonal
)
{
return this->model->getFeld(x,y); //Feld zurückgeben
}
}
}
}
return NULL; //Kein Feld gefunden
}
In dieser Methode wird das gesamte Spielfeld durchlaufen und in jedem Punkt geprüft, ob es eine Kombination mit der erforderlichen Länge und den gewünschten Startkoordinaten in der gewünschten Richtung gibt.
Wie diese Methode arbeitet, seht ihr hier.
/*
isCombination überprüft, ob es ab der Stelle (startX|startY) eine
Feldkombination des Spielers player gibt.
Mit dirX und dirY kann man steuern, ob waagrechte, senkrechte oder diagonale
Kombinationen gesucht werden sollen.
length gibt die Mindestlänge der Kombinationen an.
*/
bool Controller::isCombination(Spieler* player, int startX, int startY, int dirX, int dirY, unsigned int length)
{
//Teste, ob Startkoordinaten gültig sind
if(!this->model->isValidCoord(startX, startY))
{
return false; //Ungültige Koordinaten
}
//Teste, ob Endkoordinaten gültig sind
if(!this->model->isValidCoord(startX + (dirX*(length-1)), startY + (dirY*(length-1))))
{
return false; //Ungültige Koordinaten
}
//jetzt wird eine Schleife length mal wiederholt.
for(unsigned int i = 0; i < length; i++) { //Es wird geprüft, ob das Feld NICHT vom Spieler ist. if(this->model->getFeld(startX + (dirX*i), startY + (dirY*i))->getPlayer() != player)
{
return false; //Wenn es nicht vom Spieler ist, wird false zurückgegeben,
//da dann keine Kombination existiert.
}
}
return true; //Eine Kombination existiert!
}
Erst wird geprüft, ob gültige Koordinaten übergeben wurden und dann wird in einer Schleife für jede Länge (ich hoffe, ihr wisst, was ich meine) geprüft, ob das Feld dem Spieler gehört, dem es gehören soll. Wenn nein, wird false zurückgegeben, was bedeutet, dass keine Kombination besteht.
Die Suche nach dem Gewinner
Das ganze Spiel bringt uns nichts, wenn wir nicht sehen, wer gewonnen hat. Dazu gibt es jetzt die checkField()-Methode
/*
Überprüft, ob es im Spielfeld einen Gewinner gibt, der length
Felder in einer Kombination hat.
*/
Spieler* Controller::checkField()
{
unsigned int length = 3; //TicTacToe erfordert 3er-Kombinationen
Spieler* player = NULL; //Pointer auf spieler-Objekt
//Spielfeld durchlaufen
for(unsigned int y = 0; y < CONST__anzY; y++)
{
for(unsigned int x = 0; x < CONST__anzX; x++) { if(this->model->isValidCoord(x,y)) //Ist die Koordinate gültig?
{
player = this->model->getFeld(x, y)->getPlayer(); //Besitzer des Felds holen
if(player != NULL) //Wenn es einen Besitzer gibt
{
if( this->isCombination(player, x, y, 0, 1, length) //Senkrecht
|| this->isCombination(player, x, y, 1, 0, length) //Waagrecht
|| this->isCombination(player, x, y, 1, 1, length) //Diagonal
|| this->isCombination(player, x, y, 1, -1, length) //Diagonal
|| this->isCombination(player, x, y, -1, 1, length) //Diagonal
|| this->isCombination(player, x, y, -1, -1, length) //Diagonal
)
{
return player; //Gewinner zurückgeben
}
}
}
}
}
return NULL; //Kein Gewinner
}
Diese Methode macht eigentlich genau das gleiche wie die KI, bis auf den Unterschied, dass sie nur nach 3er-Kombinationen sucht und bei Erfolg den Spieler, dem die Kombination gehört, zurückgibt.
Die View-Schicht, auch GUI genannt
Um die ganzen Daten, die da so verarbeitet, gelesen und geschrieben werden, auch anzeigen zu können, gibt es die Präsentationsschicht, die GUI. Diese ist in unserem Fall eine Windowsoberfläche, könnte aber auch eine Konsole sein.
//VIEW
//---------------------------------------------------------------------------
#include
#pragma hdrstop
#include "__headers.h"
//---------------------------------------------------------------------------
#pragma package(smart_init)
#pragma resource "*.dfm"
TForm1 *Form1;
//---------------------------------------------------------------------------
__fastcall TForm1::TForm1(TComponent* Owner)
: TForm(Owner)
{
}
//---------------------------------------------------------------------------
//---------------------------------------------------------------------------
void __fastcall TForm1::FormCreate(TObject *Sender)
{
//Mit dieser Zeile wird der Spielablauf in Gang gesetzt.
//Dem Konstruktor des Controllers wird ein Zeiger auf die GUI übergeben
this->controller = new Controller(this);
}
void __fastcall TForm1::Optionen1Click(TObject *Sender)
{
//Da das Optionsmenü ein neues Formular ist, muss es erst sichtbar
//gemacht werden.
Form2->Visible = true;
}
//---------------------------------------------------------------------------
void __fastcall TForm1::Beenden1Click(TObject *Sender)
{
//Das Programm beenden
Application->Terminate();
}
//Diese Methode aktualisiert die GUI
void TForm1::RefreshField()
{
Feld* feld; //Temporärer Feld*-Speicher
for(int y = 0; y < CONST__anzY; y++)
{
for(int x = 0; x < CONST__anzX; x++) { feld = this->controller->getModel()->getFeld(x,y); //Das Feld steht jetzt in feld
if(!feld->isEmpty()) //Wenn das Feld nicht leer ist
{
feld->setColor(feld->getPlayer()->getColor()); //Dann wird die Farbe des Besitzers übernommen
feld->Caption = feld->getPlayer()->getName(); //Und der Name des Besitzers wird die Beschriftung
}
else
{
//Das Feld ist leer
//Deshalb wird die EmptyFieldColor-Farbe benutzt
feld->setColor(this->controller->getModel()->getEmptyFieldColor());
}
}
}
//Hintergrund des Spielfelds einfärben
//Der Hintergrund stellt eigentlich die Trennlinien dar, weil der Rest
//durch die einzelnen Felder bedeckt ist
this->panel_feldcontainer->Color = this->controller->getModel()->getBackgroundColor();
}
//Den Statustext ändern
void TForm1::setStatusMessage(AnsiString msg)
{
this->label_status->Caption = msg;
}
//Anzahl gemachter Spielzüge anzeigen
void TForm1::showSpielzuege(int zuege)
{
this->label_spielzuege->Caption = "Spielzüge: "+IntToStr(zuege);
}
//Diese Methode platziert alle Elemente relativ zueinander auf dem Spielfeld
void TForm1::placeElements()
{
//Die x-Koordinate des Spielfelds mit den einzelnen Feldern als Inhalt
//berechnet sich aus der Differenz der Breite des Fensters und der Breite des Spielfelds
//Das Ganze wird dann durch 2 geteilt, um die Mitte zu finden und um 3 Pixel nach links
//verschoben, da die Ränder nicht berücksichtigt wurden.
this->panel_feldcontainer->Left = (this->Width - this->panel_feldcontainer->Width) / 2 -3;
//Die y-Koordinate des Spielfelds mit den einzelnen Feldern als Inhalt
//wird aus der Differenz der Formularhöhe, der Spielfeldhöhe, der Titelleistenhöhe
//und der Menüleistenhöhe berechnet. Alles wird dann durch 2 geteilt, um das
//Spielfeld in der Mitte zu plazieren.
this->panel_feldcontainer->Top = (this->Height
- this->panel_feldcontainer->Height
-27 -27) / 2 ; // 27 ist die Höhe der Titelleiste
// und vom Hauptmenü
//Die y-Koordinate der Spielzugsanzeige wird aus der Summe der Spielfeld-y-Koordinate und der
//Höhe des Spielfelds berechnet
this->label_spielzuege->Top = (this->panel_feldcontainer->Top + this->panel_feldcontainer->Height);
//Die x-Koordinate der Spielzugsanzeige entspricht der y-Koordinate des Spielfelds
this->label_spielzuege->Left = this->panel_feldcontainer->Left;
//x-Koordinate der Spieldaueranzeige ist Spiezugzuganzeige(x) + Spielzuganzeige (breite) + 15
this->label_spieldauer->Left = this->label_spielzuege->Left + this->label_spielzuege->Width + 15;
//y-Koordinate der Spieldaueranzeige ist gleich wie die y-Koordinate der Spielzuganzeige
this->label_spieldauer->Top = this->label_spielzuege->Top;
//Die x-Koordinate der Spielfarbenanzeige entspricht der x-Koordinate des Spielfelds
this->label_spielerfarbe->Left = this->panel_feldcontainer->Left;
//Die y-Koordinate der Spielfarbenanzeige ist die Differenz aus y-Koordinate des Spielfelds
//und Höhe des Status-Labels
this->label_spielerfarbe->Top = (this->panel_feldcontainer->Top - this->label_status->Height);
//Das Label sitzt 1 px weiter oben als die Farbe, weil es optisch besser aussieht
this->label_status->Top = this->label_spielerfarbe->Top -1;
//3px Abstand zwischen Farbe und Status.
this->label_status->Left = this->label_spielerfarbe->Left + this->label_spielerfarbe->Width + 3;
}
//Liefert den Controller
Controller* TForm1::getController()
{
return this->controller;
}
//Die Farbe neben der Statusanzeige setzen
void TForm1::setSpielerfarbeVorschau(TColor cl)
{
this->label_spielerfarbe->Color = cl;
}
//Farbanzeige auf Standartwert (entspricht dem Formularhintergrund) setzen
void TForm1::resetSpielerfarbeVorschau()
{
this->setSpielerfarbeVorschau(this->Color);
}
//---------------------------------------------------------------------------
//Neues Spiel starten
void __fastcall TForm1::Neu1Click(TObject *Sender)
{
//Diese Aufgabe wird an Controller::reset() delegiert.
this->controller->reset();
}
//---------------------------------------------------------------------------
void __fastcall TForm1::Info1Click(TObject *Sender)
{
//Kleinen Info-Text anzeigen
ShowMessage("Dieses Spiel wurde von Simon H. als CT-OOP-Projekt in der Klasse TGI 12/1 programmiert.");
}
//---------------------------------------------------------------------------
void __fastcall TForm1::Anleitung1Click(TObject *Sender)
{
//Das Formular der Anleitung anzeigen
Form3->Visible = true;
}
//---------------------------------------------------------------------------
//Wird jede Sekunde aufgerufen
void __fastcall TForm1::Timer1Timer(TObject *Sender)
{
//Der Controller soll die Anfrage verarbeiten
this->controller->refreshTimer();
}
//Anhand der Minutenanzahl und der Sekundenanzahl die Ausgabe erstellen
void TForm1::showTimerData(int timer_min, int timer_sec)
{
AnsiString min = "", sec = ""; //Strings zur Ausgabe
//Zahlen kleiner 10 bekommen eine 0 vorangestellt
min = (timer_min < 10) ? "0"+IntToStr(timer_min) : IntToStr(timer_min);
sec = (timer_sec < 10) ? "0"+IntToStr(timer_sec) : IntToStr(timer_sec); //Ausgabe this->label_spieldauer->Caption = "Spieldauer: "+min+":"+sec+" min";
}
//---------------------------------------------------------------------------
Wenn das Spiel gestartet wird, wird das Formular automatisch erzeugt und damit das FormCreate-Event ausgelöst. Hier wollen wir die Führung sofort an den Controller übergeben und instanziieren ihn. Anschließend wird er als Attribut abgelegt.
Neben ein paar unwichtigen Methoden zum Anzeigen des Optionsmenüs oder der Anleitung gibt es dann noch Methoden, die eine Erklärung verdient haben. Da wäre z.B. die refreshField()-Methode. Diese durchläuft das gesamte Spielfeld und setzt die Farben der Felder und des Hintergrunds auf die richtigen Werte. Dies ist nötig, wenn Optionen geändert wurden oder das Spiel neu gestartet wurde.
PlaceElements() platziert alle Elemente auf dem Formular so, dass alles immer mittig ist. Ich denke, dass hier keine weitere Erklärung mehr von Nöten ist. Ansonsten einfach die Kommentarfunktion nutzen!
Auf zum Optionsmenü!
Das war jetzt der Aufbau des Controllers und der GUI in aller Kürze. Es wurden längst nicht alle Methoden erklärt, sondern nur die, die ich für äußerst wichtig halte. Falls noch Fragen offen sind, stehe ich euch in der Kommentarfunktion zur Verfügung!
Jetzt gehts zur Erklärung des Optionsmenüs (Teil 3)!


[…] Tipps und Tricks für Webmaster PHP (OOP, MVC), HTML, MySQL, Javascript, AJAX und vieles mehr! « [C++] Tic Tac Toe mit GUI (Borland C++-Builder 5 VCL) [Teil 2 von 4] […]