Karşınızda Battlescripts: Botlar, Gemiler, Kargaşa!

Yayınlanan: 2022-03-11

Programlamanın yalnızca uygulamalar oluşturmak, hedeflere ulaşmak ve proje özelliklerini karşılamakla ilgili olması gerekmez. Eğlenmekle, bir şeyler yaratma sürecinden zevk almakla da ilgili olabilir. Birçok insan bu becerinin programlanmasını ve geliştirilmesini bir eğlence biçimi olarak görür. Toptal'da topluluğumuz içinde ilginç bir şey denemek istedik. Battleship etrafında artık açık kaynaklı bir bot-bot-bot oyun platformu oluşturmaya karar verdik.

Karşınızda Battlescripts: Botlar, Gemiler, Kargaşa!

Dahili olarak ilk lansmanından bu yana platform, topluluğumuzdaki bazı harika bot üreticilerinin dikkatini çekti. Topluluk üyelerinden biri olan Toptal mühendisi Quan Le'nin Battlescripts Botlarında kolayca hata ayıklamak için bir araç bile geliştirdiğini görmek bizi gerçekten etkiledi. Duyuru ayrıca, birkaç kişi arasında farklı oyun türlerini ve farklı kuralları destekleyen kendi bot-vs-bot motorlarını yaratmaya yönelik bir ilgi uyandırdı. Battlescripts piyasaya çıktığı andan itibaren inanılmaz fikirler akmaya başladı. Bugün, Battlescripts'i açık kaynak yapmaktan mutluluk duyuyoruz. Bu, topluluğumuza ve diğer herkese kodu keşfetme, katkıda bulunma ve/veya ondan tamamen başka bir şey yapma fırsatı verir.

Savaş Senaryolarının Anatomisi

Battlescripts, bazı çok basit bileşenler kullanılarak oluşturulmuştur. Node.js üzerinde çalışır ve Express, Mongoose vb. gibi en popüler ve iyi uygulanmış paketlerden bazılarını kullanır. Arka uç, ön uç komut dosyalarının yanı sıra saf JavaScript'tedir. Bu uygulamanın yalnızca iki harici bağımlılığı MongoDB ve Redis'tir. Botlar için kullanıcı tarafından gönderilen kodlar, Node.js ile birlikte gelen “vm” modülü kullanılarak çalıştırılır. Üretimde, Docker ek güvenlik için kullanılır, ancak Battlescript'lerin kesin bir bağımlılığı değildir.

savaş senaryoları

Battlescripts kodu GitHub'da BSD 3-madde lisansı altında mevcuttur. Dahil edilen README.md dosyası, deponun nasıl klonlanacağına ve uygulamanın yerel olarak nasıl başlatılacağına ilişkin ayrıntılı talimatlar içerir.

Web sunucusu

Uygulamanın basit Express.js web uygulamalarına benzer bir yapıya sahip olduğunu fark edeceksiniz. app.js dosyası, veritabanına bir bağlantı kurarak, bazı ortak ara yazılımları kaydederek ve bazı sosyal kimlik doğrulama stratejileri tanımlayarak sunucuyu önyükler. Ayrıca tüm modeller ve rotalar “lib/” dizini içerisinde tanımlanmıştır. Tamamen uygulama yalnızca birkaç model gerektirir: Savaş, Bot, Meydan Okuma, Yarışma, Parti ve Kullanıcı. Botlar arasındaki savaşlar, web sunucusu düğümlerinin dışında simüle edilir ve Node.js paketi Kue kullanılarak yapılır. Bu, motoru web uygulamasının geri kalanından ayırmamızı sağlayarak, savaş simülasyon motorunun web sunucularına müdahale etme olasılığını azaltarak web uygulamasının kendisini daha duyarlı ve kararlı tutar.

Botlar ve Motor

