Игра Connect 4 возвращает воспоминания прошлых лет. Эта классическая игра наверняка произвела впечатление на всех, кто в нее играл. В этой статье мы собираемся создать многопользовательскую версию Connect 4 с использованием Node.js и Socket.io .
Установка зависимостей
В этом руководстве предполагается, что у вас установлены Node.js и npm . Чтобы управлять зависимостями веб-интерфейса, мы будем использовать Bower для получения пакетов и Grunt для управления задачами. Откройте терминал и установите Bower and Grunt по всему миру, выполнив:
1
|
$ sudo npm install -g bower grunt-cli
|
Примечание: Grunt требует версии Node.js> = 0.8.0. На момент написания этой статьи в репозиториях Ubuntu была более старая версия Node. Пожалуйста, убедитесь, что вы используете PPA Криса Ли, если вы используете Ubuntu. Для других дистрибутивов / операционных систем, пожалуйста, обратитесь к документации по установке Node.js для получения последней версии.
С установленными Bower и Grunt-cli давайте создадим каталог для проекта и загрузим Twitter Bootstrap и Alertify.js (для управления уведомлениями о предупреждениях) с помощью Bower.
1
2
3
|
$ mkdir connect4
$ cd connect4
$ bower install bootstrap alertify.js
|
Теперь давайте настроим каталог для управления нашими пользовательскими активами. Мы назовем его assets
и будем хранить в нем наши пользовательские файлы Less и JavaScript.
1
2
|
$ mkdir -p assets/{javascript,stylesheets}
$ touch assets/javascript/<code>frontend.js</code> assets/stylesheets/<code>styles.less</code> assets/stylesheets/variables.less
|
Для обслуживания скомпилированных ресурсов мы создадим каталог с именем static
с подкаталогами с именем javascript
и stylesheets
.
1
|
$ mkdir -p static/{javascript,stylesheets}
|
Откройте assets/stylesheets/styles.less
и импортируйте variables.less
и необходимые файлы Less из начальной загрузки.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
|
// Core variables and mixins
@import «../../bower_components/bootstrap/less/variables.less»;
@import «../../bower_components/bootstrap/less/mixins.less»;
// Reset
@import «../../bower_components/bootstrap/less/normalize.less»;
@import «../../bower_components/bootstrap/less/print.less»;
// Core CSS
@import «../../bower_components/bootstrap/less/scaffolding.less»;
@import «../../bower_components/bootstrap/less/type.less»;
@import «../../bower_components/bootstrap/less/code.less»;
@import «../../bower_components/bootstrap/less/grid.less»;
@import «../../bower_components/bootstrap/less/tables.less»;
@import «../../bower_components/bootstrap/less/forms.less»;
@import «../../bower_components/bootstrap/less/buttons.less»;
// Components
@import «../../bower_components/bootstrap/less/component-animations.less»;
@import «../../bower_components/bootstrap/less/glyphicons.less»;
@import «../../bower_components/bootstrap/less/dropdowns.less»;
@import «../../bower_components/bootstrap/less/navbar.less»;
@import «../../bower_components/bootstrap/less/jumbotron.less»;
@import «../../bower_components/bootstrap/less/alerts.less»;
@import «../../bower_components/bootstrap/less/panels.less»;
@import «../../bower_components/bootstrap/less/wells.less»;
// Utility classes
@import «../../bower_components/bootstrap/less/utilities.less»;
@import «../../bower_components/bootstrap/less/responsive-utilities.less»;
// Custom variables
@import «variables.less»;
// Alertify
@import (less) «../../bower_components/alertify.js/themes/alertify.core.css»;
@import (less) «../../bower_components/alertify.js/themes/alertify.default.css»;
// Custom Styles
|
Сделав это, давайте Gruntfile.js
для компиляции файлов Less в CSS и объединения всех файлов JavaScript в один файл. Базовая структура файла Gruntfile.js
с некоторыми задачами будет выглядеть примерно так:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
|
//Gruntfile
module.exports = function(grunt) {
//Initializing the configuration object
grunt.initConfig({
// Task configuration
less: {
//…
},
concat: {
//…
},
watch: {
//…
}
});
// Load plugins
// Define tasks
};
|
Задачи Актива
Мы определим три задачи для управления активами. Первым будет скомпилировать все файлы Less в CSS. Второй будет объединять все файлы JavaScript в один и, наконец, последняя задача будет следить за изменениями файлов. Задача наблюдения будет заданием по умолчанию и может быть запущена путем ввода grunt
в корне проекта, как только мы закончим настройку gruntfile.
Давайте настроим задачу для компиляции всех файлов Less в файлы CSS в каталоге static/stylesheets
.
01
02
03
04
05
06
07
08
09
10
|
less: {
development: {
options: {
compress: true,
},
files: {
«./static/stylesheets/styles.css»: «./assets/stylesheets/<code>styles.less</code>»,
}
}
},
|
Двигаясь дальше, мы настроим еще одну задачу, чтобы объединить все файлы JS в один.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
|
concat: {
options: {
separator: ‘;’,
},
js: {
src: [
‘./bower_components/jquery/jquery.js’,
‘./bower_components/bootstrap/dist/js/bootstrap.js’,
‘./bower_components/alertify.js/lib/alertify.js’,
‘./assets/javascript/<code>frontend.js</code>’
],
dest: ‘./static/javascript/<code>frontend.js</code>’,
},
},
|
Наконец, давайте установим задачу наблюдения, чтобы следить за изменениями в наших файлах и выполнять необходимые задачи при сохранении.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
|
watch: {
js: {
files: [
‘./bower_components/jquery/jquery.js’,
‘./bower_components/bootstrap/dist/js/bootstrap.js’,
‘./bower_components/alertify.js/lib/alertify.js’,
‘./assets/javascript/<code>frontend.js</code>’
],
tasks: [‘concat:js’]
},
less: {
files: [‘./assets/stylesheets/*.less’],
tasks: [‘less’]
},
}
|
После этого мы загрузим необходимые плагины npm и зарегистрируем задачу по умолчанию.
1
2
3
4
5
6
7
|
// Load plugins
grunt.loadNpmTasks(‘grunt-contrib-concat’);
grunt.loadNpmTasks(‘grunt-contrib-less’);
grunt.loadNpmTasks(‘grunt-contrib-watch’);
// Define tasks
grunt.registerTask(‘default’, [‘watch’]);
|
Давайте перейдем к управлению внутренними зависимостями с помощью npm. Для этого проекта мы будем использовать платформу Express с движком шаблонов Jade и Socket.io. Установите зависимости локально, выполнив следующую команду:
1
|
$ npm install express jade socket.io async grunt grunt-contrib-concat grunt-contrib-less grunt-contrib-watch
|
Структура каталогов теперь должна быть похожа на эту:
Теперь, когда мы настроили наши зависимости, пришло время перейти к созданию интерфейса нашей игры.
Фронтенд
Давайте продолжим, создав файл с именем server.js
и обслуживая контент с помощью Express.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
|
var express = require(‘express’);
var async = require(‘async’);
var app = express()
var io = require(‘socket.io’).listen(app.listen(8000));
app.use(‘/static’, express.static(__dirname + ‘/static’));
app.get(‘/’, function(req, res) {
res.render(‘index.jade’);
});
app.get(‘/landingPage’, function(req, res) {
res.render(‘landing.jade’);
});
console.log(‘Listening on port 8000’);
|
Шаблоны
Мы используем Jade Templating Engine для управления шаблонами. По умолчанию Express ищет представления внутри каталога представлений. Давайте создадим каталог views
и создадим файлы Jade для макета, индекса и страницы благодарности.
1
2
|
$ mkdir -p views
$ touch views/{layout.jade,index.jade,landing.jade}
|
Далее давайте отредактируем макет нашего проекта, индексную страницу и целевую страницу ( landing.jade
).
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
|
doctype html
html(lang=»en»)
head
title Connect 4
link(rel=’stylesheet’, href=’static/stylesheets/styles.css’)
body
#wrap
nav.navbar.navbar-default(role=’navigation’)
.container-fluid
.navbar-header
a.navbar-brand(href=’#’) Connect 4
block content
#footer
.container
p.text-muted
|
|
block javascript
script(src=’/socket.io/socket.io.js’)
script(src=’static/javascript/<code>frontend.js</code>’)
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
|
extends layout
block content
.container
.row
.col-xs-3
.p1-score
p 0
#board.col-xs-6
table.center-table
.form-group
label(for=»shareUrl»).col-sm-3.control-label.share-label Share URL:
.col-sm-9
input(type=’text’ ReadOnly).form-control
.col-xs-3
.p2-score
p 0
|
1
2
3
4
5
6
7
8
9
|
extends layout
block content
.jumbotron
.container
<h1>Thank You!</h1>
<p>Thank you for playing!
block javascript
|
Обратите внимание, что мы обслуживаем socket.io.js
, хотя он не определен нигде в static
каталоге. Это связано с тем, что модуль socket.io
автоматически управляет обслуживанием клиентского файла socket.io.js
.
Стиль
Теперь, когда у нас есть настройка HTML, давайте перейдем к определению стилей. Мы начнем с перезаписи некоторых переменных начальной загрузки значениями по нашему выбору внутри assets/stylesheets/variables.less
.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
|
@body-bg: #F1F1F1;
@text-color: #717171;
@headings-color: #333;
@brand-primary: #468847;
@brand-success: #3A87AD;
@brand-warning: #FFC333;
@brand-danger: #FB6B5B;
@navbar-default-bg: #25313E;
@navbar-default-color: #ADBECE;
@navbar-default-link-color: @navbar-default-color;
@navbar-default-link-hover-color: #333;
|
Затем мы добавим несколько пользовательских стилей в styles.less
.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
|
// Custom Styles
/* Sticky Footer */
html,
body {
height: 100%;
}
/* Wrapper for page content to push down footer */
#wrap {
min-height: 100%;
height: auto;
margin: 0 auto -60px;
padding: 0 0 60px;
}
#footer {
height: 60px;
background-color: #65BD77;
> .container {
padding-left: 15px;
padding-right: 15px;
}
}
.container .text-muted {
margin: 20px 0;
color: #fff;
}
// Grid
table {
border-collapse: separate;
border-spacing: 10px 10px;
}
table tr {
margin: 10px;
}
table tr td {
width: 50px;
height: 50px;
border: 1px solid #3A87AD;
}
.center-table {
margin: 0 auto !important;
float: none !important;
}
.p1-score, .p2-score {
padding: 185px 0;
width: 50px;
height: 50px;
font-size: 25px;
line-height: 50px;
color: #fff;
text-align: center;
}
.p1-score {
float: right;
p {
background: #FFC333;
.current {
border: 5px solid darken(#FFC333, 10%);
}
}
}
.p2-score p {
background: #FB6B5B;
.current {
border: 5px solid darken(#FB6B5B, 10%);
}
}
.share-label {
line-height: 34px;
text-align: right;
}
|
JavaScript
После этого давайте добавим JavaScript-код в assets/javascript/
чтобы создать сетку и динамически добавить атрибуты frontend.js
data-row
data-column
с правильными значениями.
1
2
3
4
5
6
7
8
9
|
$(document).ready(function() {
for(var i = 0; i < 6; i++){
$(‘#board table’).append(»);
for(var j = 0; j < 7; j++) {
$(‘#board tr’).last().append(»);
$(‘#board td’).last().addClass(‘box’).attr(‘data-row’, i).attr(‘data-column’, j);
}
}
});
|
Это охватывает настройку внешнего интерфейса. Давайте скомпилируем ресурсы и запустим сервер.
1
2
|
$ grunt less concat:js
$ node <code>server.js</code>
|
Если вы следили за этим, страница указателя должна выглядеть примерно так:
Совет: Запустите команду grunt
в корневом grunt
проекта на отдельном терминале. Это вызвало бы задание по умолчанию, которое случается наблюдать. Это позволит объединить все файлы JS или скомпилировать все файлы Less при каждом сохранении.
Бэкэнд
Цель Connect 4 — соединить четыре последовательных «блока» по горизонтали, вертикали или диагонали. Socket.io позволяет нам создавать rooms
которым могут присоединиться клиенты. Думайте о них как о каналах IRC.
Игровые комнаты
Мы будем использовать эту функцию в игре, так что в комнате могут находиться только два игрока, и комната будет уничтожена, когда один из них выйдет. Мы создадим объект, который будет отслеживать все комнаты и, в свою очередь, все игровые состояния. Давайте начнем с создания функции в server.js
для создания случайных имен комнат.
01
02
03
04
05
06
07
08
09
10
|
function generateRoom(length) {
var haystack = ‘abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789’;
var room = »;
for(var i = 0; i < length; i++) {
room += haystack.charAt(Math.floor(Math.random() * 62));
}
return room;
};
|
Функция ожидает длину случайного имени комнаты, которое мы хотим сгенерировать. Имя комнаты генерируется путем объединения случайных символов из строки haystack
. Давайте изменим наш маршрут для индексной страницы, включив URL-адрес общего ресурса, и создадим новый маршрут для обслуживания контента, если к определенной комнате обращаются.
1
2
3
4
5
6
7
8
9
|
app.get(‘/’, function(req, res) {
share = generateRoom(6);
res.render(‘index.jade’, {shareURL: req.protocol + ‘://’ + req.get(‘host’) + req.path + share, share: share});
});
app.get(‘/:room([A-Za-z0-9]{6})’, function(req, res) {
share = req.params.room;
res.render(‘index.jade’, {shareURL: req.protocol + ‘://’ + req.get(‘host’) + ‘/’ + share, share: share});
});
|
В приведенном выше коде мы генерируем идентификатор ресурса с помощью функции generateRoom()
мы определили ранее, и передаем идентификатор ресурса и URL-адрес в качестве параметров шаблона. Второй маршрут ожидает параметр с именем room, который ограничен регулярным выражением. Регулярное выражение допускает строку, содержащую только буквенно-цифровые символы, длиной шесть. Опять же, мы передаем shareURL
и id в качестве параметров в шаблон. Давайте добавим некоторые атрибуты к элементу ввода нашего индекса, чтобы мы могли получить к ним доступ в frontend.js
позже.
1
|
input(type=’text’, data-room=share, name=’shareUrl’, value=shareURL ReadOnly).form-control
|
Теперь давайте отредактируем файл frontend.js
чтобы подключиться к серверу Socket.io, присоединиться к комнате и назначить некоторые свойства текущему игроку.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
var socket = io.connect(‘localhost’);
function Player(room, pid) {
this.room = room;
this.pid = pid;
}
var room = $(‘input’).data(‘room’);
var player = new Player(room, », »);
socket.on(‘connect’, function() {
socket.emit(‘join’, {room: room});
});
socket.on(‘assign’, function(data) {
player.color = data.color;
player.pid = data.pid;
if(player.pid == 1) {
$(‘.p1-score p’).addClass(‘current’);
}
else {
$(‘.p2-score p’).addClass(‘current’);
}
});
|
Обратите внимание, что мы создали объект под названием player
для ссылки на плеер на стороне клиента. При соединении вызывается событие соединения с бэкэндом, которое inturn испускает назначенный emit на внешнем интерфейсе, чтобы назначить некоторые свойства игроку. Теперь мы можем перейти к определению кода в бэкэнде для обработки события join
.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
|
// an object to hold all gamestates.
var games = {};
io.sockets.on(‘connection’, function(socket) {
socket.on(‘join’, function(data) {
if(data.room in games) {
if(typeof games[data.room].player2 != «undefined») {
socket.emit(‘leave’);
return;
}
socket.join(data.room);
socket.set(‘room’, data.room);
socket.set(‘color’, ‘#FB6B5B’);
socket.set(‘pid’, -1);
games[data.room].player2 = socket
// Set opponents
socket.set(‘opponent’, games[data.room].player1);
games[data.room].player1.set(‘opponent’, games[data.room].player2);
// Set turn
socket.set(‘turn’, false);
socket.get(‘opponent’, function(err, opponent) {
opponent.set(‘turn’, true);
});
socket.emit(‘assign’, {pid: 2});
}
else {
socket.join(data.room);
socket.set(‘room’, data.room);
socket.set(‘color’, ‘#FFC333’);
socket.set(‘pid’, 1);
socket.set(‘turn’, false);
games[data.room] = {
player1: socket,
board: [[0,0,0,0,0,0,0], [0,0,0,0,0,0,0], [0,0,0,0,0,0,0], [0,0,0,0,0,0,0], [0,0,0,0,0,0,0], [0,0,0,0,0,0,0]],
};
socket.emit(‘assign’, {pid: 1});
}
});
});
|
Примечание . Обработчики событий Socket.io в бэкэнде должны быть добавлены в io.sockets.on('connection', function(socket) { }
. Аналогично, обработчики событий и код JavaScript внешнего интерфейса должны находиться внутри $(document).ready(function() { }
блок кода.
В приведенном выше коде мы определили обработчик события для события join
, которое отправляется внешним интерфейсом. Он проверяет, существует ли данная комната и не назначен ли второй игрок, и, если да, назначает текущего клиента вторым игроком. В противном случае он назначает текущего клиента первым игроком и инициализирует игровое поле. Мы leave
событие leave
на внешний интерфейс для клиентов, которые пытаются присоединиться к игре в процессе. Мы также устанавливаем некоторые свойства сокета с помощью socket.set()
. К ним относятся номер комнаты, цвет, pid и переменная поворота. Свойства, установленные таким образом, могут быть получены из обратного вызова socket.get()
. Далее, давайте добавим обработчик событий leave
на внешний интерфейс.
1
2
3
|
socket.on(‘leave’, function() {
window.location = ‘/landingPage’;
});
|
Обработчик события leave
просто перенаправляет клиента на целевую страницу. Теперь мы приступаем к созданию события, которое предупреждает обоих игроков о готовности игры к началу. Давайте добавим некоторый код к условию if
нашего события соединения на стороне сервера.
1
2
3
4
5
6
|
if(data.room in games) {
// … append to the code that exists
// Notify
games[data.room].player1.emit(‘notify’, {connected: 1, turn: true});
socket.emit(‘notify’, {connected: 1, turn: false});
}
|
Мы должны определить событие notify
в веб-интерфейсе, который имеет дело с уведомлением. Alert.js
предоставляет Alert.js
способ обработки всех уведомлений. Давайте добавим обработчик события notify в frontend.js
.
1
2
3
4
5
6
7
8
|
socket.on(‘notify’, function(data) {
if(data.connected == 1) {
if(data.turn)
alertify.success(‘Players Connected! Your turn’);
else
alertify.success(‘Players Connected! Opponent\’s turn’);
}
});
|
Время попробовать наш прогресс. Запустите сервер локально и получите доступ к localhost и общему URL в двух отдельных окнах. Если вы следили за этим, вы должны получить предупреждение в правом нижнем углу, как показано на рисунке ниже:
Добавление интерактивности
Теперь давайте добавим код, который генерирует событие при нажатии на блоки. Для этой части нам нужно выяснить, был ли клик сделан правильным игроком. Здесь вступает в действие свойство turn
мы установили для сокета. Добавьте следующий код в frontend.js
.
1
2
3
4
5
6
7
8
|
$(‘.box’).click(function() {
// find the box to drop the disc to
var click = {
row: $(this).data(‘row’),
column: $(this).data(‘column’)
};
socket.emit(‘click’, click);
});
|
Приведенный выше код устанавливает обработчик событий для всех ячеек таблицы. Стоит отметить, что сетка в Connect 4 похожа на добавление кирпичей в стене, то есть нельзя заполнить определенную пару (строка, столбец), если пара (строка-1, столбец) не заполнена. Следовательно, мы должны сначала получить пару (строка, столбец) ячейки, по которой щелкнули, а затем выработать способ определения фактической ячейки, которая должна быть заполнена. Это делается в бэкэнде, в обработчике событий для click
.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
socket.on(‘click’, function(data) {
async.parallel([
socket.get.bind(this, ‘turn’),
socket.get.bind(this, ‘opponent’),
socket.get.bind(this, ‘room’),
socket.get.bind(this, ‘pid’)
], function(err, results) {
if(results[0]) {
socket.set(‘turn’, false);
results[1].set(‘turn’, true);
var i = 5;
while(i >= 0) {
if(games[results[2]].board[i][data.column] == 0) {
break;
}
i—;
}
if(i >= 0 && data.column >= 0) {
games[results[2]].board[i][data.column] = results[3];
socket.get(‘color’, function(err, color) {
socket.emit(‘drop’, {row: i, column: data.column, color: color});
results[1].emit(‘drop’, {row: i, column: data.column, color: color});
});
}
}
else {
console.log(‘Opponent\’s turn’);
}
});
});
|
Приведенный выше обработчик событий использует асинхронный модуль для одновременного извлечения свойств сокета. Это позволяет избежать вложенных обратных вызовов при последовательном использовании socket.get()
. Переменная results
представляет собой массив с элементами в том же порядке, что и socket.get()
. results[0]
, поэтому относится к turn
и так далее.
После того, как свойства были выбраны, мы меняем ходы и выясняем пару (строка, столбец) для заполнения. Мы делаем это в цикле while, начиная с нижнего ряда (пятая строка) и двигаясь вверх, пока значение доски в (строка, столбец) не станет равным нулю (что означает, что она не была сыграна). Затем мы назначаем pid (один или отрицательный) элементу на доске и запускаем событие drop
для обоих игроков. Давайте добавим обработчик события drop
в frontend.js
и представим анимацию, которая дает нам эффект падения.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
|
socket.on(‘drop’, function(data) {
var row = 0;
stopVal = setInterval(function() {
if(row == data.row)
clearInterval(stopVal);
fillBox(row, data.column, data.color);
row++;
}, 25);
});
function fillBox(row, column, color) {
$(‘[data-row=»‘+(row-1)+'»][data-column=»‘+column+'»]’).css(‘background’, »);
$(‘[data-row=»‘+row+'»][data-column=»‘+column+'»]’).css(‘background’, color);
}
|
Мы реализуем анимацию отбрасывания с помощью метода JavaScript setInterval()
. Начиная с самой верхней строки (нулевая строка), мы продолжаем вызывать fillBox()
с интервалом в 25 секунд, пока значение row
data.row
равным значению data.row
. Функция fillBox
очищает фон предыдущего элемента в том же столбце и назначает фон текущему элементу. Далее мы подходим к сути игры, реализуя условия выигрыша и розыгрыша. Мы рассмотрим это в бэкэнде.
01
02
03
04
05
06
07
08
09
10
11
12
13
|
// Helper function
function getPair(row, column, step) {
l = [];
for(var i = 0; i < 4; i++) {
l.push([row, column]);
row += step[0];
column += step[1];
}
return l;
}
// a list to hold win cases
var check = [];
|
Мы начнем с определения вспомогательной функции, которая возвращает четыре пары (строка, столбец) по горизонтали, вертикали или диагонали. Функция ожидает текущую строку и столбец и массив, который определяет приращение значений строки и столбца. Например, вызов getPair(1,1, [1,1])
вернет [[1,1], [2,2], [3,3], [4,4]]
который является прямая диагональ. Таким образом, мы можем получить соответствующие пары, выбрав подходящие значения для массива step
. Мы также объявили список для хранения всех функций, которые проверяют наличие выигрышей. Давайте начнем с функции, которая проверяет выигрыши по горизонтали и вертикали.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
|
check.push(function check_horizontal(room, row, startColumn, callback) {
for(var i = 1; i < 5; i++) {
var count = 0;
var column = startColumn + 1 — i;
var columnEnd = startColumn + 4 — i;
if(columnEnd > 6 || column < 0) {
continue;
}
var pairs = getPair(row, column, [0,1]);
for(var j = column; j < columnEnd + 1; j++) {
count += games[room][‘board’][row][j];
}
if(count == 4)
callback(1, pairs);
else if(count == -4)
callback(2, pairs);
}
});
check.push(function check_vertical(room, startRow, column, callback) {
for(var i = 1; i < 5; i++) {
var count = 0;
var row = startRow + 1 — i;
var rowEnd = startRow + 4 — i;
if(rowEnd > 5 || row < 0) {
continue;
}
var pairs = getPair(row, column, [1,0]);
for(var j = row; j < rowEnd + 1; j++) {
count += games[room][‘board’][j][column];
}
if(count == 4)
callback(1, pairs);
else if(count == -4)
callback(2, pairs);
}
});
|
Давайте пройдемся по вышеописанной функции шаг за шагом. Функция ожидает четыре параметра для комнаты, строки, столбца и обратного вызова. Чтобы проверить выигрыш по горизонтали, ячейка, по которой щелкнули, может способствовать условию выигрыша максимум четырьмя способами. Например, ячейка в (5, 3) может привести к победе в любой из следующих четырех комбинаций: [[5,3], [5,4], [5,5], [5,6]], [[5,2], [5,3], [5,4], [5,5]], [[5,1], [5,2], [5,3], [5,4]], [[5,0], [5,1], [5,2], [5,3], [5,4]]
. Количество комбинаций может быть меньше для граничных условий. Приведенный выше алгоритм решает эту проблему путем вычисления самого левого столбца (переменный column
) и самого правого столбца (переменная columnEnd
) в каждой из четырех возможных комбинаций.
Если самый правый столбец больше шести, он выходит за пределы сетки, и этот проход можно пропустить. То же самое будет сделано, если левый крайний столбец будет меньше нуля. Однако, если граничные случаи попадают в сетку, мы вычисляем пары (строка, столбец) с помощью вспомогательной функции getPair()
мы определили ранее, а затем переходим к добавлению значений элементов на плате. Напомним, что мы присвоили значение плюс один на доске для игрока один и отрицательное для игрока два. Следовательно, четыре последовательных ячейки одного игрока должны привести к четырем или отрицательным четырем соответственно. Обратный вызов вызывается в случае выигрыша и передается два параметра, один для игрока (один или два), а другой для выигрышных пар. Функция, которая работает с вертикальной проверкой, очень похожа на горизонтальную, за исключением того, что она проверяет крайние случаи в строках, а не в столбцах.
Левая и Правая Диагонали
Давайте перейдем к определению проверок для левой и правой диагоналей.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
|
check.push(function check_leftDiagonal(room, startRow, startColumn, callback) {
for(var i = 1; i < 5; i++) {
var count = 0;
var row = startRow + 1 — i;
var rowEnd = startRow + 4 — i;
var column = startColumn + 1 — i;
var columnEnd = startColumn + 4 — i;
if(column < 0 || columnEnd > 6 || rowEnd > 5 || row < 0) {
continue;
}
var pairs = getPair(row, column, [1,1]);
for(var j = 0; j < pairs.length; j++) {
count += games[room][‘board’][pairs[j][0]][pairs[j][1]];
}
if(count == 4)
callback(1, pairs);
else if(count == -4)
callback(2, pairs);
}
});
check.push(function check_rightDiagonal(room, startRow, startColumn, callback) {
for(var i = 1; i < 5; i++) {
var count = 0;
var row = startRow + 1 — i;
var rowEnd = startRow + 4 — i;
var column = startColumn -1 + i;
var columnEnd = startColumn — 4 + i;
if(column < 0 || columnEnd > 6 || rowEnd > 5 || row < 0) {
continue;
}
var pairs = getPair(row, column, [1,-1]);
for(var j = 0; j < pairs.length; j++) {
count += games[room][‘board’][pairs[j][0]][pairs[j][1]];
}
if(count == 4)
callback(1, pairs);
else if(count == -4)
callback(2, pairs);
}
});
|
Проверки диагоналей очень похожи на проверки горизонтальной и вертикальной проверки. Единственное отличие состоит в том, что в случае диагоналей мы проверяем граничные случаи как для строк, так и для столбцов. Наконец, мы определяем функцию для проверки на наличие розыгрышей.
1
2
3
4
5
6
7
8
|
// Function to check for draw
function check_draw(room, callback) {
for(var val in games[room][‘board’][0]) {
if(val == 0)
return;
}
callback();
}
|
Проверка на ничьи довольно тривиальна. Ничья очевидна, если все ячейки в верхнем ряду заполнены и никто не выиграл. Таким образом, мы исключаем ничью, если ни одна из ячеек в верхнем ряду не была сыграна, и вызываем обратный вызов в противном случае
После того, как условия выигрыша и розыгрыша разобраны, мы должны теперь использовать эти функции в событии щелчка и выдать событие reset
на внешнем интерфейсе для обозначения клиентов в конце игры. Давайте отредактируем событие click для обработки этих условий.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
if(i >= 0 && data.column >= 0) {
/*
Previous code skipped
*/
var win = false;
check.forEach(function(method) {
method(results[2], i, data.column, function(player, pairs) {
win = true;
if(player == 1) {
games[results[2]].player1.emit(‘reset’, {text: ‘You Won!’, ‘inc’: [1,0], highlight: pairs });
games[results[2]].player2.emit(‘reset’, {text: ‘You Lost!’, ‘inc’: [1,0], highlight: pairs });
}
else {
games[results[2]].player1.emit(‘reset’, {text: ‘You Lost!’, ‘inc’: [0,1], highlight: pairs });
games[results[2]].player2.emit(‘reset’, {text: ‘You Won!’, ‘inc’: [0,1], highlight: pairs });
}
games[results[2]].board = [[0,0,0,0,0,0,0], [0,0,0,0,0,0,0], [0,0,0,0,0,0,0], [0,0,0,0,0,0,0], [0,0,0,0,0,0,0], [0,0,0,0,0,0,0]];
});
});
if(win) {
return;
}
check_draw(results[2], function() {
games[results[2]].board = [[0,0,0,0,0,0,0], [0,0,0,0,0,0,0], [0,0,0,0,0,0,0], [0,0,0,0,0,0,0], [0,0,0,0,0,0,0], [0,0,0,0,0,0,0]];
io.sockets.in(results[2]).emit(‘reset’, {‘text’: ‘Game Drawn’, ‘inc’: [0,0]});
});
}
|
В приведенном выше коде мы проверяем выигрыш по горизонтали, вертикали и диагонали. В случае выигрыша мы генерируем событие reset
на интерфейсе с соответствующим сообщением для обоих игроков. Свойство highlight
содержит выигрышные пары, а свойство inc
обозначает показатель приращения для обоих игроков. Например, [1,0]
будет означать увеличение счета игрока на единицу и счет игрока два на ноль.
Давайте продолжим обрабатывать событие reset
на веб-интерфейсе.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
socket.on(‘reset’, function(data) {
if(data.highlight) {
setTimeout(function() {
data.highlight.forEach(function(pair) {
$(‘[data-row=»‘+pair[0]+'»][data-column=»‘+pair[1]+'»]’).css(‘background-color’, ‘#65BD77’);
});
}, 500);
}
setTimeout(function() {
$(‘td’).css(‘background-color’, »)
alertify.confirm(data.text, function(e) {
if(e) {
socket.emit(‘continue’);
}
else {
window.location = ‘/landingPage’;
}
});
}, 1200)
// Set Scores
p1 = parseInt($(‘.p1-score p’).html())+data[‘inc’][0];
$(‘.p1-score p’).html(p1);
p2 = parseInt($(‘.p2-score p’).html())+data[‘inc’][1];
$(‘.p2-score p’).html(p2);
});
|
В обработчике reset
мы выделяем выигрышные пары через 500 мс. Причина задержки заключается в том, что анимация отбрасывания заканчивается. Затем мы перезагружаем плату в фоновом режиме и выскакиваем диалоговое окно подтверждения оповещения с текстом, отправленным с сервера. Если пользователь решает продолжить, мы отправляем событие continue
на стороне сервера или перенаправляем клиента на целевую страницу. Затем мы продолжаем увеличивать очки игрока, увеличивая текущий счет на значения, полученные с сервера.
Далее, давайте определим обработчик события continue
в серверной части.
1
2
3
4
5
|
socket.on(‘continue’, function() {
socket.get(‘turn’, function(err, turn) {
socket.emit(‘notify’, {connected: 1, turn: turn});
});
});
|
Обработчик события continue
довольно прост. Мы снова отправляем событие уведомления, и игра возобновляется во внешнем интерфейсе.
Далее, давайте определимся с тем, что происходит, когда один из игроков отключается. В этом случае мы должны перенаправить другого игрока на целевую страницу и удалить комнату из игрового состояния. Давайте добавим эту функцию в бэкэнд.
1
2
3
4
5
6
7
8
9
|
socket.on(‘disconnect’, function() {
console.log(‘Disconnected’);
socket.get(‘room’, function(err, room) {
io.sockets.in(room).emit(‘leave’);
if(room in games) {
delete games.room;
}
});
});
|
Приведенный выше обработчик события будет транслировать событие leave
другим игрокам и удалит комнату из игрового объекта, если он все еще существует.
Вывод
В этом уроке мы рассмотрели довольно много вопросов, начиная с получения зависимостей, создавая некоторые задачи, создавая фронт и бэкэнд и заканчивая готовой игрой. С учетом вышесказанного, я полагаю, вам пора провести несколько раундов с друзьями! Буду рад ответить на ваши вопросы в комментариях. Не стесняйтесь раскошелиться на GitHub и импровизировать в коде. Это все, ребята!