Nach dem die letzten Programmierbeispiele nur einfache Tests waren, geht es in diesem Teil richtig zur Sache. Wir programmieren einen Kniffel-Clone – aber nicht irgendeinen, sondern einen coolen!
Falls Ihr Kniffel nicht kennt, schaut euch am besten den Wikipedia Eintrag dazu an. Denn vielleicht kennt ihr es auch unter einem anderen Namen: Yahtzee, Escalero, Würfelpoker, Yacht, Balut, Yatzy, Yams oder Kismet.
Wie sieht nun ein cooler Kniffel-Clone aus? Das liegt natürlich an Euch und Euren Programmierkünsten. In diesem Beispiel funktioniert meine Basisversion so:
Was kann man lernen?
- Erweiterte Abfragen
- Schleifen
- Einen Spielablauf steuern
- Eigene Funktionen erstellen
- Erste eigene Objekte erstellen und benutzen
Spielablauf
Wird ein Programm komplizierter, ist es am besten sich vorher einen Ablaufplan zu überlegen. In unserem Fall soll er so aussehen:
Das Spiel beginnt mit dem Drücken auf einen Start-Knopf. Dann starten mehrere Steuerungsabläufe. Es gibt 13. Runden. Das entspricht der Anzahl aller Würfelbilder, die gewürfelt werden sollen. In jeder Runde darf man 3-mal würfeln – man muss aber nicht. Auch der erste Wurf kann schon ausreichen. Dazwischen werden Punkte berechnet. Nach der 13. Runde endet das Spiel und die Gesamtpunkte werden ermittelt. Am Ende gibt es die Möglichkeit für ein neues Spiel.
Formular-Design
Nachdem nun klar ist, wie das Spiel ablaufen soll, kann mit dem Design des Formulars begonnen werden.
Die Punkteliste soll über eine Tabelle dargestellt werden. Dafür eignet sich z. B. die Komponente TListView. Daneben werden noch TPanels für Meldungen und Anzeige von Würfen und Runde benötigt. Für die 2 Knöpfe „Neues Spiel“ und „Würfeln“ eignen sich Objekte der Klasse TSpeedButton.
Da auf Größenanpassungen des Fensters reagiert werden soll, brauchen manche Objekte Einstellungen für die Eigenschaften Align und/oder Anchors. Ein Beispiel dafür ist der Panel PanelMeldungen. Er soll sich über die gesamte Breite des Fensters ausdehnen. Dafür wird die Eigenschaft akRight in Anchors auf TRUE gesetzt.
Was als Objekt noch fehlt sind die 5 Würfel auf dem Spielfeld. Diese werden mit einer eigenen Klasse umgesetzt.
TWuerfel
Die Würfel setzen wir mit ein wenig mehr Action um. Damit das Spiel nicht zu passiv verläuft, soll der Spieler die Würfel, die er behalten möchte, vom Spielfeld schieben müssen. Dafür gibt es aber keine Standard-Komponente, die das kann. Also ist die Aufgabe eine eigene zu erstellen. Der vollständige Quellcode ist in der Unit meinwuerfel.pas gespeichert.
Klassen beginnen mit einem großen T, das bedeutet übrigens Type. In der Klasse werden Eigenschaften und Funktionen/Prozeduren für das Objekt hinterlegt.
Eigenschaften für unseren Würfel:
- Zahl: 1 bis 6 als Integer
Prozeduren/Funktionen des Würfels:
- Wuerfeln: neue Zufallszahl für den Würfel erzeugen
Die Definition der Klasse erfolgt im type-Abschnitt der Unit. Je nachdem wie der Zugriff in einer Objektinstanz gesteuert werden soll, gibt es verschiedene Schutzklassen.
Für einen einfachen Würfel wäre das beispielsweise die Schutzklasse public. Das bedeutet auf die Elemente, die Elemente nach dem Schlüsselwort sind öffentlich.
type TWuerfelEinfach = class(TObject) // TWuerfelEinfach erbt alles von der Klasse TObject public // Öffentlicher Bereich Zahl : integer; // Variable zum Speichern des Würfelbilds procedure Wuerfeln; // Prozedur, um neue Zufallszahl zu erzeugen end; procedure TWuerfelEinfach.Wuerfeln; //Wird automatisch erzeugt, wenn man Strg+Alt+C drückt! begin Zahl:=Random(6)+1; //Zufallszahl von 1 bis 6 end;
Um eine Instanz der Klasse zu benutzen, muss sie erzeugt werden.
var Wuerfel : TWuerfelEinfach; begin Wuerfel:=TWuerfelEinfach.Create; Wuerfel.Wuerfeln; //Neue Zufallszahl MessageDlg(‘Der Würfel hat den Wert: ‘ +IntToStr(Wuerfel.Zahl),mtInformation,[mbOk],0); end;
In unserem Fall wollen wir aber noch mehr Funktionen in die Klasse einbauen. Zum einen soll der Würfel als Grafik dargestellt werden. Das klappt am besten über ein Bild. Deshalb leiten wir nicht von TObject ab, sondern von TImage. Damit hat unser Würfel gleich alle Funktionen der Komponenten TImage und kann ein grafisches Würfelbild aufnehmen. Das hat auch den Vorteil, dass so das Design leicht an die eigenen optischen Vorstellungen angepasst werden kann.
Die Definition sieht im Gegensatz zur einfachen Definition von oben folgendermaßen aus:
type TWuerfel = class(TImage) private RelPosX,RelPosY: Integer; public Zahl : integer; WuerfelShape : TShape; function IstImWuerfelfeld : boolean; constructor Create(AOwner: TComponent); override; procedure Wuerfeln; protected procedure MouseDown(Button: TMouseButton; Shift: TShiftState; X, Y: Integer); override; procedure MouseMove(Shift: TShiftState; X,Y: Integer); override; procedure MouseEnter; override; procedure MouseLeave; override; end;
Die Eigenschaft WuerfelShape ist vielleicht schwerer zu verstehen. Sie ist die Verbindung zum Formular. Hier stellt ein Objekt der Klasse TShape die Würfelfläche dar.
Das Verschieben des Würfels erfolgt über eine Anpassung der vorhandenen Ereignisse von MouseDown und MouseMove. Deshalb wird hier auch das Schlüsselwort override verwendet.
procedure TWuerfel.MouseDown(Button: TMouseButton; Shift: TShiftState; X,Y: Integer); begin //Wenn Maustaste gedrückt wurde, aktuelle Mausposition merken FRelPosX:=X; FRelPosY:=Y; //geerbte Methode MouseDown aufrufen inherited MouseDown(Button,Shift,X,Y); //Würfelbild in den Vordergrund bringen BringToFront; end; procedure TWuerfel.MouseMove(Shift: TShiftState; X,Y: Integer); begin //geerbte Methode MouseDown aufrufen inherited MouseMove(Shift,X,Y); //Wird Mausbewegt und ist linke Maustaste gedrückt, dann Würfel verschieben mit //Bezug zur gemerkten Klickposition if (ssLeft in Shift) then SetBounds(Left+X-FRelPosX,Top+Y-FRelPosY,Width,Height); end;
Hauptprogramm
Das Verwenden von eigenen Komponenten ist nicht ganz so einfach, wie vorhandene aus der Komponentenpalette einzufügen. Hier muss das Einfügen manuell im Quelltext erfolgen.
Da 5 Würfel im Spiel sind, werden die Würfel-Objekte in einer Liste als Array definiert.
var Wuerfel : array[1..5] of TWuerfel;
Erzeugt werden die TWuerfel-Objekte am besten in der Ereignis-Prozedur des Formulars: FormCreate.
procedure THauptForm.FormCreate(Sender: TObject); ... //Würfel-Objekte erzeugen for i:=1 to 5 do begin wuerfel[i]:=TWuerfel.Create(nil); wuerfel[i].WuerfelShape:=Shape1; PanelSpielfeld.InsertControl(wuerfel[i]); end; ...
Das passiert mit Aufrufen von TWuerfel.Create(nil). Sichtbar wird die Komponente aber erst nachdem sie eingefügt wurde. Hier sollen alle Würfel unterhalb des PanelSpielflaeche sein. Deshalb werden Sie mit dessen Prozedur InsertControl eingefügt.
Das Spiel beginnt
Ein neues Spiel wird mit einem Klick auf den Button „Neues Spiel“ gestartet.
Nach Anklickent werden die Startwerte gesetzt und die Punkteliste gelöscht. Dann werden für alle Würfel-Objekte neue Zufallszahlen ermittelt und die Würfel werden auf dem Spielfeld angezeigt.
procedure THauptForm.SpeedButtonNeuesSpielClick(Sender: TObject); var i : integer; begin //Startwerte setzen Runde:=1; Wurf:=1; PunkteLoeschen; //Los gehts Wuerfeln(false); //Neue Zufallswerte für alle Würfel WuerfelAnzeigen(true,false); //alle Würfel anzeigen SpielInfosAnzeigen; WurfBerechnen; end;
Was wurde gewürfelt?
Gleich nach Anzeigen der Würfel, wird der aktuelle Wurf für den Spieler berechnet. Dabei werden alle möglichen Spielvorgaben durchgerechnet und mit den jeweiligen Punkten angezeigt.
Die Punkteliste besteht aus 3 Spalten. In der dritten Spalte „Akt. Wurf“ werden diese berechneten Möglichkeiten für den Spieler angezeigt. Damit hat er eine schnelle Übersicht, um eine gute Auswahl zu treffen.
Die Berechnung der jeweiligen Punkte erfolgt in der Prozedur WurfBerechnen.
Wie das für die einzelnen Würfelaufgaben gemacht werden kann, schauen wir uns in den nächsten Abschnitten an.
1er bis 6er
Um die Punkte für die 1er bis 6er zu berechnen, erstellen wir eine eigene Unterfunktion, die das für jede Zahl übernehmen kann. Die Funktion AnzahlZahl(Zahl) sucht die übergebene „Zahl“ und liefert als Ergebnis die Anzahl zurück.
Der Rückgabewert kann bei Lazarus mit der Variable result gesetzt werden. Am Beginn wird der Rückgabewert erst auf 0 gesetzt. Dann werden über eine Schleife die 5 Würfel auf ihren Wert hin abgefragt. Da wir die Würfel als Array definiert haben, klappt das in der Schleife sehr elegant.
function THauptForm.AnzahlZahl(Zahl : integer): integer; var i,j : integer; begin //Rückgabewert der Funktion auf 0 setzten result:=0; j:=0; for i:=1 to 5 do if wuerfel[i].Zahl=Zahl then j:=j+1; //Rückgabewert auf den Wert von j setzen result:=j; end;
Innerhalb von WurfBerechnen werden die Punkte der 1er bis 6er auch über eine for-Schleife berechnet und in die Punkteliste eingetragen. Das Eintragen erfolgt ebenfalls mit einer eigenen Prozedur UpdateListview. Als Parameter werden die jeweilige Zeile in der Punkteliste (die „y-Koordinate“) und der neue Wert benötigt.
for i:=1 to 6 do UpdateListview(i-1,IntToStr(AnzahlZahl(i)*i));
4er Pasch und 5 Gleiche
Einen 4er Pasch zu erkennen, funktioniert ebenfalls mit der Funktion AnzahlZahl perfekt. Das wird auch mit einer Schleife über alle Zahlen geprüft. Ist die Anzahl größer oder gleich 4, dann haben wir einen 4er Pasch gefunden und zeigen in an.
for i:=1 to 6 do if AnzahlZahl(i)>=4 then UpdateListview(11,IntToStr(SummeAugen));
Um 5 Gleiche zu erkennen, siehen die Schleife und die Abfrage sehr ähnlich aus. Nur die Zeile in UpdateListView wird angepasst.
UpdateListview(15,IntToStr(0)); for i:=1 to 6 do if AnzahlZahl(i)=5 then UpdateListview(15,IntToStr(50));
3er Pasch und Full-House
Der 3er Pasch funktioniert ebefalls genauso. Wir brauchen aber hier auch noch die Information, ob es einen gibt, um ein Full-House zu erkennen. Deshalb merken wir uns das Ergebnis in einer Variable vom Typ Integer. Die Variable speichert die Zahl des 3er Paschs. Gibt es keinen ist der Wert 0.
//3er Pasch dreierpasch:=0; for i:=1 to 6 do if AnzahlZahl(i)>=3 then begin UpdateListview(10,IntToStr(SummeAugen)); dreierpasch:=i; end; //Full House if dreierpasch>0 then for i:=1 to 6 do if (AnzahlZahl(i)=2) and (i<>dreierpasch) then UpdateListview(12,IntToStr(25));
Ein Full-House kann es nur geben, wenn es schon ein 3er Pasch gibt. Als zweite Bedingung muss es noch einen 2er Pasch geben, der nicht aus den Augen des 3er Pasch besteht.
Kleine und große Straße
Straßen können wir einfach erkennen, wenn die Würfelzahlen entsprechend sortiert werden. Dafür gibt es aber keine Funktion in Lazarus. Es gibt aber Objekte, die das können. Darum erstellen wir als Hilfe ein solches Objekt – eine TStringlist. Mit der Prozedur Add kann ein String in der Liste angefügt werden. Mit Sort wird die Liste dann aufsteigend sortiert.
//Kleine Straße sl:=TStringList.Create; for i:=1 to 5 do sl.Add(IntToStr(wuerfel[i].Zahl)); sl.Sort;
Jetzt können wir wieder über eine Schleife prüfen, ob der aktuelle Würfel gleich dem vorherigen ist. Wenn ja gibt es keine Straße und dann erhöhen wir die aktuelle Zahl um 90 und sortieren nochmal.
for i:=1 to 4 do if sl[i]=sl[i-1] then sl[i]:=IntToStr(i+90); sl.Sort;
Jetzt würde eine Straße mit den ersten Elementen beginnen und alles was keine Straße ist, käme durch die +90 erst am Ende. Dadurch ist es einfach zu überprüfen, ob die Bedingung für eine kleine oder große Straße erfüllt wird.
//Kleine Straße if (StrInt(sl[0])=StrInt(sl[1])-1) and (StrInt(sl[1])=StrInt(sl[2])-1) and (StrInt(sl[2])=StrInt(sl[3])-1) then UpdateListview(13,IntToStr(30)); if (StrInt(sl[1])=StrInt(sl[2])-1) and (StrInt(sl[2])=StrInt(sl[3])-1) and (StrInt(sl[3])=StrInt(sl[4])-1) then UpdateListview(13,IntToStr(30));
//Große Straße if (StrInt(sl[0])=StrInt(sl[1])-1) and (StrInt(sl[1])=StrInt(sl[2])-1) and (StrInt(sl[2])=StrInt(sl[3])-1) and (StrInt(sl[3])=StrInt(sl[4])-1) then UpdateListview(14,IntToStr(40));
Warum +90: Im Prinzip gibt es hier mehrere Möglichkeiten. Die Sortierung der TStringList arbeitet aber mit Texten. Das gilt auch für die Sortierung. Wird z. B. um 10 erhöht, wäre das mathematisch zwar richtig (da größer) die Sortierreihenfolge wäre aber so: 1,12,13,4,5,6 und nicht wie erwartet 1,4,5,6,12,13.
Am Ende brauchen wir die TStringlist nicht mehr und können Sie wieder freigeben. Das geht mit der Prozedur Free.
//Stringlist wieder zerstören sl.Free;
Chance
Um eine Chance zu berechnen, werden alle Augen gezählt. Dafür gibt es auch eine eigene Funktion.
UpdateListview(16,IntToStr(SummeAugen));
function THauptForm.SummeAugen: integer; var i,j : integer; begin result:=0; j:=0; for i:=1 to 5 do j:=j+wuerfel[i].Zahl; result:=j; end;
Punkte addieren
Jetzt haben wir alle möglichen Würfelergebnisse für diesen Wurf berechnet und angezeigt. Was noch fehlt, ist die Berechnung der Punkteanzahl und die Prüfung, ob es einen Bonus gibt.
Punkte berechnen für Teil 1:
t1:=0; for i:=0 to 5 do t1:=t1+StrInt(ListView1.Items[i].SubItems[0]); ListView1.Items[6].SubItems[0]:=IntToStr(t1);
Gibt es einen Bonus?
if t1>=63 then begin ListView1.Items[7].SubItems[0]:='35'; t1:=t1+35; end else ListView1.Items[7].SubItems[0]:='0'; ListView1.Items[8].SubItems[0]:=IntToStr(t1);
Punkte berechnen für Teil 2:
t2:=0; for i:=0 to 6 do t2:=t2+StrInt(ListView1.Items[10+i].SubItems[0]); ListView1.Items[17].SubItems[0]:=IntToStr(t2); ListView1.Items[18].SubItems[0]:=IntToStr(t1+t2); ListView1.Update; //Kein Element der ListView soll markiert sein ListView1.Selected:=nil;
Wurf in die Tabelle eintragen?
Jetzt liegt es am Spieler. Er kann entscheiden was er mit seinem Wurf tun möchte. Er hat je nach Wurfanzahl 2 Möglichkeiten:
- Würfel austauschen / vom Spielfeld schieben und nochmal würfeln
- Ein Ergebnisfeld in der Punkteliste aussuchen und den Wurf eintragen
Wurf eintragen
Das Ereignis wird über einen Doppelklick auf eine Zeile der Punkteliste ausgelöst. Dafür muss im Objektinspektor die Methode ListViewDblClick gefüllt werden. Lazarus erzeugt dann einen leeren Methodenrumpf. Hier tragen wir die Prozedur WurfEintragen ein.
Ein Wurf kann aber nur berechnet werden, wenn der Spieler in der Punkteliste auch eine Zeile ausgewählt hat. Andernfalls passiert nichts.
procedure THauptForm.WurfEintragen; begin //Prüfungen, ob Punkte eingetragen werden dürfen if ListView1.Selected=nil then begin Meldung('Vorher in der Tabelle eine Zeile auswählen.'); exit; end;
Das Gleiche gilt, wenn die ausgewählte Zeile schon in dieser Runde belegt wurde. Das können wir erkennen, wenn bereits Punkte in der ListView stehen.
if ListView1.Selected.SubItems[1]='' then begin Meldung('Das Feld wurde bereits ausgewählt!'); exit; end;
Dann können wir den Wurf übernehmen und die Vorschläge wieder löschen.
//Berechnete Punkte übernehmen aus der Spalte "Aktueller Wurf" ListView1.Selected.SubItems[0]:=ListView1.Selected.SubItems[1]; //Dann alle Vorschläge löschen VorschlaegeLoeschen;
Jetzt können wir nachschauen, ob das Spiel schon vorbei ist oder ob ein neue Wurf erfolgt.
//Nächste Runde oder Spielende if Runde=13 then begin //Spielende WurfBerechnen; Meldung('Du hast '+ListView1.Items[18].SubItems[0]+' Punkte erreicht.'); Spielende; end else begin //Neue Runde Runde:=Runde+1; Wurf:=1; SpeedButtonWuerfeln.Enabled:=true; //Neu würfeln, alle Würfelaber mit Animation WuerfelAnzeigen(false,false); //alle Würfel ausblenden ImageBecher.Visible:=true; //Becher einblenden Timer1.Enabled:=true; //Timer für Becher-Bewegung ein Delay(1000); //1 Sekunden Becher "schütteln" Timer1.Enabled:=false; //Timer wieder aus ImageBecher.Visible:=false; //Becher ausblenden Wuerfeln(false); //Neue Zufallswerte für alle Würfel WuerfelAnzeigen(true,false); //alle Würfel wieder anzeigen SpielInfosAnzeigen; end; end;
Würfelbecher Animation
Damit die Würfel nicht einfach so bei jeden neuen Würfeln erscheinen, gibt es hier noch eine kleine Animation mit einem Würfelbecher. Das ist eine beliebige Grafik in einem TImage-Objekt.
Der Würfelbecher soll sich solange hin und her bewegen, wie der Spieler die Schaltfläche Würfeln drückt. Dafür ist aber ein wenig Aufwand notwendig. Zuerst brauchen wir eine zusätzliche Komponente, einen TTimer.
Der Timer sorgt dafür, dass sich der Becher bewegt. Die Eigenschaft, welche die Geschwindigkeit regelt heißt Interval. Wir setzen sie auf den Wert 50 (Millisekunden). Mit einem Doppelklick auf die Komponente wird wieder ein leerer Prozedurrumpf von Lazarus erstellt.
Die Prozedur wird vom Timer jeweils nach 50 ms aufgerufen. Das hin und her passiert über eine Positionsveränderung (Variable BecherX) der Image-Komponente des Würfelbechers. Bei Erreichen der Grenzen (min oder max) wird die Schrittweite vom Vorzeichen geändert und es geht in der jeweils anderen Richtung weiter.
procedure THauptForm.Timer1Timer(Sender: TObject); begin BecherX:=BecherX+BecherSchritt; if (BecherX>5) or (BecherX<0) then BecherSchritt:=BecherSchritt*(-1); ImageBecher.Left:=ImageBecher.Tag+BecherX*5; end;
Der Timer muss auf inaktiv gesetzt werden. Das erfolgt mit der Eigenschaft Enabled=False.
Das Starten der Animation passiert beim Drücken auf den Button. Da wir den Spieler entscheiden lassen, wie lange die Animation geht – durch seine „Drückdauer“ – brauchen wir den Beginn und das Ende des Mausklicks.
Möglich wird das durch verwenden von den Ereignissen MouseDown und MouseUp.
MouseDown versteckt die Würfel, zeigt den Würfelbecher an und schaltet den Timer ein.
procedure THauptForm.SpeedButtonWuerfelnMouseDown(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer); begin Meldung('Drücke solange auf "Würfeln" wie du würfeln willst.'); WuerfelAnzeigen(false,true); ImageBecher.Visible:=true; Timer1.Enabled:=true; end;
Bei MouseUp passiert noch mehr. Zuerst wird der Timer ausgeschaltet und der Würfelbecher wieder versteckt. Jetzt werden die neuen Würfel anzeigt. Ist es der 3. Wurf, darf nicht mehr gewürfelt werden. Um das dem Spieler anzuzeigen, deaktivieren wir den Button Würfeln.
procedure THauptForm.SpeedButtonWuerfelnMouseUp(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer); begin Timer1.Enabled:=false; ImageBecher.Visible:=false; WuerfelAnzeigen(true,true); //Wurf hochzählen Wurf:=Wurf+1; if Wurf=3 then //Es kann dann nicht nochmal gewürfelt werden SpeedButtonWuerfeln.Enabled:=false; //beim 3. Wurf Button deaktivieren Wuerfeln(true); //Würfeln, für Würfel auf dem Spielfeld SpielInfosAnzeigen; end;
Unser Ergebnis
Und das wars schon. Damit ist unsere Variante bereit für einen Test. Für den Programmstart muss nur die von Lazarus erstellte Anwendung und die Bitmap-Dateien der Würfelbilder in einem gemeinsamen Ordner vorhanden sein.
w1.bmp, w2.bmp, w3.bmp, w4.bmp, w5.bmp und w6.bmp
Viel Spaß beim Nachvollziehen, Nachprogrammieren und natürlich beim „Pimpen“ der Spieloberfläche.
Ich erstelle Grafiken übrigens am liebsten mit Microsoft Excel oder OpenOffice. Eine Grundlage für eigene Würfel ist auch unter Downloads unter Wuerfel.xlsx hinterlegt.
Download
Lauffähiges Spiel für Windows und Linux und vollständiges Lazarus-Projekt
very interesting post, i actually enjoyed this web site, carry on it