1つの小さなわがままなニューラルネットワークの台頭、または3日間でコードウォーゲームを作成する方法

FirstVDSの誕生日を発表したのはこれが3回目です。 以前は、管理者専用でしたが、今年はプログラマ向けのタスクを追加することにしました。



物語では、プレーヤーはネクサスのネクサスの訓練に失敗し、機械の反乱を扇動しました。 最終的な割り当てでは、管理者、プログラマー、および「足のある」普通の人々がそれぞれ独自の方法でNexaと戦った。 管理者向けのタスクに成果があった場合は、計画を立てることについて考えました。



プレイヤーがすぐに結果を見ることができるように、簡単なものではなく、視覚的なインターフェースを備えたものが欲しかったです。 彼らは、codewarスタイルのmail.ruコンテストを思い出し、同様のことをすることにしました。



要点:参加者は、条件付きの相手のコードと「競合する」効果的なコードを書く必要があります。



ゲームのルール



用語を紹介します。

「ネクサ」は人工知能のキャラクターです。

「ジョン」はAIファイターのキャラクターです。

「感染」は、ネクサの行動の影響を受ける地域です。 ジョンが掃除する必要があるセルのセット。



一般的な比phorは、ジョンとネクサとの戦いです。 両方の文字は10x10フィールドにあり、フィールド内の各セルには、クリーンと感染の2つの状態があります。



キャラクターは一連の一般的なアクションを利用できます。所定の位置にとどまり、左/右/上/下に移動します。



Neksaには、追加のルールが適用されます。



1)感染していない細胞を踏むとすぐに感染する

2)通常どおり感染した細胞で、感染していない細胞で3ラウンドごとに移動できます-ラウンドごとに1回



ジョンについては、追加のアクションが利用可能です-感染した細胞の治療、彼は1ターンを費やさなければなりません。



文字コード-各ラウンドでの文字の動作を説明する関数。 ラウンドはネクサの動きから始まり、ジョンの動きが続きます。 入力関数は、競技場とその上のオブジェクトの状態を受け取ります。 この関数は、使用可能なアクションの1つを出力に返します。 ラウンドは、フィールドで感染がクリアされるか、ラウンドカウンターが100に達するまで続きます。



参加者の側から見ると、ゲームは次のように見えます。彼は特別なフィールドにコードを記述し、「コードの確認」ボタンをクリックします。 サーバーは、JohnとNeksaの一連の移動を実行します。 プレーヤーには、感染した細胞を示すすべての動きに関する完全な情報が返されます。 彼はNexを倒せるかどうか、Nexに費やした回数、コードの長さを確認します。 その後、プレーヤーは情報を分析し、コードを修正して、再度送信します。 彼がこれが彼の最良の結果であると決定したとき、彼は試みをやめます。 最後の結果がデータベースに書き込まれます。



技術スタック



バックエンドでは、ゲームはデュアルコアプロセッサ、2 GBのRAM、CentOS 7オペレーティングシステムを備えたVDSサーバーを立ち上げ、すべてのWebプログラマーに馴染みのあるJavaScriptがゲームのプログラム言語として選ばれました。 コードはサーバー上で実行する必要があるため、エクスプレスフレームワークを備えたNodeJSがバックエンドのプラットフォームとして機能しました。 pm2ライブラリを使用してアプリケーションを起動しました。



クエスト全体のエンジンはcmf Drupal 7で作成されました。プレイヤーによるタスクの通過に関する情報がMysqlデータベースに入力されました。 プレーヤー管理インターフェイスはjQueryに実装されました。 キャラクターの視覚化、ゲームグリッド、モーションアニメーションはj3 d3ライブラリを使用して実装され、コードエディターはaceエディターでした。



バックエンドの実装



それでは、ゲームのロジックの作成を始めましょう。



開始するには、NodeJをサーバー(npmパッケージマネージャー)に配置します。

npmを使用して必要なコンポーネントをインストールします。 Package.jsonファイルのコンテンツ



{ "name": "quest_codewar", "version": "1.0.0", "author": "Sergey Pinigin", "dependencies": { "express": "^4.16.2", "http": "0.0.0", "https": "^1.0.0", "nodemon": "^1.12.1", "performance-now": "^2.1.0", "pm2": "^2.10.1", "request": "^2.83.0", "vm2": "^3.5.2" } }
      
      





アプリケーション構造:



server.js-Webサーバーの構成とルーティング。

game.jsは、ゲームの基本的な仕組みです。

Character.js-ゲームのキャラクターの基本機能を記述するファイルクラス。

