Добро пожаловать во вторую часть обратного проектирования 2048 с использованием JavaScript. Если вы еще этого не сделали, ознакомьтесь с Часть 1. Наша цель в этом разделе — начать работу с функцией скольжения. Определив цели, приступим к работе!

Вспомогательные функции скольжения и столкновения

2048 — это «игра с перемещением плиток», поэтому очень важно, чтобы мы правильно поняли эту часть. Прежде чем мы начнем кодировать, давайте уточним, как выглядит слайд на простом английском языке. Чтобы не перегружаться, давайте специально сосредоточимся на «скольжении вверх» или на том, что произойдет, если вы нажмете клавишу со стрелкой вверх / клавишу W.

Когда происходит «скольжение вверх»:

  • Слайд начинается с нижнего ряда. Если в нижней строке нет ячеек, скользит следующая по высоте строка. Если в этой строке нет ячеек, скользит следующая по высоте строка и так далее, и так далее.
  • Если на пути ячейки ничего нет, она переместится в верхнюю строку и останется с тем же значением.
  • Если на пути ячейки есть «подобная» ячейка (например, 2 вместо 2), две ячейки объединятся (например, станут 4) на месте целевой ячейки. Новая ячейка будет продолжать скользить вверх, насколько это возможно.
  • Если на пути ячейки есть «непохожая» ячейка (например, 4 на пути 2), две ячейки не будут объединены. Две клетки будут продолжать скользить вверх, насколько это возможно.

Хорошо, давайте переведем их в технические требования, чтобы мы могли начать думать о реализации кода. Когда происходит «скольжение вверх»:

  • Все, что для нас имеет значение, — это колонка. Другими словами, мы имеем дело с одним измерением (то есть со списком) вместо двух измерений (то есть со списком списков). Давайте создадим функцию, которая по заданному местоположению ячейки возвращает массив со списком ячеек на своем пути. (Чтобы сделать функцию многократно используемой во всей программе, давайте также включим направление.) Требование 1: generateArray(cellLocation, direction).
  • Сдвиг включает в себя две точки: А (исходная точка) и В (пункт назначения). Чтобы сдвиг был успешным, A должно иметь значение, а B должно быть null. Все ячейки между A и B также должны быть null. Если условия перед сменой равны {A: x, B: null}, то условия после смены будут {A: null, B: x}. Требование 2: shift(origin, destination).
  • В столкновении участвуют две плитки: A (переключатель или плитка, с которой происходит столкновение) и B (цель или плитка, с которой происходит столкновение). Чтобы столкновение было успешным, значения A и B должны быть одинаковыми. Если условия до столкновения равны {A: x, B: x} (где x — целое число), условия после столкновения будут {A: null, B: 2x}. Требование 3: collide(shifter, target).
  • После столкновения целевая ячейка имеет некоторую инерцию. Он инициирует новую смену, где «отправной точкой» является целевая ячейка, а «назначением» является самая дальняя пустая ячейка. После второго сдвига столкновений больше не будет.

Давайте взломать!

// REQUIREMENT 1
/**
 * Given a cell's location and shift direction, this function provides a list of cells in the cell's way.
 * @param {Integer} cellRow    Zero-indexed cell row number.
 * @param {Integer} cellColumn Zero-indexed cell column number.
 * @param {String} direction   up, down, left, right.
 * @return {Array}             List of cells in the way.
 */
function generateArray(cellRow, cellColumn, direction){
    let arr = [];
    if (direction == "up"){
        for (let i=parseInt(cellRow);i>-1;i--){
            arr.push(memory[i][parseInt(cellColumn)]);
        }
    } else if (direction == "down"){
        for (let i=parseInt(cellRow);i<4;i++){
            arr.push(memory[i][parseInt(cellColumn)]);
        }
    } else if (direction == "left"){
        for (let i=parseInt(cellColumn);i>-1;i--){
            arr.push(memory[parseInt(cellRow)][i]);
        }
    } else if (direction == "right"){
        for (let i=parseInt(cellColumn);i<4;i++){
            arr.push(memory[parseInt(cellRow)][i]);
        }
    } else {
        console.log("Valid directions: up, down, left, right");
    }
    return arr;
}

