Home » Tutorials » Grafik und Spiele » Vier gewinnt

Vier gewinnt

Die Reaktion auf Klicks ins Spielfeld – Tastsinn des Programms

Wer jetzt der Meinung ist, sich eine kurze Pause gönnen zu dürfen, der hat damit durchaus recht. Schließlich haben wir inzwischen durchaus eine vorzeigbare Leistung erbracht.
Wir haben eine ansehnliche Benutzerschnittstelle die auch Benutzerfehler kompensieren kann gebastelt und unser Programm überzeugt, ein leeres Spielfeld anzuzeigen.

Im nächsten Schritt werden wir dafür sorgen, dass auch etwas passiert wenn wir ins Spielfeld klicken. Denn wenn wir ganz ehrlich sind, ist so ein Gitter zwar schön und gut, aber ein sinnvolles Spiel lässt sich damit noch nicht bewerkstelligen.

Überlegen wir zunächst einmal, was wir denn machen müssen. Zunächst ist es wichtig, zu bemerken, dass überhaupt geklickt wurde. Anschließend wäre eine Information wo genau der Klick hin ging nicht ganz schlecht, schließlich muss unser Programm ja wissen, wohin es den Spielstein setzen soll.

Den Klick „bemerken“ können wir recht einfach mit den Ereignissen der Paintbox bewerkstelligen. Eine Möglichkeit bietet hier das OnClick Ereignis. Allerdings müssten wir hier umständlich die Angeklickte Position auf der Paintbox anhand von Fensterposition, Position der Paintbox und angeklicktem Pixel auf dem Bildschirm ermitteln. Das Ereignis OnMouseDown nimmt uns das alles ab. Wir suchen also im Objektinspektor das Ereignis und klicken doppelt darauf. Delphi erstellt darauf hin das Grundgerüst der Prozedur.

Wenn wir uns jetzt den Kopf der Prozedur anschauen, sehen wir auch schon, wo wir die Position erhalten, nämlich einfach aus den übergebenen Parametern. Es gibt da die Parameter X und Y als Integer, welche nichts anderes sind, als die angeklickte Position relativ zur linken oberen Ecke der Paintbox.

Um den Umgang damit zu üben, folgt jetzt die nächste

Aufgabe: Erweitere das Programm so, dass es nach einem Klick in die Paintbox in einer Nachricht die X und Y Koordinaten der angeklickten Position ausgibt. Hierbei soll Y so umgerechnet werden, dass der Abstand zur unteren Kante angegeben wird und nicht zu oberen.

Zugegeben, die Aufgabe war einfach. Die Lösung des ganzen ist ein einziger Befehl:

ShowMessage(IntToStr(x)+ ', ' + InTtoStr(AnzeigeFlaeche.Height-y));

Beide Werte werden in einen String umgewandelt und durch Komma getrennt angezeigt. Der Y-Wert wird vorher von der Gesamthöhe der Anzeigefläche abgezogen, um zu erreichen, dass y=0 der untere Rand ist und nicht der obere.

Ziel dieser Aufgabe war auch weniger, eine komplizierte Lösung zu finden, sondern eher zu der Erkenntnis zu kommen, dass wir die Koordinaten unseres Klicks als ganz normale Variablen verarbeiten können.

Aufbauend auf dieser Erkenntnis werden wir uns jetzt daran machen, heraus zu finden, in welche Spalte unseres Spielfeldes geklickt wurde.

Das ist mit der x-Koordinate recht einfach, wenn man ein klein wenig Mathe dazu nimmt.
Die Überlegung ist folgende: Wenn wir am linken Rand des Spielfeldes anfangen, dann sind wir im ersten Feld. Je nach dem wie die Felderbreite eingestellt ist, beginnt nach beispielsweise 10 Pixeln das nächste Feld. Nach weiteren 10 Pixeln das nächste, …
Mathematisch ausgedrückt bedeutet das: Das Ergebnis der ganzzahligen Division der x-Koordinate abzüglich des linken Spielfeldrandes durch die Feldbreite ergibt die angeklickte Spalte.

Das hört sich jetzt sehr kompliziert an und bevor hier jemand Albträume von seinem Mathelehrer bekommt, zeig ich das ganze lieber als Quelltext, da sieht man nämlich, dass das eigentlich ganz einfach ist:

spalte := (x - FLinks) div FFelderBreite;

