Home » Tutorials » Grafik und Spiele » Vier gewinnt

Vier gewinnt

Das Spielfeld und die Spieler

Schon wieder das Spielfeld? Aber das haben wir doch oben schon gebastelt? Nicht ganz. Genau genommen gar nicht. Wir haben lediglich ein Spielfeld gezeichnet. Für den Computer existiert das Spielfeld noch gar nicht. Was für einen Menschen ganz einfach ist, nämlich mit einem Blick zu sehen, was ist Spielfeld, was ist ein Stein, den ein Spieler gesetzt hat, ist für einen Computer arg schwierig.
Informationen über Objekte aus einem Bild gewinnen, ist für Programme nur mit sehr großem Aufwand möglich und für uns definitiv nicht der richtige Weg.

Wir gehen den einfacheren Weg und hinterlegen im Speicher eine Tabelle bzw. ein Feld. Dieses enthält Informationen, wo und welcher Spieler gesetzt hat. Da eine solche Liste für Menschen eher unschön ist, stellen wir einen Stein an der Stelle dar, wo der Spieler gesetzt hat.
Der Spieler muss sich also nicht um das Feld kümmern, er sieht ein gewöhnliches 4-Gewinnt auf dem Bildschirm.

Zunächst müssen wir uns überlegen, welche Informationen für das Spiel benötigt werden. Das ist in diesem Falle recht einfach:

  1. Die Position (x und y) jedes Feldes muss enthalten sein
  2. der „Inhalt“ des Feldes, d.h. ob es leer ist oder ob und, wenn ja, welcher Spieler gesetzt hat

Für genau solche Fälle gibt es die Arrays, übersetzt Reihen oder Felder. Ein Array ist einfach ein Feld von Variablen eines bestimmten Typs, also zum Beispiel eine Liste mit Integer-Zahlen. Dabei hat jede Position in der Liste eine feste Nummer, den Index. Mit diesem kann man auf ein bestimmtes Feld zugreifen.

In Delphi können Arrays mehrdimensional sein. Zwei Dimensionen entsprechen dann einer Tabelle. Jedes Feld hat dann zwei Indizes, also die x- und die y-Position.

Es sind, im Rahmen des Speicherplatzes, beliebig viele Dimensionen möglich. Drei sind noch vorstellbar, spätestens bei vier wird die Übersetzung in ein vorstellbares Bild schwer.

Für unser Spielfeld benötigen wir allerdings nur zwei Dimensionen. Die Deklaration (unter Form1 ? private) sieht dann folgendermaßen aus:

FSpielfeld: array[0..6, 0..5] of TFeldBesetzung;

Zunächst, wie gewohnt, der Variablennamen, dann der Doppelpunkt, dann das Wort array und dann in eckigen Klammern die Größenangabe von..bis, die Dimensionen durch Komma getrennt.
Üblicherweise beginnt man bei 0 zu zählen. Theoretisch ist jeder andere positive Wert möglich, sollte aber aus Gründen der Lesbarkeit vermieden werden. Nach der Größenangabe folgt nach of der Variablentyp.

Hier verwenden wir einen Variablentyp, den wir selber definieren. Dadurch können wir ihn genau an unseren Bedarf anpassen.
Die Definition eines eigenen Variablentyps gehört an den Anfang der Typendeclaration, d.h. nach type und vor TForm1.

TFeldBesetzung = (sLeer, sSpieler1, sSpieler2);

Dieser Variablentyp enthält die drei möglichen Zustände eines Ortes in unserem Spielfeld. Entweder leer oder einer der beiden Spieler hat einen Stein positioniert.

Das Erstellen eines eigenen Typs ist zwar ein geringer Mehraufwand, dieser macht sich allerdings beim weiteren Programmieren bezahlt. Beispielsweise kann der Variablentyp auch als Arraygröße eines Feldes benutzt werden:

FSpielerFarbe: array[TFeldbesetzung] of TColor;
FSpielerBmp: array[TFeldBesetzung] of TBitmap;
FSpielerName: array[TFeldBesetzung] of string;

(TForm1 ? private)
Die erzeugten Arrays haben als Indices jetzt keine Nummern, sondern die Werte sLeer, sSpieler1 oder sSpieler2. Die Namen der Arrays sprechen für sich, im ersten stehen die Farben der Spieler, im zweiten das Bild, welches den Stein repräsentiert (in unserem Fall ein Kreis, aber dazu kommen wir später) und im letzten Feld der Name des Spielers. Diese Arrays benötigen alle eine sinnvolle Ausgangsbefüllung, da sie im Moment noch leer sind.

Aufgabe: Programmiere an der richtigen Stelle eine Anfangsbefüllung der Arrays.

Wer jetzt 42 Werte des Arrays von Hand auf sLeer gesetzt hat, sollte hier unterbrechen und sich noch einmal mit Schleifen beschäftigen. Schließlich holt man auch nicht jeden Ziegel einzeln aus der Brennerei, wenn man ein Haus baut.

Unter Verwendung von Schleifen ist die Grundbefüllung des Spielfeldes denkbar einfach:

