Сегодня мы займемся реверс-инжинирингом 2048! Для тех, кто не в курсе, 2048 — это игра со сдвигом плитки, разработанная Cirulli в 2014 году. Игра довольно проста и сводится к следующим пунктам:

  • Ваша цель — создать плитку со значением 2048.
  • Вы достигаете этого, сдвигая плитки. Как плитки будут сочетаться. (Например, если плитка 2 сталкивается с другой плиткой 2, они объединяются, чтобы стать плиткой 4. Точно так же, если плитка 1024 сталкивается с другой плиткой 1024, они объединяются, чтобы стать плиткой 2048.)
  • Вы перемещаете плитки с помощью клавиш со стрелками или клавиш WASD.
  • Вы выиграете, когда доберетесь до 2048. Вы проиграете, если больше не сможете делать ходы.

С этим покончено, давайте начнем!

Создание игрового дисплея

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

У меня будет два файла: один для HTML/CSS и один для JavaScript. В файле index.html будут жить функции отображения, а в файле shitty.js будет жить логика приложения. Мы делаем это так, чтобы у нас была лучшая организация и разделение задач. Структура файла будет выглядеть так:

shitty-2048
    | index.html
    | shitty.js

Перво-наперво — нам нужен способ отображения игровых данных пользователю. Обычный способ сделать это с помощью элемента холст, но, учитывая простоту платы 2048, я думаю, что это излишество. Я хочу, чтобы все было как можно проще, поэтому я собираюсь использовать контейнер сетки CSS.

<!DOCTYPE html>
<html>
<head>
<style>
.grid-container{

/* Grid system */
display:grid; 

/* Column spacing */
grid-template-columns:auto auto auto auto; 

/* Row height */
grid-template-rows:50px 50px 50px 50px; 

/* Area between cells */
gap:10px; 

/* Background color */
background-color:black; 

/* Distance between grid and the edge of the screen */
padding:10px; 
}

.grid-container>div{

/* Background color: 80% opaque white */
background-color:rgba(255,255,255,0.8); 

/* Border around cells */
border:1px solid black; 

/* Text alignment */
text-align:center; 

/* Text size */
font-size:30px; 
}
</style>
</head>
<body>

<!--- Parent grid container -->
<div class="grid-container">

<!--- Children cells -->
<div id="a1">a1</div>
<div id="b1">b1</div>
<div id="c1">c1</div>
<div id="d1">d1</div>
<div id="a2">a2</div>
<div id="b2">b2</div>
<div id="c2">c2</div>
<div id="d2">d2</div>
<div id="a3">a3</div>
<div id="b3">b3</div>
<div id="c3">c3</div>
<div id="d3">d3</div>
<div id="a4">a4</div>
<div id="b4">b4</div>
<div id="c4">c4</div>
<div id="d4">d4</div>
<!--- End children cells -->

</div>
<!--- End parent grid container -->

</body>
<!--- Link up our JavaScript file -->
<script src="shitty.js"></script>
</html>

В разделе body мы создаем родительский div с атрибутом класса grid-container. Затем мы создаем 16 дочерних элементов div (чтобы создать сетку 4×4) и помечаем их, чтобы позже можно было манипулировать ими с помощью JavaScript. Я использую то, что я называю соглашением об именах Excel, где буквы соответствуют столбцам, а числа относятся к строкам (например, c4 — это третий столбец, четвертая строка).

В разделе head мы стилизуем элементы div с помощью простого CSS. Начнем со стилизации родительского элемента div с помощью селектора классов grid-container. Мы заявляем, что используем макет сетки, определяем размер строк и столбцов и говорим, что фон будет черным. Поскольку в родительском div будут ячейки, фон соответствует цвету границ ячеек. Затем мы стилизуем дочерние элементы div, говоря, что мы хотим, чтобы ячейки были на 80% непрозрачными белыми с крупным текстом по центру.

Наконец, в разделе сценария мы ссылаемся на наш файл shitty.js, чтобы мы могли манипулировать только что созданной сеткой. Технически мы могли бы сделать все в одном файле, но мне так больше нравится, поскольку мы можем разделить наши задачи.

Хранение и передача данных

Хорошо! Теперь, когда у нас есть построенный дисплей, мы можем перейти к файлу shitty.js и сосредоточиться на логике. Наш следующий шаг — создание структуры данных, которая может хранить значения тайлов. У нас будет два типа данных — тайлы (т.е. целые числа) и пустые пространства (т.е. нулевые значения). Мы хотим иметь возможность ссылаться на конкретную ячейку, поэтому структура данных должна быть чем-то похожа на сетку. Нам не нужно, чтобы данные сохранялись между сеансами, поэтому нам не нужно беспокоиться о базах данных и можно просто хранить их в памяти.

// Declare a data structure
let memory = [
    ["a1", "b1", "c1", "d1"],
    ["a2", "b2", "c2", "d2"],
    ["a3", "b3", "c3", "d3"],
    ["a4", "b4", "c4", "d4"]
];

Мое решение — это переменная под названием память. Это список списков, который похож на сетку и позволяет нам ссылаться на конкретную ячейку, используя нотацию memory[row][column]. Самое приятное в этом то, что мы можем программно читать и записывать данные. Давайте создадим функцию, которая сканирует memory в поисках любого значения, равного 2048, и сообщает игроку, что он выиграл, если находит то, что ищет.

