Memperkenalkan Battlescripts: Bot, Kapal, Kekacauan!

Diterbitkan: 2022-03-11

Pemrograman tidak harus semua tentang membangun aplikasi, memenuhi tujuan, dan memenuhi spesifikasi proyek. Bisa juga tentang bersenang-senang, tentang menikmati proses menciptakan sesuatu. Banyak orang memperlakukan pemrograman dan pengembangan keterampilan ini sebagai bentuk rekreasi. Di Toptal, kami ingin mencoba sesuatu yang menarik dalam komunitas kami. Kami memutuskan untuk membangun platform game bot-vs-bot di sekitar Battleship, yang sekarang bersifat open-source.

Memperkenalkan Battlescripts: Bot, Kapal, Kekacauan!

Sejak peluncuran awalnya secara internal, platform ini telah menarik perhatian beberapa pembuat bot luar biasa dalam komunitas kami. Kami sangat terkesan melihat salah satu anggota komunitas, insinyur Toptal Quan Le, bahkan membuat alat untuk men-debug Battlescripts Bots dengan mudah. Pengumuman tersebut juga memicu minat beberapa orang untuk membuat mesin bot-vs-bot mereka sendiri, mendukung berbagai jenis permainan dan aturan yang berbeda. Ide-ide luar biasa mulai mengalir sejak Battlescripts diluncurkan. Hari ini, kami senang membuat Battlescripts open-source. Ini memberi komunitas kami dan semua orang kesempatan untuk menjelajahi kode, memberikan kontribusi, dan/atau membaginya untuk membuat sesuatu yang lain sepenuhnya.

Anatomi Battlescripts

Battlescripts dibangun menggunakan beberapa komponen yang sangat sederhana. Ini berjalan di Node.js dan menggunakan beberapa paket yang paling populer dan diimplementasikan dengan baik, seperti Express, Mongoose, dll. Back-end dalam JavaScript murni, serta skrip front-end. Hanya dua dependensi eksternal dari aplikasi ini adalah MongoDB dan Redis. Kode yang dikirimkan pengguna untuk bot dijalankan menggunakan modul “vm” yang disertakan dengan Node.js. Dalam produksi, Docker digunakan untuk keamanan tambahan, tetapi bukan ketergantungan keras dari Battlescripts.

naskah perang

Kode untuk Battlescripts tersedia di GitHub di bawah lisensi 3-klausa BSD. File README.md yang disertakan memiliki instruksi terperinci tentang cara mengkloning repositori dan meluncurkan aplikasi secara lokal.

Server Web

Anda akan melihat bahwa aplikasi memiliki struktur yang mirip dengan aplikasi web Express.js sederhana. File app.js mem-bootstrap server dengan membuat koneksi ke database, mendaftarkan beberapa middlewares umum, dan menentukan beberapa strategi otentikasi sosial. Selanjutnya, semua model dan rute didefinisikan dalam direktori “lib/”. Seluruh aplikasi hanya membutuhkan beberapa model: Pertempuran, Bot, Tantangan, Kontes, Pesta, dan Pengguna. Pertempuran antar bot disimulasikan di luar node server web, dan dilakukan menggunakan paket Kue Node.js. Ini memungkinkan kami untuk mengisolasi mesin dari aplikasi web lainnya, sehingga mesin simulasi pertempuran kecil kemungkinannya untuk mengganggu server web, menjaga aplikasi web itu sendiri lebih responsif dan stabil.

Bot & Mesin

Karena bot diharapkan diimplementasikan dalam JavaScript, dan itulah yang kami miliki di back-end kami dengan Node.js, lebih mudah untuk membangun mesin. Ketika datang untuk mengeksekusi kode yang dikirimkan pengguna, salah satu tantangan terbesar adalah memastikan bahwa kode tersebut tidak melakukan sesuatu yang berbahaya di server, atau bahwa kode apa pun yang bermasalah tidak mengganggu stabilitas sistem secara keseluruhan. Pustaka standar Node.js hadir dengan modul luar biasa ini yang membuat sebagian tugas ini menjadi sangat mudah. Modul “vm” diperkenalkan untuk memudahkan pengembang Node.js menjalankan kode yang tidak tepercaya dalam konteks terpisah. Meskipun menurut dokumentasi resmi, penting untuk menjalankan kode yang tidak dipercaya dalam proses terpisah - tetapi itu adalah sesuatu yang kami lakukan di server produksi. Selama pengembangan lokal, modul "vm" dan fitur yang ditawarkannya berfungsi dengan baik.

