Наверх

Урок 12. Вращения фигур

С вращением фигур в тетрисе оказалось все не так просто, как кажется на первый взгляд. Дело в том, что фигуры-то не квадратные (почти все). И даже не симметричные.

Из-за этого после вращения часть фигуры может оказаться за пределами игрового поля или «залезть» на другую фигуру, уже стоящую на поле.

Кроме того, сам алгоритм вращения неординарный, и я не смог найти какой-то закономерности в изменениях координат при вращении. Можно было бы еще посидеть и подумать, но фигур не так много и вариантов позиций у каждой максимум 4.

Поэтому я решил сделать не очень красиво. Просто вручную описать все возможные положения фигур и порядок их изменения.

Внимание! Этот код может повлиять на вашу психику.
// ...
    setRotatedCoords: function() {
      var newCoords = [];
      switch(this.type) {
        // Проверяем тип фигуры
        case 'I':
          switch(this.rotatePosition) {
            // и, в зависимости от текущего положения фигуры, изменяем координаты
            case 0:
              newCoords.push([
                  [this.coords[2][0][0], this.coords[2][0][1]-1],
                  [this.coords[2][0][0], this.coords[2][0][1]],
                  [this.coords[2][0][0], this.coords[2][0][1]+1],
                  [this.coords[2][0][0], this.coords[2][0][1]+2]
              ]);
              this.coords = newCoords;
              this.rotatePosition = 1;
              break;
            case 1:
              newCoords.push([
                  [this.coords[0][1][0]-2, this.coords[0][1][1]]
                ],[
                  [this.coords[0][1][0]-1, this.coords[0][1][1]]
                ],[
                  [this.coords[0][1][0],   this.coords[0][1][1]]
                ],[
                  [this.coords[0][1][0]+1, this.coords[0][1][1]]
              ]);
              this.coords = newCoords;
              this.rotatePosition = 0;
              break;
          }
          break;
        case 'S':
          switch(this.rotatePosition) {
            case 0:
              newCoords.push([
                  [this.coords[0][0][0], this.coords[0][0][1]-1]
                ],[
                  [this.coords[1][0][0], this.coords[1][0][1]],
                  [this.coords[1][1][0], this.coords[1][1][1]]
                ],[
                  [this.coords[1][1][0]+1, this.coords[1][1][1]]
              ]);
              this.coords = newCoords;
              this.rotatePosition = 1;
              break;
            case 1:
              newCoords.push([
                  [this.coords[0][0][0], this.coords[0][0][1]+1],
                	[this.coords[0][0][0], this.coords[0][0][1]+2]
                ],[
                  [this.coords[1][0][0], this.coords[1][0][1]],
                  [this.coords[1][1][0], this.coords[1][1][1]]
              ]);
              this.coords = newCoords;
              this.rotatePosition = 0;
              break;
          }
          break;
        case 'Z':
          switch(this.rotatePosition) {
            case 0:
              newCoords.push([
                  [this.coords[0][1][0], this.coords[0][1][1]+1]
                ],[
                  [this.coords[1][0][0], this.coords[1][0][1]],
                  [this.coords[1][1][0], this.coords[1][1][1]]
                ],[
                  [this.coords[1][0][0]+1, this.coords[1][0][1]]
              ]);
              this.coords = newCoords;
              this.rotatePosition = 1;
              break;
            case 1:
              newCoords.push([
                  [this.coords[0][0][0], this.coords[0][0][1]-2],
                	[this.coords[0][0][0], this.coords[0][0][1]-1]
                ],[
                  [this.coords[1][0][0], this.coords[1][0][1]],
                  [this.coords[1][1][0], this.coords[1][1][1]]
              ]);
              this.coords = newCoords;
              this.rotatePosition = 0;
              break;
          }
          break;
        case 'T':
          switch(this.rotatePosition) {
            case 0:
              newCoords.push([
                  [this.coords[0][1][0]-1, this.coords[0][1][1]]
                ],[
                  [this.coords[0][0][0], this.coords[0][0][1]],
                  [this.coords[0][1][0], this.coords[0][1][1]]
                ],[
                  [this.coords[1][0][0], this.coords[1][0][1]]
              ]);
              this.coords = newCoords;
              this.rotatePosition = 1;
              break;
            case 1:
              newCoords.push([
                  [this.coords[0][0][0], this.coords[0][0][1]]
                ],[
                  [this.coords[1][0][0], this.coords[1][0][1]],
                  [this.coords[1][1][0], this.coords[1][1][1]],
                  [this.coords[1][1][0], this.coords[1][1][1]+1]
              ]);
              this.coords = newCoords;
              this.rotatePosition = 2;
              break;
            case 2:
              newCoords.push([
                  [this.coords[0][0][0], this.coords[0][0][1]]
                ],[
                  [this.coords[1][1][0], this.coords[1][1][1]],
                  [this.coords[1][2][0], this.coords[1][2][1]]
                ],[
                  [this.coords[1][1][0]+1, this.coords[1][1][1]]
              ]);
              this.coords = newCoords;
              this.rotatePosition = 3;
              break;
            case 3:
              newCoords.push([
                  [this.coords[1][0][0], this.coords[0][0][1]-1],
                  [this.coords[1][0][0], this.coords[1][0][1]],
                  [this.coords[1][1][0], this.coords[1][1][1]]
                ],[
                  [this.coords[2][0][0], this.coords[2][0][1]]
              ]);
              this.coords = newCoords;
              this.rotatePosition = 0;
              break;
          }
          break;
        case 'J':
          switch(this.rotatePosition) {
            case 0:
              newCoords.push([
                  [this.coords[0][0][0], this.coords[0][0][1]-1]
                ],[
                  [this.coords[1][0][0], this.coords[1][0][1]-1],
                  [this.coords[1][0][0], this.coords[1][0][1]],
                  [this.coords[1][0][0], this.coords[1][0][1]+1]
              ]);
              this.coords = newCoords;
              this.rotatePosition = 1;
              break;
            case 1:
              newCoords.push([
                  [this.coords[0][0][0], this.coords[0][0][1]],
                  [this.coords[0][0][0], this.coords[0][0][1]+1]
                ],[
                  [this.coords[1][0][0], this.coords[1][0][1]]
                ],[
                  [this.coords[1][0][0]+1, this.coords[1][0][1]]
              ]);
              this.coords = newCoords;
              this.rotatePosition = 2;
              break;
            case 2:
              newCoords.push([
                  [this.coords[1][0][0], this.coords[1][0][1]-1],
                  [this.coords[1][0][0], this.coords[1][0][1]],
                  [this.coords[1][0][0], this.coords[1][0][1]+1]
                ],[
                  [this.coords[1][0][0]+1, this.coords[1][0][1]+1]
              ]);
              this.coords = newCoords;
              this.rotatePosition = 3;
              break;
            case 3:
              newCoords.push([
                  [this.coords[0][1][0]-1, this.coords[0][1][1]+1]
                ],[
                  [this.coords[0][1][0], this.coords[0][1][1]+1]
                ],[
                  [this.coords[1][0][0], this.coords[1][0][1]-1],
                  [this.coords[1][0][0], this.coords[1][0][1]]
              ]);
              this.coords = newCoords;
              this.rotatePosition = 0;
              break;
          }
          break;
        case 'L':
          switch(this.rotatePosition) {
            case 0:
              newCoords.push([
                  [this.coords[0][0][0], this.coords[0][0][1]+1]
                ],[
                  [this.coords[1][0][0], this.coords[1][0][1]-1],
                  [this.coords[1][0][0], this.coords[1][0][1]],
                  [this.coords[1][0][0], this.coords[1][0][1]+1]
              ]);
              this.coords = newCoords;
              this.rotatePosition = 1;
              break;
            case 1:
              newCoords.push([
                  [this.coords[0][0][0], this.coords[0][0][1]-1],
                  [this.coords[0][0][0], this.coords[0][0][1]]
                ],[
                  [this.coords[1][0][0], this.coords[1][0][1]+2]
                ],[
                  [this.coords[1][0][0]+1, this.coords[1][0][1]+2]
              ]);
              this.coords = newCoords;
              this.rotatePosition = 2;
              break;
            case 2:
              newCoords.push([
                  [this.coords[1][0][0], this.coords[1][0][1]+1],
                  [this.coords[1][0][0], this.coords[1][0][1]],
                  [this.coords[1][0][0], this.coords[1][0][1]-1]
                ],[
                  [this.coords[1][0][0]+1, this.coords[1][0][1]-1]
              ]);
              this.coords = newCoords;
              this.rotatePosition = 3;
              break;
            case 3:
              newCoords.push([
                  [this.coords[0][1][0]-1, this.coords[0][1][1]-1]
                ],[
                  [this.coords[0][1][0], this.coords[0][1][1]-1]
                ],[
                  [this.coords[1][0][0], this.coords[1][0][1]],
                  [this.coords[1][0][0], this.coords[1][0][1]+1]
              ]);
              this.coords = newCoords;
              this.rotatePosition = 0;
              break;
          }
          break;
      }
    },