// Checks if winning conditions have been met. Iterates over all items in 'memory' variable. If an item is equal to 2048, it alerts the user and tells them they won.

function checkWin(){
    for (let x=0;x<4;x++){
        for (let y=0;y<4;y++){
            if (memory[x][y] == 2048){
                alert("You won!");
            }
        }
    }
}

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

// Changes the value of a given element, provided an id attribute and the value. Changes the background and text color if certain criteria are met.

function changeValue(id, value) {
    let color;
    let text;
    switch (value) {
        case 2:
            color = "#DDFAEC";
            text = "black";
            break;
        case 4:
            color = "#C6F6E8";
            text = "black";
            break;
        case 8:
            color = "#B1F2EB";
            text = "black";
            break;
        case 16:
            color = "#9BE6ED";
            text = "black";
            break;
        case 32:
            color = "#87CDE7";
            text = "black";
            break;
        case 64:
            color = "#73AFE1";
            text = "black";
            break;
        case 128:
            color = "#5F8BDA";
            text = "black";
            break;
        case 256:
            color = "#5161C1";
            text = "black";
            break;
        case 512:
            color = "#4B44A7";
            text = "white";
            break;
        case 1024:
            color = "#4F378C";
            text = "white";
            break;
        case 2048:
            color = "#4E2B71";
            text = "white";
            break;
        default:
            color = "white";
            text = "black";
            break;
    }
    if (value != null) {
        document.getElementById(`${id}`).innerHTML = `${value}`;
    } else {
        document.getElementById(`${id}`).innerHTML = " ";
    }
    
    document.getElementById(`${id}`).style.backgroundColor = color;
    document.getElementById(`${id}`).style.color = text;
}

Здесь много кода… давайте посмотрим, что это такое. У нас есть функция, которая принимает два параметра: id (атрибут id элемента, который мы хотим изменить) и value (значение, на которое мы хотим его изменить). У нас есть неуклюжий оператор переключения, который изменяет цвет фона и текста ячейки в зависимости от значения (крик Масштаб для цветовой шкалы). Наконец, используя метод document.getElementById, мы меняем значение, цвет фона и цвет текста элемента, который мы указали в качестве параметра. Таким образом, changeValue("d4", "hello world") изменит значение ячейки D4 на hello world. Давайте масштабируем это на всю сетку, хорошо?

// Broadcasts the values from 'memory' variable onto the grid container.

function refreshTable(){
    changeValue("a1", memory[0][0])
    changeValue("b1", memory[0][1])
    changeValue("c1", memory[0][2])
    changeValue("d1", memory[0][3])
    changeValue("a2", memory[1][0])
    changeValue("b2", memory[1][1])
    changeValue("c2", memory[1][2])
    changeValue("d2", memory[1][3])
    changeValue("a3", memory[2][0])
    changeValue("b3", memory[2][1])
    changeValue("c3", memory[2][2])
    changeValue("d3", memory[2][3])
    changeValue("a4", memory[3][0])
    changeValue("b4", memory[3][1])
    changeValue("c4", memory[3][2])
    changeValue("d4", memory[3][3])
}

У нас есть структура данных, похожая на сетку, которая является переменной memory. Мы можем программно читать и записывать значения из нашей структуры данных, используя синтаксис memory[row][column], что позволяет нам выполнять вычисления и писать логические операторы, которые являются фундаментальными для нашей игры. Наконец, у нас есть способ транслировать содержимое нашей структуры данных на наш HTML-дисплей. Давайте проверим нашу установку.

let memory = [
    [2,4,8,16],
    [32,64,128,256],
    [512,1024,null,2],
    [null,null,null,null]
];
refreshTable()
checkWin()

Мы жестко закодировали переменную memory для целей тестирования. Если мы откроем наш файл index.html, мы должны ожидать увидеть:

  1. Значения контейнера сетки больше не являются метками ячеек («a1», «a2»…), какими они были до того, как мы начали писать JavaScript. Вместо этого значения контейнера сетки должны отражать значения memory.
  2. Если значения не равны нулю, цвет фона ячейки должен быть оттенком синего. Большие числа должны быть более темного оттенка синего, а меньшие числа должны быть более светлого оттенка синего.
  3. Нижняя строка должна быть белой, так как значения нулевые. (См. случай по умолчанию в операторе switch.)
  4. Поскольку плитки 2048 нет, сообщение «Вы выиграли!» сообщение не должно отображаться.

Теперь давайте посмотрим, сможем ли мы вызвать функцию checkWin(). C3 сейчас пуст, но если мы установим его равным 2048, он должен вызвать функцию. Используя консоль разработчика (щелкните правой кнопкой мыши на странице, нажмите Проверить в раскрывающемся меню, затем перейдите на вкладку Консоль), давайте установим изменение значения C3, набрав memory[2][2] = 2048; (помните, memory 0-индексируется !).

Мы успешно изменили значение memory[2][2], но оно не обновилось в контейнере сетки, поскольку мы не вызывали функцию refreshTable(). «Ты выиграл!» alert также не отображается, так как мы не вызывали функцию checkWin(). Давайте вызовем эти две функции в консоли.

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

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

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