C贸mo pasar el nivel final de JS QA Game de SEMrush

Hola, mi nombre es Timur y escrib铆 un juego de control de calidad de SEMrush . Es posible que haya o铆do hablar de este juego si particip贸 en Heisenbug en l铆nea o vio los anuncios del juego en los chats de Telegram para probadores. En resumen, en QA Game necesitas completar niveles con dificultad creciente y detectar errores usando JavaScript.



En este art铆culo, analizar茅 el s茅ptimo nivel (final y m谩s dif铆cil) y compartir茅 la decisi贸n del ganador del juego *.



imagen



* Aclaraci贸n para jugadores. QA Game se lanz贸 en 2 transmisiones: en junio y en julio. Alexander anot贸 el n煤mero m谩ximo de puntos para todo el tiempo desde la primera transmisi贸n, por lo que en el art铆culo analizamos sus resultados. El resto de l铆deres se puede ver en el enlace .



Qu茅 hay "dentro": la biblioteca Ace.js se usa para el editor de c贸digo en el juego , el resaltado de sintaxis y la finalizaci贸n autom谩tica est谩n disponibles en ella; webWorker se utiliza para ejecutar c贸digo en el lado del cliente (se inspir贸 en este art铆culo ). El backend est谩 escrito en Python & Flask, implementado en Heroku . En total, tom贸 alrededor de 2 meses escribir el juego.



Cuando escrib铆 QA Game, todav铆a no ten铆a experiencia con Ace.js y webWorkers, y fue interesante probarlos. Si quieres hacer un juego similar, te aconsejo que pienses en:



  • ejecutando el c贸digo del reproductor en el lado del servidor, no en el lado del cliente, como hice yo;
  • utilizando un marco de backend asincr贸nico. Si el backend est谩 en Python, recomiendo Quart o FastAPI ).


Leyenda del juego de control de calidad



En el juego, debes controlar al personaje ZERO2, que puede probar, buscar y corregir errores. El control se realiza mediante c贸digo JavaScript, ZERO2 tiene su propio SDK, lo que simplifica enormemente la programaci贸n.



Por ejemplo, para probar todas las funciones disponibles en el nivel, debe ejecutar el siguiente c贸digo:



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


Y luego, para corregir todos los errores encontrados durante las pruebas, esto es:



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


Cada nuevo nivel del juego contiene funciones adicionales y requiere el uso de algoritmos m谩s complejos; El an谩lisis detallado de cada uno de ellos se publica en GitHub . En este art铆culo analizar茅 en detalle el nivel 7, ya que fue en 茅l donde se determin贸 cu谩l de los jugadores recibir铆a la m谩xima cantidad de puntos.



驴C贸mo conseguir el m谩ximo de puntos? Versi贸n del creador del juego.



En el nivel 7, los jugadores deben corregir y verificar la cantidad m谩xima posible de errores en 120 segundos, mientras que:



  1. El bot贸n RUN solo se puede presionar 60 veces;
  2. Despu茅s de 120 segundos, el algoritmo finaliza autom谩ticamente, ya no se otorgan puntos (la validaci贸n se realiz贸 tanto en el front-end como en el back-end);
  3. Por cada error corregido, se otorgan 100 puntos, por un error corregido y verificado: 150 puntos;
  4. Cada vez que inicias RUN, todos los puntos se restablecen y se generan nuevos errores de forma aleatoria.


Para obtener el n煤mero m谩ximo de puntos, debe analizar los factores que afectan el resultado:



  • Simplificaci贸n del c贸digo . Es necesario eliminar todas las construcciones innecesarias y escribir c贸digo claro, comprobando la posibilidad de bucles. Muchos participantes perdieron puntos debido a errores en el c贸digo, dando lugar a bucles vac铆os sin fin;
  • Reducir el tiempo de respuesta a una solicitud . Cada m茅todo SDK realiza una solicitud al servidor y, en promedio, una solicitud tarda entre 200 y 400 ms. Para reducir esta cifra, debe encontrar un servidor adecuado y ejecutar consultas desde 茅l;
  • Optimizaci贸n de algoritmos . La mayor铆a de las veces se tarda en encontrar los pasos para reproducir el error (funci贸n research_bug). Por lo tanto, debe pensar en c贸mo optimizar el algoritmo para encontrar una soluci贸n en el n煤mero m铆nimo de intentos;
  • "Paralelizaci贸n" del algoritmo . El lanzamiento est谩ndar ocurre en un hilo (un webWorker) y todos los m茅todos API son sincr贸nicos. Puede intentar "paralelizar" el algoritmo. Y tambi茅n puede ver si es posible hacer que algunos de los m茅todos sean asincr贸nicos (alerta de spoiler: algunos pueden).


Optimizaci贸n de algoritmos



La funci贸n research_bug (bug_id, steps) devuelve 0 si los pasos de reproducci贸n especificados no son correctos, 1 si los pasos de reproducci贸n especificados son el comienzo de una combinaci贸n correcta de pasos y 100 si los pasos especificados son una combinaci贸n completa de pasos para reproducir el error.



El algoritmo para seleccionar los pasos de reproducci贸n puede tener este aspecto:



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


Esta funci贸n se puede acelerar si, para una determinada secuencia, al recibir un "0", no se vuelve a verificar la misma secuencia, reemplazando el 煤ltimo car谩cter. En su lugar, debe agregar inmediatamente otro car谩cter a la cadena y verificar el resultado para una nueva l铆nea.



