如何通过SEMrush通过JS QA游戏的最终级别

大家好,我的名字是帖木儿,我写了一个QA游戏SEMrush。如果您在线参与了Heisenbug,或者在测试人员的Telegram聊天中看到了该游戏的公告,则可能已经听说过该游戏。简而言之,在QA游戏中,您需要增加难度来完成关卡并使用JavaScript捕获错误。



在本文中,我将分析第七(最终难度)水平,并分享游戏获胜者的决定*。



图片



*为玩家澄清。 QA Game分两个阶段推出:六月和七月。从第一流开始,亚历山大就一直得分最高,因此在本文中我们分析了他的结果。其余的领导人可以在这里看到



“内部”是什么:Ace.js库用于游戏中的代码编辑器;语法高亮和自动完成功能可在其中使用; webWorker用于在客户端执行代码(受本文启发)。后端是用Python和Flask编写的,并部署在Heroku上。总共花了大约2个月来编写游戏。



当我编写质量检查游戏时,我还没有Ace.js和webWorkers的经验,尝试它们很有趣。如果您想制作类似的游戏,那么我建议您考虑一下:



  • 通过像我一样在服务器端而非客户端执行播放器代码;
  • 使用异步后端框架。如果后端使用Python,则建议使用QuartFastAPI


QA游戏传奇



在游戏中,您需要控制角色ZERO2,该角色能够测试,搜索和修复错误。控制使用JavaScript代码完成,ZERO2拥有自己的SDK,从而大大简化了编程。



例如,要测试该级别上所有可用的功能,您需要运行以下代码:



let result = scan();
for (f of result.features) {
    smoke_test(f);
}


然后修复在测试期间发现的所有错误,这是:



result = scan();
for (b of result.bugs) {
    fix_bug(b);
}


游戏中的每个新关卡都包含其他功能,并且需要使用更复杂的算法;每种方法的详细分析发布在GitHub上在本文中,我将详细分析等级7,因为它确定了哪个玩家将获得最大积分。



如何获得最高积分?游戏创作者版本。



在第7级,玩家需要修复并验证120秒内的最大错误数量,同时:



  1. RUN按钮只能被按下60次;
  2. 120秒后,算法自动结束,不再授予积分(在前端和后端均进行了验证);
  3. 对于每个固定的错误,将给予100分,对于经过纠正和验证的错误-150分;
  4. 每次启动RUN时,所有点都会重置,并且会随机生成新的错误。


要获得最大点数,您需要分析影响结果的因素:



  • 简化代码有必要删除所有不必要的结构并编写清晰的代码,并检查是否存在循环。由于代码错误,许多参与者失去了分数,从而导致无休止的空循环。
  • 减少对请求的响应时间每种SDK方法都会向服务器发出一个请求,平均每个请求需要200-400毫秒。为了减少这个数字,您需要找到合适的服务器并从中执行查询。
  • 算法优化在大多数情况下,都需要找到重现该错误的步骤(函数research_bug)。因此,您需要考虑如何优化算法,以便以最少的尝试次数找到解决方案。
  • 该算法的“并行化”标准启动发生在一个线程(一个webWorker)中,并且所有API方法都是同步的。您可以尝试“并行化”算法。您还可以查看是否有可能使某些方法异步(扰流器警报:可以)。


算法优化



如果指定的播放步骤不正确,那么research_bug(bug_id,步骤)函数将返回0;如果指定的播放步骤是正确的步骤组合的开始,则返回1;如果指定的步骤是再现该错误的完整步骤的组合,则返回100。



选择播放步骤的算法可能如下所示:



function find_steps(bug_id) {
    let path = '';
    let result = 0;
    while (result != 100) {
        path += '>';
        result = investigate_bug(bug_id, path);
        if (result === 0) {
            path = path.slice(0, -1);
            path += '<';
            result = investigate_bug(bug_id, path);
        }
    }
};


如果对于特定的序列,当接收到“ 0”时,您没有重新检查相同的序列,而是替换了最后一个字符,则可以加速此功能。相反,您需要立即在字符串中添加另一个字符并检查结果是否有换行符。



这是什么意思?使用此算法可以“节省”对research_bug的调用次数(尽管并非在所有情况下都可以更快地工作):



