Статьи

Соедините 4 с Socket.io

Игра 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

Структура каталогов теперь должна быть похожа на эту:

2014-02-05-132602_239x498_scrot

Теперь, когда мы настроили наши зависимости, пришло время перейти к созданию интерфейса нашей игры.

Давайте продолжим, создав файл с именем 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-код в 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>

Если вы следили за этим, страница указателя должна выглядеть примерно так:

2014-02-05-133240_1363x616_scrot

Совет: Запустите команду 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 в двух отдельных окнах. Если вы следили за этим, вы должны получить предупреждение в правом нижнем углу, как показано на рисунке ниже:

2014-02-05-232920_1362x615_scrot

Теперь давайте добавим код, который генерирует событие при нажатии на блоки. Для этой части нам нужно выяснить, был ли клик сделан правильным игроком. Здесь вступает в действие свойство 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 и импровизировать в коде. Это все, ребята!