Статьи

Создание многопользовательской игры TicTacToe с Meteor

Деревянная игра в крестики-нолики. Создайте многопользовательскую игру в крестики-нолики с Meteor

Meteor – популярный веб-фреймворк с полным стеком, который позволяет легко создавать прототипы ваших идей и по-настоящему быстро переходить от разработки к производству. Его реактивный характер и использование DDP делают его отличным кандидатом на создание простых многопользовательских браузерных игр.

В этом уроке я покажу вам, как создать многопользовательский TicTacToe с Meteor , используя стандартный движок шаблонов Blaze . Я предполагаю, что вы немного поиграли с Meteor, и, конечно, вы чувствуете себя комфортно при написании кода на JavaScript.

Если у вас нет опыта работы с Meteor, я бы рекомендовал вам сначала следовать руководству по приложению TODO на официальном сайте Meteor.

Вы можете найти код для завершенного приложения в прилагаемом репозитории GitHub .

Создание приложения

Если у вас не установлен Meteor, вы должны следовать инструкциям на их сайте в соответствии с вашей ОС.

Генерация лесов

Теперь, когда Meteor установлен, откройте свой терминал и выполните следующую команду:

meteor create TicTacToe-Tutorial 

Это создаст папку с названием вашего приложения (в данном случае TicTacToe-Tutorial ). Эта новая папка содержит базовую файловую структуру приложения. Там на самом деле образец приложения внутри.

Перейдите в папку:

 cd TicTacToe-Tutorial 

А теперь запустите приложение:

 meteor 

Я знаю, я знаю … это ужасно трудная для запоминания команда, и вы будете часто ее использовать, поэтому вы должны начать ее запоминать!

Если все прошло хорошо, теперь консоль должна собирать приложение. После этого откройте веб-браузер и перейдите по адресу http: // localhost: 3000, чтобы увидеть, как работает приложение. Если вы никогда не делали этого раньше, я бы порекомендовал вам поиграть с примером приложения. Попробуйте выяснить, как это работает.

Давайте посмотрим на структуру файла. Откройте папку вашего приложения. Единственное, что нас там волнует (пока) – это папка клиента и папка сервера. Файлы внутри папки клиента будут загружены и выполнены клиентом. Файлы в папке сервера будут выполняться только на сервере, и клиент не имеет к ним доступа.

Это содержимое вашей новой папки:

 client/main.js # a JavaScript entry point loaded on the client client/main.html # an HTML file that defines view templates client/main.css # a CSS file to define your app's styles server/main.js # a JavaScript entry point loaded on the server package.json # a control file for installing NPM packages .meteor # internal Meteor files .gitignore # a control file for git 

Строим доску

Доска TicTacToe – это простая таблица три на три; ничего особенного, что отлично подходит для нашей первой многопользовательской игры, поэтому мы можем сосредоточиться на функциональности.

Доска будет загружена клиентом, поэтому мы будем редактировать файлы внутри папки клиента. давайте начнем с удаления содержимого файла main.html и его замены следующим:

клиент / main.html

 <head> <title>tic-tac-toe</title> </head> <body> <table id="board"> <tr> <td class="field"></td> <td class="field"></td> <td class="field"></td> </tr> <tr> <td class="field"></td> <td class="field"></td> <td class="field"></td> </tr> <tr> <td class="field"></td> <td class="field"></td> <td class="field"></td> </tr> </table> </body> 

Не забудьте сохранить ваши файлы после внесения изменений! В противном случае они не будут признаны Метеором.

Теперь давайте добавим немного CSS на нашу доску. Откройте файл main.css и добавьте следующее содержимое:

клиент / main.css

 table { margin: auto; font-family: arial; } .field { height: 200px; width: 200px; background-color: lightgrey; overflow: hidden; } #ui { text-align: center; } #play-btn { width: 100px; height: 50px; font-size: 25px; } .mark { text-align: center; font-size: 150px; overflow: hidden; padding: 0px; margin: 0px; } .selectableField { text-align: center; height: 200px; width: 200px; padding: 0px; margin: 0px; } 