So, wir haben nebenher eindrucksvoll gezeigt, dass Mathematik nur unter sagenhaften Verrenkungen in Worte zu fassen ist und eine Formel meistens mehr sagt als tausend Worte.

Das Ergebnis muss natürlich noch auf sinnvolle Nutzbarkeit geprüft werden. Zum einen hat unser Feld nur eine begrenzte Breite, weshalb abgebrochen werden muss, wenn der Benutzer rechts daneben klickt. Andererseits haben wir auch keine negativen Spalten. Deshalb kann auch abgebrochen werden, wenn der User links daneben klickt:

if (spalte > FSpielfeldBreite-1) or (x < FLinks) then Exit;

Anschließend benötigen wir noch die Zeile, in die der Stein soll. In der Realität ist das ganz einfach: Wenn der Stein unten ist, hört er auf zu fallen. Da der Computer aber nicht sieht, wann der Stein „unten“ ist, müsste man eine komplizierte Kollisionsbehandlung durchführen um zu erkennen, wann er nicht weiter fallen kann.
Da wir aber alle Felder samt Belegung im Speicher haben, können wir einfach schauen, was das unterste leere Feld ist. Dazu gehen wir einfach in der gefundenen Spalte von unten nach oben und brechen ab wenn wir ein leeres Feld haben oder wenn wir oberhalb des Spielfeldes sind:

//Ermitteln der Zeile
i := 0;
while FSpielfeld[spalte,i] <> sLeer do
begin
  Inc(i);
  if i > FSpielfeldHoehe - 1 then Exit;
end;
zeile := i;

Nach diesen aufwändigen Vorbereitungen ist jetzt der eigentliche Spielzug, das Setzen
des Steines, erstaunlich primitiv:

FSpielfeld[spalte, zeile] := FAktiverSpieler;

Auch hier macht sich der Nutzen unseres selbstdefinierten Variablentyps bemerkbar. Wir brauchen gar nicht abfragen, wer aktiv ist, sondern setzen einfach den gerade Aktiven in das Feld, welcher das ist, ist vollkommen egal.

Zum Schluss müssen wir natürlich noch den Spieler wechseln und das Spielfeld neu zeichnen, denn schließlich muss der Zug auch sichtbar gemacht werden.

//Spieler wechseln
if SpielAktiv then
begin
  if FAktiverspieler = sSpieler1 then
    FAktiverspieler := sSpieler2
  else
    FAktiverspieler := sSpieler1;
end;
Darstellen; //Grafische Ausgabe

Jetzt ist das Spiel immerhin spielbar. Zumindestens fast. Denn wer jetzt kompiliert und anfängt zu spielen stellt fest, dass er zwar munter klicken kann, aber nicht sieht, dass etwas passiert.
Wir haben zwar Bitmaps für jeden Spieler erzeugt, allerdings haben wir die noch nirgendwo auf den Bildschirm gebracht. Das ist aber nur ein kleines Problem, welches mit wenigen neuen Zeilen in unserer „Darstellen“-Prozedur gelöst ist:

//Einzelne Felder
for I := 0 to FSpielfeldBreite - 1 do
begin
  for j := 0 to FSpielfeldHoehe - 1 do
  begin
    FGrafik.Canvas.Draw(FLinks + i*FFelderbreite+1,
    FUnten - (j+1)*FFelderbreite+1, FSpielerBmp[FSpielfeld[i,j]]);
  end;
end;

In zwei Schleifen wird einfach jedes Feld abgearbeitet und die entsprechende Bitmap hinein kopiert. Jetzt werden auch die gesetzten Steine dargestellt und wir können endlich unser Spiel spielen. Natürlich nur, wenn wir nicht vergessen haben, die nötigen Variablen zu deklarieren.

Nächste Mini-Aufgabe: Spielen und herausfinden was fehlt.

Spätestens wenn fünf Steine in einer Reihe liegen und man munter weiter setzen kann, wird klar, dass der Computer von alleine nicht sieht, wenn jemand gewinnt.

Wir müssen also eine Routine entwerfen, die herausfindet ob ein Spieler gewonnen hat. Daher jetzt die

Aufgabe: Schreibe eine Prozedur, die ermittelt ob eine Spieler gewonnen hat.

Diesmal ist die Lösung etwas aufwändiger.

