Range Error when trying to clear a full row in the middle and above from the grid

71 Views Asked by At

I am working on a Tetris game using Flutter and I am facing an issue. When I complete a row in the middle of the grid the app is crashing.

The error message on the console looks like this

======== Exception caught by widgets library =======================================================
The following RangeError was thrown building:
RangeError (index): Invalid value: Not in inclusive range 0..7: 8

When the exception was thrown, this was the stack: 
#0      List.[] (dart:core-patch/growable_array.dart:264:36)
#1      _GameBoardState.build.<anonymous closure> (package:tetris_game/tetris/board.dart:324:40)

Below is the dart file with the functions that are handling the logic of the game.

import 'dart:async';
import 'dart:math';

import 'package:assets_audio_player/assets_audio_player.dart';
import 'package:flutter/material.dart';
import 'package:vibration/vibration.dart';

import 'piece.dart';
import 'pixel.dart';
import 'values.dart';

/*

GAME BOARD
This is a 2x2 grid with null representing an empty space.
A non empty space will have the color to represent the landed pieces

*/

// create game board
List<List<Tetromino?>> gameBoard = List.generate(
    colLength,
    (i) => List.generate(
          rowLength,
          (j) => null,
        ));

class GameBoard extends StatefulWidget {
  const GameBoard({super.key});

  @override
  State<GameBoard> createState() => _GameBoardState();
}

class _GameBoardState extends State<GameBoard> {
  Piece currentPiece = Piece(type: Tetromino.L);

  // current score
  int currentScore = 0;

  // game over status
  bool gameOver = false;

  bool isPlaying = false;

  void initState() {
    super.initState();
    Future.delayed(const Duration(seconds: 3), () {
      setState(() {
        startGame();
      });
    });
  }

  void startGame() {
    currentPiece.initializePiece();

    // frame refresh rate
    Duration frameRate = const Duration(milliseconds: 500);
    gameLoop(frameRate);
  }

  // game loop
  void gameLoop(Duration frameRate) {
    Timer.periodic(frameRate, (timer) {
      setState(() {
        Vibration.hasVibrator()
            .then((value) => Vibration.vibrate(duration: 20));

        isPlaying = true;
        // clear lines
        clearLines();

        // check Landing
        checkLanding();

        // check if game is over
        if (gameOver == true) {
          timer.cancel();
          showGameOverDialog();
        }

        // move current piece down
        currentPiece.movePiece(Direction.down);
      });
    });
  }

  bool checkCollision(Direction direction) {
    // loop through each position of the current piece
    for (int i = 0; i < currentPiece.position.length; i++) {
      // calculate the row and column of the current position
      int row = (currentPiece.position[i] / rowLength).floor();
      int col = currentPiece.position[i] % rowLength;

      // adjust the row and col based on the direction
      if (direction == Direction.left) {
        col -= 1;
      } else if (direction == Direction.right) {
        col += 1;
      } else if (direction == Direction.down) {
        row += 1;
      }

      // check if the piece is out of bounds(either too low or too far to the left or right)
      if (row >= colLength || col < 0 || col >= rowLength) {
        return true;
      }

      // check for collision with existing blocks on the game board
      if (row >= 0 && col >= 0 && gameBoard[row][col] != null) {
        return true;
      }
    }
    // if no collision is detected
    return false;
  }

  void checkLanding() {
    // if going down is occupied or landed on other pieces
    if (checkCollision(Direction.down) || checkLanded()) {
      // mark position as occupied on the game board
      for (int i = 0; i < currentPiece.position.length; i++) {
        int row = (currentPiece.position[i] / rowLength).floor();
        int col = currentPiece.position[i] % rowLength;
        if (row >= 0 && col >= 0) {
          gameBoard[row][col] = currentPiece.type;
        }
      }

      // once landed, create the next piece
      createNewPiece();
    }
  }

  bool checkLanded() {
    // loop through each position of the current piece
    for (int i = 0; i < currentPiece.position.length; i++) {
      int row = (currentPiece.position[i] / rowLength).floor();
      int col = currentPiece.position[i] % rowLength;

      // check if the cell below is already occupied
      if (row + 1 < colLength && row >= 0 && gameBoard[row + 1][col] != null) {
        return true; // collision with a landed piece
      }
    }

    return false; // no collision with landed pieces
  }

  void createNewPiece() {
    // create a random object to generate random tetromino type
    Random rand = Random();

    // create a new piece with random type
    Tetromino randomType =
        Tetromino.values[rand.nextInt(Tetromino.values.length)];
    currentPiece = Piece(type: randomType);
    currentPiece.initializePiece();

    /*

    Since our game over condition is if there is a piece at the top level,
    you want to check if the game is over when you create a new piece
    instead of checking every frame, because new pieces are allowed to go through the top level
    but if there is already a piece in the top level when the new piece is created,
    then game is over

    */
    if (isGameOver()) {
      gameOver = true;
    }
  }

  // move left
  void moveLeft() {
    // make sure the move is valid before moving there
    if (!checkCollision(Direction.left)) {
      setState(() {
        currentPiece.movePiece(Direction.left);
        Vibration.hasVibrator()
            .then((value) => Vibration.vibrate(duration: 30));
      });
    }
  }

