Gobblet

Aus Informatik
Wechseln zu: Navigation, Suche
Gobblet, hier von Gigamic

Aufgabenstellung

Das Brettspiel Gobblet soll als Computerspielumsetzung realisiert werden

Spielregeln

Allgemein

Gobblet wir auf einem Feld der Größe 4x4 gespielt. Zu Beginn des Spiels erhält jeder Spieler 12 Steine, die gleichmäßig auf 4 verschiedene Größen verteilt und in-/aufeinander gestapelt sind (Becherform). Ziel des Spiels ist es, 4 Steine seiner Farbe in einer Reihe (horizontal, diagonal oder vertikal) anzuordnen, bevor der Gegner selbiges tut. Hierbei zählt, falls mehrere Steine aufeinander sitzen, immer der oberste Spielstein.

Gezogen wird abwechselnd, wobei immer nur sichtbare Spielsteine gezogen werden dürfen, sowohl die, die schon auf dem Feld sind, als auch die, die noch vor dem Spieler stehen. Wird ein Stein einmal berührt, so muss er auf ein (anderes) Feld versetzt werden. Steht auf dem Zielfeld bereits eine Figur, so gelten folgende Regeln:

  • Die stehende Figur muss kleiner als die ziehende Figur sein.
  • Die ziehende Figur muss sich schon vorher auf dem Spielfeld befunden haben, wenn nicht die Zielfigur schon Teil einer gegnerischen Reihe aus 3 Steinen war. Nur dann ist das "schlucken" direkt vom Spielfeldrand erlaubt.

Zusätzlich darf eine Figur nicht auf das Feld zurückgesetzt werden, von dem sie genommen wurde. Hat ein Spieler keine Möglichkeit mehr zu ziehen, so verliert er ebenfalls das Spiel.

Beispiele

Umsetzung

Grundidee

Das Feld wird als Array definiert, bei dem jeder Teil einem Feld entspricht, welches wiederum durch das Objekt mTFeld repräsentiert wird.

mTFeld

mTFeld besitzt folgende Prozeduren und Funktionen (diese sind nötig, um auf die geschützten Attribute zuzugreifen), dazu etwas Quelltext, um das Objekt durchschaubarer zu machen:

protected //Attribute
  FAssigned : boolean;  //wählte hier boolean, da das feld nur zugewiesen oder leer sein kann
  FColor : boolean;     //boolean, da farbe sowieso nur abgefragt wird wenn FAssigned=true
  FSize : integer;      //integer, da vier mögliche größen gegeben sind
//-------- Create (public) ---------------------------------------------
constructor TFeld.Create;
begin
  inherited create;
  Take;                 //beim erstelllen werden grundwerte genullt
end;
//-------- Assign (public) ---------------------------------------------
procedure TFeld.Assign (Color: boolean; Size: integer);
begin
  FAssigned := True;    //auf dem feld sitzt nun ein stein
  FColor := Color;      //farbe des steins (true - orange; false - weiß)
  FSize := Size;        //größe des steins
end;
//-------- Take (public) -----------------------------------------------
procedure TFeld.Take;
begin
  FAssigned := false;   //stein wird weggenommen
end;
//-------- IsAssigned (public) -----------------------------------------
function TFeld.IsAssigned : boolean;
begin
  Result := FAssigned;  //simple aussage, ob das feld besetzt ist
end;
//-------- IsColor (public) --------------------------------------------
function TFeld.IsColor : boolean;
begin
  Result := FColor;    //aussage über farbe des gesetzten steins
end;
//-------- IsSize (public) ---------------------------------------------
function TFeld.IsSize : integer;
begin
  Result := FSize;     //aussage über größe des gesetzten steins
end;
//-------- Destroy (public) --------------------------------------------
destructor TFeld.Destroy;
begin
  inherited destroy;
end;

Probleme und Lösungen

Programm in der Endversion

Verwalten der Spielsteine

Kern des Spiels ist die Speicherung jeder einzelnen Spielsteinposition. Hierzu war meine erste Idee SFeld : array[1..4,1..4] of TFeld einzuführen. Dieses besteht dann aus 4*4 Ablagemöglichkeiten und sollte somit Platz genug für alle Spielsteine auf dem Feld bieten. Was ich dabei jedoch außer Acht ließ, war die Möglichkeit, dass natürlich auf einem Feld auch mehrere Figuren übereinander stehen können, was mit meinem derzeitigen Ansatz nicht realisierbar gewesen wäre. Nun boten sich zwei Möglichkeiten:

1) Erweitern des Objekts mTFeld auf mehrere Ebenen

oder 2) Erweitern von SFeld um eine dritte Dimension

Da ich TFeld so einfach wie möglich belassen wollte entschied ich mich für zweiteres, womit SFeld : array[1..4,1..4,1..4] of TFeld eingeführt wurde. Diese Lösung erlaubte mir nun den Zugriff auf jede Ebene separat vorzunehmen. Im Rückblick kann gesagt werden, dass Methode 1 mir wohl viel Arbeit erspart hätte, da das Objekt dann seinen obersten Stein selbst hätte bestimmen können.

Der Klickbereich

Die Steine sind einzelne Bilder mit dem entsprechenden Attribut picture. Wird ein anderer Stein auf ein Feld gesetzt, so wird lediglich picture geändert, die Größe des Bildes bleibt aber gleich. Dies führte dazu, dass bei einem Klick auf ein Feld oft das falsche Feld ausgewählt wurde, da sich die Bilder zum Teil überschneiden. Dieses Problem wollte ich eigentlich ungelöst lassen und darauf hoffen, dass der User immer den mittleren Bereich eines Bildes klickt(diese Hoffnung stellte sich in der Praxis allerdings als Utopie heraus...). Aus diesem Grund entschied ich mich, doch noch eine Lösung zu suchen und fand widererwarten einen recht einfachen Weg dazu. Mit Hilfe dieses Weges schrieb ich die Funktion untransparent, die ich hier kurz erklären möchte:

function TForm1.untransparent(x: Integer; y: Integer; Send: TImage):boolean;
begin
if (Send.Canvas.Pixels[x, y] <> Send.Canvas.Pixels[0, Send.Picture.Bitmap.Height - 1]) or (y>84)  then
  result := true
else
  result:=false;
end;

Die Prozedur erhält folgende ParaVmeter: Das angeklickte Bild, die X-Koordinate, an der das Bild angeklickt wurde, sowie die entsprechende Y-Koordinate. Nun vergleicht sie den Pixel an der geklickten Stelle mit einem Pixel, der außerhalb des Bildbereichs liegt (und somit transparent ist). Ist dieser Pixel ungleich dem Ausgangspixel, so ist das Bild an dieser Stelle nicht transparent. Im zweiten Schritt der Abfrage wird zusätzlich noch geprüft, ob der Y-Wert größer (da von oben gezählt) als die Kante des Feldes ist, auf das der Stein gesetzt werden könnte. Hiermit wird gewährleistet, dass auch leere Bilder richtig wahrgenommen werden. Somit nahmen Bilder ihren Klick nur an, wenn sich an der Klickstelle tatsächlich etwas befand.

Ein Problem allerdings blieb noch: Bei einer Überschneidung wurde kein Befehl ausgeführt, da keine der obrigen Bedingungen erfüllt waren. Hierzu musste allerdings nur die Klickprozedur der Bilder etwas abgeändert werden. Sie wurde in drei Fälle aufgeteilt:

  • Der Normalfall: Es wurde in einen überschneidungsfreien Raum geklickt; nichts besonderes passiert.
  • Sonderfall 1: Es wurde in einen Bereich rechts oben geklickt, dort könnte ein anderes Feld liegen. Somit wird (falls oben eine Transparenz festgestellt wurde) die Versatzprozedur des eins höher liegenden Steines aufgerufen.
  • Sonderfall 2: Wie Sonderfall 1, nur dass hier ein links oberer Bereich geklickt wurde. Hier wird nun die Versatzprozedur des links oberen Steines ausgeführt.

Regeln

Der Stein muss größer sein

Dies lies sich über eine einfache Abfrage regeln. War der Stein größer oder gleich, so durfte er nicht gezogen werden.

Geschluckt werden darf von außen nur in einer Dreierreihe

Hierzu musste ich eine spezielle Funktion namens dreier einführen. Zum Verständnis hier der Teil, der für vertikale Überprüfung gedacht ist:

function TForm1.dreier(i,j: integer; color:Boolean):Boolean;
var
  l,k,n: integer;
  h,r:boolean;