Здесь у нас есть наша первая функция — generateArray. Он принимает три аргумента: cellRow, целое число, указывающее номер строки ячейки с нулевым индексом в memory; cellColumn, целое число, указывающее номер столбца ячейки с нулевым индексом в memory; и direction, строка, указывающая, в каком направлении мы хотим. «Вверх» сохраняет столбец постоянным и перечисляет все ячейки, в которых номер строки меньше, чем наша входная ячейка. «Вниз» сохраняет столбец постоянным и перечисляет все ячейки, в которых номер строки больше, чем наша входная ячейка. «Слева» сохраняет строку постоянной и перечисляет все ячейки, в которых номер столбца меньше, чем наша входная ячейка. «Вправо» сохраняет строку постоянной и перечисляет все ячейки, в которых номер столбца больше, чем наша входная ячейка. Он возвращает массив, который включает нашу входную ячейку в качестве первого значения.

Обратите внимание на широкое использование parseInt(). Я делаю это, потому что memory очень важна для работы этой программы, и я не хочу никакого дурачества, когда дело доходит до этой переменной. Поскольку я использую parseInt, все, что не является целым числом, выдает ошибку NaN, что приводит к неудачному поиску (менее плохому) вместо поврежденной переменной (очень плохо).

// REQUIREMENT 2
/**
 * Provided origin is not null and destination is null, this function moves origin value to destination cell.
 * @param {Integer} originRow         Zero-indexed origin cell row.
 * @param {Integer} originColumn      Zero-indexed origin cell column.
 * @param {Integer} destinationRow    Zero-indexed destination cell row.
 * @param {Integer} destinationColumn Zero-indexed destination cell column.
 * @return {NaN}
 */
function shift(originRow, originColumn, destinationRow, destinationColumn) {
    let origin = memory[parseInt(originRow)][parseInt(originColumn)];
    let destination = memory[parseInt(destinationRow)][parseInt(destinationColumn)];
    if (!origin || destination){
        console.log("Origin is null / destination is not null");
    } else {
        memory[parseInt(originRow)][parseInt(originColumn)] = null;
        memory[parseInt(destinationRow)][parseInt(destinationColumn)] = origin;
    }
}

Здесь у нас есть вторая функция — shift. Он принимает четыре аргумента: originRow, целое число, указывающее номер строки исходной ячейки с нулевым индексом в memory; originColumn, целое число, указывающее номер столбца исходной ячейки; destinationRow, целое число, указывающее номер строки ячейки назначения; и destinationColumn, целое число, указывающее номер столбца ячейки назначения. Если исходная ячейка null или ячейка назначения не null, на консоль выводится ошибка. В противном случае он перезаписывает memory, устанавливая целевую ячейку равной исходному значению и устанавливая исходную ячейку равной null.

// REQUIREMENT 3
/**
 * If two cells are the same value, this function adds them together and sets the shifter cell equal to null.
 * @param {Integer} shifterRow    Zero-indexed shifter cell row.
 * @param {Integer} shifterColumn Zero-indexed shifter cell column.
 * @param {Integer} targetRow     Zero-indexed target cell row.
 * @param {Integer} targetColumn  Zero-indexed target cell column.
 * @return {NaN}
 */
function collide(shifterRow, shifterColumn, targetRow, targetColumn){
    let shifter = memory[parseInt(shifterRow)][parseInt(shifterColumn)];
    let target = memory[parseInt(targetRow)][parseInt(targetColumn)];
    if (shifter != target){
        console.log("shifter and target values are not the same");
    } else {
        memory[parseInt(shifterRow)][parseInt(shifterColumn)] = null;
        memory[parseInt(targetRow)][parseInt(targetColumn)] = shifter + target;
    }
}