Buat bot saling bertarung, selagi masih legal!
Menciak

Menjalankan JavaScript

Jika Anda ingin menjalankan beberapa kode JavaScript arbitrer di Node.js dalam konteks terpisah, Anda dapat menggunakan modul “vm” sebagai berikut:

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

Dalam "konteks baru" ini, kode yang Anda jalankan bahkan tidak mendapatkan akses ke "console.log", karena dalam konteks itu fungsi seperti itu tidak ada. Namun, Anda dapat mengekspos fungsi "context.log" dari konteks asli ke dalam konteks baru dengan meneruskannya sebagai atribut "ctxObj".

Di Battlescripts, node yang mensimulasikan pertempuran menjalankan setiap bot di bawah konteks "vm" Node.js yang terpisah. Mesin mengambil tanggung jawab untuk menyinkronkan keadaan konteks untuk kedua bot sesuai dengan aturan permainan.

Menjalankan kode JavaScript dalam konteks terisolasi bukanlah segalanya yang dilakukan modul ini. Fungsi "runInNewContext" menerima objek sebagai parameter ketiga yang dapat mengontrol tiga aspek tambahan dari eksekusi kode ini:

  • Nama file yang akan digunakan dalam pelacakan tumpukan yang dihasilkan yang relevan dengan eksekusi ini.
  • Apakah akan mencetak kesalahan ke stderr atau tidak.
  • Jumlah milidetik untuk memungkinkan eksekusi dilanjutkan sebelum waktunya habis.

Salah satu kelemahan modul "vm" ini adalah modul ini tidak menyediakan cara apa pun untuk membatasi penggunaan memori. Ini, bersama dengan beberapa batasan lain dari modul yang dikerjakan di server melalui penggunaan Docker, dan cara node mesin dijalankan. Modul "vm", ketika digunakan sangat sering perlahan-lahan mulai membocorkan memori yang sulit dilacak dan dibebaskan. Bahkan jika objek konteks digunakan kembali, penggunaan memori terus bertambah. Kami memecahkan masalah ini dengan mengikuti strategi sederhana. Setiap kali pertempuran disimulasikan di node pekerja, node keluar. Program supervisor di server produksi kemudian me-restart node pekerja yang kemudian siap untuk menangani simulasi pertempuran berikutnya dalam sepersekian detik.

Kemungkinan diperpanjang

Battlescripts awalnya dirancang berdasarkan aturan standar Battleship. Mesin di dalamnya tidak terlalu bisa diperpanjang. Namun, setelah Battlescripts diluncurkan, salah satu permintaan yang paling umum adalah untuk memperkenalkan jenis game yang lebih baru, karena pengguna aplikasi dengan cepat menyadari bahwa beberapa game lebih mudah ditaklukkan dengan bot daripada yang lain. Misalnya, jika Anda membandingkan TicTacToe dengan Catur, yang pertama memiliki ruang status yang jauh lebih kecil, sehingga sangat mudah bagi bot untuk menemukan solusi yang akan memenangkan atau mengakhiri permainan dengan seri.

Mesin Battlescripts baru-baru ini telah dimodifikasi sedikit untuk mempermudah memperkenalkan jenis permainan yang lebih baru. Ini dapat dilakukan hanya dengan mengikuti konstruksi dengan beberapa fungsi seperti kait. Jenis permainan tambahan, TicTacToe, telah ditambahkan ke basis kode karena lebih mudah diikuti. Segala sesuatu yang relevan dengan jenis game ini dapat ditemukan di dalam file “lib/games/tictactoe.js”.

Namun, pada artikel ini, kita akan melihat implementasi dari tipe game Battleship. Eksplorasi kode game TicTacToe bisa dijadikan latihan untuk nanti.

kapal perang

Sebelum melihat bagaimana game ini diimplementasikan, mari kita lihat seperti apa bot standar untuk Battlescript:

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

Itu cukup banyak. Setiap bot didefinisikan sebagai fungsi konstruktor dengan satu metode "bermain". Metode ini dipanggil untuk setiap belokan dengan satu argumen. Untuk game apa pun, argumennya adalah objek dengan satu metode yang memungkinkan bot bergerak untuk giliran, dan bisa datang dengan beberapa atribut tambahan yang mewakili status game.

Seperti disebutkan sebelumnya, mesin telah dimodifikasi sedikit baru-baru ini. Semua logika khusus Battleship telah ditarik keluar dari kode mesin yang sebenarnya. Karena mesin masih melakukan pengangkatan berat, kode yang mendefinisikan game Battleship sangat sederhana, dan ringan.

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

