В течение последнего месяца я изучал Javascript в рамках программы Flatiron Software Engineering. Мои отношения любви и ненависти с Javascript перешли в сторону любви, поскольку я лучше понимаю его причуды. Например, понимание подъема и того, что Javascript разделяет компиляцию и выполнение во время выполнения, имеет большое значение для понимания некоторых неприятных ошибок (или тихих сбоев), которые Javascript выдает, если я плохо структурирую свой код.

Для своего портфолио-проекта Javascript / Rails API я решил создать классическую игру, Тетрис (играйте в нее вживую здесь или загляните в репозиторий github!). Другие мои портфельные проекты были в той или иной форме CMS. Я хотел построить что-то более увлекательное. Тетрис также стал для меня хорошим способом вникнуть в некоторые аспекты, которые делают Javascript уникальным, например, манипуляции с DOM и обработка событий в браузере.

Большая часть проекта прошла гладко. Я начал с представления доски Tetris вложенным массивом 12 × 24 и отображения доски в DOM с элементом таблицы. Я построил отдельные классы для представления ячеек и частей и построил дочерние классы класса Piece для представления различных типов частей. По пути я столкнулся с двумя основными проблемами:

Проблема поворота

Возможно, это скорее математическая проблема, чем проблема разработки программного обеспечения: заставить деталь вращаться вокруг точки поворота на сетке на удивление сложно. Я начал с попытки жестко запрограммировать перестановки позиций, которые фигура будет занимать при вращении. Хотя это и не долгосрочное решение, я подумал, что это, по крайней мере, даст мне представление о закономерностях, связанных с вращением. Я быстро понял, что перестановок слишком много, чтобы далеко зайти.

Например, кусок слева может быть представлен массивом:

[[1, 3], [2, 3], [2, 2], [2, 1]

Здесь каждый из 4 вложенных массивов представляет собой ячейку с координатами x и y в части L .

Если предположить, что единственное отличие этого фрагмента слева состоит в том, что он повернут по часовой стрелке, он будет представлен массивом:

[[1, 2], [1, 3], [2, 3], [3, 3]]

Задача проблемы вращения состоит в том, чтобы выяснить, как абстрактно получить координаты во втором массиве с учетом первого массива. Это становится сложнее, если принять во внимание другие типы фигур в тетрисе:

Чтобы решить эту проблему, я начал с того, что достал листок бумаги, выбрал один тип элемента, нарисовал различные положения поворота и перечислил соответствующие координаты каждой позиции. Я попытался определить, от каких переменных зависят конечные координаты. Стало ясно, что новые координаты зависят от комбинации старых координат и координат поворотной части. Оглядываясь назад, я хотел бы сказать, что я составил систему уравнений и решил ее математически, но это было немного больше методом проб и ошибок. Немного поработав, я понял, что уравнение для получения новых координат x и y для данной ячейки должно выглядеть примерно так:

xnew = xold +|- (yold +|- ypivot) +|- (xold +|- xpivot)
ynew = yold +|- (xold +|- xpivot) +|- (yold +|- ypivot)

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

В конце концов, у меня была функция, которая возвращала конечные позиции произвольного куска после поворота по часовой стрелке:

prepRotation(){
  const pivot = this.cells[1];
  function xPivot(cell){
    return pivot.y — cell.y + pivot.x
  }
  function yPivot(cell){
    return pivot.y — pivot.x + cell.x
  }
  return this.cells.map(cell => 
    return {x: xPivot(cell), y: yPivot(cell)}
  });
}

Здесь this представляет экземпляр Piece, константа pivot представляет ячейку, вокруг которой вращаются остальные ячейки, а xPivot и yPivot возвращают координаты x и y ячейки после поворота.

Вход, сохранение и загрузка с помощью асинхронных функций

Вторая основная проблема, с которой я столкнулся, заключалась в том, чтобы убедиться, что пользователь вошел в систему, прежде чем сохранять или загружать игру. В частности, я хотел написать код, который учитывает принцип единой ответственности в дизайне SOLID и использует преимущества асинхронных функций. Другими словами, функция «сохранить» должна только сохранять игру, а функция «логин» должна только входить в систему пользователя, НО мне нужно было убедиться, что эти события произошли. в определенной последовательности: (1) пользователь нажимает кнопку сохранения, (2) затем отображается форма входа в систему, (3) затем пользователь заполняет и отправляет форма, (4) затем запрос входа в систему отправляется на серверную часть, (5) затем получен ответ от серверной части и (6) если вход прошел успешно настроить и отправить еще один запрос на серверную часть, чтобы сохранить игру.

Это не так сложно сделать, если вы соедините несколько операторов `fetch` и` then` вместе, например:

saveButton.addEventListener(‘click’, ()=>{
  displayLogin();
  fetch(‘user_post_url’, configObj)
    .then (resp => resp.json())
    .then (json => {
      fetch(‘game_post_url’, formatGame(json);)
    })
})

С этим кодом связаны две большие проблемы: (1) вся логика входа и сохранения находится в одной и той же функции, и ее трудно разделить, потому что логика сохранения запускается оператором `then`, который зависит от логики входа в систему. . (2) Нет никакой логики в том, чтобы убедиться, что пользователь действительно заполняет и отправляет форму перед отправкой первого запроса на выборку. Сохранение игры зависит не только от запроса на получение входа в систему, но и от события входа в систему DOM.

Мое решение этой проблемы заключалось в создании пары промежуточных функций, которые обрабатывают взаимодействие между входом в систему, сохранением и загрузкой.

function handleLoad(){
  if (loggedIn){
    loadGame();
  } else {
    pauseGame();
    displayLogin();
    afterLogin(loadGame);
  }
}
function afterLogin(callback){
  const interval = window.setInterval(()=>{
    if (loginRequest){
      loginRequest.then(()=>{
      callback();
      window.clearInterval(interval);
      })
    }
  }, 1000);
}

В частности, после отображения имени входа «afterLogin ()» устанавливает интервал, который регулярно проверяет, отправил ли пользователь форму входа. Он знает это, потому что переменная loginRequest представляет объект выборки. Если есть запрос на вход, то вызывается обратный вызов - saveGame () или loadGame (). Передав запрос на выборку как переменную и используя setInterval () для регулярной проверки события входа в систему, я смог как соблюдать принцип единой ответственности, так и воспользоваться преимуществами асинхронных функций Javascript.

Создание тетриса с помощью JS было отличным уроком. Это помогло мне освоить манипуляции с DOM и обработку событий. Я также лучше понял, как реализовать асинхронные функции и воспользоваться преимуществами первоклассных функций Javascripts.