123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675 |
- unit utetris;
- {$mode objfpc}
- interface
- uses
- Classes, SysUtils, Web;
- Const
- SGameOver = 'Game over!';
- SPlaying = 'Playing...';
- BlockCount = 7;
- BlockHigh = BlockCount-1;
- BlockSize = 4; // Number of positions in a block
- BoardHeight = 20;
- BoardWidth = 12;
- CreatePosX = 4;
- CreatePosY = 0;
- BlockColors : Array [0..BlockCount] of String
- = ('white','#8F3985', '#39A275', '#D28140', '#194A8D', '#8D71B4', '#F0889D', '#DF1C44');
- Type
- TDirection = (dIdle, dDown, dLeft, dRight);
- TVerticalCollision = (vcNone,vcBlock,vcWall);
- {$modeswitch advancedrecords}
- TCoordinate = record
- x,y : Integer;
- Class function Create(aX,aY : integer) : TCoordinate; static;
- end;
- TBlock = Array[0..BlockSize-1] of TCoordinate;
- TBlocks = Array[0..BlockHigh] of TBlock;
- TBoard = Array[0..BoardWidth-1,0..BoardHeight-1] of Integer; // Colors
- TCoordinateBoard = Array[0..BoardWidth-1,0..BoardHeight-1] of TCoordinate; // Coordinates of squares
- { TTetris }
- TTetris = Class(TComponent)
- private
- function DoMouseClick(aEvent: TJSMouseEvent): boolean;
- function MoveBlockLeftRight(isRight: Boolean): Boolean;
- Private
- FCanvasID: String;
- FGameOver : Boolean;
- FCoordinates : TCoordinateBoard;
- FIncLevelInterval: Integer;
- FIncLevelScore: Integer;
- FInterval: Integer;
- FResetID: String;
- FTetrisLogo : TJSHTMLImageElement;
- FCanvas : TJSHTMLCanvasElement;
- FCtx : TJSCanvasRenderingContext2D;
- FScore : Integer;
- FLevel : Integer;
- FBoard : TBoard;
- FBlocks : TBlocks;
- FCurBlock : TBlock;
- FCurBlockColor : Smallint; // Index in color array
- FCurrPos : TCoordinate;
- Fdirection : TDirection;
- FElScore : TJSHTMLElement;
- FElLevel : TJSHTMLElement;
- FElStatus : TJSHTMLElement;
- FBtnReset : TJSHTMLButtonElement;
- FMyInterval : NativeInt;
- function DoResetClick(aEvent: TJSMouseEvent): boolean;
- procedure SetGameOver(AValue: Boolean);
- Procedure CheckBlockDown;
- procedure DrawBlockAt(X, Y, Color: Integer);
- procedure DrawLevel;
- Procedure DrawScore;
- procedure DrawGameStatus;
- procedure EnableTick;
- Function HittingTheWall : Boolean;
- Procedure MoveAllRowsDown(rowsToDelete, startOfDeletion : Integer);
- function CheckForVerticalCollision(aDirection: TDirection; aBlock: TBlock): TVerticalCollision;
- Function CheckForHorizontalCollision (aDirection: TDirection; aBlock: TBlock): Boolean;
- function CheckForCompletedRows : Boolean;
- Procedure CreateCoordArray;
- procedure RecalcScore(aRows: integer);
- procedure SetLevel(AValue: Integer);
- procedure SetScore(AValue: Integer);
- Procedure SetupTetris;
- Procedure DrawBlock;
- Procedure CreateBlocks;
- Procedure CreateBlock;
- Procedure DeleteBlock;
- function MoveBlockDown: Boolean;
- Procedure DropBlock;
- Procedure RotateBlock;
- Procedure ClearBoard;
- function HandleKeyPress(k : TJSKeyBoardEvent) : Boolean;
- Property GameOver : Boolean Read FGameOver Write SetGameOver;
- Public
- Constructor Create(aOwner : TComponent); override;
- Procedure Start;
- // Reset button ID
- Property ResetID : String Read FResetID Write FResetID;
- // our canvas ID
- Property CanvasID : String Read FCanvasID Write FCanvasID;
- Property Canvas : TJSHTMLCanvasElement Read FCanvas;
- Property Ctx : TJSCanvasRenderingContext2D Read FCTX;
- Property Score : Integer Read FScore Write SetScore;
- Property Level : Integer Read FLevel Write SetLevel;
- Property Board : TBoard Read FBoard Write FBoard;
- Property Stopped : TBoard Read FBoard Write FBoard;
- Property Blocks : TBlocks Read FBlocks;
- Property Coordinates : TCoordinateBoard Read FCoordinates;
- Property Interval : Integer Read FInterval Write FInterval;
- Property IncLevelScore : Integer Read FIncLevelScore Write FIncLevelScore;
- Property IncLevelInterval : Integer read FIncLevelInterval write FIncLevelInterval;
- end;
- implementation
- Class function TCoordinate.Create(aX,aY : integer) : TCoordinate;
- begin
- Result.X:=aX;
- Result.Y:=aY;
- end;
- procedure TTetris.CreateCoordArray;
- Const
- XStart = 11;
- XStep = 23;
- YStart = 9;
- YStep = 23;
- Var
- x,y,i,j : Integer;
- begin
- i:=0;
- j:=0;
- X:=XStart;
- For I:=0 to BoardWidth-1 do
- begin
- Y:=YStart;
- For J:=0 to BoardHeight-1 do
- begin
- FCoordinates[I,J]:=TCoordinate.Create(X,Y);
- Y:=Y+YStep;
- end;
- X:=X+XStep;
- end;
- end;
- Const
- ControlCount = 5;
- ControlNames : Array[1..5] of string = ('left','right','down','rotate','drop');
- procedure TTetris.SetupTetris;
- Var
- i : Integer;
- el : TJSElement;
- begin
- if FCanvasID='' then
- FCanvasID:='my-canvas';
- if FResetID='' then
- FResetID:='btn-reset';
- FCanvas:=TJSHTMLCanvasElement(Document.getElementById(FCanvasID));
- FElScore:=TJSHTMLCanvasElement(Document.getElementById('score'));
- FElLevel:=TJSHTMLCanvasElement(Document.getElementById('level'));
- FElStatus:=TJSHTMLCanvasElement(Document.getElementById('status'));
- FBtnReset:=TJSHTMLButtonElement(Document.getElementById(FResetID));
- for I:=1 to ControlCount do
- begin
- El:=Document.GetElementById('control-'+ControlNames[i]);
- if Assigned(El) then
- TJSHTMLElement(El).onClick:=@DoMouseClick;
- end;
- if Assigned(FBtnReset) then
- FBtnReset.OnClick:=@DoResetClick;
- FCtx:=TJSCanvasRenderingContext2D(FCanvas.getContext('2d'));
- FCanvas.width := Round(FCanvas.OffsetWidth);
- FCanvas.height := Round(FCanvas.OffsetHeight);
- // ctx.scale(2, 2);
- ctx.fillStyle := 'white';
- ctx.fillRect(0, 0, canvas.width, canvas.height);
- ctx.strokeStyle := 'grey';
- ctx.strokeRect(8, 8, 280, 462);
- document.onkeydown:=@HandleKeyPress;
- end;
- procedure TTetris.DrawBlock;
- Var
- i,X,Y : Integer;
- begin
- for i:=0 to 3 do
- begin
- x:=FCurBlock[i].x + FCurrPos.X;
- y:=FCurBlock[i].y + FCurrPos.Y;
- DrawBlockAt(X,Y,FCurBlockColor);
- end;
- end;
- Function TTetris.MoveBlockLeftRight(isRight : Boolean) : Boolean;
- begin
- Result:=False;
- if isRight then
- Fdirection:=dRight
- else
- Fdirection:=dLEFT;
- if (HittingTheWall() or checkForHorizontalCollision(FDirection,FCurBlock)) then
- Exit;
- DeleteBlock();
- if isRight then
- Inc(FCurrPos.X)
- else
- Dec(FCurrPos.X);
- DrawBlock();
- Result:=True;
- end;
- function TTetris.HandleKeyPress(k: TJSKeyBoardEvent) : Boolean;
- Procedure DisableKey;
- begin
- k.cancelBubble:=True;
- k.preventDefault;
- end;
- begin
- Result:=False;
- if GameOver then
- exit;
- if (k.Code = TJSKeyNames.ArrowLeft) then
- begin
- DisableKey;
- Result:=not MoveBlockLeftRight(False)
- end
- else if (k.Code = TJSKeyNames.ArrowRight) then
- begin
- DisableKey;
- Result:=not MoveBlockLeftRight(True)
- end
- else if (k.Code = TJSKeyNames.ArrowDown) then
- begin
- DisableKey;
- MoveBlockDown();
- end
- else if (k.Code = TJSKeyNames.ArrowUp) then
- begin
- DisableKey;
- RotateBlock();
- end
- else if (k.Code = TJSKeyNames.Space) then
- begin
- DisableKey;
- DropBlock();
- end;
- end;
- constructor TTetris.Create(aOwner: TComponent);
- begin
- inherited Create(aOwner);
- CreateBlocks();
- CreateCoordArray();
- FLevel:=1;
- FScore:=0;
- FInterval:=1000;
- IncLevelScore:=100;
- end;
- function TTetris.MoveBlockDown: Boolean;
- Var
- i,x,y : Integer;
- coll : TVerticalCollision;
- Procedure ShiftBlockDown;
- begin
- DeleteBlock;
- Inc(FCurrPos.Y);
- DrawBlock;
- end;
- begin
- Result:=False;
- Fdirection:=dDOWN;
- Coll:=CheckForVerticalCollision(FDirection,FCurBlock);
- Result:=Coll=vcNone;
- if Result then
- ShiftBlockDown
- else
- begin
- if Coll<>vcWall then
- ShiftBlockDown;
- GameOver:=(FCurrPos.Y<=2);
- if Not GameOver then
- begin
- for I:=0 to BlockSize-1 do
- begin
- x:=FCurBlock[i].x + FCurrPos.X;
- y:=FCurBlock[i].y + FCurrPos.Y;
- FBoard[x,y]:=FCurBlockColor;
- end;
- CheckForCompletedRows();
- CreateBlock();
- FDirection:=dIdle;
- FCurrPos.X:=4;
- FCurrPos.Y:=0;
- DrawBlock();
- end;
- end;
- end;
- procedure TTetris.DropBlock;
- begin
- While MoveBlockDown do;
- end;
- function TTetris.HittingTheWall : Boolean;
- Var
- NewX,I : Integer;
- begin
- Result:=False;
- I:=0;
- While (I<BlockSize) and Not Result do
- begin
- newX:=FCurBlock[i].X + FCurrPos.X;
- Result:=((newX <= 0) and (Fdirection = dLEFT)) or
- ((newX >= 11) and (Fdirection = dRIGHT));
- Inc(I);
- end;
- end;
- procedure TTetris.DrawGameStatus;
- Var
- S : String;
- begin
- if FGameOver then
- S:=SGameOver
- else
- S:=SPlaying;
- FElStatus.InnerText:=S
- end;
- procedure TTetris.DrawScore;
- begin
- if Assigned(FElScore) then
- FElScore.InnerText:=IntToStr(FScore);
- end;
- procedure TTetris.DrawLevel;
- begin
- if Assigned(FElLevel) then
- FElLevel.InnerText:=IntToStr(Flevel);
- end;
- function TTetris.CheckForVerticalCollision(aDirection : TDirection; aBlock : TBlock): TVerticalCollision;
- Var
- X,Y,I : integer;
- begin
- Result:=vcNone;
- I:=0;
- While (I<BlockSize) and (Result=vcNone) do
- begin
- x:=aBlock[i].x + FCurrPos.X;
- y:=aBlock[i].y + FCurrPos.Y;
- if (aDirection = dDOWN) then
- inc(Y);
- if FBoard[x,y+1]>0 then
- Result:=vcBlock
- else if (Y>=20) then
- Result:=vcWall;
- inc(I);
- end;
- end;
- function TTetris.CheckForHorizontalCollision(aDirection: TDirection; aBlock: TBlock): Boolean;
- Var
- i, X,y : Integer;
- begin
- Result:=False;
- I:=0;
- While (I<BlockSize) and Not Result do
- begin
- x:=aBlock[i].x + FCurrPos.X;
- y:=aBlock[i].y + FCurrPos.Y;
- if (adirection = dLEFT) then
- Dec(x)
- else if (adirection = dRIGHT) then
- Inc(x);
- Result:=FBoard[x,y]>0;
- Inc(i);
- end;
- end;
- function TTetris.CheckForCompletedRows : Boolean;
- Var
- i,x,y,rowsToDelete, startOfDeletion: Integer;
- begin
- Result:=False;
- rowsToDelete:=0;
- startOfDeletion:=0;
- y:=0;
- While Y<BoardHeight do
- begin
- Result:=true;
- X:=0;
- While (X<BoardWidth) and Result do
- begin
- Result:=FBoard[X,Y]>0;
- Inc(X);
- end;
- if (Result) then
- begin
- if (StartOfDeletion = 0) then
- startOfDeletion:=y;
- Inc(rowsToDelete);
- for I:=0 to BoardWidth-1 do
- begin
- FBoard[i,y]:=0;
- DrawBlockAt(i,y,0);
- end
- end;
- Inc(Y);
- end;
- if (RowsToDelete > 0) then
- begin
- MoveAllRowsDown(rowsToDelete, startOfDeletion);
- RecalcScore(rowsToDelete);
- end;
- end;
- procedure TTetris.RecalcScore(aRows : integer);
- Var
- newLevel : Integer;
- begin
- Inc(FScore,10*aRows);
- DrawScore;
- // Check if we need to increase the level.
- // We cannot use = since score could go from 90 to 110 if 2 rows are deleted
- newLevel:=1+(FScore div FIncLevelScore);
- if (NewLevel>FLevel) then
- begin
- FLevel:=NewLevel;
- FInterval:=FInterval-FIncLevelInterval;
- EnableTick;
- end;
- end;
- procedure TTetris.SetLevel(AValue: Integer);
- begin
- if FLevel=AValue then Exit;
- FLevel:=AValue;
- DrawLevel;
- end;
- procedure TTetris.SetScore(AValue: Integer);
- begin
- if FScore=AValue then Exit;
- FScore:=AValue;
- DrawScore;
- end;
- procedure TTetris.DrawBlockAt(X,Y,Color : Integer);
- Var
- Coord : TCoordinate;
- begin
- coord:=coordinates[x,y];
- ctx.fillStyle:=BlockColors[Color];
- ctx.fillRect(coord.X, coord.Y, 21, 21);
- end;
- procedure TTetris.MoveAllRowsDown(rowsToDelete, startOfDeletion: Integer);
- Var
- I,x,y,Dest : Integer;
- begin
- for i:=StartOfDeletion - 1 downto 0 do
- for X:=0 to BoardWidth-1 do
- begin
- Y:=I+RowsToDelete;
- Dest:=FBoard[x,i];
- FBoard[x,y]:=Dest;
- DrawBlockAt(X,Y,Dest);
- FBoard[x,i]:=0;
- DrawBlockAt(X,I,0);
- end;
- end;
- procedure TTetris.DeleteBlock;
- var
- I,X,Y : integer;
- begin
- For I:=0 to BlockSize-1 do
- begin
- x:=FCurBlock[i].X + FCurrPos.X;
- y:=FCurBlock[i].Y + FCurrPos.Y;
- FBoard[x,y]:=0;
- DrawBlockAt(X,Y,0);
- end;
- end;
- procedure TTetris.CreateBlocks;
- function co (x,y : Integer) : TCoordinate;
- begin
- Result:=TCoordinate.Create(X,Y);
- end;
- begin
- FBlocks[0]:=[co(1,0), co(0,1), co(1,1), co(2,1)]; // T
- FBlocks[1]:=[co(0,0), co(1,0), co(2,0), co(3,0)]; // I
- FBlocks[2]:=[co(0,0), co(0,1), co(1,1), co(2,1)]; // J
- FBlocks[3]:=[co(0,0), co(1,0), co(0,1), co(1,1)]; // square
- FBlocks[4]:=[co(2,0), co(0,1), co(1,1), co(2,1)]; // L
- FBlocks[5]:=[co(1,0), co(2,0), co(0,1), co(1,1)]; // S
- FBlocks[6]:=[co(0,0), co(1,0), co(1,1), co(2,1)]; // Z
- end;
- procedure TTetris.CreateBlock;
- Var
- rnd : Integer;
- begin
- RND:=Random(BlockCount);
- FCurBlock:=FBlocks[RND];
- FCurBlockColor:=RND+1; // 0 is white
- FCurrPos.X:=CreatePosX;
- FCurrPos.Y:=CreatePosY;
- end;
- procedure TTetris.RotateBlock;
- Var
- lBlock,newBlock:TBlock;
- x,y,i,maxX : Integer;
- begin
- lBlock:=FCurBlock;
- maxX:=0;
- for I:=0 to BlockSize-1 do
- if lBlock[i].x>MaxX then
- MaxX:=lBlock[i].x;
- for I:=0 to BlockSize-1 do
- begin
- x:=lBlock[i].x;
- y:=lBlock[i].y;
- newBlock[i].X:=maxX-y;
- newBlock[i].Y:=x;
- end;
- // It can be that because of rotation, the block goes out of the board area or collisions.
- // In that case we forbid rotating
- // In fact we could try to reposition the block both horizontally and vertically:
- if (CheckForVerticalCollision(dIdle,NewBlock)=vcNone)
- and not CheckForHorizontalCollision(dIdle,NewBlock) then
- begin
- DeleteBlock();
- FCurBlock:=newBlock;
- DrawBlock();
- end;
- end;
- procedure TTetris.ClearBoard;
- Var
- X,Y : integer;
- begin
- For X:=0 to BoardWidth-1 do
- for Y:=0 to BoardHeight-1 do
- begin
- FBoard[X,Y]:=0;
- DrawBlockAt(X,Y,0);
- end;
- end;
- procedure TTetris.Start;
- begin
- GameOver:=False;
- Level:=1;
- Score:=0;
- SetupTetris;
- ClearBoard;
- CreateBlock();
- DrawBlock();
- EnableTick;
- end;
- function TTetris.DoMouseClick(aEvent: TJSMouseEvent): boolean;
- Const
- SControl = 'control-';
- Var
- S : String;
- begin
- Result:=true;
- S:=aEvent.currentTargetElement.ID;
- aEvent.preventDefault;
- if Copy(S,1,Length(SControl))=SControl then
- begin
- Delete(S,1,Length(sControl));
- Case S of
- 'left' : MoveBlockLeftRight(False);
- 'right' : MoveBlockLeftRight(True);
- 'down' : MoveBlockDown;
- 'rotate' : RotateBlock;
- 'drop' : DropBlock;
- end;
- end;
- end;
- function TTetris.DoResetClick(aEvent: TJSMouseEvent): boolean;
- begin
- Result:=True;
- FInterval:=1000;
- Start;
- end;
- procedure TTetris.SetGameOver(AValue: Boolean);
- begin
- if FGameOver=AValue then Exit;
- FGameOver:=AValue;
- DrawGameStatus;
- end;
- procedure TTetris.CheckBlockDown;
- begin
- If Not FGameOver then
- MoveBlockDown;
- end;
- procedure TTetris.EnableTick;
- begin
- if FMyInterval>0 then
- window.clearInterval(FMyInterval);
- FMyInterval:=window.setInterval(@CheckBlockDown,FInterval);
- end;
- end.
|