نقدم لكم Battlescripts: Bots، Ships، Mayhem!
نشرت: 2022-03-11لا تحتاج البرمجة إلى أن تدور حول إنشاء التطبيقات وتحقيق الأهداف وتلبية مواصفات المشروع. يمكن أن يتعلق الأمر أيضًا بالمرح والاستمتاع بعملية إنشاء شيء ما. كثير من الناس يعاملون البرمجة وتطوير هذه المهارة كشكل من أشكال الترفيه. في Toptal ، أردنا تجربة شيء مثير للاهتمام داخل مجتمعنا. قررنا بناء منصة لعبة bot-vs-bot حول Battleship ، والتي أصبحت الآن مفتوحة المصدر.
منذ إطلاقها الأولي داخليًا ، جذبت المنصة انتباه بعض صانعي الروبوتات المذهلين داخل مجتمعنا. لقد تأثرنا حقًا برؤية أحد أعضاء المجتمع ، وهو مهندس Toptal Quan Le ، حتى أنه صمم أداة لتصحيح أخطاء Battlescripts Bots بسهولة. أثار الإعلان أيضًا اهتمامًا بين قلة لإنشاء محركات bot-vs-bot الخاصة بهم ، ودعم أنواع مختلفة من الألعاب وقواعد مختلفة. بدأت الأفكار الرائعة في التدفق منذ اللحظة التي تم فيها الكشف عن المخطوطات العسكرية. اليوم ، يسعدنا جعل Battlescripts مفتوحة المصدر. يمنح هذا مجتمعنا وكل شخص آخر فرصة لاستكشاف الكود و / أو تقديم مساهمات و / أو تفرعها لعمل شيء آخر تمامًا منه.
تشريح المخطوطات
تم إنشاء Battlescripts باستخدام بعض المكونات البسيطة جدًا. يتم تشغيله على Node.js ويستخدم بعض الحزم الأكثر شيوعًا والتنفيذ جيدًا ، مثل Express و Mongoose وما إلى ذلك. النهاية الخلفية موجودة في JavaScript خالص ، بالإضافة إلى البرامج النصية للواجهة الأمامية. التبعيتان الخارجيتان الوحيدتان لهذا التطبيق هما MongoDB و Redis. يتم تشغيل التعليمات البرمجية التي يرسلها المستخدم للروبوتات باستخدام وحدة "vm" التي تأتي مع Node.js. في الإنتاج ، يتم استخدام Docker لمزيد من الأمان ، ولكنه لا يعتمد بشدة على Battlescripts.
رمز Battlescripts متاح على GitHub بموجب ترخيص BSD 3-clause. يحتوي ملف README.md المضمن على إرشادات مفصلة حول كيفية استنساخ المستودع وتشغيل التطبيق محليًا.
قاعدة بيانات للانترنت
ستلاحظ أن التطبيق له بنية مشابهة لتلك الموجودة في تطبيقات الويب Express.js البسيطة. يقوم ملف app.js بتمهيد الخادم عن طريق إنشاء اتصال بقاعدة البيانات ، وتسجيل بعض البرامج الوسيطة الشائعة ، وتحديد بعض استراتيجيات المصادقة الاجتماعية. علاوة على ذلك ، يتم تعريف جميع النماذج والمسارات ضمن دليل "lib /". يتطلب التطبيق بالكامل نماذج قليلة فقط: Battle و Bot و Challenge و Contest و Party و User. تتم محاكاة المعارك بين الروبوتات خارج عُقد خادم الويب ، ويتم إجراؤها باستخدام حزمة Node.js Kue. يتيح لنا ذلك عزل المحرك عن باقي تطبيقات الويب ، مما يجعل من غير المرجح أن يتداخل محرك محاكاة المعركة مع خوادم الويب ، مما يجعل تطبيق الويب نفسه أكثر استجابة واستقرارًا.
الروبوتات والمحرك
نظرًا لأنه من المتوقع أن يتم تنفيذ الروبوتات في JavaScript ، وهذا هو بالضبط ما لدينا في نهايتنا الخلفية مع Node.js ، فقد كان من الأسهل بناء المحرك. عندما يتعلق الأمر بتنفيذ التعليمات البرمجية التي أرسلها المستخدم ، فإن أحد أكبر التحديات هو التأكد من أن الكود لا يفعل شيئًا ضارًا على الخادم ، أو أن أي كود به أخطاء لا يتعارض مع استقرار النظام ككل. تأتي مكتبة Node.js القياسية مع هذه الوحدة المدهشة التي تجعل جزءًا من هذه المهمة أمرًا سهلاً للغاية. تم تقديم الوحدة النمطية "vm" من أجل تسهيل تشغيل مطوري Node.js لتعليمات برمجية غير موثوق بها في سياق منفصل. على الرغم من أنه وفقًا للوثائق الرسمية ، من المهم تشغيل تعليمات برمجية غير موثوق بها في عملية منفصلة - ولكن هذا شيء نقوم به على خوادم الإنتاج. أثناء التطوير المحلي ، تعمل وحدة "vm" والميزات التي تقدمها بشكل جيد.
تنفيذ JavaScript
إذا كنت تريد تشغيل بعض تعليمات 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 ، تدير العقد التي تحاكي المعارك كل روبوت ضمن سياقات "vm" منفصلة لـ Node.js. يتحمل المحرك مسؤولية مزامنة حالة السياقات لكل من الروبوتات وفقًا لقواعد اللعبة.
تشغيل كود JavaScript في سياق منفصل ليس كل ما تفعله هذه الوحدة. تقبل وظيفة "runInNewContext" كائنًا كمعامل ثالث يمكنه التحكم في ثلاثة جوانب إضافية لتنفيذ الكود هذا:
- اسم الملف الذي سيتم استخدامه في تتبعات المكدس التي تم إنشاؤها ذات الصلة بهذا التنفيذ.
- سواء لطباعة الأخطاء أم لا إلى stderr.
- عدد المللي ثانية للسماح بمواصلة التنفيذ قبل انقضاء مهلة التنفيذ.
واحدة من مخاطر هذه الوحدة النمطية "vm" أنها لا توفر أي وسيلة للحد من استخدام الذاكرة. هذا ، إلى جانب بعض القيود الأخرى للوحدة النمطية ، يتم العمل عليها على الخادم من خلال استخدام Docker ، والطريقة التي يتم بها تشغيل عقد المحرك. عند استخدام الوحدة "vm" بشكل متكرر ، تبدأ ببطء في تسريب الذاكرة التي يصعب تعقبها وتحريرها. حتى إذا تم إعادة استخدام كائنات السياق ، يستمر استخدام الذاكرة في النمو. لقد حللنا هذه المشكلة باتباع استراتيجية بسيطة. في كل مرة يتم فيها محاكاة معركة في عقدة عاملة ، تخرج العقدة. ثم يقوم برنامج المشرف على خادم الإنتاج بإعادة تشغيل العقدة العاملة التي تصبح جاهزة بعد ذلك للتعامل مع محاكاة المعركة التالية في جزء من الثانية.
التمدد
تم تصميم المخطوطات الحربية في الأصل حول القواعد القياسية لسفينة حربية. لم يكن المحرك بداخله قابلاً للتمدد بشكل كبير. ومع ذلك ، بعد إطلاق Battlescripts ، كان أحد أكثر الطلبات شيوعًا هو تقديم أنواع ألعاب أحدث ، حيث أدرك مستخدمو التطبيق بسرعة أن بعض الألعاب أسهل في التغلب عليها باستخدام الروبوتات من غيرها. على سبيل المثال ، إذا قارنت TicTacToe مع الشطرنج ، فإن الأول به مساحة حالة أصغر بكثير ، مما يجعل من السهل جدًا على الروبوتات التوصل إلى حل يفوز في مباراة أو ينهيها.
تم تعديل محرك Battlescripts قليلاً مؤخرًا لتسهيل تقديم أنواع ألعاب أحدث. يمكن القيام بذلك ببساطة عن طريق اتباع بنية مع عدد قليل من الوظائف المشابهة للخطاف. تمت إضافة نوع لعبة إضافي ، TicTacToe ، إلى قاعدة التعليمات البرمجية لأنه من السهل متابعتها. يمكن العثور على كل ما يتعلق بنوع اللعبة هذا داخل ملف "lib / games / tictactoe.js".
ومع ذلك ، في هذه المقالة ، سوف نلقي نظرة على تنفيذ نوع لعبة Battleship. يمكن ترك استكشاف رمز لعبة TicTacToe كتمرين لوقت لاحق.
سفينة حربية
قبل إلقاء نظرة على كيفية تنفيذ اللعبة ، دعونا نلقي نظرة خاطفة على شكل الروبوت القياسي لـ Battlescript:
function Bot() {} Bot.prototype.play = function(turn) { // ... }
هذا هو الى حد كبير ذلك. يتم تعريف كل روبوت على أنه دالة مُنشئ بطريقة واحدة "تشغيل". يتم استدعاء الطريقة لكل منعطف باستخدام وسيطة واحدة. بالنسبة لأي لعبة ، فإن الوسيطة عبارة عن كائن ذو طريقة واحدة تسمح للبوت بالتحرك في الدور ، ويمكن أن يأتي مع بعض السمات الإضافية التي تمثل حالة اللعبة.
كما ذكرنا سابقًا ، تم تعديل المحرك قليلاً مؤخرًا. تم سحب كل منطق البارجة المحدد من رمز المحرك الفعلي. نظرًا لأن المحرك لا يزال يقوم بالرفع الثقيل ، فإن الكود الذي يحدد لعبة Battleship بسيط للغاية وخفيف الوزن.
function Battleships(bot1, bot2) { return new Engine(bot1, bot2, { hooks: { init: function() { // ... }, play: function() { // ... }, turn: function() { // ... } } }) } module.exports = exports = Battleships
لاحظ كيف نقوم بتعريف ثلاث وظائف تشبه الخطاف هنا: init ، play ، and turn. يتم استدعاء كل وظيفة مع المحرك كسياق لها. وظيفة "init" مباشرة حيث يتم إنشاء كائن المحرك ، من داخل وظيفة المُنشئ. عادة هذا هو المكان الذي يجب أن تعد فيه جميع سمات حالة المحرك. إحدى السمات التي يجب إعدادها لكل لعبة هي "الشبكات" و (اختياريًا) "القطع". يجب أن تكون هذه دائمًا مصفوفة تحتوي على عنصرين ، عنصر لكل لاعب يمثل حالة لوحة اللعبة.

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([]) }
يتم استدعاء الخطاف الثاني ، "تشغيل" ، قبل بدء اللعبة مباشرة. هذا مفيد ، حيث يمنحنا الفرصة للقيام بأشياء مثل وضع قطع اللعبة على السبورة نيابة عن الروبوتات.
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 }) } } }
قد يبدو هذا مربكًا بعض الشيء في البداية ، لكن الهدف الذي يحققه هذا الجزء من الكود بسيط. تقوم بإنشاء مصفوفات من القطع ، واحدة لكل روبوت ، وتضعها على الشبكات المقابلة بطريقة موحدة. لكل قطعة ، يتم فحص الشبكة ويتم تخزين كل موضع صالح في مصفوفة مؤقتة. الموضع الصالح هو المكان الذي لا تتداخل فيه قطعتان أو تتشاركان في الخلايا المتجاورة.
أخيرًا ، "منعطف" الخطاف الثالث والأخير. على عكس الخطافين الآخرين ، هذا مختلف قليلاً. الغرض من هذا الخطاف هو إرجاع كائن ، والذي يستخدمه المحرك كأول وسيطة في استدعاء طريقة تشغيل الروبوت.
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)) }
ضمن هذه الطريقة ، نبدأ بإخبار المحرك بأن الروبوت قد نجح في التحرك. الروبوت الذي يفشل في القيام بحركة هجومية لأي لعبة في أي دور يخسر اللعبة تلقائيًا. بعد ذلك ، في حالة إصابة السفينة بنجاح ، نحدد ما إذا كانت السفينة قد دمرت تمامًا. في حالة حدوث ذلك ، نعيد تفاصيل السفينة التي تم تدميرها ، وإلا فإننا نعيد "true" للإشارة إلى إصابة ناجحة دون أي معلومات إضافية.
من خلال هذه الرموز ، واجهنا بعض السمات وأسماء الطرق المتوفرة في "هذا". يتم توفيرها بواسطة كائن المحرك ولكل منها بعض الخصائص السلوكية البسيطة:
this.turn.called: يبدأ هذا على أنه خطأ قبل كل منعطف ، ويجب ضبطه على "صواب" لإبلاغ المحرك بأن الروبوت قد تصرف في المنعطف.
this.turn.botNo: سيكون إما 0 أو 1 ، اعتمادًا على الروبوت الذي لعب هذا الدور.
this.end (botNo): استدعاء هذا برقم bot ينهي اللعبة ، ويميز الروبوت بأنه منتصر. تسميته بـ -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 مفتوحة المصدر ، نرحب بالمساهمات. النظام الأساسي في مرحلته الحالية ناضج ، لكن لديه مجال كبير للتحسينات. سواء كانت ميزة جديدة ، أو تصحيح أمان ، أو حتى إصلاحات للأخطاء ، فلا تتردد في إنشاء مشكلة في المستودع تطلب معالجتها ، أو قم بتقسيم المستودع وإرسال طلب سحب. وإذا كان هذا يلهمك لبناء شيء جديد تمامًا ، فتأكد من إخبارنا وترك رابط له في قسم التعليقات أدناه!