Perhatikan bagaimana kita hanya mendefinisikan tiga fungsi seperti kait di sini: init, play, dan turn. Setiap fungsi dipanggil dengan mesin sebagai konteksnya. Fungsi "init" tepat saat objek engine dipakai, dari dalam fungsi konstruktor. Biasanya di sinilah Anda harus menyiapkan semua atribut keadaan mesin. Salah satu atribut yang harus disiapkan untuk setiap game adalah "grids" dan (opsional) "pieces". Ini harus selalu berupa larik dengan dua elemen, satu untuk setiap pemain, yang mewakili status papan permainan.

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

Kait kedua, "bermain", dipanggil tepat sebelum permainan dimulai. Ini berguna, karena ini memberi kita kesempatan untuk melakukan hal-hal seperti menempatkan potongan permainan di papan atas nama bot.

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

Ini mungkin terlihat sedikit berlebihan pada awalnya, tetapi tujuan yang dicapai oleh bagian kode ini sederhana. Ini menghasilkan array potongan, satu untuk setiap bot, dan menempatkannya di grid yang sesuai dengan cara yang seragam. Untuk setiap bagian, kisi dipindai dan setiap posisi yang valid disimpan dalam larik sementara. Posisi yang valid adalah di mana dua bagian tidak tumpang tindih atau berbagi sel yang berdekatan.

Akhirnya, kait ketiga dan terakhir "berputar". Berbeda dengan dua kait lainnya, yang satu ini sedikit berbeda. Tujuan dari hook ini adalah untuk mengembalikan sebuah objek, yang digunakan mesin sebagai argumen pertama dalam menjalankan metode permainan bot.

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

Dalam metode ini, kita mulai dengan memberi tahu mesin bahwa bot telah berhasil bergerak. Bot yang gagal melakukan gerakan menyerang untuk permainan apa pun di giliran mana pun secara otomatis kehilangan permainan. Selanjutnya, jika gerakan tersebut berhasil mengenai kapal, kami menentukan apakah kapal tersebut telah dihancurkan sepenuhnya. Jika demikian, kami mengembalikan detail kapal yang dihancurkan, jika tidak, kami mengembalikan "true" untuk menunjukkan hit yang berhasil tanpa informasi tambahan apa pun.

Sepanjang kode ini, kami menemukan beberapa atribut dan nama metode yang tersedia di "ini". Ini disediakan oleh objek Engine dan masing-masing memiliki beberapa karakteristik perilaku sederhana:

  • this.turn.call: Ini dimulai sebagai false sebelum setiap belokan, dan harus disetel ke true untuk memberi tahu mesin bahwa bot telah bertindak untuk belokan tersebut.

  • this.turn.botNo: Ini akan menjadi 0 atau 1, tergantung pada bot mana yang memainkan giliran ini.

  • this.end(botNo): Memanggil ini dengan nomor bot mengakhiri permainan, dan menandai bot sebagai pemenang. Menyebutnya dengan -1 mengakhiri permainan dengan seri.

  • this.track(botNo, isOke, data, failReason): Ini adalah metode praktis yang memungkinkan Anda merekam detail pemindahan bot, atau alasan kegagalan pemindahan. Akhirnya, data yang direkam ini digunakan untuk memvisualisasikan simulasi di front-end.

Pada dasarnya, hanya ini yang perlu dilakukan di bagian belakang untuk mengimplementasikan game di platform ini.

Memutar Ulang Game

Segera setelah simulasi pertempuran berakhir, front-end mengarahkan dirinya ke halaman replay game. Di sinilah simulasi dan hasil divisualisasikan, dan data terkait game lainnya ditampilkan.

Tampilan ini dirender oleh back-end menggunakan "battle-view-battleships.jade" di "views/" dengan semua detail pertempuran dalam konteks. Animasi pemutaran ulang game dilakukan melalui JavaScript front-end. Semua data yang direkam melalui metode "trace()" mesin tersedia dalam konteks template ini.

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

Apa selanjutnya?

Sekarang Battlescripts adalah open source, kontribusi dipersilakan. Platform pada tahap saat ini sudah matang, tetapi memiliki banyak ruang untuk perbaikan. Baik itu fitur baru, patch keamanan, atau bahkan perbaikan bug, silakan buat masalah di repositori yang memintanya untuk ditangani, atau fork repositori dan kirimkan permintaan tarik. Dan jika ini menginspirasi Anda untuk membangun sesuatu yang sama sekali baru, pastikan untuk memberi tahu kami dan tinggalkan tautan di bagian komentar di bawah!