Form1: TForm1;
board: Array[1..4, 1..4] of TPanel; //Spielfeld
randomString: Array[1..16] of Integer; //Zufallszahlen -> benötigt in shuffle()
ranList: TStringList; //verfügbaren Zahlen -> benötigt in shuffle()
redPanel: TPanel; //Panel, für die TimerRed-Animation
tri: Integer = 1; //tri = TimerRed-Index
tmi: Integer = 1; //tmi = TimerMove-Index
b1, b2: TPanel; //Block 1 und 2 (bzw. Feld) für die Verschiebung, 1=Vorher, 2=Nachher
dir: Integer; //Richtung des Zuges
playtime: Integer; //SPielzeit in Sekunden
procedure isSolved;
var n, x, y: Integer;
begin
n := 0;
for y:=1 to 4 do
begin
for x:=1 to 4 do
begin
if y*x = 16 then
begin
if board[x, y].Tag = 0 then n:=n+1;
end
else
begin
if board[x, y].Tag = (y-1)*4+x then n:=n+1;
end;
end;
end;
if n >= 16 then
begin
Form1.TimerGame.Enabled := False;
ShowMessage('Glückwunsch, du hast es in ' + Form1.LabelTimePlayed.Caption + ' geschafft!');
end;
end;
Diese Funktion überprüft, ob das Puzzle gelöst ist. Dazu werden alle Felder des Spielfeldes durchlaufen und die Tag-Werte mit der Position verglichen, die sie haben sollten. Wenn ein Felder an der richtigen Position ist, wird die Variable n um eins erhöht. Wenn n 16 erreicht, also alle Felder die richtige Position haben, ist das Puzzle gelöst.
procedure moveBlock(x, y, z: Integer);
begin
b1 := board[x, y];
case z of
1: b2 := board[x-1, y];
2: b2 := board[x, y-1];
3: b2 := board[x+1, y];
4: b2 := board[x, y+1];
end;
case z of
1: board[x-1, y] := b1;
2: board[x, y-1] := b1;
3: board[x+1, y] := b1;
4: board[x, y+1] := b1;
end;
board[x, y] := b2;
dir := z;
Form1.TimerMove.Enabled := True;
isSolved;
end;
Diese Funktion bewegt ein Feld. Dazu werden vorerst beide Felder, das freie sowie das zu bewegende, in den Variablen b1 und b2 gespeichert. Anschließend wird das zu bewegende Feld an die Position des freien Feldes gesetzt und das freie Feld an die Position des zu bewegenden Feldes. Die Richtung wird in der Variable dir gespeichert, damit die Animation weiß, in welche Richtung das Feld bewegt werden soll. Gestartet wird die Animation mit Form1.TimerMove.Enabled := True;. Zum Schluss wird die Funktion isSolved() aufgerufen, um zu überprüfen, ob das Puzzle gelöst ist.
function checkSides(x, y: Integer): Integer;
begin
//1 - left, 2 - top, 3 - right, 4- bottom
//return 0 if no side is free
Result:=0;
//check left
if x>1 then
begin
if board[x-1, y].Tag = 0 then Result := 1;
end;
//check top
if y>1 then
begin
if board[x, y-1].Tag = 0 then Result := 2;
end;
//check right
if x<4 then
begin
if board[x+1, y].Tag = 0 then Result := 3;
end;
//check bottom
if y<4 then
begin
if board[x, y+1].Tag = 0 then Result := 4;
end;
if Result=0 then //safe-check to make sure animation is done
begin
if Form1.TimerRed.Enabled=False then
begin
redPanel := board[x, y];
Form1.TimerRed.Enabled := True;
end;
end
else moveBlock(x, y, Result);
end;
Diese Funktion überprüft, ob das Feld, welches die Kooridinaten x, y hat, ein freies Feld um sich hat. Die Kooridinaten werden hier als Parameter übergeben: checkSides(x, y: Integer): Integer. Das Ergebnis (Richtung des freien Feldes) wird in der Variable Result (Standardvariable zur Ausgabe eines Ergebnis in einer Funktion) gespeichert und zurückgegeben. Die Werte der Variable Result sind:
Wenn ein freies Feld gefunden wurde, wird die Funktion moveBlock(x, y, z: Integer) aufgerufen, um das Feld zu bewegen.
function getInv(arr: array of Integer): Integer;
var i, j, inv: Integer;
begin
inv := 0;
for i:= 0 to 14 do
begin
for j := i+1 to 15 do
begin
if arr[i] < 16 then
begin
if arr[i] > arr[j] then inv := inv+1;
end;
end;
end;
Result := inv;
end;
Diese Funktion zählt die Inversionen. Dazu werden alle Zahlen miteinander verglichen und die Variable inv um eins erhöht, wenn die Zahl i größer als die Zahl j ist. Das Ergebnis wird in der Variable Result gespeichert und zurückgegeben.
Die For-Schleifen sind kompakt geschrieben, bspw. zählt die i-Schleife nur bis 14, da die letzte Zahl im Array ja nicht mit sich selbst verglichen werden muss. Die j-Schleife nutzt die Variable i als Startwert, da j immer um mind. 1 größer sein muss als i.
Simplicity is prerequisite for reliability.
- Edsger W. Dijkstra
function findFree(arr: array of Integer): Integer;
var x, y, a, b: Integer;
prevBoard: Array[1..4, 1..4] of Integer;
begin
for y:=1 to 4 do
begin
for x:=1 to 4 do
begin
prevBoard[x, y] := arr[(y-1)*4+x-1];
end;
end;
for b:=1 to 4 do
begin
for a:=1 to 4 do
begin
if prevBoard[a, b] = 16 then begin Result := (4-b)+1; Exit; end;
end;
end;
end;
Diese Funktion sucht das freie Feld im Array und gibt die Position des freien Feldes zurück.
Zu beachten:
In der Funktion shuffle() wird das Array vom Index 1-16 definiert, jedoch wurde das Array übergeben und durch Delphi um eine Stelle nach links gerückt. Das bedeutet, falls man beim Definieren eines array bei 1 anfängt und diese einer Funktion übergibt, rückt Delpi automatisch alle Elemente nach links, um die 0. Stelle aufzufüllen.
Vermutung und gefähliches Halbwissen:
Durch das automatisch Auffüllen hat Delpi auch anscheinend die Position 16, die nun eigentlich leer sein sollte mit einer anderen Zahl aufgefüllt, d.h. beim Index 16 ist nun eine weiter Zahl (wahrscheinlich die gleiche wie bei Index 1).
procedure shuffle;
var i, n: byte;
x, y, ran, pos: Integer;
begin
x:=1;
y:=1;
Form1.PanelGameBoard.DestroyComponents;
...
end;
Diese Funktion ist etwas größer, weshalb ich sie hier in einzelne Stücke aufteile.
Im ersten Teil werden alle Komponenten (Panles) des Spielfeldes entfernt, damit sie später neu erstellt werden können. Außerdem werden die Startkoordinaten (x, y) für das Spielfeld auf 1 gesetzt.
...
repeat
randomize();
ranList := TStringList.Create;
for n:=1 to 16 do randomArray[n] := 0;
for n:=1 to 16 do
begin
ranList.Add(IntToStr(n));
end;
for n:=1 to 16 do
begin
if n=16 then pos:=1
else pos := RandomRange(1, 18-n);
ran := StrToInt(ranList[pos-1]);
randomArray[n] := randomArray[n] + ran;
ranList.Delete(pos-1);
end;
until isSolvable(randomArray);
...
In der repeat-Schleife wird eine Liste mit 16 Zufallszahlen erstellt. Dazu wird ein StringList mit den Zahlen 1-16 erstellt. Die Zahlen 1-16 werden dann in die Liste ranList geschrieben. Anschließend wird eine zufällige Zahl aus der Liste gezogen, aus der Liste gelöscht (dies Liste mache ich nur, um den Prozess der Zufallsgeneration zu entlasten und zu optimieren) und in das Array randomArray geschrieben. Die Schleife wird solange wiederholt, bis die Funktion isSolvable() true zurückgibt.
...
for i:=1 to 16 do
begin
board[x, y] := TPanel.Create(Form1.PanelGameBoard);
//create empty block
if randomArray[i]=16 then
begin
with board[x, y] do
begin
Parent := Form1.PanelGameBoard;
Width := 0;
Height := 0;
Left := 0;
Top := 0;
Tag := 0;
Visible := False;
end;
end //end of If
...
Hier beginnt eine For-Schleife, die bis in den folgenden Code-Block geht.
Im diesem ersten Teil der Schleife wird geschaut, ob der aktuelle Index von randomArray[] gleich 16 ist. Wenn ja, wird ein leeres Panel erstellt, welches später das freie Feld darstellen soll.
...
else //else of If
begin
//create 15 blocks
with board[x, y] do
begin
Parent := Form1.PanelGameBoard;
Width := 100;
Height := 100;
Left := x * 100 - 100;
Top := y * 100 - 100;
Tag := randomArray[i];
Show;
OnClick := Form1.blockClick;
Caption := inttostr(randomArray[i]);
BorderStyle := bsNone;
Color := rgb(224, 224, 224);
end;
end; //end of If else
if x > 3 then y:=y+1;
if x > 3 then x:=1 else x:=x+1;
end; //end of For
...
Wenn der aktuelle Index nicht 16 ist, wird ein Panel erstellt, welches später ein Feld darstellt.
mit with ... do kann man einem Objekt mehrere Eigenschaften gleichzeitig zuweisen.
Am Ende wird noch x und y erhöht, damit das nächste Panel an der richtigen Stelle erstellt wird.
procedure TForm1.FormCreate(Sender: TObject);
begin
shuffle;
end;
Beim Start des Programms wird lediglich die shuffle Funktion aufgerufen, um ein neues Spielfeld zu erstellen.
procedure TForm1.TimerRedTimer(Sender: TObject);
begin
redPanel.Color := rgb(255, 0, 0);
if tri < 8 then
begin
redPanel.Color := rgb(255, round((tri/8)*255), round((tri/8)*255));
tri := tri+1;
end
else
begin
tri:=1;
redPanel.Color := clBtnface;
TimerRed.Enabled := false;
end;
end;
Wenn jemand auf ein Feld klickt, welches keine freien Felder neben sich hat, wird dieser Timer aus der Funktion checkSides() aktiviert.
In diesem Timer wird lediglich jedes mal die Farbe des Panels auf Rot, bzw auf ein immer helleres Rot gesetzt, bis das Panel wieder seine Ursprungsfarbe hat. Sozusagen ist diser Timer nur für eine Animation zuständig. Am Ende der Schleife deaktiviert sich der Timer selbst und die Animation ist beendet.
procedure TForm1.TimerMoveTimer(Sender: TObject);
begin
if tmi < 6 then
begin
case dir of
1: b1.Left:=b1.Left-20;
2: b1.Top:=b1.Top-20;
3: b1.Left:=b1.Left+20;
4: b1.Top:=b1.Top+20;
end;
tmi := tmi + 1;
end
else
begin
tmi := 1;
TimerMove.Enabled := False;
end;
end;
In diesem Timer wird in jedem Intervall (insgesamt 5 Intervalle) die Position des angeklickten Feldes Stück für Stück geändert, so dass eine Animation entsteht.
procedure TForm1.BitBtn1Click(Sender: TObject);
begin
shuffle;
playtime := 0;
TimerGame.Enabled := True;
end;
Wenn der Shuffle Knopf gedrückt wird, wird Spielfeld zurückgesetzt und neu generiert, außerdem wird der Timer neugestartet.
procedure TForm1.TimerGameTimer(Sender: TObject);
var m, s: String;
begin
m := IntToStr(playtime div 60);
s := IntToStr(playtime mod 60);
if StrToInt(m) < 10 then m:='0'+m;
if StrToInt(s) < 10 then s:='0'+s;
playtime := playtime + 1;
LabelTimePlayed.Caption := m + ':' + s;
end;
In diesem Timer wird die Zeit hochgezählt und im LabelTimePlayed ausgegeben. Um eine gut leserliche Form zu erstellen werden die gezählten Sekunden in Minuten und Sekunden umgewandelt. Um es noch schönder zu machen werden vor jeder Zahl unter 10 eine 0 davor geschrieben um eine gleichbleibende Form beizubehalten.