pas2js/demo/tetris/utetris.pp
2020-11-15 11:26:08 +00:00

676 lines
14 KiB
ObjectPascal

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.