首页 > 文章列表 > 使用Socket.io进行四子棋游戏的连接

使用Socket.io进行四子棋游戏的连接

连接 SocketIO 四子棋
473 2023-08-28

Connect 4 游戏带回了往日的回忆。这款经典游戏肯定给玩过它的每个人留下了深刻的印象。在本文中,我们将使用 Node.js 和 Socket.io 制作 Connect 4 的多人游戏版本。

安装依赖项

本教程假设您已安装 Node.js 和 npm。为了管理前端依赖项,我们将使用 Bower 来获取包并使用 Grunt 来管理任务。打开终端并通过执行以下命令全局安装 Bower 和 Grunt:

$ sudo npm install -g bower grunt-cli

注意:Grunt 需要 Node.js 版本 >= 0.8.0。在撰写本文时,Ubuntu 的存储库有旧版本的 Node.js。如果您使用的是 Ubuntu,请确保您使用的是 Chris Lea 的 PPA。对于其他发行版/操作系统,请参阅 Node.js 安装文档以获取最新版本。

安装 Bower 和 Grunt-cli 后,让我们为项目创建一个目录并使用 Bower 获取 Twitter Bootstrap 和 Alertify.js(用于管理警报通知)。

$ mkdir connect4
$ cd connect4
$ bower install bootstrap alertify.js

现在,让我们设置一个目录来管理我们的自定义资源。我们将其命名为 assets 并在其中存储我们的自定义 Less 和 JavaScript 文件。

$ 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 的子目录。

$ mkdir -p static/{javascript,stylesheets}

打开 assets/stylesheets/styles.less 并导入 variables.less 以及引导程序中所需的 Less 文件。

// 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 文件的基本结构如下所示:

//Gruntfile
module.exports = function(grunt) {

//Initializing the configuration object
    grunt.initConfig({
        // Task configuration
        less: {
            //...
        },
        concat: {
            //...
        },
        watch: {
            //...
        }
    });

    // Load plugins

    // Define tasks

};

资产任务

我们将定义三个任务来管理资产。第一个是将所有 Less 文件编译为 CSS。第二个任务是将所有 JavaScript 文件连接成一个,最后一个任务是监视文件的更改。 watch 任务将是默认任务,一旦我们完成 gruntfile 的配置,就可以通过在项目根目录中键入 grunt 来运行。

让我们设置一个任务,将 static/stylesheets 目录中的所有 Less 文件编译为 CSS 文件。

less: {
    development: {
        options: {
            compress: true,
        },
        files: {
            "./static/stylesheets/styles.css": "./assets/stylesheets/<code>styles.less</code>",
        }
    }
},

接下来,我们将设置另一个任务来将所有 JS 文件合并成一个。

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>',
    },
},

最后,让我们设置监视任务来监视文件的更改并在保存时执行所需的任务。

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 插件并注册默认任务。

// Load plugins
grunt.loadNpmTasks('grunt-contrib-concat');
grunt.loadNpmTasks('grunt-contrib-less');
grunt.loadNpmTasks('grunt-contrib-watch');

// Define tasks
grunt.registerTask('default', ['watch']);

让我们继续使用 npm 管理后端依赖项。对于此项目,我们将使用带有 Jade 模板引擎和 Socket.io 的 Express 框架。通过执行以下命令在本地安装依赖项:

$ npm install express jade socket.io async grunt grunt-contrib-concat grunt-contrib-less grunt-contrib-watch

目录结构现在应该类似于:

使用Socket.io进行四子棋游戏的连接

现在我们已经设置了依赖项,是时候继续制作游戏的前端了。

前端

让我们继续创建一个名为 server.js 的文件并使用 Express 提供内容。

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 模板引擎来管理模板。默认情况下,Express 在 views 目录中查找视图。让我们创建 views 目录并为布局、索引和感谢页面创建 Jade 文件。

$ mkdir -p views
$ touch views/{layout.jade,index.jade,landing.jade}

接下来,让我们编辑项目的布局、索引页面和登陆页面(landing.jade)。

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
                    | Developed by
                    | <a href="http://github.com/gnarula">Gaurav Narula</a> for Nettuts
        block javascript
            script(src='/socket.io/socket.io.js')
            script(src='static/javascript/<code>frontend.js</code>')

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

extends layout

block content
    .jumbotron
        .container
            <h1>Thank You!</h1>
            <p>Thank you for playing! We hope you enjoyed the game!</p>

block javascript

请注意,我们正在提供 socket.io.js,尽管它没有在 static 目录中的任何位置定义。这是因为 socket.io 模块自动管理 socket.io.js 客户端文件的服务。

样式

现在我们已经完成了 HTML 设置,让我们继续定义样式。我们将首先使用 assets/stylesheets/variables.less 中选择的值覆盖一些引导变量。

@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

// 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 中添加一些 JavaScript 代码来创建网格并添加 data-rowdata-动态地使用适当的值的column属性。

$(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);
        }
    }
});

这涵盖了前端设置。让我们编译资产并启动服务器。

$ grunt less concat:js
$ node <code>server.js</code>

如果您一直在关注,索引页面应该与此类似:

使用Socket.io进行四子棋游戏的连接使用Socket.io进行四子棋游戏的连接使用Socket.io进行四子棋游戏的连接

提示:在单独的终端上的项目根目录上运行 grunt 命令。这将调用恰好是 watch 的默认任务。这将在每次保存时连接所有 JS 文件或编译所有 Less 文件。

后端

Connect 4 的目标是水平、垂直或对角连接四个连续的“块”。 Socket.io 允许我们创建客户端可以加入的 rooms 。将它们视为 IRC 频道。

游戏室