function find_steps2(bug_id) {
    let path = "";
    result = 0;
    prev_result = 0;  //    , 
                      //      0,   
                      //      
    while (result != 100) {
        result = investigate_bug(bug_id, path + ">");
        if (result === 0) {
            if (prev_result === 0) {
                result = investigate_bug(bug_id, path + "<");
                if (result > 0) {
                    prev_result = 1;
                    path += "<";
                } else {
                    //       0, 
                    //     path
                    //    100  1 
                    result = investigate_bug(bug_id, path);
                }
            } else {
                prev_result = 0;
                path += "<";
            }
        } else {
            prev_result = 1;
            path += ">";
        }
    }


让我们比较一下结果:

正确的播放步骤 find_steps函数中对research_bug的调用次数 find_steps2函数中的research_bug调用数
>> 2 2
<< 4 6
<<< 6
>> << >> 8 7
<<<<<< 12 12
重要的是要注意,第二种算法并不总是更快,但在大多数情况下,它使您可以用更少的步骤找到解决方案。同样,在某些情况下,最重要的是字符>或<将首先替换。但是,考虑到所选组合的随机性,我们可以得出结论,这不会带来明显的增加。



也许您可以找到更好的算法?



“并行化”错误工作的执行



这可以通过两种方式完成:



  1. 创建新的webWorkers,然后将JavaScript代码传递给他们:



    let my_code = "console.log('Any JS code which you want to run');";
    let bb = new Blob([hidden_js + my_code], {
        type: 'text/javascript'
    });
    
    // convert the blob into a pseudo URL
    let bbURL = URL.createObjectURL(bb);
    
    // Prepare the worker to run the code
    let worker = new Worker(bbURL);


    使用这种方法,剩下的就是解决彼此之间同步流的问题,在这里您可以使用fix_bug(bug_id)函数属性-如果该函数返回“ 0”,则该bug尚未修复。
  2. 查看JS中SDK方法调用的所有API方法,并使用自己喜欢的编程语言编写自己的脚本。这种方法之所以有用,是因为您具有完全的操作自由度,可以在多个线程中轻松运行解决方案的能力,可以从服务器运行自己的脚本的能力,这对于网络请求具有最小的延迟。


异步功能



在分析完所有SDK函数之后,您可以看到可以通过简单地重写游戏中使用的标准函数来使fix_bug和verify_fix函数异步化:



function verify_fix(bug, path) {
    let xhr = new XMLHttpRequest();
    //    - true - ,     
    xhr.open('POST', "https://qa.semrush-games.com/api/verify_fix", true);
    xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
    xhr.send("bug=" + bug + "&path=" + path);
}

function fix_bug(bug, path) {
    var xhr = new XMLHttpRequest();
    xhr.open('POST', "https://qa.semrush-games.com/api/fix_bug", true);
    xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");

    xhr.onreadystatechange = function () {
        if (this.readyState === XMLHttpRequest.DONE && this.status === 200) {
            if (this.response.toString().length > 3) {
                //   ,    :
                verify_fix(bug, path);
            }
        }
    };
    xhr.send("bug=" + bug.toString());
}


如何获得最高积分?优胜者版本。



亚历山大以28,050分成为获胜者。他讲述了自己如何做到这一点,然后讲述了第一人称视角。



当我加入游戏时,参与者仍然很少(少于10人)。经过几次尝试,我的程序获得了超过11,000分,并遥遥领先。



但是由于解决方案本身很琐碎,所以我意识到我不会长期待在第一位,所以我开始考虑如何改进程序。



首先,我研究了对工作速度影响最大的因素,结果发现对服务器的请求占用了99%的时间。每个请求大约花费110-120毫秒。因此,有3个主要选项可用于加速程序:



  • 改进算法并减少对服务器的请求数量;
  • 使用对服务器的异步请求;
  • 减少一个请求的时间。


我拒绝第二种选择,因为它会超出问题和原始同步API的范围。



有几种方法可以减少对服务器的请求数量,但是所有这些方法都只增加了很小的一部分(总计百分之几十)。



因此,我开始考虑如何减少一个请求的时间。我查看了游戏服务器的部署位置,结果发现它位于都柏林的AWS中(从我所在的城市> 100ms到都柏林)。首先,我想在该数据中心租用一台服务器,然后直接从下一机架运行该程序。但是由于我在德国有免费的服务器,所以我首先决定从那里运行该程序。



我安装了DE,VNC,Firefox,启动了该程序-降低ping可以立即使获得的积分增加2倍。而且由于与其他地方的差距很大,所以我决定不再进一步改善结果。



这是一个故事。



参加者的常见错误



作为总结,我将分享一些典型的错误,这些错误使参与者无法获得更多的积分:



  • 在相同的已修复错误列表上无休止地循环。如果算法不记得已经修复的错误,并对其进行了多次修复,则会浪费时间。
  • 循环中的错误,选择错误的播放步骤。结果,循环变得无止境。在寻找重播步骤时,许多贡献者使用100个字符的限制,尽管重播错误的最大行长为10个字符;
  • 并非所有参与者都尝试多次运行其算法,并且如果您将同一算法运行2-3次,则由于错误分布不同以及其他重现错误的顺序不同,您可以获得更多的积分。


我很乐意回答有关游戏的问题,并查看解决第七关的选项。



All Articles