source: tags/1.2.0/UGame.pas

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