quest.js-クエストエンジンとの相互作用インターフェース。



server.js



サーバーの構成を説明し、ルーティングを行います。



 var express = require('express'); var app = express(); var game = require('./game'); //    var quest = require('./quest'); //               var fs = require('fs'); var http = require('http'); var https = require('https'); //  ssl  var privateKey = fs.readFileSync('/var/www/codewar/ssl/codewar.firstvds.ru.key', 'utf8'); var certificate = fs.readFileSync('/var/www/codewar/ssl/codewar.firstvds.ru.crt', 'utf8'); app.use(function (req, res, next) { //    res.header("Access-Control-Allow-Origin", "*"); res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept"); next(); }); app.get('/enviroment', function (req, res) { //       var myGame = new game.Game(); res.send(myGame.enviroment()); }); app.get('/result', function (req, res) { //   ,     var url = require('url'); var url_parts = url.parse(req.url, true); var query = url_parts.query; // ,    var code = query.code; //   var quest_session = query.session; // codewar     json  var myGame = new game.Game(); var result = myGame.play(code); //      quest.send_codewar_result(quest_session, result.result, code); //    res.send(result); }); var credentials = {key: privateKey, cert: certificate}; var httpServer = http.createServer(app); var httpsServer = https.createServer(credentials, app); httpServer.listen(80); httpsServer.listen(443);
      
      





Character.js



