source: tags/1.1.0/UGame.pas

Last change on this file was 38, checked in by chronos, 5 years ago
  • Added: In computer AI form allow to set delay of steps computation. Set to zero means fastest processing.
File size: 29.0 KB
Line 
1unit UGame;
2
3{$mode delphi}{$H+}
4
5interface
6
7uses
8 Classes, SysUtils, Dialogs, fgl, Graphics, Types, Forms, Math, DateUtils,
9 Controls, URegistry;
10
11type
12 TGame = class;
13 TMoveDirection = (drNone, drLeft, drUp, drRight, drDown);
14
15 { TTile }
16
17 TTile = class
18 Index: TPoint;
19 Value: Integer;
20 NewValue: Integer;
21 Merged: Boolean;
22 Moving: Boolean;
23 Shift: TPoint;
24 procedure Assign(Source: TTile);
25 end;
26
27 TTiles = class(TFPGObjectList<TTile>)
28 end;
29
30 { TArea }
31
32 TArea = record
33 P1, P2: TPoint;
34 function Increment: TPoint;
35 function Create(P1, P2: TPoint): TArea; overload;
36 function Create(X1, Y1, X2, Y2: Integer): TArea; overload;
37 end;
38
39 { THistoryMove }
40
41 THistoryMove = class
42 Direction: TMoveDirection;
43 NewItemPos: TPoint;
44 NewItemValue: Integer;
45 procedure SaveToRegistry(Reg: TRegistryEx; RegContext: TRegistryContext);
46 procedure LoadFromRegistry(Reg: TRegistryEx; RegContext: TRegistryContext);
47 end;
48
49 { THistoryMoves }
50
51 THistoryMoves = class(TFPGObjectList<THistoryMove>)
52 procedure SaveToRegistry(Reg: TRegistryEx; RegContext: TRegistryContext);
53 procedure LoadFromRegistry(Reg: TRegistryEx; RegContext: TRegistryContext);
54 end;
55
56 { THistory }
57
58 THistory = class
59 Game: TGame;
60 Moves: THistoryMoves;
61 InitialTilesPos: array of TPoint;
62 procedure GetStep(GameStep: TGame; Step: Integer);
63 procedure Clear;
64 constructor Create;
65 destructor Destroy; override;
66 procedure SaveToRegistry(Reg: TRegistryEx; RegContext: TRegistryContext);
67 procedure LoadFromRegistry(Reg: TRegistryEx; RegContext: TRegistryContext);
68 end;
69
70 { TBoard }
71
72 TBoard = class
73 private
74 FSize: TPoint;
75 procedure SetSize(AValue: TPoint);
76 public
77 Tiles: array of array of TTile;
78 procedure Assign(Source: TBoard);
79 procedure Clear;
80 procedure ClearMerged;
81 function GetValueSum: Integer;
82 function GetHighestTileValue: Integer;
83 function GetEmptyTilesCount: Integer;
84 procedure GetEmptyTiles(EmptyTiles: TTiles);
85 procedure SaveToRegistry(Reg: TRegistryEx; RegContext: TRegistryContext);
86 procedure LoadFromRegistry(Reg: TRegistryEx; RegContext: TRegistryContext);
87 destructor Destroy; override;
88 property Size: TPoint read FSize write SetSize;
89 end;
90
91 { TGame }
92
93 TGame = class
94 private
95 FMoving: Boolean;
96 FOnChange: TNotifyEvent;
97 FOnGameOver: TNotifyEvent;
98 FOnWin: TNotifyEvent;
99 FRecordHistory: Boolean;
100 FRunning: Boolean;
101 FScore: Integer;
102 FCanUndo: Boolean;
103 FBoardUndo: TBoard;
104 function GetTileColor(Value: Integer): TColor;
105 procedure SetRecordHistory(AValue: Boolean);
106 procedure SetScore(AValue: Integer);
107 procedure DoChange;
108 procedure RenderTile(Canvas: TCanvas; Tile: TTile; TileRect: TRect; WithText: Boolean);
109 procedure GameOver;
110 procedure Win;
111 function FillRandomTile(Value4Change: Double = 0.1): TTile;
112 function GetMoveArea(Direction: TMoveDirection): TArea;
113 procedure MoveAllAnimate(Direction: TMoveDirection);
114 public
115 Board: TBoard;
116 TopScore: Integer;
117 AnimationDuration: Integer;
118 WinScore: Integer;
119 UndoEnabled: Boolean;
120 History: THistory;
121 function CanUndo: Boolean;
122 procedure Undo;
123 function CanMergeDirection(Direction: TMoveDirection): Boolean;
124 function CanMoveDirection(Direction: TMoveDirection): Boolean;
125 function CanMove: Boolean;
126 procedure Assign(Source: TGame);
127 procedure New;
128 procedure Render(Canvas: TCanvas; CanvasSize: TPoint);
129 procedure MoveAll(Direction: TMoveDirection; Animation: Boolean);
130 procedure MoveAllAndUpdate(Direction: TMoveDirection; Animation: Boolean);
131 procedure MoveTile(SourceTile, TargetTile: TTile);
132 function IsValidPos(Pos: TPoint): Boolean;
133 procedure SaveToRegistry(RegContext: TRegistryContext);
134 procedure LoadFromRegistry(RegContext: TRegistryContext);
135 constructor Create;
136 destructor Destroy; override;
137 property Score: Integer read FScore write SetScore;
138 property Running: Boolean read FRunning write FRunning;
139 property OnChange: TNotifyEvent read FOnChange write FOnChange;
140 property OnWin: TNotifyEvent read FOnWin write FOnWin;
141 property OnGameOver: TNotifyEvent read FOnGameOver write FOnGameOver;
142 property Moving: Boolean read FMoving;
143 property RecordHistory: Boolean read FRecordHistory write SetRecordHistory;
144 end;
145
146 TGames = class(TFPGObjectList<TGame>)
147 end;
148
149const
150 DirectionDiff: array[TMoveDirection] of TPoint = (
151 (X: 0; Y: 0), (X: -1; Y: 0), (X: 0; Y: -1), (X: 1; Y: 0), (X: 0; Y: 1)
152 );
153 DirectionText: array[TMoveDirection] of string = ('None', 'Left', 'Up', 'Right', 'Down');
154 InitialTileCount = 2;
155
156resourcestring
157 SScore = 'Score';
158 STopScore = 'Top score';
159
160implementation
161
162{ THistoryMoves }
163
164procedure THistoryMoves.SaveToRegistry(Reg: TRegistryEx; RegContext: TRegistryContext);
165var
166 I: Integer;
167begin
168 with Reg do begin
169 CurrentContext := RegContext;
170 WriteInteger('Count', Count);
171 for I := 0 to Count - 1 do begin
172 Items[I].SaveToRegistry(Reg, TRegistryContext.Create(RegContext.RootKey, RegContext.Key + '/' + IntToStr(I)));
173 end;
174 end;
175end;
176
177procedure THistoryMoves.LoadFromRegistry(Reg: TRegistryEx; RegContext: TRegistryContext);
178var
179 I: Integer;
180begin
181 with Reg do begin
182 CurrentContext := RegContext;
183 Count := ReadIntegerWithDefault('Count', 0);
184 for I := 0 to Count - 1 do begin
185 Items[I] := THistoryMove.Create;
186 THistoryMove(Items[I]).LoadFromRegistry(Reg, TRegistryContext.Create(RegContext.RootKey, RegContext.Key + '/' + IntToStr(I)));
187 end;
188 end;
189end;
190
191{ THistoryMove }
192
193procedure THistoryMove.SaveToRegistry(Reg: TRegistryEx; RegContext: TRegistryContext);
194begin
195 with Reg do begin
196 CurrentContext := RegContext;
197 WriteInteger('Direction', Integer(Direction));
198 WriteInteger('NewItemPosX', NewItemPos.X);
199 WriteInteger('NewItemPosY', NewItemPos.Y);
200 WriteInteger('NewItemValue', NewItemValue);
201 end;
202end;
203
204procedure THistoryMove.LoadFromRegistry(Reg: TRegistryEx; RegContext: TRegistryContext);
205begin
206 with Reg do begin
207 CurrentContext := RegContext;
208 Direction := TMoveDirection(ReadIntegerWithDefault('Direction', Integer(drNone)));
209 NewItemPos := Point(ReadIntegerWithDefault('NewItemPosX', 0),
210 ReadIntegerWithDefault('NewItemPosY', 0));
211 NewItemValue := ReadIntegerWithDefault('NewItemValue', 0);
212 end;
213end;
214
215{ THistory }
216
217procedure THistory.GetStep(GameStep: TGame; Step: Integer);
218var
219 I: Integer;
220begin
221 GameStep.Board.Size := Game.Board.Size;
222 GameStep.Board.Clear;
223 GameStep.Score := 0;
224 for I := 0 to Length(InitialTilesPos) - 1 do
225 GameStep.Board.Tiles[InitialTilesPos[I].Y, InitialTilesPos[I].X].Value := 2;
226 for I := 0 to Step - 1 do
227 with Moves[I] do begin
228 GameStep.MoveAll(Direction, False);
229 if GameStep.Board.Tiles[NewItemPos.Y, NewItemPos.X].Value = 0 then
230 GameStep.Board.Tiles[NewItemPos.Y, NewItemPos.X].Value := NewItemValue
231 else raise Exception.Create('Tile should be empty');
232 end;
233end;
234
235procedure THistory.Clear;
236begin
237 Moves.Clear;
238 SetLength(InitialTilesPos, 0);
239end;
240
241constructor THistory.Create;
242begin
243 Moves := THistoryMoves.Create;
244end;
245
246destructor THistory.Destroy;
247begin
248 FreeAndNil(Moves);
249 inherited Destroy;
250end;
251
252procedure THistory.SaveToRegistry(Reg: TRegistryEx; RegContext: TRegistryContext);
253var
254 I: Integer;
255begin
256 with Reg do begin
257 CurrentContext := TRegistryContext.Create(RegContext.RootKey, RegContext.Key + '\InitialTilesPos');
258 WriteInteger('Count', Length(InitialTilesPos));
259 for I := 0 to Length(InitialTilesPos) - 1 do begin
260 WriteInteger('X' + IntToStr(I), InitialTilesPos[I].X);
261 WriteInteger('Y' + IntToStr(I), InitialTilesPos[I].Y);
262 end;
263 Moves.SaveToRegistry(Reg, RegContext);
264 end;
265end;
266
267procedure THistory.LoadFromRegistry(Reg: TRegistryEx; RegContext: TRegistryContext);
268var
269 I: Integer;
270begin
271 with Reg do begin
272 CurrentContext := TRegistryContext.Create(RegContext.RootKey, RegContext.Key + '\InitialTilesPos');
273 SetLength(InitialTilesPos, ReadIntegerWithDefault('Count', 0));
274 for I := 0 to Length(InitialTilesPos) - 1 do begin
275 InitialTilesPos[I] := Point(ReadIntegerWithDefault('X' + IntToStr(I), 0),
276 ReadIntegerWithDefault('Y' + IntToStr(I), 0));
277 end;
278 end;
279 Moves.LoadFromRegistry(Reg, RegContext);
280end;
281
282{ TArea }
283
284function TArea.Increment: TPoint;
285begin
286 Result := Point(Sign(P2.X - P1.X), Sign(P2.Y - P1.Y));
287end;
288
289function TArea.Create(P1, P2: TPoint): TArea;
290begin
291 Result.P1 := P1;
292 Result.P2 := P2;
293end;
294
295function TArea.Create(X1, Y1, X2, Y2: Integer): TArea;
296begin
297 Result.P1 := Point(X1, Y1);
298 Result.P2 := Point(X2, Y2);
299end;
300
301{ TBoard }
302
303procedure TBoard.SetSize(AValue: TPoint);
304var
305 X, Y: Integer;
306begin
307 if FSize = AValue then Exit;
308 for Y := 0 to FSize.Y - 1 do
309 for X := 0 to FSize.X - 1 do
310 Tiles[Y, X].Free;
311 FSize := AValue;
312 SetLength(Tiles, FSize.Y, FSize.X);
313 for Y := 0 to FSize.Y - 1 do
314 for X := 0 to FSize.X - 1 do begin
315 Tiles[Y, X] := TTile.Create;
316 Tiles[Y, X].Index := Point(X, Y);
317 end;
318end;
319
320procedure TBoard.Assign(Source: TBoard);
321var
322 X, Y: Integer;
323begin
324 Size := Source.Size;
325 for Y := 0 to Size.Y - 1 do
326 for X := 0 to Size.X - 1 do
327 Tiles[Y, X].Assign(Source.Tiles[Y, X]);
328end;
329
330procedure TBoard.GetEmptyTiles(EmptyTiles: TTiles);
331var
332 X, Y: Integer;
333begin
334 EmptyTiles.Clear;
335 for Y := 0 to Size.Y - 1 do
336 for X := 0 to Size.X - 1 do
337 if Tiles[Y, X].Value = 0 then
338 EmptyTiles.Add(Tiles[Y, X]);
339end;
340
341procedure TBoard.SaveToRegistry(Reg: TRegistryEx; RegContext: TRegistryContext);
342var
343 X, Y: Integer;
344 Value: string;
345begin
346 with Reg do begin
347 CurrentContext := RegContext;
348
349 WriteInteger('SizeX', Size.X);
350 WriteInteger('SizeY', Size.Y);
351 Value := '';
352 for Y := 0 to Size.Y - 1 do begin
353 for X := 0 to Size.X - 1 do begin
354 Value := Value + IntToStr(Tiles[Y, X].Value);
355 if X < Size.X - 1 then Value := Value + ',';
356 end;
357 if Y < Size.Y - 1 then Value := Value + ';'
358 end;
359 WriteString('TileValues', Value);
360 end;
361end;
362
363procedure TBoard.LoadFromRegistry(Reg: TRegistryEx; RegContext: TRegistryContext);
364var
365 X, Y: Integer;
366 Items: TStringList;
367 Lines: TStringList;
368 Number: Integer;
369begin
370 with Reg do begin
371 CurrentContext := RegContext;
372
373 Size := Point(ReadIntegerWithDefault('SizeX', 4), ReadIntegerWithDefault('SizeY', 4));
374 Items := TStringList.Create;
375 Items.Delimiter := ',';
376 Lines := TStringList.Create;
377 Lines.Delimiter := ';';
378 Lines.DelimitedText := ReadStringWithDefault('TileValues', '');
379 for Y := 0 to Lines.Count - 1 do begin
380 Items.DelimitedText := Lines[Y];
381 for X := 0 to Items.Count - 1 do begin
382 if TryStrToInt(Items[X], Number) and (X < Size.X) and (Y < Size.Y) then
383 Tiles[Y, X].Value := Number;
384 end;
385 end;
386 Lines.Free;
387 Items.Free;
388 end;
389end;
390
391destructor TBoard.Destroy;
392begin
393 Size := Point(0, 0);
394 inherited Destroy;
395end;
396
397procedure TBoard.ClearMerged;
398var
399 X, Y: Integer;
400begin
401 for Y := 0 to Size.Y - 1 do
402 for X := 0 to Size.X - 1 do
403 Tiles[Y, X].Merged := False;
404end;
405
406function TBoard.GetValueSum: Integer;
407var
408 X, Y: Integer;
409begin
410 Result := 0;
411 for Y := 0 to Size.Y - 1 do
412 for X := 0 to Size.X - 1 do
413 Inc(Result, Tiles[Y, X].Value);
414end;
415
416function TBoard.GetEmptyTilesCount: Integer;
417var
418 X, Y: Integer;
419begin
420 Result := 0;
421 for Y := 0 to Size.Y - 1 do
422 for X := 0 to Size.X - 1 do
423 if Tiles[Y, X].Value = 0 then
424 Inc(Result);
425end;
426
427function TBoard.GetHighestTileValue: Integer;
428var
429 X, Y: Integer;
430begin
431 Result := 0;
432 for Y := 0 to Size.Y - 1 do
433 for X := 0 to Size.X - 1 do
434 if Result < Tiles[Y, X].Value then Result := Tiles[Y, X].Value;
435end;
436
437procedure TBoard.Clear;
438var
439 X, Y: Integer;
440begin
441 for Y := 0 to Size.Y - 1 do
442 for X := 0 to Size.X - 1 do
443 Tiles[Y, X].Value := 0;
444end;
445
446
447{ TTile }
448
449procedure TTile.Assign(Source: TTile);
450begin
451 Value := Source.Value;
452 Merged := Source.Merged;
453end;
454
455{ TGame }
456
457procedure TGame.DoChange;
458begin
459 if Assigned(FOnChange) then FOnChange(Self);
460end;
461
462procedure TGame.GameOver;
463begin
464 if Running and Assigned(FOnGameOver) then FOnGameOver(Self);
465 Running := False;
466end;
467
468procedure TGame.Win;
469begin
470 if Assigned(FOnWin) then FOnWin(Self);
471end;
472
473function TGame.FillRandomTile(Value4Change: Double = 0.1): TTile;
474var
475 EmptyTiles: TTiles;
476 NewValue: Integer;
477begin
478 Result := nil;
479 EmptyTiles := TTiles.Create(False);
480 Board.GetEmptyTiles(EmptyTiles);
481 if EmptyTiles.Count > 0 then begin
482 if Random < Value4Change then NewValue := 4 else NewValue := 2;
483 Result := EmptyTiles[Random(EmptyTiles.Count)];
484 Result.Value := NewValue;
485 end;
486 EmptyTiles.Free;
487end;
488
489function TGame.GetMoveArea(Direction: TMoveDirection): TArea;
490begin
491 case Direction of
492 drNone: Result := TArea.Create(0, 0, 0, 0);
493 drLeft: Result := TArea.Create(1, 0, Board.Size.X - 1, Board.Size.Y - 1);
494 drUp: Result := TArea.Create(0, 1, Board.Size.X - 1, Board.Size.Y - 1);
495 drRight: Result := TArea.Create(Board.Size.X - 2, 0, 0, Board.Size.Y - 1);
496 drDown: Result := TArea.Create(0, Board.Size.Y - 2, Board.Size.X - 1, 0);
497 end;
498end;
499
500function TGame.CanMove: Boolean;
501begin
502 Result := CanMoveDirection(drLeft) or CanMoveDirection(drRight) or
503 CanMoveDirection(drUp) or CanMoveDirection(drDown);
504end;
505
506procedure TGame.Assign(Source: TGame);
507begin
508 Board.Assign(Source.Board);
509 FBoardUndo.Assign(Source.FBoardUndo);
510 FCanUndo := Source.FCanUndo;
511 TopScore := Source.TopScore;
512 AnimationDuration := Source.AnimationDuration;
513 WinScore := Source.WinScore;
514 UndoEnabled := Source.UndoEnabled;
515 FScore := Source.FScore;
516 FRunning := Source.FRunning;
517end;
518
519procedure TGame.New;
520var
521 I: Integer;
522begin
523 FCanUndo := False;
524 Board.Clear;
525 Score := 0;
526 Running := True;
527 History.Clear;
528 if RecordHistory then begin
529 for I := 0 to InitialTileCount - 1 do begin
530 SetLength(History.InitialTilesPos, Length(History.InitialTilesPos) + 1);
531 History.InitialTilesPos[Length(History.InitialTilesPos) - 1] := FillRandomTile(0).Index;
532 end;
533 end else begin
534 for I := 0 to InitialTileCount - 1 do
535 FillRandomTile(0);
536 end;
537 DoChange;
538end;
539
540procedure TGame.Render(Canvas: TCanvas; CanvasSize: TPoint);
541var
542 X, Y: Integer;
543 TileSize: TPoint;
544 ValueStr: string;
545 Frame: TRect;
546 TileRect: TRect;
547 TopBarHeight: Integer;
548 TileMargin: Integer;
549begin
550 TopBarHeight := ScaleY(24, 96);
551 TileMargin := Round(CanvasSize.X / Board.Size.X * 0.02);
552 Canvas.Brush.Style := bsSolid;
553 Canvas.Brush.Color := clBlack;
554 Canvas.FillRect(0, 0, Canvas.Width, Canvas.Height);
555
556 ValueStr := SScore + ': ' + IntToStr(Score);
557 Canvas.Font.Color := clWhite;
558 Canvas.Font.Height := Trunc(TopBarHeight * 0.7);
559 Canvas.TextOut(ScaleY(16, 96), (TopBarHeight - Canvas.TextHeight(ValueStr)) div 2, ValueStr);
560
561 ValueStr := STopScore + ': ' + IntToStr(TopScore);
562 Canvas.Font.Color := clWhite;
563 Canvas.Font.Height := Trunc(TopBarHeight * 0.7);
564 Canvas.TextOut(ScaleY(136, 96), (TopBarHeight - Canvas.TextHeight(ValueStr)) div 2, ValueStr);
565
566 // Form.Canvas.Width and Form.Canvas.Height is not working correctly under Windows.
567 // So dimensions are provided by CanvasSize parameter.
568 Frame := Rect(2, TopBarHeight, CanvasSize.X - 2, CanvasSize.Y - 2);
569 TileSize := Point(Frame.Width div Board.Size.X, Frame.Height div Board.Size.Y);
570 if TileSize.X < TileSize.Y then TileSize.Y := TileSize.X;
571 if TileSize.Y < TileSize.X then TileSize.X := TileSize.Y;
572 Frame := Rect(Frame.Width div 2 - (Board.Size.X * TileSize.X) div 2,
573 Frame.Top + Frame.Height div 2 - (Board.Size.Y * TileSize.Y) div 2,
574 Frame.Width div 2 + (Board.Size.X * TileSize.X) div 2,
575 Frame.Top + Frame.Height div 2 + (Board.Size.Y * TileSize.Y) div 2);
576
577 Canvas.Brush.Style := bsSolid;
578 Canvas.Brush.Color := clGray;
579 Canvas.FillRect(Frame);
580
581 Canvas.Font.Color := clBlack;
582
583 // Draw static tiles
584 for Y := 0 to Board.Size.Y - 1 do
585 for X := 0 to Board.Size.X - 1 do begin
586 if Board.Tiles[Y, X].Moving then Canvas.Brush.Color := GetTileColor(0)
587 else Canvas.Brush.Color := GetTileColor(Board.Tiles[Y, X].Value);
588 Canvas.Brush.Style := bsSolid;
589 TileRect := Bounds(
590 Frame.Left + X * TileSize.X + TileMargin,
591 Frame.Top + Y * TileSize.Y + TileMargin,
592 TileSize.X - 2 * TileMargin, TileSize.Y - 2 * TileMargin);
593 RenderTile(Canvas, Board.Tiles[Y, X], TileRect, not Board.Tiles[Y, X].Moving);
594 end;
595
596 // Draw moving Tiles
597 for Y := 0 to Board.Size.Y - 1 do
598 for X := 0 to Board.Size.X - 1 do
599 if Board.Tiles[Y, X].Moving then begin
600 Canvas.Brush.Color := GetTileColor(Board.Tiles[Y, X].Value);
601 Canvas.Brush.Style := bsSolid;
602 TileRect := Bounds(
603 Frame.Left + X * TileSize.X + Trunc(Board.Tiles[Y, X].Shift.X / 100 * TileSize.X + TileMargin),
604 Frame.Top + Y * TileSize.Y + Trunc(Board.Tiles[Y, X].Shift.Y / 100 * TileSize.Y + TileMargin),
605 TileSize.X - 2 * TileMargin, TileSize.Y - 2 * TileMargin);
606 RenderTile(Canvas, Board.Tiles[Y, X], TileRect, True);
607 end;
608end;
609
610procedure TGame.MoveAll(Direction: TMoveDirection; Animation: Boolean);
611var
612 P: TPoint;
613 PNew: TPoint;
614 I: Integer;
615 Area: TArea;
616begin
617 if Animation then begin
618 MoveAllAnimate(Direction);
619 Exit;
620 end;
621 if Direction = drNone then Exit;
622 if not CanMoveDirection(Direction) then Exit;
623 FMoving := True;
624 FBoardUndo.Assign(Board);
625 FCanUndo := True;
626 Area := GetMoveArea(Direction);
627 Board.ClearMerged;
628 for I := 0 to Max(Board.Size.X, Board.Size.Y) - 1 do begin
629 P := Area.P1;
630 while P.Y <> Area.P2.Y + Area.Increment.Y do begin
631 P.X := Area.P1.X;
632 while P.X <> Area.P2.X + Area.Increment.X do begin
633 PNew := P + DirectionDiff[Direction];
634 if IsValidPos(PNew) then begin
635 if (Board.Tiles[P.Y, P.X].Value <> 0) then begin
636 if (Board.Tiles[PNew.Y, PNew.X].Value = 0) then begin
637 Board.Tiles[P.Y, P.X].Moving := True;
638 Board.Tiles[PNew.Y, PNew.X].Value := Board.Tiles[P.Y, P.X].Value;
639 Board.Tiles[PNew.Y, PNew.X].Merged := Board.Tiles[P.Y, P.X].Merged;
640 Board.Tiles[P.Y, P.X].Value := 0;
641 Board.Tiles[P.Y, P.X].Merged := False;
642 end else
643 if (not Board.Tiles[P.Y, P.X].Merged) and (not Board.Tiles[PNew.Y, PNew.X].Merged) and
644 (Board.Tiles[PNew.Y, PNew.X].Value = Board.Tiles[P.Y, P.X].Value) then begin
645 Board.Tiles[P.Y, P.X].Moving := True;
646 Board.Tiles[PNew.Y, PNew.X].Value := Board.Tiles[PNew.Y, PNew.X].Value + Board.Tiles[P.Y, P.X].Value;
647 Board.Tiles[PNew.Y, PNew.X].Merged := True;
648 Board.Tiles[P.Y, P.X].Value := 0;
649 Board.Tiles[P.Y, P.X].Merged := False;
650 Score := Score + Board.Tiles[PNew.Y, PNew.X].Value;
651 end;
652 end;
653 end;
654 Inc(P.X, Area.Increment.X);
655 end;
656 Inc(P.Y, Area.Increment.Y);
657 end;
658 DoChange;
659 end;
660 FMoving := False;
661end;
662
663procedure TGame.RenderTile(Canvas: TCanvas; Tile: TTile; TileRect: TRect; WithText: Boolean);
664var
665 ValueStr: string;
666 TextSize: TSize;
667begin
668 Canvas.FillRect(TileRect);
669 if WithText and (Tile.Value <> 0) then begin
670 ValueStr := IntToStr(Tile.Value);
671 Canvas.Brush.Style := bsClear;
672 Canvas.Font.Height := Trunc(TileRect.Height * 0.7);
673 TextSize := Canvas.TextExtent(ValueStr);
674 if TextSize.Width > TileRect.Width then
675 Canvas.Font.Height := Trunc(Canvas.Font.Height / TextSize.Width * TileRect.Width);
676 TextSize := Canvas.TextExtent(ValueStr);
677 Canvas.TextOut(TileRect.Left + TileRect.Width div 2 - TextSize.Width div 2,
678 TileRect.Top + TileRect.Height div 2 - TextSize.Height div 2, ValueStr);
679 end;
680end;
681
682function TGame.CanUndo: Boolean;
683begin
684 Result := UndoEnabled and FCanUndo;
685end;
686
687procedure TGame.Undo;
688begin
689 if UndoEnabled and CanUndo then begin
690 Board.Assign(FBoardUndo);
691 FCanUndo := False;
692 FRunning := CanMove;
693 History.Moves.Delete(History.Moves.Count - 1);
694 DoChange;
695 end;
696end;
697
698function TGame.CanMergeDirection(Direction: TMoveDirection): Boolean;
699var
700 P: TPoint;
701 PNew: TPoint;
702 I: Integer;
703 Area: TArea;
704begin
705 Result := False;
706 if Direction = drNone then Exit;
707 Area := GetMoveArea(Direction);
708 for I := 0 to Max(Board.Size.X, Board.Size.Y) - 1 do begin
709 P := Area.P1;
710 while P.Y <> Area.P2.Y + Area.Increment.Y do begin
711 P.X := Area.P1.X;
712 while P.X <> Area.P2.X + Area.Increment.X do begin
713 PNew := P + DirectionDiff[Direction];
714 if IsValidPos(PNew) then begin
715 if (Board.Tiles[PNew.Y, PNew.X].Value = 0) then begin
716 Board.Tiles[PNew.Y, PNew.X].Value := Board.Tiles[P.Y, P.X].Value;
717 Board.Tiles[P.Y, P.X].Value := 0;
718 end else
719 if (Board.Tiles[P.Y, P.X].Value <> 0) then begin
720 if Board.Tiles[PNew.Y, PNew.X].Value = Board.Tiles[P.Y, P.X].Value then begin
721 Result := True;
722 Break;
723 end;
724 end;
725 end;
726 Inc(P.X, Area.Increment.Y);
727 end;
728 if Result then Break;
729 Inc(P.Y, Area.Increment.Y);
730 end;
731 end;
732end;
733
734function TGame.CanMoveDirection(Direction: TMoveDirection): Boolean;
735var
736 P: TPoint;
737 PNew: TPoint;
738 Area: TArea;
739begin
740 Result := False;
741 if Direction = drNone then Exit;
742 Area := GetMoveArea(Direction);
743 P := Area.P1;
744 while P.Y <> Area.P2.Y + Area.Increment.Y do begin
745 P.X := Area.P1.X;
746 while P.X <> Area.P2.X + Area.Increment.X do begin
747 PNew := P + DirectionDiff[Direction];
748 if IsValidPos(PNew) then begin
749 if (Board.Tiles[P.Y, P.X].Value <> 0) then begin
750 if (Board.Tiles[PNew.Y, PNew.X].Value = 0) or
751 (Board.Tiles[PNew.Y, PNew.X].Value = Board.Tiles[P.Y, P.X].Value) then begin
752 Result := True;
753 Break;
754 end;
755 end;
756 end;
757 Inc(P.X, Area.Increment.X);
758 end;
759 if Result then Break;
760 Inc(P.Y, Area.Increment.Y);
761 end;
762end;
763
764procedure TGame.MoveAllAnimate(Direction: TMoveDirection);
765var
766 P: TPoint;
767 PNew: TPoint;
768 X, Y: Integer;
769 I: Integer;
770 StartTime: TDateTime;
771 EndTime: TDateTime;
772 Time: TDateTime;
773 Part: Double;
774 Area: TArea;
775 TileMoved: Boolean;
776begin
777 if Direction = drNone then Exit;
778 if not CanMoveDirection(Direction) then Exit;
779 FMoving := True;
780 FBoardUndo.Assign(Board);
781 FCanUndo := True;
782 Area := GetMoveArea(Direction);
783 Board.ClearMerged;
784 for I := 0 to Max(Board.Size.X, Board.Size.Y) - 1 do begin
785 for Y := 0 to Board.Size.Y - 1 do
786 for X := 0 to Board.Size.X - 1 do begin
787 Board.Tiles[Y, X].NewValue := Board.Tiles[Y, X].Value;
788 Board.Tiles[Y, X].Moving := False;
789 end;
790
791 TileMoved := False;
792 P := Area.P1;
793 while P.Y <> Area.P2.Y + Area.Increment.Y do begin
794 P.X := Area.P1.X;
795 while P.X <> Area.P2.X + Area.Increment.X do begin
796 PNew := P + DirectionDiff[Direction];
797 if IsValidPos(PNew) then begin
798 if (Board.Tiles[P.Y, P.X].NewValue <> 0) then begin
799 if (Board.Tiles[PNew.Y, PNew.X].NewValue = 0) then begin
800 Board.Tiles[P.Y, P.X].Moving := True;
801 Board.Tiles[PNew.Y, PNew.X].NewValue := Board.Tiles[P.Y, P.X].NewValue;
802 Board.Tiles[PNew.Y, PNew.X].Merged := Board.Tiles[P.Y, P.X].Merged;
803 Board.Tiles[P.Y, P.X].NewValue := 0;
804 Board.Tiles[P.Y, P.X].Merged := False;
805 TileMoved := True;
806 end else
807 if (not Board.Tiles[P.Y, P.X].Merged) and (not Board.Tiles[PNew.Y, PNew.X].Merged) and
808 (Board.Tiles[PNew.Y, PNew.X].NewValue = Board.Tiles[P.Y, P.X].NewValue) then begin
809 Board.Tiles[P.Y, P.X].Moving := True;
810 Board.Tiles[PNew.Y, PNew.X].NewValue := Board.Tiles[PNew.Y, PNew.X].NewValue + Board.Tiles[P.Y, P.X].NewValue;
811 Board.Tiles[PNew.Y, PNew.X].Merged := True;
812 Board.Tiles[P.Y, P.X].NewValue := 0;
813 Board.Tiles[P.Y, P.X].Merged := False;
814 Score := Score + Board.Tiles[PNew.Y, PNew.X].NewValue;
815 TileMoved := True;
816 end;
817 end;
818 end;
819 Inc(P.X, Area.Increment.X);
820 end;
821 Inc(P.Y, Area.Increment.Y);
822 end;
823 if not TileMoved then Break;
824
825 // Animate tiles move
826 StartTime := Now;
827 EndTime := StartTime + AnimationDuration / 300 * OneSecond / Max(Board.Size.X, Board.Size.Y);
828 if AnimationDuration > 0 then
829 repeat
830 Time := Now;
831 Part := (Time - StartTime) / (EndTime - StartTime);
832 if Part > 1 then Part := 1;
833 for Y := 0 to Board.Size.Y - 1 do
834 for X := 0 to Board.Size.X - 1 do begin
835 if Board.Tiles[Y, X].Moving then
836 Board.Tiles[Y, X].Shift := Point(Trunc(Part * DirectionDiff[Direction].X * 100),
837 Trunc(Part * DirectionDiff[Direction].Y * 100));
838 end;
839 DoChange;
840 Application.ProcessMessages;
841 Sleep(1);
842 until Time > EndTime;
843
844 // Set final tiles values
845 for Y := 0 to Board.Size.Y - 1 do
846 for X := 0 to Board.Size.X - 1 do begin
847 Board.Tiles[Y, X].Value := Board.Tiles[Y, X].NewValue;
848 end;
849 end;
850
851 // Set final tiles values
852 for Y := 0 to Board.Size.Y - 1 do
853 for X := 0 to Board.Size.X - 1 do begin
854 Board.Tiles[Y, X].Shift := Point(0, 0);
855 Board.Tiles[Y, X].Moving := False;
856 Board.Tiles[Y, X].Value := Board.Tiles[Y, X].NewValue;
857 end;
858 DoChange;
859 FMoving := False;
860end;
861
862procedure TGame.MoveAllAndUpdate(Direction: TMoveDirection; Animation: Boolean);
863var
864 HighestValue: Integer;
865 HistoryMove: THistoryMove;
866 NewTile: TTile;
867begin
868 if CanMoveDirection(Direction) then begin
869 HighestValue := Board.GetHighestTileValue;
870 MoveAll(Direction, Animation);
871
872 NewTile := FillRandomTile;
873 if RecordHistory then begin
874 HistoryMove := THistoryMove.Create;
875 HistoryMove.Direction := Direction;
876 HistoryMove.NewItemPos := NewTile.Index;
877 HistoryMove.NewItemValue := NewTile.Value;
878 History.Moves.Add(HistoryMove);
879 end;
880
881 if not CanMove and (Board.GetEmptyTilesCount = 0) then
882 GameOver;
883 if (HighestValue < WinScore) and
884 (Board.GetHighestTileValue >= WinScore) then Win;
885 end;
886end;
887
888procedure TGame.MoveTile(SourceTile, TargetTile: TTile);
889begin
890 TargetTile.Value := SourceTile.Value;
891 SourceTile.Value := 0;
892 TargetTile.Merged := SourceTile.Merged;
893 SourceTile.Merged := False;
894end;
895
896function TGame.IsValidPos(Pos: TPoint): Boolean;
897begin
898 Result := (Pos.X >= 0) and (Pos.X < Board.Size.X) and
899 (Pos.Y >= 0) and (Pos.Y < Board.Size.Y);
900end;
901
902procedure TGame.SaveToRegistry(RegContext: TRegistryContext);
903var
904 Reg: TRegistryEx;
905begin
906 Reg := TRegistryEx.Create;
907 with Reg do
908 try
909 CurrentContext := RegContext;
910
911 WriteInteger('TopScore', TopScore);
912 WriteInteger('AnimationDuration', AnimationDuration);
913 WriteInteger('Score', Score);
914 WriteBool('GameRunning', FRunning);
915 WriteBool('CanUndo', FCanUndo);
916 WriteBool('UndoEnabled', UndoEnabled);
917 WriteBool('RecordHistory', RecordHistory);
918 FBoardUndo.SaveToRegistry(Reg, TRegistryContext.Create(RegContext.RootKey, RegContext.Key + '\BoardUndo'));
919 Board.SaveToRegistry(Reg, TRegistryContext.Create(RegContext.RootKey, RegContext.Key + '\Board'));
920 History.SaveToRegistry(Reg, TRegistryContext.Create(RegContext.RootKey, RegContext.Key + '\History'));
921 finally
922 Free;
923 end;
924end;
925
926procedure TGame.LoadFromRegistry(RegContext: TRegistryContext);
927var
928 Reg: TRegistryEx;
929begin
930 Reg := TRegistryEx.Create;
931 with Reg do
932 try
933 CurrentContext := RegContext;
934 AnimationDuration := ReadIntegerWithDefault('AnimationDuration', 30);
935 TopScore := ReadIntegerWithDefault('TopScore', 0);
936 Score := ReadIntegerWithDefault('Score', 0);
937 FRunning := ReadBoolWithDefault('GameRunning', False);
938 FCanUndo := ReadBoolWithDefault('CanUndo', False);
939 UndoEnabled := ReadBoolWithDefault('UndoEnabled', True);
940 RecordHistory := ReadBoolWithDefault('RecordHistory', False);
941 FBoardUndo.LoadFromRegistry(Reg, TRegistryContext.Create(RegContext.RootKey, RegContext.Key + '\BoardUndo'));
942 Board.LoadFromRegistry(Reg, TRegistryContext.Create(RegContext.RootKey, RegContext.Key + '\Board'));
943 History.LoadFromRegistry(Reg, TRegistryContext.Create(RegContext.RootKey, RegContext.Key + '\History'));
944 finally
945 Free;
946 end;
947 DoChange;
948end;
949
950constructor TGame.Create;
951begin
952 AnimationDuration := 30;
953 WinScore := 2048;
954 Board := TBoard.Create;
955 FBoardUndo := TBoard.Create;
956 History := THistory.Create;
957 History.Game := Self;
958end;
959
960destructor TGame.Destroy;
961begin
962 FreeAndNil(History);
963 FreeAndNil(FBoardUndo);
964 FreeAndNil(Board);
965 inherited;
966end;
967
968function TGame.GetTileColor(Value: Integer): TColor;
969begin
970 case Value of
971 0: Result := $f2f6f9;
972 2: Result := $dae4ee;
973 4: Result := $c8e0ed;
974 8: Result := $79b1f2;
975 16: Result := $6395f5;
976 32: Result := $5f7cf6;
977 64: Result := $3b5ef6;
978 128: Result := $72cfed;
979 256: Result := $61cced;
980 512: Result := $50c8ed;
981 1024: Result := $3fc5ed;
982 2048: Result := $2ec2ed;
983 else Result := $323a3c;
984 end;
985end;
986
987procedure TGame.SetRecordHistory(AValue: Boolean);
988begin
989 if FRecordHistory = AValue then Exit;
990 FRecordHistory := AValue;
991 if not FRecordHistory then History.Clear;
992end;
993
994procedure TGame.SetScore(AValue: Integer);
995begin
996 if FScore = AValue then Exit;
997 FScore := AValue;
998 if FScore > TopScore then TopScore := FScore;
999end;
1000
1001end.
1002
Note: See TracBrowser for help on using the repository browser.