إحصائيات اللعبة ، أو كيف توقفت عن الخوف وأحببت Google Apps Script





تحية طيبة! أود اليوم أن أتحدث عن موضوع واحد يصادفه أي مصمم ألعاب بطريقة أو بأخرى. وهذا الموضوع هو الألم والمعاناة والعمل مع السكون . ما هو الإحصاء؟ باختصار ، هذه هي جميع البيانات الدائمة التي يتفاعل معها اللاعب ، سواء كانت خصائص سلاحه أو معلمات الزنزانة وسكانها.



تخيل أن لديك 100500 نوع من السيوف المختلفة في اللعبة وقد احتاجوا جميعًا فجأة إلى زيادة الضرر الأساسي قليلاً. عادةً ، في هذه الحالة ، يتم استخدام برنامج Excel القديم الجيد ، ثم تُدرج النتائج في JSON / XML يدويًا أو باستخدام العناصر النظامية ، ولكن هذا طويل ومزعج ومحفوف بأخطاء التحقق من الصحة.



دعونا نرى كيف يمكن أن تكون جداول بيانات Google وجداول بيانات Google المدمجة مناسبة لمثل هذه الأغراضGoogle Apps Script وهل من الممكن توفير الوقت عليه.



سأحجز مقدمًا أننا نتحدث عن الإحصائيات الخاصة بألعاب f2p أو خدمات الألعاب ، والتي تتميز بالتحديثات المنتظمة للميكانيكا وتجديد المحتوى ، أي العملية المذكورة أعلاه ± ثابتة.



لذلك ، لتحرير نفس السيوف ، تحتاج إلى إجراء ثلاث عمليات:



  1. استخراج مؤشرات الضرر الحالية (إذا لم يكن لديك جداول حسابية جاهزة) ؛
  2. حساب القيم المحدثة في Excel القديم الجيد ؛
  3. نقل قيم جديدة للعبة JSONs.


طالما لديك أداة جاهزة وتناسبك ، كل شيء على ما يرام ويمكنك التعديل بالطريقة التي اعتدت عليها. ولكن ماذا لو كانت الأداة مفقودة؟ أو حتى الأسوأ ، لا توجد لعبة بحد ذاتها. هل ما زالت قيد التطوير؟ في هذه الحالة ، بالإضافة إلى تحرير البيانات الموجودة ، تحتاج أيضًا إلى تحديد مكان تخزينها والهيكل الذي ستحتوي عليه.



مع التخزين ، لا يزال واضحًا وموحدًا إلى حد ما: في معظم الحالات ، يكون الثابت مجرد مجموعة من JSONs منفصلة موجودة في مكان ما في VCS... هناك ، بالطبع ، حالات أكثر غرابة عندما يتم تخزين كل شيء في قاعدة بيانات علائقية (أو ليست كذلك) ، أو الأسوأ من ذلك كله ، في XML. ولكن ، إذا قمت باختيارهم ، وليس JSON العاديين ، فعلى الأرجح لديك بالفعل أسباب وجيهة لذلك ، لأن يعتبر أداء هذه الخيارات وإمكانية استخدامها موضع شك كبير.



ولكن فيما يتعلق بهيكل الإحصائيات وتحريرها - غالبًا ما تكون التغييرات جذرية ويومية. بالطبع ، في بعض الحالات ، لا شيء يمكن أن يحل محل كفاءة Notepad ++ العادية ، إلى جانب النظامين العاديين ، لكننا ما زلنا نرغب في الحصول على أداة ذات حد دخول أقل وسهولة في التحرير بواسطة أمر.



جاءت جداول بيانات Google العادية والمعروفة إلي شخصيًا كأداة. مثل أي أداة ، لها إيجابيات وسلبيات. سأحاول النظر فيها من وجهة نظر مجلس الدوما



الايجابيات سلبيات
  • التحرير المشترك
  • من الملائم نقل الحسابات من جداول البيانات الأخرى
  • وحدات ماكرو (برمجة تطبيقات Google)
  • يوجد سجل تحرير (وصولاً إلى الخلية)
  • التكامل الأصلي مع Google Drive والخدمات الأخرى


  • يتأخر مع الكثير من الصيغ
  • لا يمكنك إنشاء فروع تغيير منفصلة
  • المهلة الزمنية لتشغيل البرامج النصية (6 دقائق)
  • صعوبة في عرض JSONs المتداخلة




بالنسبة لي ، فاقت الإيجابيات السلبيات بشكل كبير ، وفي هذا الصدد ، تقرر محاولة إيجاد حل بديل لكل من السلبيات المقدمة.



ماذا حدث في النهاية؟



في جداول بيانات Google ، تم عمل مستند منفصل ، والذي يحتوي على الورقة الرئيسية ، حيث نتحكم في التفريغ ، وبقية الأوراق ، واحدة لكل كائن لعبة.