//Leeren des Spielfeldes
for i := 0 to FSpielfeldBreite - 1 do
begin
  for j := 0 to FSpielfeldhoehe - 1 do
    FSpielfeld[i,j] := sLeer;
end;

Natürlich müssen die beiden Variablen i und j noch als lokale Integer deklariert werden.

Nur wohin jetzt damit? Der erste Gedanke wäre sicher Form.Create. Schließlich soll das Spielfeld ja beim Starten des Spiels entstehen. Allerdings kann ja auch zu einem beliebigen Zeitpunkt das Spiel neu gestartet werden. Auch dann muss das Spielfeld zurückgesetzt werden.

Da es sich von selbst verbietet, die Schleife zusätzlich noch an eine andere Stelle zu setzen, bleiben nur zwei Möglichkeiten. Entweder, das Leeren des Spielfeldes kommt in eine eigene Prozedur, die am Anfang und beim Neustart aufgerufen wird, oder, was in unserem Fall viel praktischer ist, das Spielfeld wird in der Klick-Prozedur das „Neu“-Buttons behandelt und wir rufen diese auch beim Spielstart auf, da ja zu Beginn auch nur ein neues Spiel gestartet wird. Da der letzte Satz doch recht kompliziert ist, legen wir jetzt einfach los, indem wir doppelt auf den „Neues Spiel“-Button klicken, wodurch Delphi die btnNeuClick-Prozedur erzeugt.
In diese kommt die Ausgangsbefüllung des Spielfeldes.

Die anderen Felder können allerdings getrost im Form.Create belegt werden:

FSpielerBmp[sSpieler1] := TBitmap.Create;
FSpielerBmp[sSpieler2] := TBitmap.Create;
FSpielerBmp[sLeer] := Tbitmap.Create;
FSpielerFarbe[sLeer] := clWhite;
FSpielerFarbe[sSpieler1] := clGreen;
FSpielerFarbe[sSpieler2] := clRed;
FSpielerName[sLeer] := 'Leer';
FSpielerName[sSpieler1] := 'Spieler 1';
FSpielerName[sSpieler2] := 'Spieler 2';

Wichtig: Die drei Bitmaps im Form.Destroy mit FspielerBmp[sSpieler1].free und analog für die anderen beiden wieder freigeben.

Hieraus wird schon ersichtlich, dass offenbar jeder Spieler eine eigene Bitmap bekommt.
Diese werden wir verwenden um die Spielsteine der Spieler in das Spielfeld zu bekommen. Dazu zeichnen wir unseren Spielstein am Anfang in die Bitmap und kopieren dann immer die Bitmap an die richtige Stelle des Spielfeldes.
Das ist zwar am Anfang etwas umständlich, hat aber den Vorteil, dass man das Ganze später ohne Probleme umschreiben kann, so dass zum Beispiel ein Bild in die Bitmap geladen wird. Dadurch kann der Spieler sich dann prinzipiell selber seinen „Spielstein“ aussuchen. Das werden wir jetzt allerdings nicht tun, wir begnügen uns mit einem einfachen Kreis in der Farbe des Spielers. Das gibt es jetzt direkt als kleine

Aufgabe: Schreibe eine Prozedur, die als Parameter den Durchmesser des Kreises und den Spieler erhält und in die entsprechende Bitmap einen Kreis mit der richtigen Farbe zeichnet.

procedure TForm1.SymbolErstellen(breite: Integer; spieler: TFeldBesetzung);
begin
  //Breite und Höhe einstellen
  FSpielerBmp[spieler].Width := breite;
  FSpielerBmp[spieler].Height := breite;
  //Bitmap leeren
  FSpielerBmp[spieler].Canvas.Pen.Color := clWhite;
  FSpielerBmp[spieler].Canvas.Brush.Color := clWhite;
  FSpielerBmp[spieler].Canvas.Rectangle(0, 0,breite, breite);
  //Kreis in der Farbe des jeweiligen Spielers zeichnen
  FSpielerBmp[spieler].Canvas.Pen.Color := FSpielerFarbe[spieler];
  FSpielerBmp[spieler].Canvas.Brush.Color := FSpielerFarbe[spieler];
  FSpielerBmp[spieler].Canvas.Ellipse(0, 0, breite, breite);
end;

Diese Prozedur muss natürlich auch noch aufgerufen werden. Das machen wir in der Prozedur „GroessenEinstellungenAnpassen“, da die immer dann aufgerufen wird, wenn an den grafischen Rahmenbedingungen etwas verändert wird:

// Neuerstellen der Symbole der jeweiligen Spieler
SymbolErstellen(FFelderbreite-1, sSpieler1);
SymbolErstellen(FFelderbreite-1, sSpieler2);
SymbolErstellen(1, sLeer);

Das Symbol für die leeren Felder wird nur der Vollständigkeit halber befüllt. Es entsteht nur ein weißer Kreis auf weißem Hintergrund. Theoretisch ließe sich damit aber eine Hintergrundbefüllung des Spielfeldes realisieren.