begin
r:=false;                                              //r bestimmt das ergebnis - ist r false, so liegt kein dreier vor
n:=0;                                                  //n zählt die zahl der steine, die in einer bestimmten farbe vorliegen
for l := 1 to 4 do                                     //da ich im gesamten projekt i und j als x- und y-koordiante verwendet habe,
begin                                                  //musste hier l als schleifenvariable herhalten
  k := 4;                                              //k dient dem abbruch
  h := false;                                          //h ebenso
  while not h and not SFeld[i,l,k].IsAssigned do       //das überlieferte i bestimmt die x-koordinate, l ist in einem schleifendurchlauf
  begin                                                //gegeben und k wird nun solange gesenkt,bis auf dem feld ein stein zu finden ist 
    dec(k);                                            //oder die unterste ebene erreicht wurde
    if k = 0 then h := true;
  end;
  if not h then
    if SFeld[i,l,k].IsColor = color then inc(n);       //falls ein stein der passenden farbe gefunden wurde, wird der zähler erhöht
end;                                                   //wurden exakt 3 steine gefunen, so gibt die funktion true zurück. r dient hier
if n=3 then r := true;                                 //als ersatz und wird in der horizontalen und diagonalen anfrage weiterverwandt
(...)
end;
Werden zwei Reihen zugleich gebildet, so gewinnt der Spieler, der nicht am Zug ist

Recht einfache Lösung: Die Prozedur win wird mit zwei boolschen Parametern aufgerufen - einer für den Sieg des orangenen, einer für den Sieg des weißen Spielers. Es wird nun überprüft, wer an der Reihe ist und dem jeweils anderen Spieler Priorität zugesprochen.

Ein Stein darf nicht wieder an den selben Ort gesetzt werden

Für diesen Zweck existieren die globalen Variablen Herkunfti und Herkunftj, die jeweils die X- und Y-Koordinate eines aufgenommenen Steines speichern. Somit kann vor dem Setzen überprüft werden, ob der Bestimmungs- dem Ausgangsort gleicht.

Ist keine Zugmöglichkeit gegeben, so ist das Spiel verloren

Hierfür wird am Ende jedes Wegnehmens eines Steines vom Feld die Funktion zuegeverbleibend befragt, ob es für den gewählten Stein eine Setzmöglichkeit gibt. Hierbei werden alle oben genannten Punkte berücksichtigt.

function TForm1.zuegeverbleibend : boolean;
var
  i,j,k:integer;                                            //übliche variablen
  h,r:boolean;                                              //r steht für das vorläufige result
begin
r:=false;                                                   //zu anfang gibt es keinen beweis, dass ein feld besetzbar ist
  for i := 1 to 4 do
    for j := 1 to 4 do
      if (Herkunfti <> i) or (Herkunftj <> j) then          //auf das herkunfts feld darf sowieso nicht gesetzt werden
      begin
        k:=4;
        h:=false;
        while not h and not SFeld[i,j,k].IsAssigned do      //zuerst wird überprüft ob auf feld [i,j] ein stein sitzt
        begin
          dec(k);
          if k=0 then h:=true;          
        end;
      if h then                                             //sitzt dort keiner ist bewiesen, dass eine zugmöglichkeit besteht
        r:=true
      else
      if Hilfsfeld.IsSize > SFeld[i,j,k].IsSize then        //sitzt dort einer, wird die größe berglichen
        if Herkunfti = 0 then                               //ist sie kleiner als der zugstein, so wird für steine vom 
        begin                                               //startfeld(herkunfti=0 entspricht startfeld)die dreierregel angewandt,
          if dreier(i,j,Hilfsfeld.IsColor) then             //für alle anderen gilt r als wahr
            r:=true;                                        //ist auch die dreieregel bestätigt --> zug möglich
        end                                                 //(begin und end stehen, damit das else zum richtigen if gezählt wird)
        else r:=true;
      end;
  if r then result:=true                                    //wurde r bewiesen, so gibt die Funktion true als ergebnis aus
    else result:=false;
end;
Benutzerfreundlichkeit

Das Spiel befolgt die Regeln und lässt keinerlei unerlaubte Handlungen zu. Ursprünglich hatte ich geplant, keinerlei Fehlermeldungen auszugeben. Der Benutzerfreundlichkeit wegen entschied ich mich allerdings dafür, bei einem unerlaubten Spielzug für kurze Zeit einen kleinen Schriftzug einzublenden. Dieser stört nicht wie eine MessageBox, ist allerdings trotzdem gut sichtbar.

Dateien

Jegliche im Programm verwendete Bilder stammen entweder von Delphi selbst(Hintergrundmuster des Headers, Shapes) oder wurden von mir(Steine, Lautsprecher) angefertigt.

  • Zu finden ist die unkompilierte Endversion hier: Gobblet
  • Und die kompilierte .exe befindet sich hier: Gobbletexe - die beigelegte Wave-Datei sollte möglichst immer im Ordner der Anwendung sein, das Programm kommt aber auch ohne sie aus.