Мы также добавили несколько дополнительных идентификаторов и классов, которые мы будем использовать позже в этом руководстве.

Наконец, удалите файл client / main.js , так как он нам больше не понадобится, и откройте приложение в браузере, чтобы посмотреть, как оно выглядит.

Это нормально и все, но не является оптимальным решением. Давайте сделаем рефакторинг, представив шаблоны Blaze .

Создание шаблона

Шаблоны – это фрагменты HTML-кода со своими собственными функциями, которые вы можете использовать в любом месте своего приложения. Это отличный способ разбить ваши приложения на повторно используемые компоненты.

Перед созданием нашего первого шаблона мы добавим еще две папки в папку клиента. Мы назовем один html, а другой js .

Внутри html-папки создайте новый файл board.html со следующим содержимым:

клиент / html / board.html

 <template name="board"> <table id="board"> <tr> <td class="field"></td> <td class="field"></td> <td class="field"></td> </tr> <tr> <td class="field"></td> <td class="field"></td> <td class="field"></td> </tr> <tr> <td class="field"></td> <td class="field"></td> <td class="field"></td> </tr> </table> </template> 

Теперь в папке main.html замените содержимое внутри тега body следующим кодом:

клиент / main.html

 <head> <title>tic-tac-toe</title> </head> <body> {{>board}} </body> 

Это вставит наш шаблон со свойством name="board" внутри тега body .

Но это та же жестко закодированная доска, которая была у нас раньше. Только сейчас он находится внутри шаблона, поэтому давайте воспользуемся преимуществами помощников шаблонов для динамического создания нашей доски.

Использование помощников

Мы объявим помощника в шаблоне доски, который предоставит нам массив той же длины, что и размеры, которые мы хотим, чтобы наша доска имела.

внутри папки js создайте файл board.js со следующим содержимым:

клиент / JS / board.js

 import { Meteor } from 'meteor/meteor'; import { Template } from 'meteor/templating'; Template.board.helpers({ sideLength: () => { let side = new Array(3); side.fill(0); return side; } }); 

Теперь мы будем использовать этот помощник в HTML-шаблоне доски, чтобы повторить одну строку для каждого элемента в массиве, предоставленном помощником. Чтобы помочь нам в этом, мы будем использовать вспомогательный блок Every-in Spacebars.

Замените содержимое файла board.html следующим:

клиент / html / board.html

 <template name="board"> <table id="board"> {{#each sideLength}} {{#let rowIndex=@index}} <tr> {{#each sideLength}} <td class="field" id="{{rowIndex}}{{@index}}"> {{{isMarked rowIndex @index}}} </td> {{/each}} </tr> {{/let}} {{/each}} </table> </template> 

Обратите внимание, что мы дважды перебираем массив, один раз для строк и один раз для столбцов , создавая соответствующий тег ( tr или td ) по мере продвижения. Мы также устанавливаем их свойства id как @index строки + @index столбца . Мы получаем двухзначный номер, который поможет нам идентифицировать этот элемент с его положением на доске.

Проверьте приложение на http: // localhost: 3000, чтобы увидеть, как оно выглядит до сих пор.

UI

Теперь, когда у нас есть хорошо выглядящая доска, нам понадобится кнопка воспроизведения и тег для отображения информации о текущей игре.

Давайте начнем с создания файла ui.html внутри папки html … вы знаете, что делать. Теперь добавьте следующий контент:

клиент / html / ui.html

 <template name ="ui"> <div id="ui"> {{#if inGame}} <p id="status"> {{status}} </p> {{else}} <button id="play-btn">Play</button> {{/if}} </div> </template> 

Как вы можете видеть, мы используем хелпер блока #if inGame хелпер inGame (который мы еще не определили) в качестве условия. Внутри тега p тоже есть помощник по status . Мы определим это позже также.

Как это работает? inGame помощник inGame возвращает true , игрок увидит все, что находится в помощнике status . В противном случае мы просто покажем кнопку воспроизведения.

Не забудьте, что для отображения этого компонента нам нужно добавить его в наш основной шаблон клиента:

клиент / main.html

 <head> <title>tic-tac-toe</title> </head> <body> {{>ui}} {{>board}} </body> 

Происходит вход в систему

Мы не будем иметь дело с любым интерфейсом входа в систему. Мы установим очень полезный пакет под названием brettle: account-anonymous-auto , который автоматически анонимно регистрирует всех пользователей в нашем приложении.

Перейдите на консоль и выполните следующую команду:

 meteor add brettle:accounts-anonymous-auto 

Теперь, когда вы впервые открываете приложение после добавления этого пакета, оно создает нового пользователя, и каждый раз, когда вы открываете приложение в том же браузере, оно запоминает вас. Если мы не храним никаких данных от указанного пользователя, может быть лучше просто удалить их, когда они выйдут из системы. Но мы не будем это обсуждать в этом уроке.

Сборка игры

Наконец, мы собираемся начать строить саму игру! Давайте рассмотрим функциональность, которую мы реализуем, чтобы иметь четкое представление о том, что будет дальше.

Нам понадобится функциональность для:

  • Создание игры
  • Присоединение к существующей игре
  • Делать ход
  • Установление выигрышных условий
  • Отображение статуса игры для игроков
  • Уничтожение законченного игрового экземпляра

Чтобы воспользоваться компенсацией задержки Meteor, мы разместим большую часть этого кода в месте, доступном как для клиента, так и для сервера.

Для этого мы создадим папку с именем lib в корне нашего проекта. Все, что мы добавим, будет загружено клиентом, поэтому мы должны быть очень осторожны. Вы не хотите давать клиенту какие-либо ключи API или доступ к скрытым функциям случайно.

Коллекция игр

Метеор использует Коллекции Монго . Если вы не очень хорошо знакомы с Mongo , но использовали любую другую документно-ориентированную базу данных , все будет в порядке. В противном случае представьте коллекции как таблицы, где каждая строка не зависит от следующей. Одна строка может иметь шесть столбцов, а другая строка в той же таблице может иметь четыре совершенно разных столбца.

Нам нужно создать коллекцию и сделать ее доступной как для клиента, так и для сервера. Итак, мы создадим файл games.js внутри папки lib и там мы создадим экземпляр коллекции под названием «games» и сохраним его в глобальной переменной Games :

Библиотека / games.js

 import { Mongo } from 'meteor/mongo'; Games = new Mongo.Collection("games"); 

Теперь вы, вероятно, задаетесь вопросом, почему мы даем игроку доступ к базе данных и игровой логике. Ну, мы только предоставляем локальный доступ к игроку. Meteor предоставляет клиенту локальную базу данных мини-монго, которую мы можем заполнить только шаблоном « Публикация- подписка» , как я покажу вам чуть позже. Это единственное, к чему клиент имеет доступ. И даже если клиенты пишут в свою локальную базу данных, если информация не совпадает с данными в базе данных сервера, она будет переопределена.

Тем не менее, Meteor поставляется по умолчанию с установленной парой очень небезопасных пакетов. Один называется autopublish , он автоматически публикует все ваши коллекции и подписывает клиента. Другой называется небезопасным и предоставляет клиенту доступ на запись в базу данных.

Оба этих пакета отлично подходят для создания прототипов, но мы должны прямо сейчас удалить их. Перейдите в консоль и выполните следующую команду:

 meteor remove insecure meteor remove autopublish 

При этом теперь нам нужен способ синхронизировать то, что мы делаем на клиенте, с тем, что мы делаем на сервере. Введите Метеоритные Методы .

Метод games.play

Meteor.methods – это объект, в котором мы можем зарегистрировать методы, которые могут быть вызваны клиентом с помощью функции Meteor.call . Они будут выполнены сначала на клиенте, а затем на сервере. Таким образом, клиенты смогут видеть, что изменения происходят мгновенно благодаря локальной базе данных Mongo. Затем сервер запустит тот же код в основной базе данных.

Давайте создадим пустой метод games.play под коллекцией games :

Библиотека / games.js

 Meteor.methods({ "games.play"() { } }); 

Создание игры

Создайте файл в папке lib с именем gameLogic.js, и в нем мы создадим класс newGame методом newGame , куда мы newGame новый документ в нашу коллекцию игр:

Библиотека / gameLogic.js

 class GameLogic { newGame() { if(!this.userIsAlreadyPlaying()) { Games.insert({ player1: Meteor.userId(), player2: "", moves: [], status: "waiting", result: "" }); } } } 

В этом фрагменте кода мы спрашиваем, играет ли игрок уже перед тем, как вставить новую игру, поскольку мы не собираемся поддерживать более одной игры одновременно для каждого игрока. Это очень важный шаг, иначе мы можем столкнуться с огромной ошибкой.

Давайте добавим метод newGame() ниже newGame() :

Библиотека / gameLogic.js

 userIsAlreadyPlaying() { const game = Games.findOne({$or:[ {player1: Meteor.userId()}, {player2: Meteor.userId()}] }); if(game !== undefined) return true; return false; } 

Давайте рассмотрим процесс запуска новой игры.

Когда игрок нажимает кнопку воспроизведения, мы ищем существующую игру, к которой он присоединится. Если указанный игрок не может найти игру для присоединения, будет создана новая игра. В нашей модели player1 – это игрок, который создал игру, player2 – пустая строка, и status по умолчанию «ожидающий».

Таким образом, если другой игрок player2 кнопку воспроизведения, он будет искать игру с пустым полем player2 полем status со значением «wait». Затем мы установим этого игрока как player2 и соответственно изменим status .

Теперь мы должны сделать наш класс GameLogic доступным для методов Meteor внутри games.js . Мы экспортируем экземпляр нашего класса, а затем импортируем его в файл games.js . Добавьте эту строку внизу файла gameLogic.js вне класса:

 export const gameLogic = new GameLogic(); 

Добавьте следующую строку вверху файла games.js :

 import { gameLogic } from './gameLogic.js'; 

Теперь мы можем добавить логику в наш пустой метод games.play () . Сначала мы ищем игру со статусом «ожидание», а затем мы вызываем newGame() если никакая другая игра не была найдена:

Библиотека / games.js

 Meteor.methods({ "games.play"() { const game = Games.findOne({status: "waiting"}); if(game === undefined) { gameLogic.newGame(); } } }); 

Публикации

Чтобы найти игру, нам нужно предоставить клиенту доступ к коллекции games . Для этого мы создадим публикацию . Публикации позволяют нам показывать клиентам только те данные, которые мы хотим, чтобы они видели. Затем мы подписываем клиентов на публикацию , чтобы предоставить им доступ к этим данным.

Чтобы предоставить игрокам доступ к коллекции игр, мы создадим публикацию «Игры» . Но когда игроки будут добавлены в новую игру, мы предоставим им доступ ко всем полям в этой конкретной игре. Так что также будет публикация «Моя игра» .

Перейдите в файл main.js внутри папки сервера и замените его содержимое следующим:

сервер / main.js

 import { Meteor } from 'meteor/meteor'; Meteor.publish('Games', function gamesPublication() { return Games.find({status: "waiting"}, { fields:{ "status": 1, "player1": 1, "player2": 1 } }); }); Meteor.publish('MyGame', function myGamePublication() { return Games.find({$or:[ {player1: this.userId}, {player2: this.userId}] }); }); 

Теперь нам нужно подписаться на публикацию «Игры». Мы сделаем это в обратном вызове метода onCreated шаблона пользовательского интерфейса.

Создайте файл ui.js в client / js / со следующим кодом:

 import { Meteor } from 'meteor/meteor'; import { Template } from 'meteor/templating'; Template.ui.onCreated(() => { Meteor.subscribe('Games'); }); 

Play Event

Шаблоны предоставляют объект событий, где мы можем зарегистрироваться … Угадай, что? Бинго! События. Мы создадим событие в шаблоне пользовательского интерфейса. Всякий раз, когда игрок щелкает элемент DOM с идентификатором «play-btn», мы устанавливаем для переменной сеанса inGame значение true, мы вызываем метод games.play и подписываемся на коллекцию MyGame .

Переменные сеанса могут использоваться в любом месте клиентского кода, даже от шаблона к шаблону. Чтобы использовать их, нам нужно добавить пакет Session :

 meteor add session 

Перейдите в файл ui.js и добавьте следующие строки после метода onCreated :

клиент / JS / ui.js

 Template.ui.events({ "click #play-btn": () => { Session.set("inGame", true); Meteor.call("games.play"); Meteor.subscribe('MyGame'); } }); 

Рекомендуется импортировать пакеты, которые мы используем в каждом файле. Поскольку мы используем пакет Session в файле ui.js, мы должны импортировать его. Просто добавьте следующую строку вверху:

 import { Session } from 'meteor/session'; 

Хорошо! Теперь нам нужно добавить пару помощников. Помните, ui.html ? Дайте это быстрый взгляд. Мы использовали помощник inGame помощник по status . давайте объявим их под объектом events :

клиент / JS / ui.js

 Template.ui.helpers({ inGame: () => { return Session.get("inGame"); }, status: () => { } }); 

Как видите, помощник inGame возвращает значение, хранящееся в переменной сеанса inGame . Мы оставим status помощника пока пустым.

Присоединение к игре

В конце концов, вы уже сделали, присоединение к игре должно быть довольно простым.

Сначала мы добавим метод GameLogic класс GameLogic :

Библиотека / gameLogic.js

 joinGame(game) { if(game.player2 === "" && Meteor.userId() !== undefined) { Games.update( {_id: game._id}, {$set: { "player2": Meteor.userId(), "status": game.player1 } } ); } } 

Как видите, мы передаем игровую переменную и устанавливаем в поле player2 значение player2 игрока, а в поле status_id_ player1 . Вот как мы узнаем, чья это очередь.

Теперь мы будем вызывать этот метод из games.play() . Перейдите в файл games.js и замените содержимое метода games.play следующим:

Библиотека / games.js

 Meteor.methods({ "games.play"() { const game = Games.findOne({status: "waiting"}); if(game === undefined) { gameLogic.newGame(); } else if(game !== undefined && game.player1 !== this.userId && game.player2 === "") { gameLogic.joinGame(game); } } }); 

Итак, теперь мы добавили else если с тремя условиями: если мы нашли игру, и player1 не является этим игроком, а player2 – пустая строка, мы присоединяемся к игре.

Делать ход – Логика

Когда мы определяли нашу модель для каждой новой игры, мы объявили поле ходов с пустым массивом ( [] ) в качестве значения по умолчанию. Ходом будет объект JSON, составленный из _id игрока, который сделал ход, и выбранной позиции.

Перейдите в файл games.js и добавьте следующий метод ниже games.play() . Помните, что Meteor.methods принимает объект JSON, поэтому методы должны быть разделены запятыми:

Библиотека / games.js

 "games.makeMove"(position) { check(position, String); gameLogic.validatePosition(position); let game = Games.findOne({status: this.userId}); if(game !== undefined) { gameLogic.addNewMove(position); if(gameLogic.checkIfGameWasWon()) { gameLogic.setGameResult(game._id, this.userId); } else { if(game.moves.length === 8) { gameLogic.setGameResult(game._id, "tie"); } else { gameLogic.updateTurn(game); } } } } 

Давайте рассмотрим этот метод построчно. Он принимает position строки в качестве параметра. Сначала мы используем пакет проверки, чтобы убедиться, что мы получили строку, а не какой-то вредоносный код, который может повредить наш сервер, а затем мы проверяем позицию.

После этого мы находим игру, в которой поле состояния совпадает с _id игрока, делающего ход; Таким образом, мы знаем, что их очередь. Если мы нашли эту игру или, другими словами, если это ход этого игрока, мы добавим ход в наш массив moves . Затем мы проверяем, была ли игра выиграна после этого хода. Если он действительно был выигран, то мы выберем текущего игрока победителем. В противном случае, если он не был выигран, но в массиве уже восемь ходов, мы объявляем ничью. Если еще нет восьми ходов, мы обновляем ход, чтобы позволить следующему игроку двигаться.

Точно так же, как мы сделали с пакетом Session в файле ui.js. Мы должны импортировать пакет check в файле games.js . Вы знаете, как это происходит … добавьте следующую строку вверху.

 import { check } from 'meteor/check'; 

Мы используем несколько методов из класса GameLogic которые мы еще не определили. Итак, давайте продолжим и сделаем это.

Перейдите к gameLogic.js и добавьте следующие методы в класс GameLogic :

validatePosition ()

 validatePosition(position) { for (let x = 0; x < 3; x++) { for (let y = 0; y < 3; y++) { if (position === x + '' + y) return true; } } throw new Meteor.Error('invalid-position', "Selected position does not exist... please stop trying to hack the game!!"); } 

Здесь мы просто перемещаемся по сетке 3 × 3, чтобы убедиться, что отправленная позиция находится в своих пределах. Если мы не можем найти позицию, отправленную клиентом, в сетке мы выдаем ошибку.

addNewMove ()

 addNewMove(position) { Games.update( {status: Meteor.userId()}, { $push: { moves: {playerID: Meteor.userId(), move: position} } } ); } 

Здесь мы используем оператор $ push Mongo для перемещения нового массива, содержащего текущий _id игрока и position , в массив.

setGameResult ()

 setGameResult(gameId, result) { Games.update( {_id: gameId}, { $set: { "result": result, "status": "end" } } ); } 

Снова используя оператор $ set , мы обновляем поле результата до значения параметра result который может быть либо _id одного из игроков, либо «tie», и мы устанавливаем status «end».

updateTurn ()

 updateTurn(game) { let nextPlayer; if(game.player1 === Meteor.userId()) nextPlayer = game.player2; else nextPlayer = game.player1; Games.update( {status: Meteor.userId()}, { $set: { "status": nextPlayer } } ); } 

Это довольно просто. Мы берем обоих игроков в качестве параметров и выясняем, кто из них является текущим игроком, затем устанавливаем в поле status _id другого игрока.

Победа в игре

Остался еще один метод для объявления из метода games.makeMove ; Алгоритм победы. Существуют и другие, более эффективные способы подсчета победителей в игре TicTacToc , но я решил выбрать наиболее интуитивно понятное и простое решение, которое я мог придумать для этого урока.

Перейдите в файл gameLogic.js и добавьте следующий метод в класс GameLogic :

Библиотека / gameLogic.js

 checkIfGameWasWon() { const game = Games.findOne({status: Meteor.userId()}); const wins = [ ['00', '11', '22'], ['00', '01', '02'], ['10', '11', '12'], ['20', '21', '22'], ['00', '10', '20'], ['01', '11', '21'], ['02', '12', '22'] ]; let winCounts = [0,0,0,0,0,0,0]; for(let i = 0; i < game.moves.length; i++) { if(game.moves[i].playerID === Meteor.userId()) { const move = game.moves[i].move; for(let j = 0; j < wins.length; j++) { if(wins[j][0] == move || wins[j][1] == move || wins[j][2] == move) winCounts[j] ++; } } } for(let i = 0; i < winCounts.length; i++) { if(winCounts[i] === 3) return true; } return false; } 

Давайте посмотрим на этот метод внимательно.

Сначала мы находим текущую игру. Затем мы объявляем матрицу со всеми возможными выигрышными комбинациями и другую переменную с массивом из семи нулей: по одной для каждой комбинации. После этого мы пройдемся по всем ходам текущего игрока и сравним их с каждой позицией каждой комбинации. Для каждого совпадения мы добавляем 1 к соответствующей winCount индекса winCount . Если какой-либо из индексов winCount 3, мы узнаем, что текущий игрок выиграл.

Не волнуйтесь, если вы не получили это в первый раз. Сделайте небольшой перерыв, выпейте кофе и прочитайте его позже пару раз с новыми глазами. Объяснение кода может привести к путанице. Иногда даже лучше просто прочитать код и понять, что он делает.

Делать ход – Контроллер

Наш игровой контроллер для этой игры – не более чем простой щелчок. Так что это должно быть просто. Давайте перейдем к файлу board.js и добавим объект шаблона событий в наш файл после helpers :

клиент / JS / board.js

 Template.board.events({ "click .selectableField": (event) => { Meteor.call("games.makeMove", event.target.id); } }); 

Просто, правда? Когда игрок щелкает элемент DOM с классом «selectableField», мы вызываем метод games.makeMove , передавая идентификатор элемента DOM в качестве параметра позиции. Помните, что мы называем идентификатор после позиции элемента в сетке. Взгляните на файл board.html, чтобы обновить вашу память, если вам нужно.

Показаны ходы

Теперь в том же файле мы создадим помощника isMarked , который будет переключаться между mark и selectableFields . Таким образом, мы сможем увидеть, какие позиции были выбраны, и позволить выбрать пустые позиции.

Добавьте этот помощник ниже помощника sideLength :

клиент / JS / board.js

 isMarked: (x, y) => { if(Session.get("inGame")) { let myGame = Games.findOne(); if(myGame !== undefined && myGame.status !== "waiting") { for(let i = 0; i < myGame.moves.length; i++) { if(myGame.moves[i].move === x + '' + y) { if(myGame.moves[i].playerID === Meteor.userId()) return "<p class='mark'>X</p>"; else return "<p class='mark'>O</p>"; } } if(myGame.status === Meteor.userId()) return "<div class='selectableField' id='"+x+y+"'></div>"; } } } 

и добавьте помощника в шаблон:

клиент / html / board.html

 ... <td class="field" id="{{rowIndex}}{{@index}}"> {{{isMarked rowIndex @index}}} </td> ... 

Давайте рассмотрим эту функцию. В качестве параметров мы берем строку и столбец (x, y). Если мы в inGame , мы ищем эту игру. Если мы находим его и status «жду», мы перебираем все ходы, и если данная строка + столбец соответствует одному из наших moves , мы нарисуем X на доске. Если он совпадает с ходом другого игрока, мы нарисуем O.

Наши ходы всегда будут Х, а оппонент – О , в каждой игре. Хотя ваши противники увидят, что их ходы нарисованы как X Нам не важно, кто получил Х или О, потому что мы играем на разных устройствах, возможно, даже в разных странах. Здесь важно то, что каждый игрок знает, какие у него ходы, а какие у противника.

Отображение статуса

Мы почти закончили! Помните пустой помощник по статусу в файле ui.js ? Заполните его следующим кодом:

клиент / JS / ui.js

 status: () => { if(Session.get("inGame")) { let myGame = Games.findOne(); if(myGame.status === "waiting") return "Looking for an opponent..."; else if(myGame.status === Meteor.userId()) return "Your turn"; else if(myGame.status !== Meteor.userId() && myGame.status !== "end") return "opponent's turn"; else if(myGame.result === Meteor.userId()) return "You won!"; else if(myGame.status === "end" && myGame.result !== Meteor.userId() && myGame.result !== "tie") return "You lost!"; else if(myGame.result === "tie") return "It's a tie"; else return ""; } } 

Это довольно очевидно, но я объясню это на всякий случай. Если мы в inGame , мы ищем текущую игру. Если status равен «ожидание», мы говорим игроку ждать оппонента. Если status равен _id игрока, мы говорим, что это их очередь. Если status не их _id и матч не закончен, мы сообщаем им, что настала очередь противника. Если результат равен _id игрока, мы сообщаем игроку, что он выиграл. Если матч подошел к концу, и результатом является не их _id и не «ничья», то они проиграли. Если результат равен «ничья», мы говорим им, что это ничья… 😉

Как сейчас, вы можете взять его на спин. Да! Откройте обычное окно браузера и личную вкладку и играйте против себя. Постарайтесь не слишком веселиться, иначе вы останетесь одни на всю оставшуюся жизнь (это правда, клянусь).

Выйти

Buuuuuut, мы еще не закончили. Нет! Что если мы отключим и оставим других игроков сами? Как насчет всех этих законченных игр, занимающих драгоценное место в нашей базе данных? Нам нужно отслеживать соединение игрока и действовать соответственно.

Но сначала нам понадобится способ удалить игры и удалить игроков из игр. Перейдите к gamesLogic.js и добавьте следующие методы в класс GameLogic :

Библиотека / gameLogic.js

 removeGame(gameId) { Games.remove({_id: gameId}); } removePlayer(gameId, player) { Games.update({_id: gameId}, {$set:{[player]: ""}}); } 

Метод removeGame принимает аргумент gameId и удаляет его.
removePlayer() принимает gameId и player (строку, которая может быть как player1 и player2 ) в качестве аргументов и очищает поле этого игрока в этой конкретной игре.

Чтобы отслеживать подключение пользователя, мы установим полезный пакет под названием mizzao: user-status . Зайдите в консоль, закройте запущенное приложение с помощью Ctrl + с и выполните следующую команду:

 meteor add mizzao:user-status 

Этот пакет имеет обратный вызов connectionLogout который предоставляет параметр с важной информацией, такой как userId отключающего пользователя.

Перейдите к файлу main.js в папке сервера и добавьте следующий обратный вызов внизу.

/server/main.js

 UserStatus.events.on("connectionLogout", (fields) => { const game = Games.findOne( {$or:[ {player1: fields.userId}, {player2: fields.userId}] }); if(game != undefined) { if(game.status !== "waiting" && game.status !== "end") { if(game.player1 === fields.userId) { gameLogic.setGameResult(game._id, game.player2); gameLogic.removePlayer(game._id, "player1"); } else if(game.player2 === fields.userId) { gameLogic.setGameResult(game._id, game.player1); gameLogic.removePlayer(game._id, "player2"); } } else { if(game.player1 === "" || game.player2 === "") { gameLogic.removeGame(game._id); } else { if(game.player1 === fields.userId) gameLogic.removePlayer(game._id, "player1"); else if(game.player2 === fields.userId) gameLogic.removePlayer(game._id, "player2"); } } } }); 

Итак, если мы можем найти игру, в которой отключенным игроком является player1 или player2 , мы проверяем, не является ли состояние этой игры «ожидающим», и игра не подошла к концу. Если это так, мы отдаем победу противнику и удаляем разъединяющего игрока. В противном случае мы либо удаляем игру (если какое-либо из полей игрока пустое), либо. если это не так, мы удаляем отключившегося игрока из игры.

Как и в случае с другими пакетами, мы должны импортировать пакет UserStatus . Мы также использовали некоторые методы из класса GameLogic в GameLogic connectionLogout , так что GameLogic импортируем оба из них вверху файла server / main.js :

 import { UserStatus } from 'meteor/mizzao:user-status'; import { gameLogic } from '../lib/gameLogic.js'; 

Завершение

Наконец, у вас должна быть рабочая игра! Вы можете загрузить его и попробовать с друзьями … или самостоятельно.

Если что-то из того, что мы сделали, сейчас для вас мало что значит, не беспокойтесь об этом; Это будет иметь смысл довольно скоро, если вы продолжите изучать код. Вам просто нужно время, чтобы обдумать некоторые понятия. Это совершенно естественный процесс. Если вы застряли, не забудьте проверить код готового приложения .

Когда вы чувствуете себя достаточно комфортно с кодом, вы должны начать пытаться добавить некоторые функциональные возможности. Может быть, реализовать другой алгоритм выигрыша, который может позволить вам увеличить размер доски. Возможно, реализовать постоянство для игроков, чтобы сохранить статистику и вести учет игр. Вы даже можете реализовать интерфейс входа в систему и позволить игрокам выбирать имя пользователя. Как насчет того, чтобы бросить вызов другу? И, конечно же, вы можете использовать те же концепции, чтобы создать совершенно другую игру.

Я хотел бы увидеть, что вы придумали, поэтому, пожалуйста, дайте мне знать! Надеюсь, вам понравился этот урок, оставьте свои сомнения и комментарии в комментариях. Увидимся в следующем!