في الوقت نفسه ، من أجل ملاءمة JSON المتداخلة المعتادة في طاولة مسطحة ، كان من الضروري إعادة اختراع الدراجة قليلاً. لنفترض أن لدينا JSON التالي:



{
  "test_craft_01": {
    "id": "test_craft_01",
    "tags": [ "base" ],
	"price": [ {"ident": "wood", "count":100}, {"ident": "iron", "count":30} ],
	"result": {
		"type": "item",
		"id": "sword",
		"rarity_wgt": { "common": 100, "uncommon": 300 }
	}
  },
  "test_craft_02": {
    "id": "test_craft_02",
	"price": [ {"ident": "sword", "rarity": "uncommon", "count":1} ],
	"result": {
		"type": "item",
		"id": "shield",
		"rarity_wgt": { "common": 100 }
	}
  }
}


في الجداول ، يمكن تمثيل هذه البنية كزوج من القيم "المسار الكامل" - "القيمة". من هنا ولدت لغة ترميز مسار ذاتية الصنع حيث:



  • النص هو حقل أو كائن
  • / - فاصل التسلسل الهرمي
  • نص [] - مجموعة
  • #number - فهرس العنصر في المصفوفة


وبالتالي ، سيتم كتابة JSON في الجدول على النحو التالي:







وفقًا لذلك ، فإن إضافة كائن جديد من هذا النوع هو عمود آخر في الجدول ، وإذا كان الكائن يحتوي على أي حقول خاصة ، فسيتم توسيع قائمة السلاسل باستخدام المفاتيح في مسار المفاتيح.



يعتبر التقسيم إلى جذر ومستويات أخرى راحة إضافية لاستخدام المرشحات في الجدول. بالنسبة للباقي ، تعمل قاعدة بسيطة: إذا كانت القيمة في الكائن ليست فارغة ، فسنضيفها إلى JSON ونفصلها.



في حالة إضافة حقول جديدة إلى JSON وخطأ شخص ما في المسار ، يتم فحصه من خلال النظام العادي التالي على مستوى التنسيق الشرطي:



=if( LEN( REGEXREPLACE(your_cell_name, "^[a-zA_Z0-9_]+(\[\])*(\/[a-zA_Z0-9_]+(\[\])*|\/\#*[0-9]+(\[\])*)*", ""))>0, true, false)


والآن عن عملية التفريغ. للقيام بذلك ، انتقل إلى الورقة الرئيسية ، وحدد الكائنات المطلوبة لتفريغها في العمود #ACTION و ...

انقر فوق Palpatine (͡ ° ͜ʖ ͡ °)







نتيجة لذلك ، سيتم تشغيل برنامج نصي يأخذ البيانات من الأوراق المحددة في الحقل #OBJECT وتفريغها إلى JSON. تم تحديد مسار التحميل في الحقل #PATH ، والمكان الذي سيتم فيه تحميل الملف هو Google Drive الشخصي المرتبط بحساب Google الذي تعرض المستند تحته.



يسمح لك الحقل #METHOD بتهيئة الطريقة التي تريد بها تحميل JSON:



  • إذا واحد - يتم تحميل ملف واحد مع اسم يساوي اسم الكائن (بدون رمز تعبيري، بالطبع، فهي هنا فقط من أجل قراءة)
  • إذا كان منفصلاً - سيتم تفريغ كل عنصر من الورقة في JSON منفصل.


الحقول المتبقية أكثر إعلامية بطبيعتها وتسمح لك بفهم عدد الكائنات الجاهزة الآن للتفريغ ومن قام بتفريغها مؤخرًا.



عند محاولة تنفيذ مكالمة صادقة لطريقة التصدير ، صادفت ميزة مثيرة للاهتمام في جداول البيانات: يمكنك تعليق استدعاء دالة على صورة ، لكن لا يمكنك تحديد الوسائط في استدعاء هذه الوظيفة. بعد فترة قصيرة من الإحباط ، تقرر مواصلة التجربة بالدراجة وولدت فكرة تمييز أوراق البيانات نفسها.



لذلك ، على سبيل المثال ، ظهرت الارتساءات ### data ### و ### end_data ### في الجداول الموجودة في أوراق البيانات ، والتي يتم من خلالها تحديد مناطق السمات للتحميل.



رموز المصدر



