Не е необходимо програмирането да се състои в изграждането на приложения, постигане на целите и удовлетворяване на спецификациите на проекта. Може да става дума и за забавление, за наслада от процеса на създаване на нещо . Много хора третират програмирането и развитието на това умение като форма на отдих. В ApeeScape искахме да изпробваме нещо интересно в нашата общност. Решихме да изградим платформа за игри bot-vs-bot около Battleship, това е сега отворен код .
От първоначалното си стартиране вътрешно, платформата привлече вниманието на някои невероятни производители на ботове в нашата общност. Наистина бяхме впечатлени, когато видяхме, че един от членовете на общността, Инженер от ApeeScape Куан Ле , дори изграден a инструмент за лесно отстраняване на грешки в Battlescripts Bots . Съобщението също предизвика интерес сред малцина да създадат свои собствени двигатели bot-vs-bot, поддържащи различни видове игри и различни правила. Удивителни идеи започнаха да се вливат от момента на представяне на Battlescripts. Днес сме щастливи да направим Battlescripts с отворен код. Това дава на нашата общност и на всички останали възможност да изследват кода, да правят принос и / или да го разклоняват, за да направи нещо друго изцяло от него.
Battlescripts се изгражда с помощта на някои много прости компоненти. Той работи на Node.js и използва някои от най-популярните и добре внедрени пакети, като Експрес , Мангуста и т.н. Задният край е на чист JavaScript, както и предните скриптове. Единствените две външни зависимости на това приложение са MongoDB и Редис . Изпратеният от потребителя код за ботове се изпълнява с помощта на Модул “vm” който идва с Node.js. В производството, Докер се използва за допълнителна безопасност, но не е твърда зависимост от Battlescripts.
Кодът за Battlescripts е на разположение на GitHub съгласно лиценза за 3-клауза BSD. Включените README.md файл съдържа подробни инструкции за това как да клонирате хранилището и да стартирате приложението локално.
Ще забележите, че приложението има структура, подобна на тази на обикновените уеб приложения на Express.js. Файлът app.js стартира сървъра, като установява връзка с базата данни, регистрира някои често срещани посредници и дефинира някои стратегии за социално удостоверяване. Освен това всички модели и маршрути са дефинирани в директорията “lib /”. Изцяло приложението изисква само няколко модела: Battle, Bot, Challenge, Contest, Party и User. Битките между ботове се симулират извън възлите на уеб сървъра и се извършват с помощта на пакета Node.js Kue. Това ни позволява да изолираме двигателя от останалата част на уеб приложението, което прави по-малко вероятно механизмът за симулация на битка да пречи на уеб сървърите, като поддържа самото уеб приложение по-отзивчиво и стабилно.
Тъй като се очаква ботовете да бъдат внедрени в JavaScript и точно това имаме в нашия back-end с Node.js, беше по-лесно да изградим двигателя. Що се отнася до изпълнението на изпратения от потребителя код, едно от най-големите предизвикателства е да се уверите, че кодът не прави нещо злонамерено на сървъра или че всеки код, който е бъги, не пречи на стабилността на цялостната система. Стандартната библиотека на Node.js се доставя с този невероятен модул, който прави много лесна част от тази задача. Модулът “vm” беше въведен, за да го улесни Разработчици на Node.js за да стартирате ненадежден код в отделен контекст. Въпреки че според официалната документация е важно да стартираме ненадежден код в отделен процес - но това е нещо, което правим на производствените сървъри. По време на местното развитие модулът „vm“ и функциите, които предлага, работят добре.
Накарайте ботовете да се бият помежду си, докато това все още е законно! TweetАко искате да стартирате произволен JavaScript код в Node.js под отделен контекст, можете да използвате модула “vm”, както следва:
var vm = require(‘vm’) var ctxObj = { result: ‘’ } vm.runInNewContext(‘ result = “0xBEEF” ’, ctxObj ) console.log(ctxObj); // { result: “0xBEEF” }
В рамките на този „нов контекст“ кодът, който стартирате, дори няма достъп до „console.log“, тъй като в този контекст такава функция не съществува. Можете обаче да изложите функцията „context.log“ на оригиналния контекст в новия контекст, като го предадете като атрибут на „ctxObj“.
В Battlescripts, възлите, които симулират битки, изпълняват всеки бот под отделен контекст на Node.js „vm“. Двигателят поема отговорността да синхронизира състоянието на контекста и за двата бота според правилата на играта.
Изпълнението на JavaScript код в изолиран контекст не е всичко, което този модул прави. Функцията “runInNewContext” приема обект като трети параметър, който може да контролира три допълнителни аспекта на изпълнението на този код:
Една от клопките на този модул „vm“ е, че той не предоставя никакви средства за ограничаване на използването на паметта. Това, заедно с няколко други ограничения на модула се работи на сървъра чрез използването на Docker и начина, по който се изпълняват двигателните възли. Модулът “vm”, когато се използва много често, започва да изтича памет, която е трудно да се проследи и освободи. Дори ако обектите на контекста се използват повторно, използването на паметта продължава да расте. Решихме този проблем, следвайки проста стратегия. Всеки път, когато битката се симулира в работния възел, възелът излиза. След това програмата за надзор на производствения сървър рестартира работния възел, който след това става готов да обработи следващата битова симулация за части от секундата.
Battlescripts първоначално е проектиран около стандартните правила на Battleship. Двигателят вътре не беше много разтегателен. След пускането на Battlescripts обаче една от най-често срещаните заявки беше да се въведат по-нови типове игри, тъй като потребителите на приложението бързо осъзнаха, че някои игри са по-лесни за завладяване с ботове, отколкото други. Например, ако сравните TicTacToe с шах, първият има много по-малко пространство на държавата, което улеснява много ботовете да излязат с решение, което или ще спечели, или ще сложи край на равенство.
Двигателят Battlescripts наскоро беше малко модифициран, за да улесни въвеждането на по-нови типове игри. Това може да стане, като просто следвате конструкция с шепа функции, подобни на кука. Допълнителен тип игра, TicTacToe, беше добавен към кодовата база, тъй като е по-лесна за следване. Всичко свързано с този тип игра може да бъде намерено във файла „lib / games / tictactoe.js“.
В тази статия обаче ще разгледаме внедряването на типа игра Battleship. Изследването на кода на играта TicTacToe може да бъде оставено като упражнение за по-късно.
Преди да разгледаме как се изпълнява играта, нека надникнем как изглежда стандартният бот за Battlescript:
function Bot() {} Bot.prototype.play = function(turn) { // ... }
Това е почти всичко. Всеки бот се дефинира като конструктор с един метод „play“. Методът се извиква за всеки ход с един аргумент. За всяка игра аргументът е обект с един метод, който позволява на бота да направи хода си за търна и може да дойде с някои допълнителни атрибути, представящи състоянието на играта.
Както бе споменато по-рано, двигателят е модифициран малко наскоро. Цялата специфична логика на Battleship е извадена от действителния код на двигателя. Тъй като двигателят все още прави тежки повдигания, кодът, който определя играта „Боен кораб“, е много прост и лек.
function Battleships(bot1, bot2) { return new Engine(bot1, bot2, { hooks: { init: function() { // ... }, play: function() { // ... }, turn: function() { // ... } } }) } module.exports = exports = Battleships
Забележете как тук просто дефинираме три функции, подобни на кука: init, play и turn. Всяка функция се извиква с двигателя като свой контекст. Функцията “init” вдясно като обект на двигателя е инстанцирана, от функцията конструктор. Обикновено тук трябва да подготвите всички атрибути на състоянието на двигателя. Един такъв атрибут, който трябва да бъде подготвен за всяка игра, е „решетки“ и (по желание) „фигури“. Това винаги трябва да е масив с два елемента, по един за всеки играч, представящ състоянието на игралната дъска.
for(var i = 0; i Втората кука, „play“, се извиква точно преди началото на играта. Това е полезно, тъй като това ни дава възможност да правим неща като поставяне на фигури от играта на дъската от името на ботовете.
for(var botNo = 0; botNo = consts.gridSize.width || yn >= consts.gridSize.height || this.grids[botNo][yn][xn].pieceNo >= 0) { return false } } return true; }.bind(this))) switch(true) { case square.direction === 'h': for(var k = square.x; k В началото това може да изглежда малко поразително, но целта, която постига тази част от кода, е проста. Той генерира масиви от парчета, по един за всеки бот, и ги поставя на съответните решетки по еднакъв начин. За всяка фигура мрежата се сканира и всяка валидна позиция се съхранява във временен масив. Валидна позиция е, когато две части не се припокриват или споделят съседни клетки.
И накрая, третото и последното „завъртане“. За разлика от другите две куки, тази е малко по-различна. Целта на тази кука е да върне обект, който двигателят използва като първи аргумент при извикване на метода за игра на бота.
return { attack: _.once(function(x, y) { this.turn.called = true var botNo = this.turn.botNo var otherNo = (botNo+1)%2 var baam = false var square = this.grids[otherNo][y][x] square.attacked = true if(square.pieceNo >= 0) { baam = true this.turn.nextNo = botNo var pieceNo = square.pieceNo var pieces = this.pieces[otherNo] var piece = pieces[pieceNo] piece.hits += 1 if(piece.hits === piece.size) { piece.dead = true baam = { no: pieceNo, kind: piece.kind, size: piece.size, x: piece.x, y: piece.y, direction: piece.direction } } var undead = false for(var i = 0; i В рамките на този метод започваме, като информираме двигателя, че ботът е направил ход успешно. Бот, който не успее да направи атакуващ ход за която и да е игра, в който и да е ход автоматично се отказва от играта. След това, в случай че ходът успешно е ударил кораба, ние определяме дали корабът е бил напълно унищожен. В случай, че е, ние връщаме подробности за кораба, който е бил унищожен, в противен случай връщаме „true“, за да покажем успешен удар без никаква допълнителна информация.
По време на тези кодове сме срещнали някои атрибути и имена на методи, които са налични в “this”. Те се предоставят от обекта Engine и всеки има някои прости поведенчески характеристики:
-
this.turn.called: Това започва като false преди всеки завой и трябва да бъде зададено на true, за да информира двигателя, че ботът е действал за завоя.
-
this.turn.botNo: Това ще бъде 0 или 1, в зависимост от това кой бот е играл този ход.
-
this.end (botNo): Извикването на това с номер на бот завършва играта и маркира бота като победител. Извикването му с -1 завършва играта с равен резултат.
-
this.track (botNo, isOkay, data, failReason): Това е удобен метод, който ви позволява да записвате подробности за преместването на бота или причина за неуспешен ход. В крайна сметка тези записани данни се използват за визуализиране на симулацията на предния край.
По същество това е всичко, което трябва да се направи от задната страна, за да се внедри игра на тази платформа.
Възпроизвеждане на игри
Веднага след като приключи симулация на битка, предният край се пренасочва към страницата за преиграване на играта. Тук се визуализират симулацията и резултатите и се показват други данни, свързани с играта.

Този изглед се изобразява от задната част, използвайки “battle-view-warships.jade” в “views /” с всички подробности за битката в контекст. Повторната анимация на играта се извършва чрез JavaScript отпред. Всички данни, записани чрез метода “trace ()” на двигателя, са достъпни в контекста на този шаблон.
function play() { $('.btn-play').hide() $('.btn-stop').show() if(i === moves.length) { i = 0 stop() $('.ul-moves h4').fadeIn() return } if(i === 0) { $('.ul-moves h4').hide() $('table td').removeClass('warning danger') $('.count span').text(0) } $('.ul-moves li').slice(0, $('.ul-moves li').length-i).hide() $('.ul-moves li').slice($('.ul-moves li').length-i-1).show() var move = moves[i] var $td = $('table').eq((move.botNo+1)%2).find('tr').eq(move.data.y+1).find('td').eq(move.data.x+1) if(parseInt($td.text()) >= 0) { $td.addClass('danger') } else { $td.addClass('warning') } ++i $('.count span').eq(move.botNo).text(parseInt($('.count span').eq(move.botNo).text())+1) var delay = 0 switch(true) { case $('.btn-fast').hasClass('active'): delay = 10 break case $('.btn-slow').hasClass('active'): delay = 100 break case $('.btn-slower').hasClass('active'): delay = 500 break case $('.btn-step').hasClass('active'): stop() return } playTimer = setTimeout(function() { play() }, delay) } function stop() { $('.btn-stop').hide() $('.btn-play').text(i === 0 ? 'Re-play' : ($('.btn-step').hasClass('active') ? 'Next' : 'Resume')).show() clearTimeout(playTimer) } $('.btn-play').click(function() { play() }) $('.btn-stop').click(function() { stop() })
Какво следва?
Сега, когато Battlescripts е с отворен код, приносът е добре дошъл. Платформата на сегашния си етап е зряла, но има много място за подобрения. Независимо дали става дума за нова функция, корекция на защитата или дори корекции на грешки, не се колебайте да създадете проблем в хранилището с искане да бъде адресиран или да разклоните хранилището и да подадете заявка за изтегляне. И ако това ви вдъхновява да изградите нещо изцяло ново, не забравяйте да ни уведомите и да оставите връзка към него в раздела за коментари по-долу!