Пишем Тетрис на Flutter

Дело было вечером, делать было нечего (с)..В качестве разминки решил в кои-то веки написать игру. Пришла на ум идея написать давнюю мечту — тетрис. В т.ч. использовать будем более ранние наработки по работе с Hive и Requets оформленные в виде отдельных классов.

Из интересного:

1) Для отображения стакана и фигур используем три массива:

LandMatrix — массив вида ширина/высота — фактически «стакан» с «приземленными» фигурами. Точка пересечения массива — цвет квадратика. Если пусто — значит в точке стакана ничего нет

MoveFig — массив движущейся по стакану фигуры. В момент совпадения низа фигуры с любой не пустой точкой стакана фигура переносится в «стакан»

ShadowRotareFig — вспомогательный массив, используемый для поворота фигуры. В момент когда фигуру необходимо повернуть, она переносится из MoveFig в этот массив, поворачивается, и переносится обратно в MoveFig. Создание отдельного массива для поворота было обусловлено способом хранения фигур — хранится не матрица, а просто массив текущих координат квадратиков из которых составлена фигура.

2) Отрисовка стакана происходит после того, как фигура перенесена в массив стакана. Причины переноса — касание дна стакана.

List <Widget> MyLandView(){
    int poz=0;
    return List<Widget>.generate(lh, (int index_h){
      return Row(
        children:
        List<Widget>.generate(lw, (int index_w){
          poz++;
          Color BoxColor=FigColors[99]??Colors.black;
          if (MovedFig.contains(poz)==true){
            BoxColor=FigColors[fcurrent]??Colors.black;
          };
          Coords crd=Poz2Coors(poz);
          // если в стакане что-то есть - рисуем
          if (LandMatrix[crd.y][crd.x]>0){  // если  клетка не пустая
            BoxColor=FigColors[100]??Colors.black;
          };
          if (LandMatrix[crd.y][crd.x]==101){  //
            BoxColor=FigColors[101]??Colors.black;
          };
          if (BoxColor==FigColors[100]){
            //определяем это верхний блок или под ним еще что есть?
            bool ground_top=false;
              Coords crd_tmp=Poz2Coors(poz-lw);
              print("Строчка: ${crd.y},${crd.x}");
              print("Строчка вниз: ${crd_tmp.y},${crd_tmp.x}");
              if (crd_tmp.y>0) {
                if (LandMatrix[crd_tmp.y][crd_tmp.x] >0) {
                  print("-- ниже  есть земля!");
                  ground_top = true;
                }
              }
            return
                Container(
                  color: BoxColor,
                  height: MediaQuery.of(context).size.height/lh,
                  width: MediaQuery.of(context).size.width/lw,
                  child:
                    Image.asset(
                        !ground_top?"lib/images/ground.png":"lib/images/ground_full.png",
                      height: 10,
                      width: 10,
                      fit: BoxFit.fill,
                    ),
                );
          } else {
            return
              Padding(
                  padding: EdgeInsets.all(1),
                  child:
                  Container(
                    color: BoxColor,
                    height: MediaQuery.of(context).size.height / lh - 2,
                    width: MediaQuery.of(context).size.width / lw - 2,
                    //child: Text("$poz")
                  )
              );
          };
        }),
      );
    });
  }

3) При старте игры назначаем периодический таймер, при выполнении которого фигура движется на линию ниже. При движении выполняется проверка касания дна в стакане.

  void StartTimer(context){
    timer = Timer.periodic(Duration(seconds: 1), (Timer _) {
      DestroyFullLines();
      print("Сработал таймер..");
      MoveFigDown(context);
    });
  }
  // двигаем фигуру вниз
  void MoveFigDown(context){
      if (GameOver==true) {return;};
      bool move=true;
      // проверим,при следующем шаге, ни одна ли из точек не достигнет дна колодца при движении вниз?
      for (var i = 0; i < MovedFig.length; i++) {
        // проверка дна колодца
        if ((MovedFig[i] + lw)>lw*lh){
          move=false;
        };
        // проверка что наезд на другую фигуру
        Coords crd=Poz2Coors(MovedFig[i] + lw);
        if (MovedFig[i] + lw<=lw*lh) {
          if (LandMatrix[crd.y][crd.x] > 0) {
            move = false;
          };
        };
      };
      if (move==true) {
        for (var i = 0; i < MovedFig.length; i++) {
          MovedFig[i] = MovedFig[i] + lw;
        };
      };
      // если не можем двигаться вниз, то переносим фигуру в "стакан"
      if (move==false){
        print("-переносим фигуру в стакан");
        for (var i = 0; i < MovedFig.length; i++) {
          Coords crd=Poz2Coors(MovedFig[i]);
          LandMatrix[crd.y][crd.x]=1;
        };
        DestroyFullLines();
        // создаём новую фигуру
        FigureSelections();
      };
      setState(() {
        MovedFig=MovedFig;
      });
    print("Фигура двинулась вниз: ${MovedFig}");
      String audioasset = "lib/sounds/click.mp3";
      AssetsAudioPlayer.newPlayer().open(
          Audio(audioasset)
      );
  }

4) Поворот фигуры, как уже писал выше осуществляется переносом фигуры в матрицу, поворот матрицы и обратный перенос фигуры

  // поворачиваем блок
  void RotateFig(context){
    print("До поворота: ${MovedFig}");
    List <int> OldFig=List<int>.from(MovedFig); //запоминаем фигуру и положение до поворота

    // найдем минимальные координаты x и y до поворота
    Coords crd_before_rotare=GetStartXYFigure(MovedFig);
    print("min_x=${crd_before_rotare.x},min_y=${crd_before_rotare.y}");

    // передвинем фигуру в центр
    int center_x=4;
    int center_y=4;

    MovedFig=FigureMove(center_x,center_y,MovedFig);

    for (var i = 0; i < MovedFig.length; i++) {
      Coords crd=Poz2Coors(MovedFig[i]);
      int new_x=(crd.x * cos(3.1415926535897932/2) - crd.y * sin(3.1415926535897932/2)).round();
      int new_y=(crd.x * sin(3.1415926535897932/2) + crd.y * cos(3.1415926535897932/2)).round();
      MovedFig[i]=Coors2Poz(new_x,new_y);
      //print("x=${new_x},y=${new_y}");
    };
    MovedFig=FigureMove(crd_before_rotare.x-2,crd_before_rotare.y,MovedFig);

    print("После поворота: ${MovedFig}");
    // проверяем, а можно ли было поворачивать?
    if (MovedFig[0]==-1){
      MovedFig=OldFig;
    }
    // проверяем, а нет ли наложения элементов на то что уже в стакане?
    for (var i = 0; i < MovedFig.length; i++) {
      Coords crd=Poz2Coors(MovedFig[i] + lw);
      if (MovedFig[i] + lw<=lw*lh) {
        if (LandMatrix[crd.y][crd.x] > 0) {
          MovedFig=OldFig;
        };
      };
    };
  }

Исходный код игры можно скачать тут