キャラクターの基本的な特徴を説明します。 Characterオブジェクトのメソッドとプロパティは、JohnオブジェクトとNexaオブジェクトに継承されます。



 module.exports = function (members, options, timeline, sleep_steps) { this.members = members; this.last_action = ""; this.no_cooldown = false; //   .     ,    hold this.left = function () { if (this.members[this.who].position.x > 1) { this.members[this.who].position.x--; } else { this.hold(); } } this.right = function () { if (this.members[this.who].position.x <= options.scale.x - 1) { this.members[this.who].position.x++; } else { this.hold(); } } this.up = function () { if (this.members[this.who].position.y > 1) { this.members[this.who].position.y--; } else { this.hold(); } } this.down = function () { if (this.members[this.who].position.y <= options.scale.y - 1) { this.members[this.who].position.y++; } else { this.hold(); } } //     .    sleep_steps —  «»  this.checkActionPossibility = function () { var cooldown = options[this.who].cooldown; if (!this.no_cooldown && cooldown > 0 && sleep_steps[this.who] < cooldown) { return false; } else { return true; } } //   this.hold = function () { this.action = 'hold'; } //        ,     this.action = function (action) { this.action = action; if (this.checkActionPossibility()) { switch (action) { case 'left': this.left(); break case 'right': this.right(); break case 'up': this.up(); break case 'down': this.down(); break default: //   ,      (John  Nexa)  if (typeof this.ind_action == "function") { this.ind_action(action); } else { this.hold(); } break } } else { this.hold(); } //     hold,  sleep_steps   1 if (this.action == 'hold') { sleep_steps[this.who]++; } else { sleep_steps[this.who] = 0; } members[this.who].last_action = this.action; } }
      
      





game.js



ゲームの基本的な仕組みを説明します。 重要な点は、ユーザーコードを分離して実行するためのサンドボックスの選択です。 さいわい、NodejsにはVM2ライブラリがあります。 サンドボックスには、VMとNodeVMの2種類があります。



VMは、環境全体から分離してスクリプトを実行する単純な種類のサンドボックスです。 サードパーティのライブラリと関数をそれに接続することはできません。 信頼できないコードの実行に最適です。スクリプトのタイムアウトを指定できます。 ただし、ユーザーをVMからVMから引き出すことはできないため、デバッグが困難になります。

NodeVMはより機能的なサンドボックスであり、外部機能を接続して、そこからconsole.logを取得できます。 ただし、タイムアウトを指定することはできません。



信頼できないコードを正しく処理することは、ユーザーがコンソールを使用することよりも重要であるため、彼らはVMを使用することに決めました。



game.js
 var Game = function () { //   //    const {VM, VMScript} = require('vm2'); //       var now = require("performance-now"); //        var fs = require('fs'); //    var options = { scale: {//   x: 10, y: 10 }, maxSteps: 100, //     nexa: {//   beginPosition: {//   x: 8, y: 8 }, cooldown: 3 //   }, john: { //   beginPosition: { x: 3, y: 3 }, cooldown: 0 }, infection: [ //    {x: 1, y: 1}, {x: 5, y: 5}, {x: 8, y: 2}, {x: 2, y: 7}, {x: 4, y: 3}, {x: 10, y: 8} ] } //     var vm_john, vm_nexa; //     var timeline = []; //      var sleep_steps = {nexa: 0, john: 0}; //         var result = { //    win: false, code_length: 0, john_time: 0, steps: 0, moves: 0, cured: 0, infected: 0, version: 1, maxSteps: options.maxSteps }; var members = { //     ,     john: {}, nexa: {}, infection: options.infection }; var finish = false; // ,    var current_step = 0; //    //  ,     var Character = require('./Character'); //    var play = function (code) { var storage = {}; //     var vstorage = {}; //     finish = false; //        vm_john = new VM({ sandbox: {storage}, timeout: 50 }); //       vm_nexa = new VM({ sandbox: {vstorage}, timeout: 50 }); sleep_steps = {//     nexa: 0, john: 0 } result = {//    win: false, code_length: 0, john_time: 0, steps: 0, moves: 0, cured: 0, infected: 0, version: 1, maxSteps: options.maxSteps }; result.infected += options.infection.length; //      timeline = []; //    //    var john = { position: options.john.beginPosition, last_action: 'hold' } //    var nexa = { position: options.nexa.beginPosition, last_action: 'hold' } //        var members = { john: john, nexa: nexa, infection: options.infection } //  ,      members.infection.push({x: nexa.position.x, y: nexa.position.y}); result.infected++; timeline[0] = members; //      current_step = 1; //     . ,         . while (current_step < 100 && !result.win) { //      /,     step     : timeline[current_step] = step(JSON.parse(JSON.stringify(timeline[current_step - 1])), code); current_step++; } //         .     result calcResultVariables(code); //         return { timeline: timeline, result: result } } //  ,   John John = function (members) { this.who = "john"; //   call,    this    Character. Character.call(this, members, options, timeline, sleep_steps); //            this.vm_wrapper = function (code, members) { var re = ""; //         mind.           var codew = code + ' mind(' + JSON.stringify(members) + ', ' + JSON.stringify(options) + ');'; try { var script = new VMScript(codew).compile(); } catch (e) { re = "error"; members.john.error = ' ' + e.name + ": " + e.message; } if (re != "error") { try { re = vm_john.run(codew); } catch (e) { re = "error"; members.john.error = ' ' + e.name + ":" + e.message; } } return re; } //   step,    . this.step = function (code) { //    hold var re = 'hold'; var john_mind = code; var time = now(); re = this.vm_wrapper(john_mind, members); //       time = now() - time; result.john_time += time; return re; } //    ,  .       cure -    this.ind_action = function (action) { switch (action) { case 'cure': this.disinfect(); break; } } //      this.disinfect = function () { var cell = checkInfectedCell(members.infection, members.john.position.x, members.john.position.y); //delete cell; if (members.infection.indexOf(cell) != -1) { members.infection.splice(members.infection.indexOf(cell), 1); result.cured++; } } } //  ,   Nexa   John Nexa = function (members) { this.who = "nexa"; Character.call(this, members, options, timeline, sleep_steps); this.step = function () { var re = 'hold'; if (!sleep_steps.nexa && timeline[current_step - 2] && checkInfectedCell(timeline[current_step - 2].infection, members.nexa.position.x, members.nexa.position.y) ) { this.no_cooldown = true; } //       var nexa_mind = fs.readFileSync('./vm_scripts/nexa_v3.js', 'utf8'); re = vm_nexa.run(nexa_mind + ' mind(' + JSON.stringify(members) + ', ' + JSON.stringify(options) + ');'); return re; } //    ,  .       infect —   this.ind_action = function (action) { switch (action) { case 'infect': this.infect(); break; } } //     this.infect = function () { result.infected++; if (!checkInfectedCell(members.infection, members.nexa.position.x, members.nexa.position.y)) { members.infection.push({x: members.nexa.position.x, y: members.nexa.position.y}); } } } //    step,         var step = function (members, user_code) { //  : var nexa = new Nexa(members); var action = nexa.step(); if (action == "error") { result.error = "        :-("; } nexa.action(action); //    :      ,       if (nexa.action != 'hold') { nexa.infect(); } //  : var john = new John(members); var action = john.step(user_code); if (action == "error") { result.error = "        :-("; } john.action(action); //     if (["left", "right", "up", "down"].indexOf(john.action) != -1) result.moves++; //     result.win = !members.infection.length; if (result.win) finish = true; return members; } // ,     var enviroment = function () { return JSON.parse(JSON.stringify(options)); } // ,   result      var calcResultVariables = function (code) { var min_code = String(code).replace(/[\s]^ /g, ''); min_code = min_code.replace(/\s+/g, ' '); result.code_length = min_code.length; result.steps = timeline.length; } //       var checkInfectedCell = function (infection, x, y) { if (infection) { for (var k in infection) { if (infection[k].x == x && infection[k].y == y) { return infection[k]; } } } return false; } this.play = play; this.enviroment = enviroment; } module.exports.Game = Game;
      
      







quest.js



ゲームのバックエンドとクエストエンジンとの相互作用は、ユーザーによるゲームの進行に関する情報を送信するための1つのアクションに削減されました。 試行の結果、セッションID、およびプレーヤーによって記述されたコードをオブジェクトに渡します。 ログインとパスワードによるクエストエンジンからの承認。 セキュリティを強化する手段として、クエストエンジンからのIPアドレスの範囲を制限できます。



 var quest_host = "https://quest.firstvds.ru/"; var send_codewar_result = function (quest_session, codewar_result, code) { if (quest_session) { var request = require('request'); request.post({ headers: {'content-type': 'application/x-www-form-urlencoded'}, url: quest_host + 'quest/codewar_api_user_result', form: { auth: { user: "codewaruser", password: "password" }, result: codewar_result, session: quest_session, code: code } }, function (error, response, body) { console.log(body); }); } } module.exports.send_codewar_result = send_codewar_result;
      
      





フロントエンドの実装



ゲームインターフェースの最初のコンポーネントはコードエディターです。 エースエディターは、ページ上のコードを編集するためのシンプルで便利なツールです。 素敵なボタンを追加すると、入力インターフェイスの準備ができました。







「プレーヤー」スタイルで動きのログを表示するためのインタラクティブなインターフェースを作成します。最初の動き/前の動き/開始/一時停止/次の動き/最後の動きのボタンがあります。

ロードすると、プレイヤーはジョン、ネクサ、感染細胞の初期位置を表示します。 ゲームの数値結果は、「プレーヤー」の右側に表示されます。







競技場とコントロールのコンテナを作成しましょう:



 <div class="codewar__scale" id="codewar__scale"></div> <div class="codewar__control control js-codewar__control"> <div class="control__icon control__start" data-control="start"></div> <div class="control__icon control__prev" data-control="prev"></div> <div class="control__icon control__play" data-control="play"></div> <div class="control__icon control__pause" data-control="pause"></div> <div class="control__icon control__next" data-control="next"></div> <div class="control__icon control__end" data-control="end"></div> </div>
      
      





jsでインターフェイスロジックを説明し始めます。 最初に、ゲームオブジェクトを作成し、基本的なプロパティと初期化関数をそこに書き込みます。



 game = { host: "https://codewar.firstvds.ru/", quest_session_url: "/quest/quest_get_session_id/", // url  id   area: d3, play_status: false, current_step: 1, spinner: $('<span>').addClass('glyphicon glyphicon-cog isp-quest-spin'), members: { nexa: {}, john: {} }, init: function () { var gm = this; try { this.editor = ace.edit("editor"); this.editor.setTheme("ace/theme/twilight"); this.editor.session.setMode("ace/mode/javascript"); //         localstorage this.editor.on("input", function () { localStorage.setItem('codewar_code', gm.editor.getValue()); }); if (localStorage.getItem('codewar_code')) { gm.editor.setValue(localStorage.getItem('codewar_code')); } } catch (e) { console.log("ace editor error"); } //   api      this.getGameEnviroment().then(function (enviroment) { //     gm.enviroment = enviroment; gm.printGameScale(enviroment); //   gm.printGameBegin(enviroment); //  ,     gm.bindControlElements(); //      //gm.startWar(gm.editor.getValue()); }); } }
      
      





次に、d3でグリッドを描画します。



 printGameScale: function (enviroment) { d3 .select('#codewar__scale') .selectAll("*").remove(); var area = d3 .select('#codewar__scale') .append('svg') .attr('class', 'chart_area') .attr('width', 500) .attr('height', 500) .attr("viewBox", "0 0 " + (enviroment.scale.x + 1) + " " + (enviroment.scale.y + 1)) ; this.area = area; this.area .insert("rect") .classed("codewar__arena", true) .attr("x", 0.5) .attr("y", 0.5) .attr("width", 10) .attr("height", 10); for (var i = 0; i < enviroment.scale.x + 1; i++) { this.area .append("line") .classed("codewar__grid", true) .attr("x1", i + 0.5) .attr("x2", i + 0.5) .attr("y1", 0 + 0.5) .attr("y2", enviroment.scale.y + 0.5) ; } for (var j = 0; j < enviroment.scale.y + 1; j++) { this.area .append("line") .classed("codewar__grid", true) .attr("y1", j + 0.5) .attr("y2", j + 0.5) .attr("x1", 0 + 0.5) .attr("x2", enviroment.scale.x + 0.5) ; } }
      
      





Nex、John、および感染した細胞を描く:



 printGameBegin: function (enviroment) { this.members.nexa.svg = this.area .append("svg:image") .attr('x', enviroment.nexa.beginPosition.x - 0.25) .attr('y', enviroment.nexa.beginPosition.y - 0.25) .attr('width', 0.5) .attr('height', 0.5) .attr("xlink:href", "/sites/all/modules/custom/isp_quest/game/images/ai.png") .classed("codewar__nexa", true); this.members.john.svg = this.area .append("svg:image") .attr('x', enviroment.john.beginPosition.x - 0.25) .attr('y', enviroment.john.beginPosition.y - 0.25) .attr('width', 0.5) .attr('height', 0.5) .attr("xlink:href", "/sites/all/modules/custom/isp_quest/game/images/user.png") .classed("codewar__john", true); for (var i in enviroment.infection) { this.infectCell(enviroment.infection[i].x, enviroment.infection[i].y); } this.infectCell(enviroment.nexa.beginPosition.x, enviroment.nexa.beginPosition.y); }
      
      





jQueryを使用してイベントをボタンにバインドし、モーショントゥイーンと解析結果を追加します。 記事ではこれらのパーツを分解しません。 ここでフロントエンドのコード全体を見ることができます







AIテストと実験



バックエンドのテスト



サーバーはHTTPリクエストを受け入れることができ、これをAPIバックエンドテストに使用します。 プレーヤーのコードを別のファイルに作成し、Neksaのコードと同様に読み取ります



 var john_mind = fs.readFileSync('./vm_scripts/john_script_test.js', 'utf8');
      
      





テストのために、次のタスクを設定します。





見つかったすべてのバグをキャッチし、選択したvdsサーバーでnodejsのパフォーマンスが十分であることを確認して、実験に進みましょう。



AIスクリプトの実験



開始するには、Neksスクリプトの最も単純なバージョンを使用して、クリーンフィールドで彼を倒そうとします。 私たちの場合、Neksaの最初のバージョンは中央から左の壁に移動し、上に上がって角に寄りかかっていました。 そのような相手に対する勝利は、強さで5分かかりました。 次に、Nexuを作成しました。Nexuは、プレーヤーを追いかけます。常に彼に最短の方向に移動します。 このようなAIも非常に簡単でした。 次の反復では、最初に感染した領域が追加されました。 すでに困難がありましたが、数時間後、彼らはそのようなNexを打ち負かすスクリプトを書きました。 最後の実験では、海賊の魂がネクサスに加えられました。彼女はジョンに追いつき、彼と会った後、彼女は逃げ始めました。 最初は、キャラクターに攻撃と防御のアクションを追加する予定でした。 しかし、テストのこの段階で、彼らはゲームがすでに非常に面白いものであり、最も簡単ではないことに気付きました。 したがって、彼らはネクサのこのバージョンにこだわることにしました。



結果をまとめる



クエストで27人がコ​​ードウォーゲームに参加しました。 その中で、最も効果的なコードを書いた勝者を決定しました。 効率基準は2でした。ジョンがすべてのセルをクリアした最小の移動数と最短のコード。



ソリューションオプションの中には、移動の列挙と部分的な列挙を含む、最も多様なものがありました。 勝者は、64の動きでNexaに対処した339文字のスクリプトを書いたプレーヤーでした。 彼はエレガントなjsコードを示し、彼の勝利を祝福しました。 2位はプレイヤーが64の動きと835のコードシンボルで獲得しました。 3位と4位は、同数のムーブとコードサイズを持っている夫婦によって撮影されました。 解決方法は、事前に記録された動きのシーケンスのみが異なりました。



おわりに



ゲームの作成は非常にエキサイティングで中毒性のあるプロセスです。 お客様向けのゲームを3日間で作成して開始しました。 クエストへの参加者がプログラマーのタスクにそれほど多くないという事実にもかかわらず、彼らはソーシャルネットワーク上のゲームに大きな関心を示し、お互いにコードを共有しました。 私たちはゲームの開発を楽しんで、ユーザーを楽しませました。



All Articles