В этом уроке мы собираемся построить простую игру в крестики-нолики с помощью фреймворка Vue. В этой игре один игрок ставит крестик, а другой нолик. Выиграет тот, кто может поместить три последовательных крестика или нолика в линию. Линия может быть вертикальной, горизонтальной или диагональной.
В этом руководстве предполагается, что вы немного знакомы с JavaScript и фреймворком Vue. Также необходимо, чтобы у вас были установлены Node и Git.
Причина, по которой мы создаем эту игру на Vue, кроется в простоте работы с этим фреймворком.
Настройка
Для настройки мы будем использовать vue-loader с vue-cli.
Vue-cli
Vue-cli представляет собой простой интерфейс командной строки (CLI) для построения проектов Vue. Он предоставляет шаблонный проект, позволяет писать ES2015, преобразовывать процессоры в простой CSS и обрабатывать все остальное. Установите Vue-cli на свой компьютер, используя следующую команду:
npm install -g vue-cli
Теперь, когда у вас есть Vue-cli, запустите в терминале Vue-cli-команды, чтобы смонтировать структуру проекта для вас:
vue init webpack-simple vue-tic-tac-toe
Здесь "vue-tic-tac-toe" - это название проекта, который инициализирует Vue-cli. "webpack-simple" - это шаблон, который включает в себя как Webpack, так и Vue-loader.
После этого перейдите в каталог "vue-tic-tac-toe"> и установите зависимости с помощью npm:
cd vue-tic-tac-toe
npm install
Vue-loader
Vue-loader - это загрузчик для Webpack, который позволяет вам писать шаблон и CSS-стили для компонента в одном файле (файл должен быть с расширением .vue). Вот пример .vue-файла:
<template>
<div class="message">
says: to the <world></world>
</div>
</template>
<script>
import World from './World.vue'
export default {
components: { World },
data () {
return {
speaker: 'Hammad',
message: 'Я захвачу мир'
}
}
}
</script>
<style>
.message {
padding: 10px;
background-color: steelblue;
color: #fff;
}
</style>
Создайте папку components в каталоге src. Она будет содержать все наши компоненты. Теперь, когда все готово, запустите эту команду, чтобы просмотреть это приложение в браузере:
npm run dev
Эта команда запускает http://localhost:8080/
в браузере. Каждое изменение, внесенное в ваш код, будет отображаться в браузере даже без обновления страницы. Откройте App.vue в редакторе кода и удалите все ненужные элементы, присутствующие в шаблоне и теге script. Теперь ваш файл App.vue выглядит следующим образом:
<template>
<div id="app"></div>
</template>
<script>
export default {
name: 'app',
data () {
return {
}
}
}
</script>
<style>
#app {
font-family: 'Avenir', Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
h1, h2 {
font-weight: normal;
}
ul {
list-style-type: none;
padding: 0;
}
li {
display: inline-block;
margin: 0 10px;
}
a {
color: #42b983;
}
</style>
Компоненты Vue
Преимущество использования компонентов в том, что мы можем их повторно использовать. Например, мы можем использовать компонент Cell
9 раз, без необходимости дублировать его. Наша структура компонентов будет выглядеть так:
-- App
---- Board
------ Cell x9
Создайте следующие компоненты: Board.vue и Cell.vue в папке components со следующим кодом:
<template></template>
<script>
export default {
data () {}
}
</script>
<style></style>
Стилизация
Чтобы убедиться, что эта игра не повредит вашим глазам, вы можете взять все стили и шрифты отсюда и вставить их в свой код.
В файле index.html я добавил только два шрифта и изменил название. Шрифт Dosis для всех элементов body, а шрифт Gochi Hand - для X и O, помещенных в сетку. Оба взяты из Google Fonts. Вот как выглядит наш index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Игра «Крестики-Нолики»</title>
<link href="https://fonts.googleapis.com/css?family=Dosis|Gochi+Hand" rel="stylesheet">
</head>
<body>
<div id="app"></div>
<script src="/dist/build.js"></script>
</body>
</html>
Измените код в теге <style>
в вашем компоненте App
:
<style>
body {
background-color: #fff;
color: #fff;
font-family: 'Dosis', sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
margin: 0px;
}
#app {
margin: 0 auto;
max-width: 270px;
color: #34495e;
}
h1 {
text-transform: uppercase;
font-weight: bold;
font-size: 3em;
}
.restart {
background-color: #e74c3c;
color: #fff;
border: 0px;
border-bottom-left-radius,
border-bottom-right-radius: 10px;
font-family: 'Dosis', sans-serif;
font-size: 1.4em;
font-weight: bold;
margin: 0px;
padding: 15px;
width: 100%;
}
.restart:hover {
background-color: #c0392b;
cursor: pointer;
}
.scoreBoard {
display: flex;
flex-direction: row;
justify-content: space-around;
align-items: center;
width: 100%;
height: 15px;
background-color: #16a085;
box-shadow: 10px solid #fff;
padding: 20px;
overflow-x: none;
}
.scoreBoard h2 {
margin: 0px;
}
.scoreBoard span {
float: right;
font-size: 1.5em;
font-weight: bold;
margin-left: 20px;
}
</style>
Добавьте этот код в тег <style>
в компоненте Grid
:
.grid {
background-color: #34495e;
color: #fff;
width: 100%;
border-collapse: collapse;
}
.gameStatus {
margin: 0px;
padding: 15px;
border-top-left-radius,
border-top-right-radius: 20px;
background-color: #f1c40f;
color: #fff;
font-size: 1.4em;
font-weight: bold;
}
.statusTurn {
background-color: #f1c40f;
}
.statusWin {
background-color: #2ecc71;
}
.statusDraw {
background-color: #9b59b6;
}
Добавьте этот код в компонент Cell
:
.cell {
width: 33.333%;
height: 90px;
border: 6px solid #2c3e50;
font-size: 3.5em;
font-family: 'Gochi Hand', sans-serif;
}
.cell:hover {
background-color: #7f8c8d;
}
.cell::after {
content: '';
display: block;
}
.cell:first-of-type {
border-left-color,
border-top-color: transparent;
}
.cell:nth-of-type(2) {
border-top-color: transparent;
}
.cell:nth-of-type(3) {
border-right-color,
border-top-color: transparent;
}
tr:nth-of-type(3) .cell {
border-bottom-color: transparent;
}
Компонент Templates
Раздел template
компонента содержит всю разметку, составляющую компонент. Компонент App
будет содержать компонент Gird
, а компонент Gird
будет содержать 9 компонентов Cell
. Компонент App
очень прост и содержит только заголовок и сетку для игры. Мы добавим дополнительные функции позже.
<div id="app">
<div id="details">
<h1>Tic Tac Toe</h1>
</div>
<grid></grid>
</div>
Компонент Grid
содержит таблицу, содержащую три строки и три ячейки в каждой строке. Номер ячейки передается в качестве идентификатора, чтобы однозначно идентифицировать каждую ячейку. Шаблон компонента Grid
следующий:
<table class="grid">
<tr>
<cell name="1"></cell>
<cell name="2"></cell>
<cell name="3"></cell>
</tr>
<tr>
<cell name="4"></cell>
<cell name="5"></cell>
<cell name="6"></cell>
</tr>
<tr>
<cell name="7"></cell>
<cell name="8"></cell>
<cell name="9"></cell>
</tr>
</table>
Компонент Cell
содержит только тег td для отметок X
или O
.
<td class="cell"></td>
Игровой поток
Чтобы начать добавлять функциональные возможности в нашу игру, нам нужно определить поток событий, который будет происходить при каждом взаимодействии пользователя. Поток игры выглядит следующим образом:
- Приложение загружено.
- Все ячейки пусты.
- Первый игрок рисует
O
в любой понравившейся ему ячейке.
- Настала очередь второго игрока (каждый раз, когда игрок ставит метку, ход передается неактивному игроку).
- После каждого хода мы должны проверить, соответствует ли игра каким-либо условиям победы или произошла ничья.
- После завершения игры или во время игры можно нажать кнопку "Перезапустить">, чтобы начать игру сначала.
- Должен показываться статус игры (выполняется, выиграна или ничья).
- С точки зрения прогресса, состояние отображает ход соответствующего игрока.
- В игре также отображается количество матчей и количество побед для каждого игрока.
Чтобы все это могло произойти, нашим компонентам нужны некоторые свойства и методы данных.
Свойства данных
Мы разделим данные между компонентами в соответствии с их отношением или легкости доступа к этому компоненту. Компонент App
будет содержать количество матчей и количество побед для каждого игрока.
data () {
return {
matches: 0,
wins: {
O: 0,
X: 0
}
}
}
Компонент Grid содержит данные для активного игрока (X
или O
), статус игры, сообщение о статусе, цвет статуса (для отображения на верхней панели), количество ходов, которые оба игрока сыграли, место для каждой ячейки и все 8 условий выигрыша. Массив условий победы содержит 8 массивов, и каждый массив содержит возможную выигрышную ячейку (все X
или все O
). Эти условия можно сравнить с проверкой победы объектов ячеек.
data () {
return {
// может быть O или X
activePlayer: 'O',
// отображает статус игры: игра идет, победа или ничья
gameStatus: 'turn',
gameStatusMessage: `O's turn`,
// цвет статуса используется в качестве цвета фона в строке состояния.
// он может содержать имя любого из следующих CSS классов
// StatusTurn (по умолчанию) - желтый во время игры
// statusWin - зеленый при победе
// statusDraw - фиолетовый при ничьей
gameStatusColor: 'statusTurn',
// количество ходов, сделанных игроками (max = 9)
moves: 0,
// сохраняет размещение X и O в ячейках по номеру их ячейки
cells: {
1: '', 2: '', 3: '',
4: '', 5: '', 6: '',
7: '', 8: '', 9: ''
},
// Содержит все (8) возможных условий победы
winConditions: [
[1, 2, 3], [4, 5, 6], [7, 8, 9], // строки
[1, 4, 7], [2, 5, 8], [3, 6, 9], // столбцы
[1, 5, 9], [3, 5, 7] // диагонали
],
}
}
Компонент Cell
содержит отметку о том, что игрок поместил в нее. По умолчанию для этого значения задается пустая строка. Свойство frozen используется для того, чтобы гарантировать, что игрок не сможет изменить метку после ее помещения.
props: ['name'],
data () {
return {
// позволяет игроку размещать метку
frozen: false,
// содержит X или O, которые будут отображаться в td
mark: ''
}
}
Компонент Cell
также имеет свойство имени для уникального определения каждой клетки по ее номеру.
Событие Bus
Наши компоненты должны общаться друг с другом, чтобы информировать об изменении своего свойства данных или о действии, выполняемом пользователем, например о размещении метки в ячейке. Для этого мы будем использовать шину событий. Чтобы создать шину событий, мы присвоим новый экземпляр Vue свойству, называемому Event в окне объекта. Его можно изменить на что угодно в зависимости от контекста вашего кода, но здесь именно Event подойдет наилучшим образом.
window.Event = new Vue()
С его помощью вы можете делать что-то вроде Event.$emit()
и Event.$on
для запуска и прослушивания событий соответственно. Имя события будет первым аргументом. Вы можете также включить любые другие данные после аргумента name. Другие данные называются полезной нагрузкой. Рассмотрим следующий пример:
Event.$emit('completed', this.task)
Он запускает событие completed и передает this.task
как полезная нагрузка. Вы можете прослушать это событие с помощью метода Event.$on()
следующим образом:
Event.$on('completed' (task) => {
// что-нибудь делаем
})
Чтобы продолжить прослушивание fire, мы можем поместить метод Event.$on
в созданный метод Vue-компонента.
created () {
Event.$on('completed' (task) => {
// что-нибудь делаем
})
}
После добавления события Event в ваш файл main.js
, как это показано здесь:
import Vue from 'vue'
import App from './App.vue'
window.Event = new Vue()
new Vue({
el: '#app',
render: h => h(App)
})
Теперь мы готовы стрелять и слушать события в нашей игре. Мы продолжим, добавив функциональность к нашей игре в следующем уроке.