И последняя наша функция — collide. Он принимает четыре аргумента: shifterRow, целое число, указывающее номер строки ячейки сдвигателя с нулевым индексом в memory; shifterColumn, целое число, указывающее номер столбца ячейки сдвига; targetRow, целое число, указывающее номер строки целевой ячейки; и targetColumn, целое число, указывающее номер столбца целевой ячейки. Если значения переключателя и целевой ячейки различаются, он выводит сообщение на консоль. В противном случае он перезаписывает memory, устанавливая целевую ячейку равной сумме значений целевой ячейки и ячейки сдвига, а ячейку сдвига устанавливая равной null.

Тестирование наших вспомогательных функций

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

// Test 1: generateArray()
// Test 1.1: Test success: up
generateArray(0,0,"up")
>> Array [ 2 ] 
// Test 1.2: Test success: down
generateArray(1,1,"down")
>> Array(3) [ 64, 1024, null ] 
// Test 1.3: Test success: left
generateArray(2,2,"left")
>> Array(3) [ null, 1024, 512 ] 
// Test 1.4: Test success: right
generateArray(3,3,"right")
>> Array [ null ] 
// Test 1.5: Test error: non-integer inputs for cell location
generateArray("not a number", "not a number", "up")
>> Array [] 
// Test 1.6: Test error: invalid direction
generateArray(0,0,"not a direction")
>> Valid directions: up, down, left, right
>> Array []

Хорошо, generateArray() делает то, что мы хотим. При наличии нечисловых входных данных в тесте 1.5 он просто не генерирует массив, как мы и ожидали. Когда в тесте 1.6 указано неверное направление, он печатает сообщение об ошибке, как мы и хотели. Следующий.

// Test 2: shift()
// Test 2.1: Test success
memory[2][0]
>> 512
memory[3][0]
>> null
shift(2,0,3,0)
>> undefined
memory[2][0]
>> null
memory[3][0]
>> 512
// Test 2.2: Test error: destination not null
memory[3][0]
>> 512
memory[0][0]
>> 2
shift(3,0,0,0)
>> Origin is null / destination is not null
>> undefined
memory[3][0]
>> 512
memory[0][0]
>> 2 
// Test 2.3: Test error: origin is null
memory[3][3]
>> null
memory[2][3]
>> 2
shift(3,3,2,3)
>> Origin is null / destination is not null
>> undefined
memory[3][3]
>> null
memory[2][3]
>> 2

Большой! Наша функция сдвигает ячейки, если исходная ячейка имеет значение, а ячейка назначения равна нулю (тест 2.1). Если в ячейке назначения есть значение, она не работает, как мы и ожидали (тест 2.2). Если исходная ячейка пуста, она также не работает, как мы и ожидали (тест 2.3). Следующий.

// Test 3: collide()
// Setup
memory[3][0] = 2
memory[3][1] = 2
memory[3][2] = 4
// Test 3.1: Test success
memory[3][0]
>> 2
memory[3][1]
>> 2
collide(3,0,3,1)
>> undefined
memory[3][0]
>> null
memory[3][1]
>> 4
collide(3,1,3,2)
>> undefined
memory[3][1]
>> null
memory[3][2]
>> 8
// Test 3.2: Test error: Unequal cell values
memory[3][2]
>> 8
memory[0][0]
>> 2
collide(3,2,0,0)
>> shifter and target values are not the same
>> undefined
memory[3][2]
>> 8
memory[0][0]
>> 2

Хороший. Если значения двух ячеек совпадают, они столкнутся и сложится вместе (тест 3.1). Если они не одного и того же значения, они не будут сталкиваться, и их значения не изменятся (тест 3.2).

В этом разделе мы определили, как выглядит сдвиг, прокладывая путь для будущего развития. Мы также создали три вспомогательные функции — generateArray(), которая создает массив относительно ячейки и направления; shift(), который перемещает ячейку в другое место; и collide(), который объединяет две ячейки, если они имеют одинаковое значение. Позже они послужат строительными блоками для более масштабных слайдов.

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

Вот исходный код на GitHub, если интересно. Спасибо за прочтение.