@@ -246,27 +246,95 @@ void main() {
246246 expect (expectedGhostY, greaterThanOrEqualTo (initialY));
247247 });
248248
249- test ('should update score and level when lines are cleared ' , () {
249+ test ('dropPiece should be blocked during grace period ' , () {
250250 gameLogic.startGame ();
251251
252- // Fill bottom row except one column
253- for (int col = 0 ; col < GameConstants .boardWidth - 1 ; col++ ) {
254- gameLogic.board[GameConstants .boardHeight +
255- GameConstants .previewRows -
256- 1 ][col] = Colors .red;
257- }
252+ // Grace period is always active immediately after a piece spawns
253+ expect (gameLogic.isNewPieceGracePeriod, true );
254+
255+ gameLogic.dropPiece (); // Should return early without placing anything
256+
257+ // The board should still be empty — no blocks placed
258+ bool boardHasPlacedBlocks =
259+ gameLogic.board.any ((row) => row.any ((cell) => cell != null ));
260+ expect (boardHasPlacedBlocks, false ,
261+ reason:
262+ 'Hard drop during grace period must not place the piece on the board' );
263+ });
264+
265+ test ('should start line clear animation when a full row is detected' , () {
266+ gameLogic.startGame ();
258267
259- // Place a piece to complete the line
260- gameLogic.board[GameConstants .boardHeight + GameConstants .previewRows - 1 ]
261- [GameConstants .boardWidth - 1 ] = Colors .blue;
268+ // Fill the bottom row completely
269+ final bottomRow =
270+ GameConstants .boardHeight + GameConstants .previewRows - 1 ;
271+ for (int col = 0 ; col < GameConstants .boardWidth; col++ ) {
272+ gameLogic.board[bottomRow][col] = Colors .red;
273+ }
262274
263275 gameLogic.clearLines ();
264276
265- // Wait for animation to complete
266277 expect (gameLogic.clearingLines.length, 1 );
267278 expect (gameLogic.isAnimatingClear, true );
268279 });
269280
281+ test ('should update score correctly after single line clear' , () async {
282+ gameLogic.startGame ();
283+
284+ final bottomRow =
285+ GameConstants .boardHeight + GameConstants .previewRows - 1 ;
286+ for (int col = 0 ; col < GameConstants .boardWidth; col++ ) {
287+ gameLogic.board[bottomRow][col] = Colors .red;
288+ }
289+
290+ gameLogic.clearLines ();
291+ // Wait for the 350 ms clear animation timer to fire
292+ await Future .delayed (const Duration (milliseconds: 400 ));
293+
294+ expect (gameLogic.score, GameConstants .lineClearScores[1 ] * 1 ); // 100
295+ expect (gameLogic.linesCleared, 1 );
296+ expect (gameLogic.isAnimatingClear, false );
297+ });
298+
299+ test ('should award full TETRIS bonus for clearing 4 lines at once' ,
300+ () async {
301+ gameLogic.startGame ();
302+
303+ final totalRows = GameConstants .boardHeight + GameConstants .previewRows;
304+ for (int row = totalRows - 4 ; row < totalRows; row++ ) {
305+ for (int col = 0 ; col < GameConstants .boardWidth; col++ ) {
306+ gameLogic.board[row][col] = Colors .red;
307+ }
308+ }
309+
310+ gameLogic.clearLines ();
311+ await Future .delayed (const Duration (milliseconds: 400 ));
312+
313+ expect (gameLogic.score, GameConstants .lineClearScores[4 ] * 1 ); // 800
314+ expect (gameLogic.linesCleared, 4 );
315+ expect (gameLogic.clearBonusLabel, 'TETRIS!' );
316+ });
317+
318+ test ('should increase level after clearing linesPerLevel lines' , () async {
319+ gameLogic.startGame ();
320+
321+ // Fill exactly linesPerLevel full rows
322+ final totalRows = GameConstants .boardHeight + GameConstants .previewRows;
323+ for (int row = totalRows - GameConstants .linesPerLevel;
324+ row < totalRows;
325+ row++ ) {
326+ for (int col = 0 ; col < GameConstants .boardWidth; col++ ) {
327+ gameLogic.board[row][col] = Colors .red;
328+ }
329+ }
330+
331+ gameLogic.clearLines ();
332+ await Future .delayed (const Duration (milliseconds: 400 ));
333+
334+ expect (gameLogic.linesCleared, GameConstants .linesPerLevel);
335+ expect (gameLogic.level, 2 );
336+ });
337+
270338 test ('should detect game over correctly' , () {
271339 gameLogic.startGame ();
272340
@@ -286,6 +354,90 @@ void main() {
286354 expect (gameLogic.isGameRunning, false );
287355 });
288356
357+ test ('should pause and resume game correctly' , () {
358+ gameLogic.startGame ();
359+ expect (gameLogic.isPaused, false );
360+ expect (gameLogic.isGameRunning, true );
361+
362+ gameLogic.pauseGame ();
363+ expect (gameLogic.isPaused, true );
364+ expect (gameLogic.isGameRunning, true ); // still running, just paused
365+ expect (gameLogic.isGameOver, false );
366+
367+ gameLogic.resumeGame ();
368+ expect (gameLogic.isPaused, false );
369+ expect (gameLogic.isGameRunning, true );
370+ });
371+
372+ test ('pauseGame should be idempotent' , () {
373+ gameLogic.startGame ();
374+ gameLogic.pauseGame ();
375+ gameLogic.pauseGame (); // second call should be a no-op
376+ expect (gameLogic.isPaused, true );
377+ });
378+
379+ test ('should receive garbage rows from opponent' , () {
380+ gameLogic.startGame ();
381+
382+ final totalRows = GameConstants .boardHeight + GameConstants .previewRows;
383+ // Board bottom should be empty initially
384+ expect (gameLogic.board[totalRows - 1 ][0 ], isNull);
385+
386+ gameLogic.receiveGarbage (2 );
387+
388+ // Count filled vs gap cells in the bottom 2 rows
389+ int filledCells = 0 ;
390+ int gapCells = 0 ;
391+ for (int row = totalRows - 2 ; row < totalRows; row++ ) {
392+ for (int col = 0 ; col < GameConstants .boardWidth; col++ ) {
393+ if (gameLogic.board[row][col] != null ) {
394+ filledCells++ ;
395+ } else {
396+ gapCells++ ;
397+ }
398+ }
399+ }
400+
401+ // Each garbage row has exactly one gap column
402+ expect (filledCells, (GameConstants .boardWidth - 1 ) * 2 );
403+ expect (gapCells, 2 );
404+ });
405+
406+ test ('receiveGarbage should be a no-op when lines <= 0' , () {
407+ gameLogic.startGame ();
408+ final boardBefore =
409+ gameLogic.board.map ((row) => List <Color ?>.from (row)).toList ();
410+
411+ gameLogic.receiveGarbage (0 );
412+
413+ for (int row = 0 ; row < gameLogic.board.length; row++ ) {
414+ expect (gameLogic.board[row], equals (boardBefore[row]));
415+ }
416+ });
417+
418+ test ('should export board snapshot with correct dimensions' , () {
419+ gameLogic.startGame ();
420+
421+ final snapshot = gameLogic.exportBoardSnapshot ();
422+
423+ // Snapshot covers only the visible board (no preview rows)
424+ expect (
425+ snapshot.length,
426+ GameConstants .boardWidth * GameConstants .boardHeight,
427+ );
428+ // All values must be valid palette indices
429+ expect (snapshot.every ((cell) => cell >= 0 && cell <= 8 ), true );
430+ });
431+
432+ test ('exportBoardSnapshot should include the current piece' , () {
433+ gameLogic.startGame ();
434+
435+ final snapshot = gameLogic.exportBoardSnapshot ();
436+
437+ // Current piece should appear somewhere in the snapshot as a non-zero index
438+ expect (snapshot.any ((cell) => cell > 0 ), true );
439+ });
440+
289441 test ('should get board with current piece correctly' , () {
290442 gameLogic.startGame ();
291443
0 commit comments