2048 - популярная однопользовательская игра. В игре используется сетка 4 на 4 для плиток. Каждая плитка может быть пустой или содержать карточку со значением. Как игрок, вы можете перемещаться влево, вправо, вверх и вниз. Когда карты объединяются, номера на картах также меняются. Цель состоит в том, чтобы число на карте было как можно большим. Игра заканчивается, когда все плитки заполнены и не остается следующих ходов.

Вы можете найти демо-проект здесь, а репозиторий Github здесь. В этом сообщении блога (часть 1) и следующем сообщении блога (часть 2) мы шаг за шагом рассмотрим, как мы создаем игру!

Начать работу с проектными каркасами

Во-первых, нам нужно настроить каркас проекта для создания проекта Vue. Мы собираемся использовать простой javascript, чтобы понять, что происходит. Начнем с клонирования простой основы JavaScript Vue. Scaffold предоставляет нам несколько каталогов:

  • активы. Все сторонние библиотеки javascript, таблицы стилей и изображения.
  • компоненты. Все собственные компоненты Vue.
  • миксины. Все нативные миксины Vue.
  • магазин. Магазин Vuex для управления состоянием.
  • main.js. Место, где инициируется новый экземпляр Vue.
  • index.html. Место, где отображается все приложение.

Шаг 1. Настройте игровую доску

На этом этапе мы:

  1. создать game компонент с board
  2. создать теневую доску
  3. создать функцию посева

Сначала у нас есть game компонент, отвечающий за плату. Доска имеет много плиток, и каждая плитка имеет значение по умолчанию 0. Мы можем начать с регистрации компонент game:

src / components / game.js

((() => {
  const html = `
    <div class="game">
    </div>
  `
Vue.component("game", {
    template: html,
    data () {
      return {
        board: [],
      }
    },
  })
}))()

В компоненте Vue game у нас есть свойство данных board, которое содержит массив плиток, каждая плитка имеет значение по умолчанию 0. Затем мы добавляем теневую доску в качестве базовый слой настольной игры внутри нашего html.

src / components / game.js

((() => {
  const html = `
    <div class="game">
      <div class="game-container">
       <div class="board shadow-board">
          <div v-for="n in board.length" :key="n" class="tile shadow-tile"></div>
        </div>
      </div>
    </div>
  `
  ...
}))()

Теперь, когда вы откроете index.html, это должно выглядеть так:

Чтобы начать игру и продолжать играть, нам нужно присвоить некоторым плиткам игровое значение (т.е. значение ›0). Для этого мы создадим метод seedTwo (). В начале каждой игры мы будем использовать это для создания начального состояния, а после правильного хода мы будем использовать это, чтобы игра продолжала двигаться вперед.

src / components / game.js

Vue.component("game", {
  ...
  methods: {
    seedTwo() {
      const self = this
      let getRandomItem = function() {
        let row =   
           self.board[Math.floor(Math.random()*self.board.length)]
        return row[Math.floor(Math.random()*row.length)]
      }
      let initialRandomItem = getRandomItem()
      while (initialRandomItem.value != 0) {
        initialRandomItem = getRandomItem()
      }
      initialRandomItem.value = 2
    }
  }
})
...

Шаг 2. определение объектных моделей

На этом этапе мы:

  1. создать Tile() компонент
  2. изменить Tile() класс CSS в зависимости от того, пуста плитка или нет
  3. создать GameMenu() компонент

Доска может содержать 16 плиток в виде плоского массива, хотя мы представим его на доске как двумерный массив. Добавляем еще один компонент tile под src/components/tile.js. Компоненты tile принимают объект плитки как обязательные свойства, которые должны включать {value: X} как часть объекта.

src / components / tile.js

((() => {
  const html = `
    <div class="tile">
      {{ value }}
    </div>
  `
Vue.component("tile", {
    template: html,
    props: {
      tile: {
        type: Object,
        required: true
      },
    },
computed: {
      value() {
        return this.tile.value
      },
    }
  })
}))()

Обратите внимание, что в нашем конечном продукте стиль каждой плитки меняется, когда мы меняем плитку с пустой (значение 0) на непустую (значение не 0). Внутри компонента мы конструируем вычисляемые свойства на основе значения, а позже мы используем вычисляемое свойство для переключения стиля с помощью привязки класса:

src / components / tile.js

((() => {
  const html = `
    <div class="tile">
      {{ displayingValue }}
    </div>
  `
Vue.component("tile", {
    template: html,
    props: {
      tile: {
        type: Object,
        required: true
      },
    },
    computed: {
      value() {
        return this.tile.value
      },
     displayingValue() {
       if (this.value > 0) {
         return this.value
       }
       return null
     },
     emptyTile() {
      return this.displayingValue === null
     },
    }
  })
}))()

Если у нас есть компонент tile, мы снова подключаем tile к компоненту game:

src / components / game.js

((() => {
  const html = `
    <div class="game">
      <div class="game-container">
        <tile v-for="tile in board" :tile="tile" :key="tile.id"></tile>
        <div class="board shadow-board">
          <div v-for="n in board.length" :key="n" class="tile shadow-tile"></div>
        </div>
      </div>
    </div>
  `
  ...
}))()

Теперь, когда у нас есть вычисленное свойство emptyTile, мы можем подключиться к нашему html и переключить класс:

src / components / tile.js

((() => {
  const html = `
    <div class="tile" v-bind:class="{'tile-empty': emptyTile}">
      {{ displayingValue }}
    </div>
  `
Vue.component("tile", {
    ...
  })
}))()

Затем мы можем определить game-menu, чтобы показать текущий результат, лучший результат и начать новую игру. Начнем с простой лески:

src / components / game-menu.js