Zunächst einmal gehen wir davon aus, dass der eben gesetzte Stein immer ein Teil der 4er-Kette, die zum Gewinn führt, sein muss. Logisch, denn ist er es nicht, gab es die Kette schon vorher, also bei einem vorherigen Zug und das Spiel wurde dann schon beendet.
Wir betrachten also nur die Umgebung des gesetzten Steines.

Zunächst werden alle direkt angrenzenden Felder betrachtet. Die Richtungen, in denen ein weiterer Stein von diesem Spieler liegt, werden weiter verfolgt. In den Richtungen wo kein Stein ist, kann logischerweise auch keine 4er-Kette sein.

Die gefundenen Richtungen werden solange weiter verfolgt, bis ein andersfarbiger Stein oder ein leeres Feld bzw. der Rand kommt. Anschließend wird noch in die entgegengesetzte Richtung geschaut.

Werden auf diese Art und Weise vier oder mehr Steine gefunden hat der Spieler gewonnen.
Da das Ganze doch eher abstrakt ist, hier eine Darstellung mit ein paar Bilder:

Vier gewinnt Steine

Das erste Bild Zeigt den neuen Spielstein. Im zweiten Bild zeigen die Striche die Richtungen, die weiter verfolgt werden. Das dritte Bild zeigt schließlich die Reihenfolge, in der die Steine gefunden worden.

Als Quelltext sieht das Ganze so aus:

function TForm1.GewinnerFeststellen(x: Integer; y: Integer; spieler: TFeldBesetzung): Boolean;
var
  richtung: array[0..7,0..1] of Integer;
  a,b,i,j,k: Integer;
begin
  //Überprüfung, ob der Spieler gewonnen hat
  Result := false;
  a := 0;
  i := x-1;
  j := y-1;
  if i < 0 then Inc(i);
  if j < 0 then Inc(j);
  //Überprüfung ob der Spieler auf den umliegenden Feldern schon gesetzt hat
  while (j <= y+1) and (j <= FSpielfeldHoehe-1) do
  begin
    if (FSpielfeld[i,j] = spieler) and not((i=x) and (j=y)) then
    begin
      richtung[a,0] := i-x;
      richtung[a,1] := j-y;
      Inc(a);
    end;
    Inc(i);
    if (i > x+1) or (i > FSpielfeldBreite-1) then
    begin
      i := x-1;
      if i < 0 then Inc(i);
      Inc(j);
    end;
  end;
  //Weiterverfolgung in den jeweiligen Richtungen, ob mindestens 4 Steine in einer
  //Reihe sind
  for k := 0 to a - 1 do
  begin
    i := x; j := y; b := 0;
    i := x-richtung[k,0]; j := y-richtung[k,1];
    while (i >= 0) and (j >= 0) and (i < FSpielfeldBreite-1) and (j < FSpielfeldHoehe-1)
    and (FSpielfeld[i,j] = spieler) do
    begin
      Inc(b);
      i := i-richtung[k,0];
      j := j-richtung[k,1];
    end;
    if b > 3 then Result := true;
  end;

 

Unserer Prozedur werden die Position des neuen Steines und der Spieler, der ihn gesetzt hat, übergeben.
Als Rückgabewert gibt die Funktion true aus, wenn der Spieler gewonnen hat, und false, wenn er nicht gewonnen hat.

Hat der Spieler gewonnen, muss natürlich eine entsprechende Meldung ausgegeben und das Spiel angehalten werden, schließlich soll danach niemand mehr weiterspielen können. Um das Spiel anzuhalten, benötigen wir die weiter oben kommentarlos eingeführte Variable SpielAktiv. Ist sie true wird gespielt, bei false nicht.

Die Gewinnabfrage sieht dann so aus:

if GewinnerFeststellen(spalte, zeile, FAktiverspieler) then
begin
  Darstellen;
  ShowMessage(FSpielerName[FAktiverSpieler] + ' gewinnt');
  SpielAktiv := false;
end;

Und damit auch tatsächlich nicht weitergespielt wird, muss an den Anfang der MouseDown-Prozedur noch eine Zeile:

if not SpielAktiv then Exit;

 

Wichtig! Da der aktive Spieler weitergegeben wird, muss die Überprüfung auf den Gewinner im Quelltext vor dem Wechsel der Spieler kommen!

Jetzt können wir „4 gewinnt“ spielen und sogar gewinnen.
Eigentlich könnte man jetzt meinen, wir seinen fertig. Allerdings erinnern wir uns an unsere Anforderungsliste und stellen fest, dass da doch noch einiges fehlt.