//...
При создании фигуры будем не только выбирать тип фигуры, но и задавать случайным образом поворот:
// ...
    getRandomFigure: function() {
      var keys = Object.keys(Tetris.config.figureTypes);
      var randKey = Math.floor(Math.random() * keys.length);
      // Запомним тип фигуры, чтобы потом знать, по какому алгоритму поворачивать фигуру
      Tetris.figure.type = keys[randKey];
      this.coords = Tetris.config.figureTypes[keys[randKey]]();
      // Выберем случайное количество поворотов
      var rotatePosition = Math.floor(Math.random() * 4);
      // и вращаем, вращаем, вращаем
      if (rotatePosition) {
        for (var i = 0; i < rotatePosition; i++) {
          this.setRotatedCoords();
        }
      }
      console.log(rotatePosition);
    },
// ...
Не забудем обнулять позицию поворота при уничтожении фигуры
// ...
    destroy: function() {
      this.coords = [];
      this.rotatePosition = 0;
    },
// ...
Добавим вызов метода при нажатии кнопки «Вверх»
// ...
    window.onkeydown = function (event) {
      var direction = '';
      if (event.keyCode == 39) {
        direction = 'right';
      } else if(event.keyCode == 37) {
        direction = 'left';
      }
      if (direction) {
        Tetris.figure.sideStepStart(direction);
      } else if(event.keyCode == 40) {
        Tetris.setSpeed(30);
      }
      if (event.keyCode == 38) {
        Tetris.figure.rotate();
      }
    }