  // rotate piece
  void rotatePiece() {
    setState(() {
      currentPiece.rotatePiece();
      Vibration.hasVibrator().then((value) => Vibration.vibrate(duration: 30));
    });
  }

  // move right
  void moveRight() {
    // make sure the move is valid before moving there
    if (!checkCollision(Direction.right)) {
      setState(() {
        currentPiece.movePiece(Direction.right);
        Vibration.hasVibrator()
            .then((value) => Vibration.vibrate(duration: 30));
      });
    }
  }

  // clear lines
  void clearLines() {
    // step 1: Loop through each row of the game board from bottom to top
    for (int row = colLength - 1; row >= 0; row--) {
      // step 2: Initialize a variable to track if the row is full
      bool rowIsFull = true;
      // step 3: Check if the row if full (all columns in the row are filled with pieces)
      for (int col = 0; col < rowLength; col++) {
        // if there's an empty column, set rowlsFult to false and break the loop
        if (gameBoard[row][col] == null) {
          rowIsFull = false;
          break;
        }
      }

      // step 4: if the row is full, clear the row and shift rows down
      if (rowIsFull) {
        // step 5: move all rows above the cleared row down by one position
        for (int r = row; r > 0; r--) {
          // copy the above row to the current row
          gameBoard[r] = List.from(gameBoard[r - 1]);

          // step 6: set top row to empty
          gameBoard[0] = List.generate(row, (index) => null);
          AssetsAudioPlayer.playAndForget(Audio('assets/audio/laser.mp3'));
        }
        // step 7: Increase the score
        currentScore++;
      }
    }
  }

  bool isGameOver() {
    // Check if all columns in the top row are filled
    for (int col = 0; col < rowLength; col++) {
      if (gameBoard[0][col] != null) {
        return true; // If any block is empty, game is not over
      }
    }
    return false; // All blocks in the top row are filled
  }

  // game over message
  void showGameOverDialog() {
    showDialog(
      barrierDismissible: false,
      context: context,
      builder: (context) => AlertDialog(
        backgroundColor: Colors.black,
        titleTextStyle: TextStyle(fontFamily: 'Pixel', color: Colors.white),
        contentTextStyle: TextStyle(fontFamily: 'Pixel', color: Colors.white),
        title: const Text('Game Over!'),
        content: Text('You Scored: $currentScore'),
        actions: [
          TextButton(
            onPressed: () {
              // reset the game
              resetGame();
              Navigator.pop(context);
            },
            child: const Text(
              'Restart',
              style: TextStyle(fontFamily: 'Pixel', color: Colors.amber),
            ),
          )
        ],
      ),
    );
  }

  // reset game
  void resetGame() {
    // clear the game board
    gameBoard =
        List.generate(colLength, (i) => List.generate(rowLength, (j) => null));

    // new game
    gameOver = false;
    currentScore = 0;

    // create new piece
    createNewPiece();

    // Start game again
    startGame();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.black,
      body: Column(
        mainAxisAlignment: MainAxisAlignment.end,
        children: [
          // GAME
          Expanded(
            child: GridView.builder(
              itemCount: rowLength * colLength,
              physics: const NeverScrollableScrollPhysics(),
              gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
                  crossAxisCount: rowLength),
              itemBuilder: (context, index) {
                // get row and col of each index
                int row = (index / rowLength).floor();
                int col = index % rowLength;
                // current piece
                if (currentPiece.position.contains(index)) {
                  return Pixel(color: currentPiece.color);
                }
                // landed pieces
                else if (gameBoard[row][col] != null) {
                  final Tetromino? tetrominoType = gameBoard[row][col];
                  return Pixel(color: tetrominoColors[tetrominoType]);
                }

                // blank pixel
                else {
                  return Pixel(color: Colors.grey[900]);
                }
              },
            ),
          ),
          Text(
            'Score: $currentScore',
            style: TextStyle(
                color: Colors.white, fontSize: 20, fontFamily: 'Orbitron'),
          ),
          // GAME CONTROLS
          Padding(
            padding: const EdgeInsets.only(bottom: 50, top: 50),
            child: Row(
              mainAxisAlignment: MainAxisAlignment.spaceEvenly,
              children: [
                // TextButton(
                //   onPressed: isPlaying ? null : startGame,
                //   child: Text(
                //     isPlaying ? 'Playing' : 'Play',
                //     style: const TextStyle(color: Colors.amber),
                //   ),
                // ),
                // left
                IconButton(
                  onPressed: moveLeft,
                  icon: Icon(
                    Icons.arrow_left,
                    color: Colors.amber,
                    size: 50,
                  ),
                ),
                // rotate
                IconButton(
                  onPressed: rotatePiece,
                  icon: Icon(
                    Icons.rotate_right,
                    color: Colors.amber,
                    size: 50,
                  ),
                ),
                // right
                IconButton(
                  onPressed: moveRight,
                  icon: Icon(
                    Icons.arrow_right,
                    color: Colors.amber,
                    size: 50,
                  ),
                ),
              ],
            ),
          )
        ],
      ),
    );
  }
}

Maybe am missing something in the logic where I clear the lines and I have to move the remaining blocks down I don't know. Thank in advance for any help

0

There are 0 best solutions below