utetris.pp 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675
  1. unit utetris;
  2. {$mode objfpc}
  3. interface
  4. uses
  5. Classes, SysUtils, Web;
  6. Const
  7. SGameOver = 'Game over!';
  8. SPlaying = 'Playing...';
  9. BlockCount = 7;
  10. BlockHigh = BlockCount-1;
  11. BlockSize = 4; // Number of positions in a block
  12. BoardHeight = 20;
  13. BoardWidth = 12;
  14. CreatePosX = 4;
  15. CreatePosY = 0;
  16. BlockColors : Array [0..BlockCount] of String
  17. = ('white','#8F3985', '#39A275', '#D28140', '#194A8D', '#8D71B4', '#F0889D', '#DF1C44');
  18. Type
  19. TDirection = (dIdle, dDown, dLeft, dRight);
  20. TVerticalCollision = (vcNone,vcBlock,vcWall);
  21. {$modeswitch advancedrecords}
  22. TCoordinate = record
  23. x,y : Integer;
  24. Class function Create(aX,aY : integer) : TCoordinate; static;
  25. end;
  26. TBlock = Array[0..BlockSize-1] of TCoordinate;
  27. TBlocks = Array[0..BlockHigh] of TBlock;
  28. TBoard = Array[0..BoardWidth-1,0..BoardHeight-1] of Integer; // Colors
  29. TCoordinateBoard = Array[0..BoardWidth-1,0..BoardHeight-1] of TCoordinate; // Coordinates of squares
  30. { TTetris }
  31. TTetris = Class(TComponent)
  32. private
  33. function DoMouseClick(aEvent: TJSMouseEvent): boolean;
  34. function MoveBlockLeftRight(isRight: Boolean): Boolean;
  35. Private
  36. FCanvasID: String;
  37. FGameOver : Boolean;
  38. FCoordinates : TCoordinateBoard;
  39. FIncLevelInterval: Integer;
  40. FIncLevelScore: Integer;
  41. FInterval: Integer;
  42. FResetID: String;
  43. FTetrisLogo : TJSHTMLImageElement;
  44. FCanvas : TJSHTMLCanvasElement;
  45. FCtx : TJSCanvasRenderingContext2D;
  46. FScore : Integer;
  47. FLevel : Integer;
  48. FBoard : TBoard;
  49. FBlocks : TBlocks;
  50. FCurBlock : TBlock;
  51. FCurBlockColor : Smallint; // Index in color array
  52. FCurrPos : TCoordinate;
  53. Fdirection : TDirection;
  54. FElScore : TJSHTMLElement;
  55. FElLevel : TJSHTMLElement;
  56. FElStatus : TJSHTMLElement;
  57. FBtnReset : TJSHTMLButtonElement;
  58. FMyInterval : NativeInt;
  59. function DoResetClick(aEvent: TJSMouseEvent): boolean;
  60. procedure SetGameOver(AValue: Boolean);
  61. Procedure CheckBlockDown;
  62. procedure DrawBlockAt(X, Y, Color: Integer);
  63. procedure DrawLevel;
  64. Procedure DrawScore;
  65. procedure DrawGameStatus;
  66. procedure EnableTick;
  67. Function HittingTheWall : Boolean;
  68. Procedure MoveAllRowsDown(rowsToDelete, startOfDeletion : Integer);
  69. function CheckForVerticalCollision(aDirection: TDirection; aBlock: TBlock): TVerticalCollision;
  70. Function CheckForHorizontalCollision (aDirection: TDirection; aBlock: TBlock): Boolean;
  71. function CheckForCompletedRows : Boolean;
  72. Procedure CreateCoordArray;
  73. procedure RecalcScore(aRows: integer);
  74. procedure SetLevel(AValue: Integer);
  75. procedure SetScore(AValue: Integer);
  76. Procedure SetupTetris;
  77. Procedure DrawBlock;
  78. Procedure CreateBlocks;
  79. Procedure CreateBlock;
  80. Procedure DeleteBlock;
  81. function MoveBlockDown: Boolean;
  82. Procedure DropBlock;
  83. Procedure RotateBlock;
  84. Procedure ClearBoard;
  85. function HandleKeyPress(k : TJSKeyBoardEvent) : Boolean;
  86. Property GameOver : Boolean Read FGameOver Write SetGameOver;
  87. Public
  88. Constructor Create(aOwner : TComponent); override;
  89. Procedure Start;
  90. // Reset button ID
  91. Property ResetID : String Read FResetID Write FResetID;
  92. // our canvas ID
  93. Property CanvasID : String Read FCanvasID Write FCanvasID;
  94. Property Canvas : TJSHTMLCanvasElement Read FCanvas;
  95. Property Ctx : TJSCanvasRenderingContext2D Read FCTX;
  96. Property Score : Integer Read FScore Write SetScore;
  97. Property Level : Integer Read FLevel Write SetLevel;
  98. Property Board : TBoard Read FBoard Write FBoard;
  99. Property Stopped : TBoard Read FBoard Write FBoard;
  100. Property Blocks : TBlocks Read FBlocks;
  101. Property Coordinates : TCoordinateBoard Read FCoordinates;
  102. Property Interval : Integer Read FInterval Write FInterval;
  103. Property IncLevelScore : Integer Read FIncLevelScore Write FIncLevelScore;
  104. Property IncLevelInterval : Integer read FIncLevelInterval write FIncLevelInterval;
  105. end;
  106. implementation
  107. Class function TCoordinate.Create(aX,aY : integer) : TCoordinate;
  108. begin
  109. Result.X:=aX;
  110. Result.Y:=aY;
  111. end;
  112. procedure TTetris.CreateCoordArray;
  113. Const
  114. XStart = 11;
  115. XStep = 23;
  116. YStart = 9;
  117. YStep = 23;
  118. Var
  119. x,y,i,j : Integer;
  120. begin
  121. i:=0;
  122. j:=0;
  123. X:=XStart;
  124. For I:=0 to BoardWidth-1 do
  125. begin
  126. Y:=YStart;
  127. For J:=0 to BoardHeight-1 do
  128. begin
  129. FCoordinates[I,J]:=TCoordinate.Create(X,Y);
  130. Y:=Y+YStep;
  131. end;
  132. X:=X+XStep;
  133. end;
  134. end;
  135. Const
  136. ControlCount = 5;
  137. ControlNames : Array[1..5] of string = ('left','right','down','rotate','drop');
  138. procedure TTetris.SetupTetris;
  139. Var
  140. i : Integer;
  141. el : TJSElement;
  142. begin
  143. if FCanvasID='' then
  144. FCanvasID:='my-canvas';
  145. if FResetID='' then
  146. FResetID:='btn-reset';
  147. FCanvas:=TJSHTMLCanvasElement(Document.getElementById(FCanvasID));
  148. FElScore:=TJSHTMLCanvasElement(Document.getElementById('score'));
  149. FElLevel:=TJSHTMLCanvasElement(Document.getElementById('level'));
  150. FElStatus:=TJSHTMLCanvasElement(Document.getElementById('status'));
  151. FBtnReset:=TJSHTMLButtonElement(Document.getElementById(FResetID));
  152. for I:=1 to ControlCount do
  153. begin
  154. El:=Document.GetElementById('control-'+ControlNames[i]);
  155. if Assigned(El) then
  156. TJSHTMLElement(El).onClick:=@DoMouseClick;
  157. end;
  158. if Assigned(FBtnReset) then
  159. FBtnReset.OnClick:=@DoResetClick;
  160. FCtx:=TJSCanvasRenderingContext2D(FCanvas.getContext('2d'));
  161. FCanvas.width := Round(FCanvas.OffsetWidth);
  162. FCanvas.height := Round(FCanvas.OffsetHeight);
  163. // ctx.scale(2, 2);
  164. ctx.fillStyle := 'white';
  165. ctx.fillRect(0, 0, canvas.width, canvas.height);
  166. ctx.strokeStyle := 'grey';
  167. ctx.strokeRect(8, 8, 280, 462);
  168. document.onkeydown:=@HandleKeyPress;
  169. end;
  170. procedure TTetris.DrawBlock;
  171. Var
  172. i,X,Y : Integer;
  173. begin
  174. for i:=0 to 3 do
  175. begin
  176. x:=FCurBlock[i].x + FCurrPos.X;
  177. y:=FCurBlock[i].y + FCurrPos.Y;
  178. DrawBlockAt(X,Y,FCurBlockColor);
  179. end;
  180. end;
  181. Function TTetris.MoveBlockLeftRight(isRight : Boolean) : Boolean;
  182. begin
  183. Result:=False;
  184. if isRight then
  185. Fdirection:=dRight
  186. else
  187. Fdirection:=dLEFT;
  188. if (HittingTheWall() or checkForHorizontalCollision(FDirection,FCurBlock)) then
  189. Exit;
  190. DeleteBlock();
  191. if isRight then
  192. Inc(FCurrPos.X)
  193. else
  194. Dec(FCurrPos.X);
  195. DrawBlock();
  196. Result:=True;
  197. end;
  198. function TTetris.HandleKeyPress(k: TJSKeyBoardEvent) : Boolean;
  199. Procedure DisableKey;
  200. begin
  201. k.cancelBubble:=True;
  202. k.preventDefault;
  203. end;
  204. begin
  205. Result:=False;
  206. if GameOver then
  207. exit;
  208. if (k.Code = TJSKeyNames.ArrowLeft) then
  209. begin
  210. DisableKey;
  211. Result:=not MoveBlockLeftRight(False)
  212. end
  213. else if (k.Code = TJSKeyNames.ArrowRight) then
  214. begin
  215. DisableKey;
  216. Result:=not MoveBlockLeftRight(True)
  217. end
  218. else if (k.Code = TJSKeyNames.ArrowDown) then
  219. begin
  220. DisableKey;
  221. MoveBlockDown();
  222. end
  223. else if (k.Code = TJSKeyNames.ArrowUp) then
  224. begin
  225. DisableKey;
  226. RotateBlock();
  227. end
  228. else if (k.Code = TJSKeyNames.Space) then
  229. begin
  230. DisableKey;
  231. DropBlock();
  232. end;
  233. end;
  234. constructor TTetris.Create(aOwner: TComponent);
  235. begin
  236. inherited Create(aOwner);
  237. CreateBlocks();
  238. CreateCoordArray();
  239. FLevel:=1;
  240. FScore:=0;
  241. FInterval:=1000;
  242. IncLevelScore:=100;
  243. end;
  244. function TTetris.MoveBlockDown: Boolean;
  245. Var
  246. i,x,y : Integer;
  247. coll : TVerticalCollision;
  248. Procedure ShiftBlockDown;
  249. begin
  250. DeleteBlock;
  251. Inc(FCurrPos.Y);
  252. DrawBlock;
  253. end;
  254. begin
  255. Result:=False;
  256. Fdirection:=dDOWN;
  257. Coll:=CheckForVerticalCollision(FDirection,FCurBlock);
  258. Result:=Coll=vcNone;
  259. if Result then
  260. ShiftBlockDown
  261. else
  262. begin
  263. if Coll<>vcWall then
  264. ShiftBlockDown;
  265. GameOver:=(FCurrPos.Y<=2);
  266. if Not GameOver then
  267. begin
  268. for I:=0 to BlockSize-1 do
  269. begin
  270. x:=FCurBlock[i].x + FCurrPos.X;
  271. y:=FCurBlock[i].y + FCurrPos.Y;
  272. FBoard[x,y]:=FCurBlockColor;
  273. end;
  274. CheckForCompletedRows();
  275. CreateBlock();
  276. FDirection:=dIdle;
  277. FCurrPos.X:=4;
  278. FCurrPos.Y:=0;
  279. DrawBlock();
  280. end;
  281. end;
  282. end;
  283. procedure TTetris.DropBlock;
  284. begin
  285. While MoveBlockDown do;
  286. end;
  287. function TTetris.HittingTheWall : Boolean;
  288. Var
  289. NewX,I : Integer;
  290. begin
  291. Result:=False;
  292. I:=0;
  293. While (I<BlockSize) and Not Result do
  294. begin
  295. newX:=FCurBlock[i].X + FCurrPos.X;
  296. Result:=((newX <= 0) and (Fdirection = dLEFT)) or
  297. ((newX >= 11) and (Fdirection = dRIGHT));
  298. Inc(I);
  299. end;
  300. end;
  301. procedure TTetris.DrawGameStatus;
  302. Var
  303. S : String;
  304. begin
  305. if FGameOver then
  306. S:=SGameOver
  307. else
  308. S:=SPlaying;
  309. FElStatus.InnerText:=S
  310. end;
  311. procedure TTetris.DrawScore;
  312. begin
  313. if Assigned(FElScore) then
  314. FElScore.InnerText:=IntToStr(FScore);
  315. end;
  316. procedure TTetris.DrawLevel;
  317. begin
  318. if Assigned(FElLevel) then
  319. FElLevel.InnerText:=IntToStr(Flevel);
  320. end;
  321. function TTetris.CheckForVerticalCollision(aDirection : TDirection; aBlock : TBlock): TVerticalCollision;
  322. Var
  323. X,Y,I : integer;
  324. begin
  325. Result:=vcNone;
  326. I:=0;
  327. While (I<BlockSize) and (Result=vcNone) do
  328. begin
  329. x:=aBlock[i].x + FCurrPos.X;
  330. y:=aBlock[i].y + FCurrPos.Y;
  331. if (aDirection = dDOWN) then
  332. inc(Y);
  333. if FBoard[x,y+1]>0 then
  334. Result:=vcBlock
  335. else if (Y>=20) then
  336. Result:=vcWall;
  337. inc(I);
  338. end;
  339. end;
  340. function TTetris.CheckForHorizontalCollision(aDirection: TDirection; aBlock: TBlock): Boolean;
  341. Var
  342. i, X,y : Integer;
  343. begin
  344. Result:=False;
  345. I:=0;
  346. While (I<BlockSize) and Not Result do
  347. begin
  348. x:=aBlock[i].x + FCurrPos.X;
  349. y:=aBlock[i].y + FCurrPos.Y;
  350. if (adirection = dLEFT) then
  351. Dec(x)
  352. else if (adirection = dRIGHT) then
  353. Inc(x);
  354. Result:=FBoard[x,y]>0;
  355. Inc(i);
  356. end;
  357. end;
  358. function TTetris.CheckForCompletedRows : Boolean;
  359. Var
  360. i,x,y,rowsToDelete, startOfDeletion: Integer;
  361. begin
  362. Result:=False;
  363. rowsToDelete:=0;
  364. startOfDeletion:=0;
  365. y:=0;
  366. While Y<BoardHeight do
  367. begin
  368. Result:=true;
  369. X:=0;
  370. While (X<BoardWidth) and Result do
  371. begin
  372. Result:=FBoard[X,Y]>0;
  373. Inc(X);
  374. end;
  375. if (Result) then
  376. begin
  377. if (StartOfDeletion = 0) then
  378. startOfDeletion:=y;
  379. Inc(rowsToDelete);
  380. for I:=0 to BoardWidth-1 do
  381. begin
  382. FBoard[i,y]:=0;
  383. DrawBlockAt(i,y,0);
  384. end
  385. end;
  386. Inc(Y);
  387. end;
  388. if (RowsToDelete > 0) then
  389. begin
  390. MoveAllRowsDown(rowsToDelete, startOfDeletion);
  391. RecalcScore(rowsToDelete);
  392. end;
  393. end;
  394. procedure TTetris.RecalcScore(aRows : integer);
  395. Var
  396. newLevel : Integer;
  397. begin
  398. Inc(FScore,10*aRows);
  399. DrawScore;
  400. // Check if we need to increase the level.
  401. // We cannot use = since score could go from 90 to 110 if 2 rows are deleted
  402. newLevel:=1+(FScore div FIncLevelScore);
  403. if (NewLevel>FLevel) then
  404. begin
  405. FLevel:=NewLevel;
  406. FInterval:=FInterval-FIncLevelInterval;
  407. EnableTick;
  408. end;
  409. end;
  410. procedure TTetris.SetLevel(AValue: Integer);
  411. begin
  412. if FLevel=AValue then Exit;
  413. FLevel:=AValue;
  414. DrawLevel;
  415. end;
  416. procedure TTetris.SetScore(AValue: Integer);
  417. begin
  418. if FScore=AValue then Exit;
  419. FScore:=AValue;
  420. DrawScore;
  421. end;
  422. procedure TTetris.DrawBlockAt(X,Y,Color : Integer);
  423. Var
  424. Coord : TCoordinate;
  425. begin
  426. coord:=coordinates[x,y];
  427. ctx.fillStyle:=BlockColors[Color];
  428. ctx.fillRect(coord.X, coord.Y, 21, 21);
  429. end;
  430. procedure TTetris.MoveAllRowsDown(rowsToDelete, startOfDeletion: Integer);
  431. Var
  432. I,x,y,Dest : Integer;
  433. begin
  434. for i:=StartOfDeletion - 1 downto 0 do
  435. for X:=0 to BoardWidth-1 do
  436. begin
  437. Y:=I+RowsToDelete;
  438. Dest:=FBoard[x,i];
  439. FBoard[x,y]:=Dest;
  440. DrawBlockAt(X,Y,Dest);
  441. FBoard[x,i]:=0;
  442. DrawBlockAt(X,I,0);
  443. end;
  444. end;
  445. procedure TTetris.DeleteBlock;
  446. var
  447. I,X,Y : integer;
  448. begin
  449. For I:=0 to BlockSize-1 do
  450. begin
  451. x:=FCurBlock[i].X + FCurrPos.X;
  452. y:=FCurBlock[i].Y + FCurrPos.Y;
  453. FBoard[x,y]:=0;
  454. DrawBlockAt(X,Y,0);
  455. end;
  456. end;
  457. procedure TTetris.CreateBlocks;
  458. function co (x,y : Integer) : TCoordinate;
  459. begin
  460. Result:=TCoordinate.Create(X,Y);
  461. end;
  462. begin
  463. FBlocks[0]:=[co(1,0), co(0,1), co(1,1), co(2,1)]; // T
  464. FBlocks[1]:=[co(0,0), co(1,0), co(2,0), co(3,0)]; // I
  465. FBlocks[2]:=[co(0,0), co(0,1), co(1,1), co(2,1)]; // J
  466. FBlocks[3]:=[co(0,0), co(1,0), co(0,1), co(1,1)]; // square
  467. FBlocks[4]:=[co(2,0), co(0,1), co(1,1), co(2,1)]; // L
  468. FBlocks[5]:=[co(1,0), co(2,0), co(0,1), co(1,1)]; // S
  469. FBlocks[6]:=[co(0,0), co(1,0), co(1,1), co(2,1)]; // Z
  470. end;
  471. procedure TTetris.CreateBlock;
  472. Var
  473. rnd : Integer;
  474. begin
  475. RND:=Random(BlockCount);
  476. FCurBlock:=FBlocks[RND];
  477. FCurBlockColor:=RND+1; // 0 is white
  478. FCurrPos.X:=CreatePosX;
  479. FCurrPos.Y:=CreatePosY;
  480. end;
  481. procedure TTetris.RotateBlock;
  482. Var
  483. lBlock,newBlock:TBlock;
  484. x,y,i,maxX : Integer;
  485. begin
  486. lBlock:=FCurBlock;
  487. maxX:=0;
  488. for I:=0 to BlockSize-1 do
  489. if lBlock[i].x>MaxX then
  490. MaxX:=lBlock[i].x;
  491. for I:=0 to BlockSize-1 do
  492. begin
  493. x:=lBlock[i].x;
  494. y:=lBlock[i].y;
  495. newBlock[i].X:=maxX-y;
  496. newBlock[i].Y:=x;
  497. end;
  498. // It can be that because of rotation, the block goes out of the board area or collisions.
  499. // In that case we forbid rotating
  500. // In fact we could try to reposition the block both horizontally and vertically:
  501. if (CheckForVerticalCollision(dIdle,NewBlock)=vcNone)
  502. and not CheckForHorizontalCollision(dIdle,NewBlock) then
  503. begin
  504. DeleteBlock();
  505. FCurBlock:=newBlock;
  506. DrawBlock();
  507. end;
  508. end;
  509. procedure TTetris.ClearBoard;
  510. Var
  511. X,Y : integer;
  512. begin
  513. For X:=0 to BoardWidth-1 do
  514. for Y:=0 to BoardHeight-1 do
  515. begin
  516. FBoard[X,Y]:=0;
  517. DrawBlockAt(X,Y,0);
  518. end;
  519. end;
  520. procedure TTetris.Start;
  521. begin
  522. GameOver:=False;
  523. Level:=1;
  524. Score:=0;
  525. SetupTetris;
  526. ClearBoard;
  527. CreateBlock();
  528. DrawBlock();
  529. EnableTick;
  530. end;
  531. function TTetris.DoMouseClick(aEvent: TJSMouseEvent): boolean;
  532. Const
  533. SControl = 'control-';
  534. Var
  535. S : String;
  536. begin
  537. Result:=true;
  538. S:=aEvent.currentTargetElement.ID;
  539. aEvent.preventDefault;
  540. if Copy(S,1,Length(SControl))=SControl then
  541. begin
  542. Delete(S,1,Length(sControl));
  543. Case S of
  544. 'left' : MoveBlockLeftRight(False);
  545. 'right' : MoveBlockLeftRight(True);
  546. 'down' : MoveBlockDown;
  547. 'rotate' : RotateBlock;
  548. 'drop' : DropBlock;
  549. end;
  550. end;
  551. end;
  552. function TTetris.DoResetClick(aEvent: TJSMouseEvent): boolean;
  553. begin
  554. Result:=True;
  555. FInterval:=1000;
  556. Start;
  557. end;
  558. procedure TTetris.SetGameOver(AValue: Boolean);
  559. begin
  560. if FGameOver=AValue then Exit;
  561. FGameOver:=AValue;
  562. DrawGameStatus;
  563. end;
  564. procedure TTetris.CheckBlockDown;
  565. begin
  566. If Not FGameOver then
  567. MoveBlockDown;
  568. end;
  569. procedure TTetris.EnableTick;
  570. begin
  571. if FMyInterval>0 then
  572. window.clearInterval(FMyInterval);
  573. FMyInterval:=window.setInterval(@CheckBlockDown,FInterval);
  574. end;
  575. end.