Botların JavaScript'te uygulanması beklendiğinden ve Node.js ile arka uçta tam olarak sahip olduğumuz şey bu olduğundan, motoru oluşturmak daha kolaydı. Kullanıcı tarafından gönderilen kodu yürütmek söz konusu olduğunda, en büyük zorluklardan biri, kodun sunucuda kötü amaçlı bir şey yapmadığından veya hatalı olan herhangi bir kodun genel sistemin kararlılığını engellemediğinden emin olmaktır. Node.js'nin standart kitaplığı, bu görevin bir kısmını çok kolaylaştıran bu harika modülle birlikte gelir. Node.js geliştiricilerinin güvenilmeyen kodu ayrı bir bağlamda çalıştırmasını kolaylaştırmak için "vm" modülü tanıtıldı. Resmi belgelere göre, güvenilmeyen kodu ayrı bir süreçte çalıştırmak önemlidir - ancak bu, üretim sunucularında yaptığımız bir şeydir. Yerel geliştirme sırasında “vm” modülü ve sunduğu özellikler sorunsuz çalışıyor.

Hala yasalken botların birbirleriyle savaşmasını sağlayın!
Cıvıldamak

JavaScript'i yürütme

Node.js'de ayrı bir bağlam altında rastgele bir JavaScript kodu çalıştırmak istiyorsanız, “vm” modülünü aşağıdaki gibi kullanabilirsiniz:

 var vm = require('vm') var ctxObj = { result: '' } vm.runInNewContext(' result = “0xBEEF” ', ctxObj ) console.log(ctxObj); // { result: “0xBEEF” }

Bu "yeni bağlam" içinde, çalıştırdığınız kod "console.log" a bile erişemez, çünkü bu bağlamda böyle bir işlev yoktur. Bununla birlikte, orijinal bağlamın “context.log” işlevini “ctxObj” özniteliği olarak ileterek yeni bağlama gösterebilirsiniz.

Battlescript'lerde, savaşları simüle eden düğümler, her bir botu ayrı Node.js "vm" bağlamları altında çalıştırır. Motor, oyunun kurallarına göre her iki bot için bağlamların durumunu senkronize etme sorumluluğunu üstlenir.

JavaScript kodunu yalıtılmış bağlamda çalıştırmak bu modülün yaptığı tek şey değildir. "runInNewContext" işlevi, bu kod yürütmesinin üç ek yönünü kontrol edebilen üçüncü parametre olarak bir nesneyi kabul eder:

  • Bu yürütmeyle ilgili oluşturulan yığın izlemelerinde kullanılacak dosya adı.
  • Hataların stderr'e yazdırılıp yazdırılmayacağı.
  • Yürütmenin zaman aşımına uğramadan önce devam etmesine izin vermek için milisaniye sayısı.

Bu “vm” modülünün tuzaklarından biri, bellek kullanımını sınırlamak için herhangi bir yol sağlamamasıdır. Bu, modülün diğer birkaç sınırlaması ile birlikte, sunucuda Docker kullanımı ve motor düğümlerinin çalışma şekli aracılığıyla çözülür. “vm” modülü, çok sık kullanıldığında, takibi zor ve boş olan bellek sızdırmaya başlar. Bağlam nesneleri yeniden kullanılsa bile bellek kullanımı artmaya devam eder. Bu sorunu basit bir strateji izleyerek çözdük. Çalışan düğümde her savaş simülasyonu yapıldığında, düğüm çıkar. Üretim sunucusundaki süpervizör programı, daha sonra, bir sonraki savaş simülasyonunu bir saniyeden kısa bir sürede işlemeye hazır hale gelen çalışan düğümü yeniden başlatır.

genişletilebilirlik

Battlescripts orijinal olarak Battleship'in standart kuralları etrafında tasarlandı. İçerideki motor çok genişletilebilir değildi. Bununla birlikte, Battlescripts piyasaya sürüldükten sonra, en yaygın isteklerden biri daha yeni oyun türlerini tanıtmaktı, çünkü uygulamanın kullanıcıları, bazı oyunların botlarla diğerlerinden daha kolay fethedilmesinin daha kolay olduğunu hemen fark etti. Örneğin, TicTacToe'yu Satranç ile karşılaştırırsanız, birincisinin durum alanı çok daha küçüktür, bu da botların oyunu kazanacak veya berabere bitirecek bir çözüm bulmasını çok kolaylaştırır.

Battlescripts motoru, daha yeni oyun türlerini tanıtmayı kolaylaştırmak için yakın zamanda biraz değiştirildi. Bu, bir avuç kanca benzeri işleve sahip bir yapıyı takip ederek yapılabilir. Ek bir oyun türü olan TicTacToe, takip etmesi daha kolay olduğu için kod tabanına eklendi. Bu oyun türüyle ilgili her şey “lib/games/tictactoe.js” dosyasında bulunabilir.

