[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] […]