Большинство из нас знает, как играть в Scrabble. Мы строим слова на доске 15x15, вытягивая из руки 7 плиток, которые были выбраны случайным образом из пула из 100 плиток. Все слова, размещенные на доске, должны основываться на существующем слове. Каждая буква имеет разное количество очков, и цель состоит в том, чтобы набрать как можно больше очков до того, как закончатся все плитки.

Все это довольно просто, если у вас есть приличный словарный запас на родном языке, на котором вы играете. Поэтому я подумал, что не составит большого труда создать игру Scrabble, одну из моих любимых игр детства, за три дня, которые нам дали на создание игры по нашему выбору. Мы будем использовать javascript и jQuery с API-вызовами стороннего словаря для подтверждения отправки слов. Кусок пирога? Эм, не совсем. Оказывается, создать приложение Scrabble немного сложнее, чем играть в физическую настольную игру. Ниже приведены некоторые препятствия, которые мне и моему парному партнеру пришлось преодолеть.
Сетка доски
Сетки есть сетки, верно? Неправильный. Нам нужно было выяснить, как визуализировать 225 квадратов, любой квадрат, к которому можно было получить доступ в любое время. Мы решили создать доску на основе вложенных массивов. Для каждой строки мы создаем пустой массив, а затем добавляем 15 массивов. В каждый из этих вложенных массивов мы рендерим div, который, что наиболее важно, содержит уникальный идентификатор, который мы используем для игры.
//in Board.js
buildBoardGrid(row, col){
let square = null
let boardArray = []
for(var tempRow = 0; tempRow < row; tempRow++){
boardArray.push([])
boardArray[tempRow].push(new Array(col))
for(var tempCol = 0; tempCol < col; tempCol++){
square = new Square(tempRow, tempCol)
boardArray[tempRow][tempCol] = square.render()
}
}
return boardArray
}
//In Square.js
render(){
return `<div id="${this.tempRow}_${this.tempCol}" class="square" ></div>`
}
Таким образом, третий квадрат четвертой строки имеет идентификатор «3_2», соответствующий его месту в массиве. Чтобы упростить наш код, мы решили сохранить фактическую позицию массива, а не истинный номер строки или столбца. Мы также начали массив с верхнего левого угла и строили оттуда. Таким образом, идентификаторы имеют форму «{row}_{column}» (а не координаты x/y).
Отслеживание плиток
Когда вы играете в скрэббл на физической доске, довольно просто отслеживать плитки. Они либо в пуле тайлов, либо в руках одного из игроков, либо на доске. Когда плитка перемещается из одного места в другое или даже когда игрок решает вернуться назад и начать все сначала, у нас есть запас плиток. Совсем другая ситуация в виртуальном мире. Чтобы решить эту проблему, мы создали стеки. Стопки, стопки, стопки. Эти стопки отслеживают плитки за ход и очищаются после окончания хода.
var boardStack = [] // tiles placed on the board var handStack = [] // tiles removed from the hand (needs to be separate from the boardStack because sometimes a tile does not make it to the board var wordStack = [] // all tiles making up word being played (need to be separate from boardStack because includes letters already on board)
Каждый стек представляет собой массив массивов. Элементы массива каждого стека имеют разные свойства в зависимости от желаемого варианта использования. Эти стеки позволяют нам правильно находить буквы на доске, оценивать новые слова и позволяют игроку отменить ход, если он того пожелает.
Вызовы API в асинхронной среде
Сначала нам нужно было найти сторонний API, который предоставил бы нам доступ из вызова API на стороне клиента и был бы в формате, который нам не нужно было утомительно анализировать. Мы прошли три, прежде чем остановились на четвертом: WordsAPI, который оказался относительно удобным для пользователя через учетную запись на Mashape. Мы думали, что были настроены, когда вызывали WordsAPI с запросом hello и могли вернуть объект в формате JSON, или когда мы отправляли вызов с бормотанием вроде wshtn и возвращали ошибку. Но нет. Нас ужалила асинхронная природа Javascript.
Когда игрок отправляет слово, мы вызываем метод onFinishTurn, который вызывает API и следует различным сценариям в зависимости от успеха или неудачи вызова API. Из-за асинхронного javascript сценарии разыгрываются до того, как будут возвращены результаты вызова API. Стихийное бедствие.
Привет обещания! Да, нам нужно было найти способ дождаться возврата вызова API, чтобы продолжить завершение поворота. Введите обещания, но особым образом из-за того, что вызов jQuery ajax используется для доступа к API. Начиная с jQuery 1.5 (темные века на данный момент), объекты jQuery XHR, возвращаемые $.ajax(), реализуют интерфейс Promise, предоставляя им все свойства, методы и поведение Promise (поэтому нам не нужно создавать отдельный новый вызов Promise). Эти методы принимают один или несколько аргументов функции, которые вызываются при завершении запроса $.ajax(). Наиболее подходящим для нас, когда нам нужен был как сценарий успеха, так и сценарий отказа, является метод .then(), который включает в себя функциональность метода .done() [эквивалентная конструкция опции обратного вызова успеха] и .fail( ) метод [эквивалентная конструкция опции обратного вызова ошибки]. В дополнение к методу .then() нам нужен .catch(), который служит обработчиком, вызываемым при отклонении отложенного объекта [из вызова .then()]. Пример реализации кода выглядит следующим образом:
onFinishTurn(){
$('.finish').click((event) => {
this.wordCheck().then((response) =>
// success scenario:
// code to calculate points, draw new
// tiles and change turns
}).catch((e) => {
// failure scenario:
// code to reject word, return tiles and lose turn
})
})
}
wordCheck(){
return $.ajax({
url: `${URL}${word}`,
headers: {'X-Mashape-Key': wordsApiKey},
success : function(data, textStatus, XMLHttpRequest){
},
error: function(XMLHttpRequest, textStatus, errorThrown){
}
})
}
Вышесказанное кажется очевидным сейчас, но это было большим камнем преткновения для нас на этом пути.
Логика игры
Мы думали, что как только мы создадим игру и у нас будет способ проверить допустимое слово, мы будем в свободном плавании. Но когда вы на самом деле приступаете к реализации правил игры, код очень быстро усложняется. По сути, мы должны проверить множество различных вариантов использования.
- 100 плиток. Официальный Scrabble состоит из 100 плиток, каждая из которых имеет букву и значение. Мы могли бы жестко запрограммировать эти тайлы, но это неинтересно. Поэтому каждый раз, когда запускается новая игра, мы устанавливаем var tileArray в пустой массив, а затем вызываем метод tileMaker, который перебирает массив из 26 объектов. Каждый объект включает в себя букву, значение и количество вхождений. Для каждой буквы мы перебираем эту букву в соответствии с количеством вхождений, и для каждого вхождения мы помещаем новую плитку в tileArray.
- Слово должно основываться на другом слове. Сначала мы установили для var crossTile значение false. По мере построения слова (на доску добавляются плитки) мы проверяем, не примыкает ли вновь размещенная буква к существующей букве. Если это так, мы устанавливаем var crossTile в true. Когда мы вызываем onFinishTurn, мы проверяем, истинно ли значение crossTile. Если false, плитки возвращаются, и игрок проигрывает ход.
- Слова читаются слева направо или сверху вниз. При размещении плитки вы можете размещать только справа или ниже существующей буквы. Квадраты «назад» в сетке не включены.
- Очки начисляются на основе количества баллов за каждую использованную плитку. Когда мы помещаем плитку в стек слов, мы включаем значение плитки в массив. Затем вычисление значения слова становится довольно простым, поскольку мы перебираем значение индекса каждого элемента в массиве wordStack.
Будущие функции:
Мы получили MVP за три дня, которые нам дали на создание игры. Но у нас есть более важные функции, которые мы планируем реализовать в ближайшие недели.
- Вспомогательные слова, построенные в любом порядке. Сейчас наши стеки зависят от упорядоченного построения слов. Но мы знаем, что на самом деле мы не всегда ставим букву в том порядке, в котором она стоит в слове. При написании «привет» вокруг буквы «о» мы должны иметь возможность поставить две «л», затем «е», а затем «ч» (при условии, что последнее слово читается слева направо). Чтобы реализовать эту функциональность, нам нужно изменить способ построения нашего стека слов. Вместо того, чтобы строить его, когда плитки размещаются на доске, нам нужно построить его после того, как слово будет завершено.
- Обработка смежности. В настоящее время мы предоставляем кредит только за построенное базовое слово. Мы не учитываем смежность, которая, как мы знаем, является одной из самых забавных частей Scrabble. Для этого после завершения хода нам нужно перебрать каждую букву нашего стека слов и проверить соседние буквы, а затем создать дополнительные стека слов для проверки и учета.
- Отображение значений точек плитки на доске. Мы отслеживаем значения плитки и присуждаем очки на основе значения плитки, и эти значения включаются в рендеринг div, но мы не показываем эти значения на плитках. Для этого нам нужно внести некоторые изменения в наши параметры CSS, не раздувая сетку и сохраняя полную видимость игры на экране нормального размера. Целью Stretch здесь является сделать игру полностью отзывчивой.
- Внедрение премиальных квадратов на доске. Прямо сейчас все квадраты доски предлагают 1-кратную ценность буквы, размещенной на них. В Real Scrabble есть квадраты 2x и 3x, что еще раз придает игре больше азарта и стратегии. Чтобы реализовать это, нам нужно добавить еще один элемент в каждый квадрат сетки, который отслеживает кратное значение этого квадрата. Затем этот кратный будет применен к словесному стеку после завершения действительного хода.
Удачного скраблинга!