[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)!

1 Star2 Stars3 Stars4 Stars5 Stars (1 Stimme, durchschnittlich 5,00 / 5)
Loading...


Ein Kommentar zu “[C++] Tic Tac Toe mit GUI (Borland C++-Builder 5 VCL) [Teil 2 von 4]”

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

Hinterlasse einen Kommentar!

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert

Diese Website verwendet Akismet, um Spam zu reduzieren. Erfahre mehr darüber, wie deine Kommentardaten verarbeitet werden.

»Informationen zum Artikel

Autor: Simon
Datum: 17.07.2009
Zeit: 15:00 Uhr
Kategorien: C/C++
Gelesen: 10933x heute: 2x

Kommentare: RSS 2.0.
Diesen Artikel kommentieren oder einen Trackback senden.

»Meta