Coding für Kids: Ein Kniffel-Clone

Coding für Kids: Ein Kniffel-Clone

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:

Ablauf Diagramm für Kniffel-Clone
Spielablauf

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.

Haupt Formular des Kniffel-Clones
Design der Spieloberfläche mit Standard-Komponenten

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.

Objektinspektor Anchors
Eigenschaft Anchors von PanelMeldung

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.

Button Neues Spiel
Button “Neues Spiel”

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?

Punkteliste
Punkteliste

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.

Ereignisroutine
Aufruf von WurfEintragen über ListView1DblClick

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

Fertiges Spiel: Kniffel-Clone
Der fertige Kniffel-Clone

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.

Grundlage für Bitmaps für Kniffel
Excel: Grundlage für Bitmaps und Grafiken

Download

symbol_download

  Lauffähiges Spiel für Windows und Linux und vollständiges Lazarus-Projekt

Links

Dieser Beitrag hat einen Kommentar

Kommentar verfassen

Menü schließen