وفقًا لذلك ، كيف تبدو مجموعة JSON على مستوى الكود:



  1. نأخذ الحقل #OBJECT ونبحث عن جميع بيانات الورقة بهذا الاسم



    var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(name)
  2. , ( , == )



    function GetAnchorCoordsByName(anchor, data){
      var coords = { x: 0, y: 0 }
      
      for(var row=0; row<data.length; row++){
        for(var column=0; column<data[row].length; column++){
          if(data[row][column] == anchor){
            coords.x = column;
            coords.y = row;  
          }
        }
      }
      return coords;
    }
    
  3. , ( ###enable### true|false)



    function FilterActiveData(data, enabled){  
      for(var column=enabled.x+1; column<data[enabled.y].length; column++){
        if(!data[enabled.y][column]){
          for(var row=0; row<data.length; row++){
            data[row].splice(column, 1);
          }
          column--;
        }
      }
      return data
    }
    
  4. ###data### ###end_data###



    function FilterDataByAnchors(data, start, end){
      data.splice(end.y)
      data.splice(0, start.y+1);
      
      for(var row=0; row<data.length; row++){
        data[row].splice(0,start.x);
      }
      return data;
    }
    




  5. function GetJsonKeys(data){
      var keys = [];
      
      for(var i=1; i<data.length; i++){
        keys.push(data[i][0])
      }
      return keys;
    }
    




  6. //    . 
    // ,     single-file, -      . 
    // -    ,    separate JSON-
    function PrepareJsonData(filteredData){
      var keys = GetJsonKeys(filteredData)
      
      var jsonData = [];
      for(var i=1; i<filteredData[0].length; i++){
        var objValues = GetObjectValues(filteredData, i);   
        var jsonObject = {
          "objName": filteredData[0][i],
          "jsonBody": ParseToJson(keys, objValues)
        }
        jsonData.push(jsonObject)
      }  
      return jsonData;
    }
    
    //  JSON   ( -)
    function ParseToJson(fields, values){
      var outputJson = {};
      for(var field in fields){
        if( IsEmpty(fields[field]) || IsEmpty(values[field]) ){ 
          continue; 
        }
        var key = fields[field];
        var value = values[field];
        
        var jsonObject = AddJsonValueByPath(outputJson, key, value);
      }
      return outputJson;
    }
    
    //    JSON    
    function AddJsonValueByPath(jsonObject, path, value){
      if(IsEmpty(value)) return jsonObject;
      
      var nodes = PathToArray(path);
      AddJsonValueRecursive(jsonObject, nodes, value);
      
      return jsonObject;
    }
    
    // string     
    function PathToArray(path){
      if(IsEmpty(path)) return [];
      return path.split("/");
    }
    
    // ,    ,    - 
    function AddJsonValueRecursive(jsonObject, nodes, value){
      var node = nodes[0];
      
      if(nodes.length > 1){
        AddJsonNode(jsonObject, node);
        var cleanNode = GetCleanNodeName(node);
        nodes.shift();
        AddJsonValueRecursive(jsonObject[cleanNode], nodes, value)
      }
      else {
        var cleanNode = GetCleanNodeName(node);
        AddJsonValue(jsonObject, node, value);
      }
      return jsonObject;
    }
    
    //      JSON.    .
    function AddJsonNode(jsonObject, node){
      if(jsonObject[node] != undefined) return jsonObject;
      var type = GetNodeType(node);
      var cleanNode = GetCleanNodeName(node);
      
      switch (type){
        case "array":
          if(jsonObject[cleanNode] == undefined) {
            jsonObject[cleanNode] = []
          }
          break;
        case "nameless": 
          AddToArrayByIndex(jsonObject, cleanNode);
          break;
        default:
            jsonObject[cleanNode] = {}
      }
      return jsonObject;
    }
    
    //       
    function AddToArrayByIndex(array, index){
      if(array[index] != undefined) return array;
      
      for(var i=array.length; i<=index; i++){
        array.push({});
      }
      return array;
    }
    
    //    ( ,      )
    function AddJsonValue(jsonObject, node, value){
      var type = GetNodeType(node);
      var cleanNode = GetCleanNodeName(node);
      switch (type){
        case "array":
          if(jsonObject[cleanNode] == undefined){
            jsonObject[cleanNode] = [];
          }
          jsonObject[cleanNode].push(value);
          break;
        default:
          jsonObject[cleanNode] = value;
      }
      return jsonObject
    }
    
    //  .
    // object -      
    // array -     ,   
    // nameless -         ,     - 
    function GetNodeType(key){
      var reArray       = /\[\]/
      var reNameless    = /#/;
      
      if(key.match(reArray) != null) return "array";
      if(key.match(reNameless) != null) return "nameless";
      
      return "object";
    }
    
    //           JSON
    function GetCleanNodeName(node){
      var reArray       = /\[\]/;
      var reNameless    = /#/;
      
      node = node.replace(reArray,"");
      
      if(node.match(reNameless) != null){
        node = node.replace(reNameless, "");
        node = GetNodeValueIndex(node);
      }
      return node
    }
    
    //     nameless-
    function GetNodeValueIndex(node){
      var re = /[^0-9]/
      if(node.match(re) != undefined){
        throw new Error("Nameless value key must be: '#[0-9]+'")
      }
      return parseInt(node-1)
    }
    
  7. JSON Google Drive



    // ,    : ,   ( )  string  .
    function CreateFile(path, filename, data){
      var folder = GetFolderByPath(path) 
      
      var isDuplicateClear = DeleteDuplicates(folder, filename)
      folder.createFile(filename, data, "application/json")
      return true;
    }
    
    //    GoogleDrive   
    function GetFolderByPath(path){
      var parsedPath = ParsePath(path);
      var rootFolder = DriveApp.getRootFolder()
      return RecursiveSearchAndAddFolder(parsedPath, rootFolder);
    }
    
    //      
    function ParsePath(path){
      while ( CheckPath(path) ){
        var pathArray = path.match(/\w+/g);
        return pathArray;
      }
      return undefined;
    }
    
    //     
    function CheckPath(path){
      var re = /\/\/(\w+\/)+/;
      if(path.match(re)==null){
        throw new Error("File path "+path+" is invalid, it must be: '//.../'");
      }
      return true;
    }
    
    //         ,      , -    . 
    // -   , ..    
    function DeleteDuplicates(folder, filename){
      var duplicates = folder.getFilesByName(filename);
      
      while ( duplicates.hasNext() ){
        duplicates.next().setTrashed(true);
      }
    }
    
    //     ,         ,      
    function RecursiveSearchAndAddFolder(parsedPath, parentFolder){
      if(parsedPath.length == 0) return parentFolder;
       
      var pathSegment = parsedPath.splice(0,1).toString();
    
      var folder = SearchOrCreateChildByName(parentFolder, pathSegment);
      
      return RecursiveSearchAndAddFolder(parsedPath, folder);
    }
    
    //  parent  name,    - 
    function SearchOrCreateChildByName(parent, name){
      var childFolder = SearchFolderChildByName(parent, name); 
      
      if(childFolder==undefined){
        childFolder = parent.createFolder(name);
      }
      return childFolder
    }
    
    //    parent    name  
    function SearchFolderChildByName(parent, name){
      var folderIterator = parent.getFolders();
      
      while (folderIterator.hasNext()){
        var child = folderIterator.next();
        if(child.getName() == name){ 
          return child;
        }
      }
      return undefined;
    }
    


منجز! الآن نذهب إلى Google Drive ونأخذ ملفنا هناك.



لماذا كان من الضروري العبث بالملفات في Google Drive ، ولماذا لا تنشر مباشرة على Git؟ بشكل أساسي - فقط حتى تتمكن من فحص الملفات قبل أن تطير إلى الخادم وتنفذ ما لا يمكن إصلاحه . في المستقبل ، سيكون من الأسرع دفع الملفات مباشرة.



ما لا يمكن حله بشكل طبيعي: عند إجراء اختبارات A / B المختلفة ، يصبح من الضروري دائمًا إنشاء فروع منفصلة للإحصاءات ، حيث يتغير جزء من البيانات. ولكن نظرًا لأن هذه في الواقع نسخة أخرى من dict ، يمكننا نسخ جدول البيانات نفسه لاختبار A / B ، وتغيير البيانات الموجودة فيه ومن هناك تفريغ البيانات للاختبار.



خاتمة



كيف ينتهي هذا القرار بالتكيف؟ سريع بشكل مدهش. شريطة أن يتم تنفيذ معظم هذا العمل بالفعل في جداول البيانات ، فقد تبين أن استخدام الأداة الصحيحة هو أفضل طريقة لتقليل وقت التطوير.



نظرًا لحقيقة أن المستند تقريبًا لا يستخدم الصيغ التي تؤدي إلى تحديثات متتالية ، فلا يوجد شيء عمليًا لإبطاء السرعة. يستغرق تحويل حسابات الرصيد من جداول أخرى الآن بشكل عام حدًا أدنى من الوقت ، منذ ذلك الحين ما عليك سوى الانتقال إلى الورقة المطلوبة وتعيين عوامل التصفية ونسخ القيم.



تتمثل العقبة الرئيسية في الأداء في واجهة برمجة تطبيقات Google Drive: يستغرق البحث عن الملفات وحذفها / إنشائها أقصى وقت ، ولا يتم تحميل جميع الملفات دفعة واحدة فقط أو تحميل ورقة ليس كملفات منفصلة ، ولكن في ملف JSON واحد يساعد.



آمل أن يكون هذا التشابك من الانحرافات مفيدًا لأولئك الذين ما زالوا يقومون بتحرير JSON بأيديهم ونظاميهم ، وأيضًا إجراء حسابات التوازن للإحصاءات في Excel بدلاً من جداول بيانات Google.



الروابط



مثال على رابط مصدر جدول البيانات

إلى مشروع في برمجة تطبيقات Google



All Articles