// ...
Теперь в методе rotate пропишем вызов вращения и различные проверки
// ...
  figure: {
    // ...
    create: function() {
      // В методе getRandomFigure уже прописана установка координат, поэтому просто вызываем
      this.getRandomFigure();
    },
    rotatePosition: 0,
    rotate: function() {
      // Если кнопку нажали до создания фигуры, ничего не делаем
      if (this.coords.length == 0) {
        return false;
      }
      // Заставляем фигуру совершить поворот
      this.setRotatedCoords();
      // Проверяем - не вылезла ли фигура за пределы поля
      Tetris.each(this.coords, function(i,j){
        var figureRow = Tetris.figure.coords[i][j][0];
        var figureCol = Tetris.figure.coords[i][j][1];
        // Если вылезла слева, будем двигать фигуру вправо
        if (figureCol < 0) {
          Tetris.figure.needStepSide = 'right';
        }
        // Если справа - влево
        if (figureCol >= Tetris.pitch.width) {
          Tetris.figure.needStepSide = 'left';
        }
      });
      // Если мы определили, что надо бы подвинуть фигуру, делаем это
      if (this.needStepSide) {
        this.sideStep(this.needStepSide);
        // обнуляем флаг
        this.needStepSide = false;
      }
      // Проверяем, не налезла ли фигура на другие фигуры и не провалилась ли она под пол
      if (this.touched()) {
        // Если да, то откатываем поворот
        this.rotateRollback();
        return false;
      }
      // Если всё ОК, рисуем новое положение фигуры
      Tetris.draw();
    },
    needStepSide: false,
    // Возможных позиций у фигуры 4, поэтому, чтобы вернуться к предыдущему
    // состоянию, нужно повернуться еще 3 раза
    rotateRollback: function() {
      	this.setRotatedCoords();
        this.setRotatedCoords();
        this.setRotatedCoords();
    },
    // ...
 },
 // ...

Теперь в тетрис можно уже и поиграть: http://jsfiddle.net/ilyautkin/aavju3jd/42/