Ancak bu yazımızda Battleship oyun türünün uygulanmasına bir göz atacağız. TicTacToe oyun kodunun keşfi daha sonra bir alıştırma olarak bırakılabilir.

savaş gemisi

Oyunun nasıl uygulandığına bir göz atmadan önce, Battlescript için standart bir botun nasıl göründüğüne bir göz atalım:

 function Bot() {} Bot.prototype.play = function(turn) { // ... }

Bu oldukça fazla. Her bot, tek bir "oynat" yöntemiyle yapıcı bir işlev olarak tanımlanır. Yöntem, her dönüş için bir argümanla çağrılır. Herhangi bir oyun için, argüman, botun sıra için hamlesini yapmasına izin veren ve oyun durumunu temsil eden bazı ek niteliklerle birlikte gelebilen tek yöntemli bir nesnedir.

Daha önce de belirtildiği gibi, motor son zamanlarda biraz değiştirildi. Tüm Battleship'e özgü mantık, gerçek motor kodundan çıkarıldı. Motor hala ağır kaldırmayı yaptığından, Battleship oyununu tanımlayan kod çok basit ve hafiftir.

 function Battleships(bot1, bot2) { return new Engine(bot1, bot2, { hooks: { init: function() { // ... }, play: function() { // ... }, turn: function() { // ... } } }) } module.exports = exports = Battleships

Burada yalnızca kanca benzeri üç işlevi nasıl tanımladığımıza dikkat edin: init, play ve turn. Her işlev, bağlamı olarak motorla birlikte çağrılır. "init" işlevi, motor nesnesi oluşturulduğunda, yapıcı işlevinden başlatılır. Tipik olarak bu, motorun tüm durum özniteliklerini hazırlamanız gereken yerdir. Her oyun için hazırlanması gereken bu niteliklerden biri de “ızgaralar” ve (isteğe bağlı olarak) “parçalar”dır. Bu her zaman, oyun tahtasının durumunu temsil eden, her oyuncu için bir tane olmak üzere iki elemanlı bir dizi olmalıdır.

 for(var i = 0; i < this.bots.length; ++i) { var grid = [] for(var y = 0; y < consts.gridSize.height; ++y) { var row = [] for(var x = 0; x < consts.gridSize.width; ++x) { row.push({ attacked: false }) } grid.push(row) } this.grids.push(grid) this.pieces.push([]) }

İkinci kanca, "play", oyun başlamadan hemen önce başlatılır. Bu yararlıdır, çünkü bu bize oyun parçalarını botlar adına tahtaya yerleştirmek gibi şeyler yapma fırsatı verir.

 for(var botNo = 0; botNo < this.bots.length; ++botNo) { for(var i = 0; i < consts.pieces.length; ++i) { var piece = consts.pieces[i] for(var j = 0; j < piece.many; ++j) { var pieceNo = this.pieces[botNo].length var squares = [] for(var y = 0; y < consts.gridSize.height; ++y) { for(var x = 0; x < consts.gridSize.width; ++x) { squares.push({ x: x, y: y, direction: 'h' }) squares.push({ x: x, y: y, direction: 'v' }) } } var square = _.sample(squares.filter(function(square) { var f = { 'h': [1, 0], 'v': [0, 1] } for(var xn = square.x, yn = square.y, i = 0; i < piece.size; xn += f[square.direction][0], yn += f[square.direction][1], ++i) { var d = [[0, -1], [0, 1], [-1, 0], [1, 0], [-1, -1], [-1, 1], [1, -1], [1, 1]] for(var j = 0; j < d.length; ++j) { var xp = xn+d[j][0] var yp = yn+d[j][1] if(xp >= 0 && xp < 10 && yp >= 0 && yp < 10 && this.grids[botNo][yp][xp].pieceNo >= 0) { return false } } if(xn >= 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 < square.x+piece.size; ++k) { this.grids[botNo][square.y][k].pieceNo = pieceNo } break case square.direction === 'v': for(var k = square.y; k < square.y+piece.size; ++k) { this.grids[botNo][k][square.x].pieceNo = pieceNo } break } this.pieces[botNo].push({ kind: piece.kind, size: piece.size, x: square.x, y: square.y, direction: square.direction, hits: 0, dead: false }) } } }

Bu ilk başta biraz bunaltıcı görünebilir, ancak bu kod parçasının ulaştığı hedef basittir. Her bot için bir tane olmak üzere parça dizileri oluşturur ve bunları tek tip bir şekilde karşılık gelen ızgaralara yerleştirir. Her parça için ızgara taranır ve her geçerli konum geçici bir dizide saklanır. Geçerli bir konum, iki parçanın üst üste gelmediği veya bitişik hücreleri paylaşmadığı yerdir.

Son olarak, üçüncü ve son kanca “döner”. Diğer iki kancanın aksine, bu biraz farklı. Bu kancanın amacı, motorun botun oynatma yöntemini çağırırken ilk argüman olarak kullandığı bir nesneyi döndürmektir.

 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 < pieces.length; ++i) { if(!pieces[i].dead) { undead = true } } if(!undead) { this.end(botNo) } } this.track(botNo, true, { x: x, y: y, baam: !!baam }) return baam }.bind(this)) }

Bu yöntem içerisinde botun başarılı bir şekilde hareket ettiğini motora bildirerek başlıyoruz. Herhangi bir sırayla herhangi bir oyun için saldıran bir hamle yapamayan bir bot, oyunu otomatik olarak kaybeder. Ardından, hareketin gemiye başarılı bir şekilde çarpması durumunda, geminin tamamen yok edilip edilmediğini belirleriz. Öyle olması durumunda, yok edilen geminin ayrıntılarını döndürürüz, aksi takdirde herhangi bir ek bilgi olmadan başarılı bir isabeti belirtmek için "true" değerini döndürürüz.

Bu kodlar boyunca “this” de bulunan bazı öznitelikler ve yöntem adlarıyla karşılaştık. Bunlar Engine nesnesi tarafından sağlanır ve her birinin bazı basit davranışsal özellikleri vardır:

  • this.turn.çağrıldı: Bu, her turdan önce false olarak başlar ve motora botun tur için hareket ettiğini bildirmek için true olarak ayarlanmalıdır.

  • this.turn.botNo: Bu turda hangi botun oynadığına bağlı olarak bu 0 veya 1 olacaktır.

  • this.end(botNo): Bunu bir bot numarası ile çağırmak oyunu bitirir ve botu muzaffer olarak işaretler. -1 ile çağırmak oyunu berabere bitirir.

  • this.track(botNo, isOkay, data, failReason): Bu, bot için hareket ayrıntılarını veya başarısız bir hareketin nedenini kaydetmenizi sağlayan bir kolaylık yöntemidir. Sonunda, bu kaydedilen veriler, simülasyonu ön uçta görselleştirmek için kullanılır.

Esasen, bu platformda bir oyun uygulamak için arka uçta yapılması gereken tek şey budur.

Oyunları Tekrar Oynamak

Bir savaş simülasyonu biter bitmez, ön uç kendisini oyun tekrar sayfasına yönlendirir. Burası simülasyonun ve sonuçların görselleştirildiği ve oyunla ilgili diğer verilerin görüntülendiği yerdir.

Bu görünüm, bağlam içindeki tüm savaş ayrıntılarıyla birlikte "views/" içindeki "battle-view-battleships.jade" kullanılarak arka uç tarafından oluşturulur. Oyunun yeniden oynatma animasyonu, ön uç JavaScript aracılığıyla yapılır. Motorun “trace()” yöntemiyle kaydedilen tüm veriler bu şablon bağlamında mevcuttur.

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

Sıradaki ne?

Artık Battlescripts açık kaynak olduğuna göre katkılarınızı bekliyoruz. Platform şu anki aşamasında olgun, ancak iyileştirmeler için çok fazla alana sahip. Yeni bir özellik, bir güvenlik yaması veya hatta hata düzeltmeleri olsun, depoda çözülmesini talep eden bir sorun oluşturmaktan çekinmeyin veya depoyu çatallayın ve bir çekme isteği gönderin. Ve bu size tamamen yeni bir şey inşa etmeniz için ilham veriyorsa, aşağıdaki yorumlar bölümünde bize bir bağlantı bıraktığınızdan ve bize bildirdiğinizden emin olun!