我们将在游戏中使用此功能,这样一个房间中只能有两个玩家,当他们中的任何一个退出时,房间就会被摧毁。我们将创建一个对象来跟踪所有房间,进而跟踪所有游戏状态。让我们首先在 server.js 中创建一个函数来创建随机房间名称。

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,并创建一个新路由来在访问特定房间时提供内容。

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() 函数生成共享 ID,并将共享 ID 和 URL 作为参数传递给模板。第二条路由需要一个名为 room 的参数,该参数受正则表达式限制。正则表达式允许使用仅包含字母数字字符且长度为 6 的字符串。再次,我们将 shareURL 和 id 作为参数传递给模板。让我们向索引的 input 元素添加一些属性,以便稍后可以在 frontend.js 中访问它们。

input(type='text', data-room=share, name='shareUrl', value=shareURL ReadOnly).form-control

接下来,我们编辑 frontend.js 连接到 Socket.io 服务器,加入房间并为当前玩家分配一些属性。

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 的对象来引用客户端的播放器。连接时,后端会调用 join 事件,该事件又会在前端发出分配信号,以将一些属性分配给玩家。我们现在可以继续在后端定义代码来处理 join 事件。

// an object to hold all gamestates. Key denotes room id
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 事件。我们还使用 socket.set() 在套接字上设置一些属性。其中包括房间 ID、颜色、PID 和轮次变量。通过这种方式设置的属性可以从 socket.get() 的回调中检索。接下来,让我们在前端添加 leave 事件处理程序。

socket.on('leave', function() {
    window.location = '/landingPage';
});

leave 事件处理程序只是将客户端重定向到登录页面。现在,我们继续发出一个事件,如果游戏已准备好开始,则会提醒双方玩家。让我们在服务器端的 join 事件的 if 条件中附加一些代码。

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 提供了一种简洁的方法来处理所有通知。让我们在 frontend.js 中添加通知事件处理程序。

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。如果您一直在关注,您应该会在右下角看到一个警报,如下图所示:

使用Socket.io进行四子棋游戏的连接使用Socket.io进行四子棋游戏的连接使用Socket.io进行四子棋游戏的连接

添加交互性

现在让我们继续添加在单击块时发出事件的代码。对于这一部分,我们需要确定点击是否是由正确的玩家进行的。这是我们在套接字上设置的 turn 属性发挥作用的地方。将以下代码附加到 frontend.js

$('.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 的事件处理程序中完成的。

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');
        }
    });
});

上面的事件处理程序使用 async 模块同时获取套接字属性。这可以避免在连续使用 socket.get() 时嵌套回调。 results 变量是一个数组,其元素的顺序与 socket.get() 调用的顺序相同。 results[0],因此指turn等等。

获取属性后,我们交换轮数并找出要填充的(行,列)对。我们在 while 循环中执行此操作,从底行(第五行)开始向上移动,直到(行,列)处的棋盘值为零(这意味着它尚未被玩过)。然后,我们将 pid(1 或负 1)分配给棋盘上的元素,并在两个玩家上发出 drop 事件。让我们在 frontend.js 上添加 drop 事件处理程序,并引入一个为我们提供坠落效果的动画。

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() 方法实现下降动画。从最上面的行(第 0 行)开始,我们以 25 秒的间隔继续调用 fillBox(),直到 row 的值等于 data.row 的值。 fillBox 函数清除同一列中前一个元素的背景,并为当前元素分配背景。接下来,我们来到游戏的关键,实现获胜和平局的条件。我们将在后端介绍这一点。

// 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 数组选择合适的值来获得相应的对。我们还声明了一个列表来保存所有检查胜利的函数。让我们首先浏览一下水平和垂直检查胜利的函数。

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)来处理当前的问题。< /p>

如果最右边的列大于六,则它脱离网格并且可以跳过该过程。如果最左边的列小于零,也会执行相同的操作。但是,如果边缘情况落在网格中,我们将使用之前定义的 getPair() 辅助函数来计算(行,列)对,然后继续添加板上元素的值。回想一下,我们在棋盘上为玩家一指定了加一的值,为玩家二指定了负一的值。因此,一名玩家连续四个单元格应分别导致计数为四或负四。如果获胜,则调​​用该回调,并传递两个参数,一个用于玩家(一个或两个),另一个用于获胜对。处理垂直检查的函数与水平检查的函数非常相似,只是它检查的是行而不是列中的边缘情况。

左对角线和右对角线

让我们继续定义左右对角线的检查。

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);
    }
});

对角线的检查与水平和垂直检查非常相似。唯一的区别是,在对角线的情况下,我们检查行和列的边缘情况。最后,我们定义一个函数来检查平局。

// Function to check for draw
function check_draw(room, callback) {
    for(var val in games[room]['board'][0]) {
        if(val == 0)
            return;
    }
    callback();
}

检查平局相当简单。如果顶行的所有单元格都已填满并且没有人获胜,则显然是平局。因此,如果顶行中的任何单元格尚未播放,我们就排除平局,否则调用回调。

确定获胜和平局条件后,我们现在必须在点击事件中使用这些函数,并在前端发出 reset 事件,以指示客户端游戏结束。让我们编辑点击事件来处理这些条件。

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 事件。

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 事件处理程序。

socket.on('continue', function() {
    socket.get('turn', function(err, turn) {
        socket.emit('notify', {connected: 1, turn: turn});
    });
});

continue 事件处理程序非常简单。我们再次发出通知事件,游戏在前端恢复。

接下来,让我们决定当任一玩家断开连接时会发生什么。在这种情况下,我们应该将其他玩家重定向到登陆页面,并将房间从游戏状态中删除。让我们在后端添加此功能。

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 上分叉我的存储库并即兴创作代码。这就是大家!