5 комментариев

  1. Tarson_1 04 марта 2017, 23:18(Комментарий был изменён) # 0
    Ну, молодежь пошла:(
    Я теперь понимаю, почему гигабайты требуются там, где раньше хватало килобайт.
    Ну, как нет закономерности во вращении?
    Возьмем для простоты квадрат три на три (четыре нужно только для палок и добавляется элементарно)
    _____Y
    X___44__45__46 ________ O
    ____54__55__56 _____ O O О
    ____64__65__66

    Назначаем приказным порядком один квадратик в фигурке центральным (вокруг него и будем вращать)

    пусть это будет точка с координатами 55

    теперь вычтем ее коорд. из коорд. остальных точек. Это не обычное вычитание, а вычитание поразрядно, что в общем тоже элементарно реализуется (причем это все только для наглядности)

    Получим такую матрицу

    -1 -1 ___ -1 0 ___ {-1 1}
    0 -1 ___ 0 0 ___ (0 1)
    1 -1 ___ 1 0 ___ 1 1

    Для перемещения точки ( 0: 1) (Y: X ) при повороте на 90 градусов в точку ( -1: 0) (Ynext: Xnext)против часовой стрелки выводится простейшая формула
    if (Y == 0) {Xnext = 0; Ynext = -X} else {Xnext = Y; Ynext = 0}

    для точки {-1 1} будет такая же формула, но чуть с другими параметрами.
    ВСЁ! Мы умеем врашать фигуры 3 *3.
    Для палок используется третья формула, аналогичная предыдущим.
    Итого три строчки кода. И одна строчка ДО, что бы отделить нужные точки для нужных формул (ну там все просто — переход по условию)

    Зачем ты столько нагородил???

    PS ecли мы для интереса построим графики изменения X и Y от поворота, то получим пилу для точки ( 0 1) и меандр для точки{-1 1}. По сути это разложение в ряд синусоиды вращения. А графики — гармоники:)

    1. Илья Уткин 06 марта 2017, 10:23 # 0
      Так я и не говорил, что закономерности нет. Просто я не смог найти этой закономерности.
    2. Tarson_1 06 марта 2017, 12:20(Комментарий был изменён) # 0
      ОК:)
      Ну как неординарный алгоритм?!
      Очень даже ординарный.
      Как раз вчера дописал функционирующую программку тетриса, рабочий блок кода на джаве для вращения. Сильно за
      стиль не ругайте, я не погромист.

      int Y_temp = Y_current-Y_central;
      int X_temp = X_current-X_central;

      int sWitch = Math.abs(Y_temp)+Math.abs(X_temp);
      if ((Math.abs(Y_temp)|Math.abs(X_temp))==2){sWitch=3;}

      switch (sWitch)
      {

      case 1:

      if (Y_temp == 0) {Xnext = 0; Ynext = -X_temp;} else {Xnext = Y_temp; Ynext = 0;}

      break;

      case 2:

      if (Y_temp == -1 & X_temp == 1) {Ynext = -1; Xnext = -1;}
      if (Y_temp == -1 & X_temp == -1) {Ynext = 1; Xnext = -1;}
      if (Y_temp == 1 & X_temp == -1) {Ynext = 1; Xnext = 1;}
      if (Y_temp == 1 & X_temp == 1) {Ynext = -1; Xnext = 1;}

      break;

      case 3:

      if (Y_temp == 0) {Xnext = 0; Ynext = -X_temp;} else {Xnext = Y_temp; Ynext = 0;}

      break;

      }

      Вращает все фигуры в том числе и палки. Ну и это все завернуто в цикл FOR для трех обсчитываемых точек (четвертая центральная — неподвижна). Для точки {-1;1} (case 2:) не нашел более короткого алгоритма, зато для крайней точки палки ОООО (case 3:) он тоже в одну строчку.Если у кого получится короче, выкладывайте.

      1. Tarson_1 06 марта 2017, 19:05(Комментарий был изменён) # 0
        Можно сократить совместив case 1 и case 2, они выполняют одинаковую работу, тогда будет так:

        int Y_temp = Y_current-Y_central;
        int X_temp = X_current-X_central;

        int sWitch = Math.abs(Y_temp)+Math.abs(X_temp);
        if ((Math.abs(Y_temp)|Math.abs(X_temp))==2){sWitch=1;}

        switch (sWitch)
        {

        case 1:

        if (Y_temp == 0) {Xnext = 0; Ynext = -X_temp;} else {Xnext = Y_temp; Ynext = 0;}

        break;

        case 2:

        if (Y_temp == -1 & X_temp == 1) {Ynext = -1; Xnext = -1;}
        if (Y_temp == -1 & X_temp == -1) {Ynext = 1; Xnext = -1;}
        if (Y_temp == 1 & X_temp == -1) {Ynext = 1; Xnext = 1;}
        if (Y_temp == 1 & X_temp == 1) {Ynext = -1; Xnext = 1;}

        }

        Ynext= Ynext + Y_central;
        Xnext= Xnext + X_central;
        1. Максим 20 марта 2018, 12:17(Комментарий был изменён) # 0
          Добрый день!
          Илья, подскажите откуда newCoords.push, появляется 2, и как образуются,
          эти данные в массиве.Может есть литература на эту тему?

          Авторизация

          через сервис Loginza:


          Шаблоны MODX

          1 2 Дальше »

          Объектная
          модель
          MODX