Qu茅 significa eso? Es posible "guardar" el n煤mero de llamadas a research_bug usando este algoritmo (aunque no funcionar谩 m谩s r谩pido en todos los casos):



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 += ">";
        }
    }


Comparemos los resultados:

Pasos correctos de reproducci贸n N煤mero de llamadas a research_bug en la funci贸n find_steps N煤mero de llamadas a research_bug en la funci贸n find_steps2
>> 2 2
<< 4 6
<<< 6 cinco
>> << >> 8 7
<<<<<< 12 12
Es importante tener en cuenta que el segundo algoritmo no siempre es m谩s r谩pido, pero en la mayor铆a de los casos le permite encontrar una soluci贸n en menos pasos. Adem谩s, en algunos casos, importa cu谩l de los caracteres> o <se sustituir谩 en primer lugar. Sin embargo, dada la aleatoriedad de las combinaciones seleccionadas, podemos concluir que esto no dar谩 un aumento notable.



驴Quiz谩s puedas encontrar un algoritmo mejor?



"Paralelizar" la ejecuci贸n del trabajo sobre errores



Esto se puede hacer de 2 formas:



  1. Cree nuevos webWorkers y p谩selos c贸digo JavaScript en la l铆nea:



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


    Con este enfoque, solo queda resolver el problema de sincronizar diferentes flujos entre s铆, y aqu铆 puede usar la propiedad de la funci贸n fix_bug (bug_id); si la funci贸n devuelve "0", entonces el error a煤n no se ha solucionado.
  2. Vea todos los m茅todos API que son llamados por m茅todos SDK desde JS y cree su propio script en su lenguaje de programaci贸n favorito. Este enfoque es bueno porque tiene total libertad de acci贸n, la capacidad de ejecutar f谩cilmente la soluci贸n en varios subprocesos, la capacidad de ejecutar su propio script desde el servidor, que tendr谩 una latencia m铆nima para las solicitudes de red.


Funciones asincr贸nicas



Despu茅s de analizar todas las funciones del SDK, puede ver que las funciones fix_bug y verify_fix pueden hacerse asincr贸nicas simplemente reescribiendo las funciones est谩ndar que se utilizan en el juego:



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


驴C贸mo conseguir el m谩ximo de puntos? Versi贸n ganadora.



Alexander se convirti贸 en el ganador con 28.050 puntos. Cont贸 c贸mo logr贸 lograrlo, luego la narraci贸n en primera persona.



Cuando me un铆 al juego, todav铆a hab铆a pocos participantes (menos de 10). Despu茅s de varios intentos, mi programa super贸 los 11.000 puntos y ocup贸 el primer lugar por un amplio margen.



Pero como la soluci贸n en s铆 era bastante trivial, me di cuenta de que no me quedar铆a en primer lugar por mucho tiempo, as铆 que comenc茅 a pensar en c贸mo mejorar el programa.



Primero, mir茅 qu茅 afecta m谩s la velocidad de trabajo, result贸 que el 99% del tiempo estaba ocupado por solicitudes al servidor. Cada solicitud tom贸 aproximadamente 110-120 ms. En consecuencia, hab铆a 3 opciones principales para acelerar el programa:



  • Mejorar el algoritmo y reducir el n煤mero de solicitudes al servidor;
  • Usar solicitudes asincr贸nicas al servidor;
  • Reducir el tiempo de una solicitud.


Rechac茅 la segunda opci贸n, ya que ir铆a m谩s all谩 de las condiciones del problema y de la API s铆ncrona original.



Hab铆a varias formas de reducir la cantidad de solicitudes al servidor, pero todas dieron solo un peque帽o aumento (unas pocas decenas de por ciento en total).



Por lo tanto, comenc茅 a pensar en c贸mo reducir el tiempo de una solicitud. Mir茅 d贸nde se implement贸 el servidor del juego, result贸 que estaba en AWS en Dubl铆n (ping a Dubl铆n desde mi ciudad> 100 ms). Al principio, quer铆a alquilar un servidor en este centro de datos y ejecutar el programa directamente desde el siguiente bastidor. Pero como ten铆a un servidor gratuito en Alemania, primero decid铆 ejecutar el programa desde all铆.



Instal茅 DE, VNC, Firefox, lanc茅 el programa y un ping m谩s bajo aument贸 inmediatamente la cantidad de puntos ganados en 2 veces. Y como la diferencia con el resto era muy grande, decid铆 no mejorar m谩s el resultado.



He aqu铆 una historia.



Errores comunes de los participantes



Como ep铆logo, compartir茅 algunos errores t铆picos que impidieron a los participantes obtener m谩s puntos:



  • Bucles interminables sobre la misma lista de errores ya corregidos. Si el algoritmo no recuerda los errores ya corregidos y los corrige varias veces, se pierde tiempo;
  • Errores en bucles con selecci贸n de pasos de reproducci贸n para errores. Como resultado, los ciclos se volvieron interminables. Muchos participantes utilizaron el l铆mite de 100 caracteres al buscar pasos de repetici贸n, aunque la longitud m谩xima de l铆nea para reproducir errores fue de 10 caracteres;
  • No todos los participantes intentaron ejecutar sus algoritmos varias veces, y si ejecuta el mismo algoritmo 2-3 veces, podr铆a obtener un poco m谩s de puntos debido a una distribuci贸n diferente de errores y otras secuencias para reproducir errores.


Estar茅 encantado de responder preguntas sobre el juego y ver tus opciones para resolver el s茅ptimo nivel.



All Articles