Gobblet
Inhaltsverzeichnis
- 1 Aufgabenstellung
- 2 Spielregeln
- 3 Umsetzung
- 3.1 Grundidee
- 3.2 Probleme und Lösungen
- 3.2.1 Verwalten der Spielsteine
- 3.2.2 Der Klickbereich
- 3.2.3 Regeln
- 3.2.3.1 Der Stein muss größer sein
- 3.2.3.2 Geschluckt werden darf von außen nur in einer Dreierreihe
- 3.2.3.3 Werden zwei Reihen zugleich gebildet, so gewinnt der Spieler, der nicht am Zug ist
- 3.2.3.4 Ein Stein darf nicht wieder an den selben Ort gesetzt werden
- 3.2.3.5 Ist keine Zugmöglichkeit gegeben, so ist das Spiel verloren
- 3.2.3.6 Benutzerfreundlichkeit
- 4 Dateien
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
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.