((() => {
  const html = `
    <div class="game-menu">
      <div class="row">
        <div class="title">2048</div>
        <div class="scores space-right">
          <div class="score">
            <div class="score-title">SCORE</div>
            <div class="score-value">0000</div>
          </div>
          <div class="score">
            <div class="score-title">BEST</div>
            <div class="score-value">0000</div>
          </div>
        </div>
      </div>
      <a class="button space-right">New Game</a>
    </div>
  `
Vue.component("game-menu", {
    template: html,
  })
}))()

Подключим кнопку Новая игра! Потому что game-menu компонент вложен в game компонент. Мы можем рассматривать game-menu как дочерний компонент для родительского компонента game. Vue имеет прекрасную парадигму для взаимодействия дочерних и родительских элементов, когда дочерний компонент может генерировать события до родительского компонента и позволять родительскому компоненту обрабатывать событие. Эта парадигма называется данные вниз, событие вверх. Подробнее об обработке событий Vue можно прочитать здесь.

Итак, давайте зарегистрируем события на game-menu.js

src / components / game-menu.js

((() => {
  const html = `
    <div class="game-menu">
      ...
      <a class="button space-right" @click="newGame()">New Game</a>
    </div>
  `
Vue.component("game-menu", {
    template: html,
    methods: {
      newGame() {
        this.$emit("new-game")
      }
    }
  })
}))()

this. $ emit («new-game») отправляет событие своему родительскому game, и внутри game.js нам нужно обработать событие. При получении события new-game компонент game вызывает метод newGame (), который сбрасывает доску и заполняет две плитки, дважды вызывая seedTwo ().

src / components / game.js

((() => {
  const html = `
    <div class="game">
      <game-menu @new-game="newGame()"></game-menu>
      ...
    </div>
  `
  Vue.component("game", {
    template: html,
    ...
    methods: {
      seedTwo() {
        ...
      },
      newGame() {
        this.resetBoard()
        this.seedTwo()
        this.seedTwo()
      },
      resetBoard() {
        this.board = Array.apply(null, { length: 16 })
          .map(function (_, index) { 
           return { id: index, value: 0 }
        })
      }
    }
  })
}))()

Шаг 3. Выполните слияние и сдвиньте плитки

На этом этапе мы:

  1. Используя moveRight() в качестве примера, поймите, как алгоритмически объединять и сдвигать плитки
  2. Используя moveRight() в качестве примера, реализуйте слияние и слайд
  3. Зарегистрируйте элементы управления в компоненте game с помощью Vue mixin

По сути, у нас есть четыре разных метода реализации moveRight, moveLeft, moveUp и moveDown. Как только мы реализуем один метод, остальные будут следовать тому же шаблону и, следовательно, их будет легче реализовать. Давайте рассмотрим moveRight() в качестве примера.

moveRight() состоит из двух этапов: слияние и слайд. На этапе слияния мы выясняем две плитки, которые нужно объединить в одну; На втором шаге мы выясняем будущее положение каждой плитки. Поскольку мы движемся горизонтально для moveRight(), алгоритм просматривает каждую строку и запускает mergeRight, затем slideRight. Анимация ниже демонстрирует, как алгоритм moveRight() работает с одной строкой:

Ниже приведен код, реализующий алгоритм для moveRight():

src / mixins / control.js

mergeRight(board, a) {
  let i = board.length - 2
  let j = board.length - 1
        
  while (i >= 0) {
   if (board[a][i].value === 0 && board[a][j].value === 0) { 
   // if both elements are zero
     j --
     i --
   } else if (board[a][i].value === board[a][j].value) { 
   // if two elements have same value
     board[a][j].value = board[a][i].value + board[a][j].value
     board[a][i].value = 0
     
     j--
     i--
   } else if (board[a][j].value === 0) { 
   // if the right most has 0
     j--
     i--
   } else if (board[a][i].value != 0 && board[a][j].value != 0 && (i + 1 == j)) { // if both are non zero and next from each other
     j--
     i--
   } else if (board[a][i].value != 0 && board[a][j].value != 0) { 
     // if both are non zero and not next from each other
     j--
   } else if (board[a][i].value === 0) { 
     // if the left most element is zero
     i--
   }
  }
},
slideRight(board, a) {
  let k = board.length - 2
  let l = board.length - 1
  while (k >= 0) {
    if (board[a][l].value !== 0) { 
      // if right most element is 0
      l --
      k --
    } else if (board[a][l].value !== 0 && board[a][k].value !== 0) {      
      // if right most and left most elements are not 0
      l --
      k --
    } else if (board[a][l].value === 0 && board[a][k].value === 0) { 
      // if right most and left most elements are 0
      k --
    } else if (board[a][l].value === 0 && board[a][k].value !== 0) { 
      // if right most element is 0 and left most element is not 0
      board[a][l].value = board[a][k].value + board[a][l].value
      board[a][k].value = 0
      l --
      k --
    }
  }
}

После реализации moveRight, moveLeft, moveUp и moveDown. Мы можем изолировать все эти методы в src/mixins/control.js, а затем мы можем включить элемент управления как миксин для game компонента. Теперь, когда установлен game, мы регистрируем элементы управления:

src / components / game.js

((() => {
  const html = `
    ...
  `
  Vue.component("game", {
    template: html,
    mixins: [window.app.mixins.control],
    ...
    methods() {
      setupBoard() {
        this.newGame()
        this.registerControl()
      }
    }
  })
}))()

Мы довольно далеко зашли в этом сообщении в блоге! Во второй части этой серии блогов мы рассмотрим, как:

  1. Добавьте анимацию FLIP к движению наших плиток с помощью Vue.js
  2. Используйте Vuex для отслеживания результатов и состояния игры.