В этой части мы закончим нашу игру, применяя все функциональные возможности, чтобы в нее можно было играть. Мы будем активно использовать события, которые реализовали в предыдущей части, а еще дадим нашим игрокам возможность перезапустить игру и отобразить количество сыгранных матчей.
Listening For Strikes
Чтобы игрок мог пометить любую из ячеек, мы должны позволить ему нажать на нее и отследить это действие. В компоненте Cell добавьте атрибут v-on:click
в тег <td>. Мы будем использовать сокращенный синтаксис @click
. Ответ на клик реализуем через метод strike.
<td class="cell" @click="strike">{{ mark }}</td>
В методе strike нам нужно получить активного игрока из его родительского компонента, который является компонентом Grid, и использовать его для изменения свойства mark. Cвойство frozen имеет значение true
, потому что мы не хотим, чтобы какой-либо игрок заменял уже отмеченную ячейку. Нам также нужно сообщить компоненту Grid об этом изменении. Мы будем запускать событие strike для компонента Grid:
methods: {
strike () {
if (! this.frozen) {
// Получаем либо X, либо O из компонента Grid
this.mark = this.$parent.activePlayer
this.frozen = true
// Запускаем событие, чтобы уведомить
// компонент Grid о размещении метки
Event.$emit('strike', this.name)
}
}
}
Теперь компонент Grid должен прослушивать это событие и реагировать соответствующим образом, а именно: компонент Grid должен заполнить соответствующий номер ячейки меткой в объекте cells, увеличить количество ходов, изменить статус игры и сменить активного игрока на неактивного. Проверка статуса игры также определяет была ли игра выиграна или же вышла «ничья». Добавьте следующий код в метод created компонента Grid, чтобы мы всегда слушали событие fire:
// прослушиваем клик пользователя по ячейке
Event.$on('strike', (cellNumber) => {
// устанавливает X, либо O в массиве cells
this.cells[cellNumber] = this.activePlayer
// увеличиваем количество ходов
this.moves++
// сохраняем статус игры, вызывая метод changeGameStatus
this.gameStatus = this.changeGameStatus()
this.changePlayer()
})
Первые две строки кода задают только свойства данных. Следующие две строки вызывают методы, которые нам еще предстоит создать. Метод changePlayer()
очень прост. Все, что он делает, - меняет активного игрока на неактивного. Если активным игроком является игрок O
, активным станет игрок X
и наоборот. Чтобы упростить наш код, мы можем использовать вычисленное свойство для получения nonActivePlayer. Свойство computed обработает эту задачу и оставит методу changePlayer()
. Свойство computed будет выглядеть так:
computed: {
// вспомогательное свойство,
// чтобы получить неактивного игрока
nonActivePlayer () {
if (this.activePlayer === 'O') {
return 'X'
}
return 'O'
}
}
Метод changePlayer()
выглядит так:
// Изменяет активного игрока на неактивного
// с помощью вычисленного свойства nonActivePlayer
changePlayer () {
this.activePlayer = this.nonActivePlayer
},
Отображение статуса
Событие strike изменяет значение свойства gameStatus с помощью метода changeGameStatus()
(который нам еще предстоит создать). В этом методе нам нужно проверить произошел ли выигрыш, «ничья» или игра продолжается. Это мы сделаем с помощью метода checkForWin(), который создадим позже. Если условие выигрыша выполнено, мы возвращаем метод gameisWon()
(им мы тоже потом займемся). Также мы должны проверить на «ничью», чтобы гарантировать, что игра еще не выиграна ни одним из игроков и все ячейки заполнены. Если ни одино из условий не выполнено, мы просто возвращаем turn
, то есть значение gameStatus по умолчанию.
// возвращаем статус игры в свойство gameStatus
changeGameStatus () {
if (this.checkForWin()) {
return this.gameIsWon()
// проверяет, была ли победа в игре и есть ли свободные ячейки
} else if (this.moves === 9) {
// устанавливаем статус «draw»
return 'draw'
}
// устанавливаем статус «turn»
return 'turn'
}
// сохраняем статус игры, вызывая метод changeGameStatus
this.gameStatus = this.changeGameStatus()
В зависимости от значения gameStatus нам нужно изменить и другие свойства, например gameStatusMessage и gameStatusColor. Мы можем использовать функцию watch, доступную в Vue. Объект watch прослушивает любое изменение какого-либо свойства данных и помогает вам выполнять определенные действия.
Используя его, мы можем изменить значение gameStatusMessage и gameStatusColor, если gameStatus изменилось:
watch: {
// для изменения статусов сообщения и цвета
gameStatus () {
if (this.gameStatus === 'win') {
this.gameStatusColor = 'statusWin'
this.gameStatusMessage = `${this.activePlayer} Wins !`
return
} else if (this.gameStatus === 'draw') {
this.gameStatusColor = 'statusDraw'
this.gameStatusMessage = 'Draw !'
return
}
this.gameStatusMessage = `${this.activePlayer}'s turn`
}
}
Вышеприведенный код довольно прост. Он устанавливает статусы сообщения и цвет, используя значение gameStatus. В этом коде я использую шаблонные строки (это новая фишка ES6). Проблема состоит в том, что многие браузеры не понимают новый JavaScript-синтаксис (ES6), но нас это не пугает, поскольку мы используем настройку vue-loader. Он использует Babel для перекомпиляции кода для более старых браузеров.
Мы можем отображать статус, используя data-свойства игрока. Добавьте <div> к тегу шаблона компонента Grid непосредственно перед тегом <table>.
<div class="gameStatus" :class="gameStatusColor">
{{ gameStatusMessage }}
</div>
:class - это сокращенный синтаксис для v-bind:class. Это помогает динамически переключать классы в зависимости от значения атрибута.
Проверка на выигрыш
При написании кода для изменения статуса игры мы вызывали методы checkForWin()
и gameIsWon()
. Нам нужно их сделать. Помните, в начале этого урока мы добавили массив, называемый winConditions как data-свойство. Воспользуемся этим. Чтобы проверить выигрыш, мы переберем массив winConditions как номер ячейки и проверим, есть ли в трех из них одинаковый символ X
или O
.
// Проверяет возможные условия выигрыша из data
checkForWin () {
for (let i = 0; i < this.winConditions.length; i++) {
// Получает единственное wc-условие из всего массива
let wc = this.winConditions[i]
let cells = this.cells
// Сравнивает 3 значения ячеек с условием
if (this.areEqual(cells[wc[0]], cells[wc[1]], cells[wc[2]])) {
return true
}
}
return false
}
Этот метод использует дополнительную вспомогательную функцию areEqual
, чтобы сохранить код чистым. Метод areEqual
выглядит следующим образом:
// Вспомогательная функция для сравнения значений ячейки
areEqual () {
var len = arguments.length;
// Проходит через каждое значение и сравнивает их
// с пустой строкой и на неравенство
for (var i = 1; i < len; i++){
if (arguments[i] === '' || arguments[i] !== arguments[i-1])
return false;
}
return true;
}
Данный метод берет все аргументы, проходит по ним круг, проверяет и сравнивает их с пустой строкой и возвращает нулевое значение если текущий аргумент не равен предыдущему. Если круг пройден, то есть, не вернул нулевое значение, то наконец-то возвращается значение «истина».
В методе gameIsWon()
, который еще предстоит создать, мы будем делать следующее. Во-первых, мы будем запускать событие, называемое win с activePlayer в качестве полезной нагрузки, а затем вернем строку, содержащую win
, для размещения в свойстве gameStatus. С помощью этого события мы можем управлять количеством побед каждого игрока и отображать это число на табло. Во-вторых, мы устанавливаем сообщение статуса. В-третьих, мы запускаем событие, чтобы заморозить ячейки. И, наконец, возвращаем строку «win».
gameIsWon () {
// Срабатывает событие win для компонента App,
// чтобы изменить оценку
Event.$emit('win', this.activePlayer)
// Устанавливает сообщение о статусе игры
this.gameStatusMessage = `${this.activePlayer} Wins !`
// Запускает событие для блокировки ячейки
Event.$emit('freeze')
// Установить статус win
return 'win'
}
Нужно проверить его и установить постоянное свойство компонента ячейки на «true». Добавьте этот код в созданный метод компонента Cell.
Event.$on('freeze', () => this.frozen = true)
Табло
Компонент приложения должен прослушать событие win, которое мы инициировали в gameIsWon. Все, что нам нужно делать при прослушивании этого события, - увеличить число выигрышей для соответствующего игрока. Этот игрок нам доступен, поскольку он был передан как полезная нагрузка при запуске события. Добавьте следующий созданный метод компонента App.
created () {
Event.$on('win', winner => this.wins[winner]++)
}
Мы знаем количество побед, так почему бы не отобразить его игроку? Добавьте приведенную ниже разметку в шаблон компонента App чуть выше <div>
c идентификатором app.
<div class="scoreBoard">
<span>O победил {{ wins.O }} раз</span>
<h2>абло</h2>
<span>X победил {{ wins.X }} раз</span>
</div>
Вы должны заключить всю разметку в шаблоне в родительский <div>
, чтобы Vue не выдавал предупреждений. У нас в шаблоне может быть только один элемент верхнего уровня.
Перезапуск игры
В нашу Vue-игру уже можно уже играть, вот только пользователи могут провести только один бой, не прибегая к перезагрузке браузера. Давайте сделаем кнопку перезапуска, чтобы наши игроки могли сыграть столько матчей, сколько захотят. Добавьте следующий тег в шаблон компонента App сразу после тега <grid>
:
<button class="restart"
@click="restart">Restart</button>
Эта кнопка прослушивает клики и вызывает метод перезапуска при каждом нажатии. Осталось добавить этот метод, чтобы кнопка работала. В этом методе мы должны очистить свойство метки у всех ячеек и сбросить все данные компонента Grid.
restart () {
Event.$emit('clearCell')
Event.$emit('gridReset')
this.matches++
}
Здесь мы вызываем два метода и увеличиваем число матчей. Первый метод вызывается для компонента ячейки, чтобы очистить все из них. Второй – для компонента сетки, он сбрасывает все данные. Нам нужно выслушать оба события.
С первым все просто. В компоненте Cell мы установим в свойство mark пустую строку, чтобы ее можно было заменить меткой любого игрока. Мы также переключаем свойство frozen на false
, чтобы наш игрок мог поместить отметку. Добавьте этот код в метод created компонента Cell.
Event.$on('clearCell', () => {
this.mark = ''
this.frozen = false
})
Второй метод, как уже говорилось, очистит все свойства данных компонента сетки. Для этого мы будем использовать функцию Object.assign()
. Он принимает целевой объект как первый аргумент, а исходный объект - как второй аргумент. Добавьте этот код в метод created компонента Grid.
// Прослушивает кнопку перезапуска
// Нажмите для повторной инициализации
// Вызывается компонентом App
Event.$on('gridReset', () => {
Object.assign(this.$data, this.$options.data())
})
$data
возвращает весь объект данных, а $options.data()
– начальное состояние данных, как указано в объекте. Таким образом, в этом коде начальные данные настроены в объекте данных, что, другими словами, сбрасывает данные компонента.
Номер матча
Наконец, мы можем отобразить номер матча, в который играет игрок. Добавьте этот тег <h2> под тегом <h1> в App-компоненте.
<h2>Match #{{ matches + 1 }}</h2>
Мы добавляем 1
, потому что хотим отобразить номер текущего матча, а не количество матчей, которые были сыграны.
Заключение
Вот и закончилась вторая часть урока. Надеюсь, вам понравилось играть со мной в эту игру.