Hola Mi nombre es Dmitry Andriyanov, trabajo como desarrollador de interfaces en Yandex. El año pasado participé en la preparación de nuestra competencia de front-end en línea.

Hace un par de días recibí una carta de los organizadores preguntándome si me gustaría volver a participar, para presentar tareas frontend para el segundo campeonato de programación . Acepté, y pensé que era un tema interesante para el artículo. Sirve café, siéntate. Te diré cómo preparamos las tareas hace un año.
Éramos unos diez, casi todos eran desarrolladores front-end de varios servicios de Yandex. Tuvimos que hacer una selección de tareas que serían verificadas por autotests.
Para las competencias de programación, hay un servicio especial: Yandex.Contest . Allí puede publicar tareas, y los participantes se registran y resuelven. Las pruebas de tareas se realizan automáticamente, los resultados de los participantes se publican en una tabla especial. Por lo tanto, la infraestructura ya estaba lista. Todo lo que se necesitaba era idear tareas. Pero resultó que hay una advertencia. Anteriormente, Yandex realizó competencias en algoritmos, aprendizaje automático y otros temas, pero nunca en competencias de front-end. Nadie entendía en qué debería consistir la competencia y cómo automatizar la verificación.

Decidimos que para los desarrolladores front-end, las tareas que requieren diseño, JavaScript y conocimiento de la API del navegador son adecuadas. El diseño se puede verificar comparando capturas de pantalla. Las tareas algorítmicas pueden ejecutarse en Node.js y verificarse comparando el resultado con la respuesta correcta. Los programas que funcionan con la API del navegador se pueden iniciar a través de Puppeteer y el script puede verificar el estado de la página después de la ejecución.
Las competiciones consisten en dos rondas: calificación y final, con 6 tareas en cada ronda. Las tareas de calificación deben ser variadas para que diferentes participantes obtengan diferentes opciones. Seleccionamos el número y el tipo de tareas para cada ronda, divididas en equipos de dos personas y distribuimos tareas entre equipos. Cada grupo tuvo que proponer dos problemas variativos para la calificación y dos tareas no variacionales para la final.

Hagamos clic en los elementos DOM ...
La idea surgió: dar un juego de navegador, en el que debe hacer clic en los elementos DOM, como una de las tareas variables. La tarea del participante era escribir un programa que juegue este juego y gane. Inventado 4 opciones:
Si lo desea, puede seguir los enlaces y jugar. Si toca "teléfono" o "piano", no olvide activar el sonido.
Escribió una parte común para todas las opciones. Contenía la lógica para mostrar elementos en los que se puede hacer clic, así como elementos con información sobre dónde hacer clic (notas, números escritos a mano, tarjetas con imágenes y colores). Conjuntos de información y elementos en los que se puede hacer clic se establecen a través de parámetros.
La apariencia se controló a través de CSS. Resultó muy similar a csszengarden.com : un diseño con diferentes estilos se ve diferente.




El resultado del programa del participante es un registro de clics en elementos. Se agregó un controlador que escribe información sobre los elementos en los que se hizo clic en una variable global. Para que el participante, en lugar de clics honestos, no pueda escribir inmediatamente el resultado en esta variable, pasamos su nombre en el exterior.
function initGame(targetClasses, keyClasses, resultName) {
El script para ejecutar el programa participante fue algo como esto:
Añadir sonido
Decidimos que necesitamos revivir un poco el juego con el teléfono y agregar el sonido de las teclas. Tales sonidos se llaman tonos DTMF . Encontré un artículo sobre cómo generarlos. En resumen, es necesario reproducir simultáneamente dos sonidos con diferentes frecuencias. Los sonidos de una frecuencia dada se pueden reproducir usando la API de audio web . El resultado es algo como este código:
function playSound(num) {
También se han agregado sonidos para tocar el piano. Si alguno de los participantes intentara tocar las notas escritas en la página, habría escuchado la marcha imperial de Star Wars.

Vamos a complicar la tarea.
Nos regocijamos por la tarea genial con los sonidos que hicimos, pero la alegría no duró mucho. Durante la prueba del juego, resultó que el programa hace clic en los botones muy rápidamente y todos nuestros sonidos geniales se fusionan en un desastre común. Decidimos agregar un retraso de 50 ms entre las pulsaciones de teclas, para que los sonidos se reproduzcan por turnos. Al mismo tiempo, esto complicó un poco la tarea.
function initGame(targetClasses, keyClasses, resultName) {
Pero eso no es todo. Pensamos que los participantes podían ver fácilmente el código fuente e inmediatamente ver el retraso. Para complicar su tarea, minimizamos todo el código JS en la página usando UglifyJS . Pero esta biblioteca no cambia la API pública de las clases. Por lo tanto, las partes que UglifyJS dejaron igual (es decir, los nombres de los métodos y los campos de clase), reemplazamos a través de replace
.
El guión para la ofuscación del juego se parecía a esto:
const minified = uglifyjs.minify(lines.join('\n')); const replaced = minified.code .replaceAll('this.window', 'this.') .replaceAll('this.document', 'this.') .replaceAll('this.log', 'this.') .replaceAll('this.lastClick', 'this.') .replaceAll('this.target', 'this.') .replaceAll('this.resName', 'this.') .replaceAll('this.audioContext', 'this.') .replaceAll('this.keyCount', 'this.') .replaceAll('this.classMap', 'this.') .replaceAll('_createDiv', '_') .replaceAll('_renderTarget', '_') .replaceAll('_renderKeys', '_') .replaceAll('_updateLog', '_') .replaceAll('_generateAnswer', '') .replaceAll('_createKeyElement', '') .replaceAll('_getMessage', '') .replaceAll('_next', '_____') .replaceAll('_pos', '__') .replaceAll('PhoneGame', '') .replaceAll('MusicGame', '') .replaceAll('BaseGame', 'xyz');
Escribamos una condición creativa
Preparamos la parte técnica del juego, pero necesitábamos un texto creativo de la condición, no solo con los requisitos que debían cumplirse, sino con algún tipo de historia.
Mi tipo de humor favorito es el absurdo. Esto es cuando con una mirada seria dices tonterías ridículas. Las tonterías generalmente suenan inesperadas y provocan risas. Quería hacer absurdas las condiciones de las tareas para complacer a los participantes. Así que había una historia sobre el caballo de Adolf, que no puede llamar a un amigo, porque él no tiene sus grandes pezuñas en las teclas del teléfono.

Luego hubo una historia sobre una chica que se dedica al piano y quiere automatizarlo, de modo que en lugar de las clases, ella salga a caminar. Estaba la frase "Si una niña deja de jugar, mamá sale de la habitación y le da una bofetada". Nos dijeron que esto es propaganda de abuso infantil y que necesitamos escribir otro texto. Luego se nos ocurrió una historia sobre una orquesta en la que un pianista se enfermó antes de un concierto, y uno de los músicos escribió un programa en JS que interpretaría su papel.
En general, logramos el efecto deseado de los textos. Si quieres, puedes leerlos aquí .
Establecer tareas en concurso
Entonces, teníamos listas las condiciones de la tarea, los scripts para verificar soluciones y las soluciones de referencia. Luego fue necesario configurar todo esto en el concurso. Para cualquier tarea, hay varias pruebas, cada una de las cuales contiene un conjunto de datos de entrada y la respuesta correcta. El siguiente diagrama muestra las etapas del concurso. La primera etapa es la ejecución del programa, la segunda es la verificación del resultado:

En la entrada de la primera etapa, se reciben un conjunto de datos de prueba y un programa participante. En el interior, funciona el script run.js, cuyo código escribimos anteriormente. Es responsable de ejecutar el programa del participante, recibir y escribir el resultado de su trabajo en un archivo. El programa se ejecuta en una máquina virtual separada, que se eleva desde la imagen de Docker antes de ejecutarse. Esta máquina virtual tiene recursos limitados, no tiene acceso a la red.
La segunda etapa (verificar el resultado) se realiza en otra máquina virtual. Por lo tanto, el programa del participante no tiene acceso físico al entorno donde se realiza la verificación. La entrada de la segunda etapa es el resultado del trabajo del programa del participante (obtenido en la primera etapa) y el archivo con la respuesta correcta. El resultado es el código de salida del script de verificación, según el cual el Concurso comprende cómo terminó la verificación:
- OK
= 0,
- PE
(error de presentación - formato de resultado incorrecto) = 4
- WA
(respuesta incorrecta) = 5
- CF
(error durante la verificación) = 6
El concurso estaba mal adaptado a las tareas en el front end, incluido Node.js. Resolvimos el problema empacando los scripts de validación en un archivo binario usando pkg junto con Node.js y node_modules. Ahora tenemos un conocimiento secreto sobre el concurso y experimentamos muchas menos dificultades para preparar el campeonato actual.
Entonces, hemos preparado las tareas. Después de eso, había mucho más: pruebas públicas para calibrar la complejidad, publicación de tareas, tareas de soporte técnico durante la competencia y premiar a los ganadores en la oficina de Yandex. Pero estas son historias completamente diferentes.
Ahora, en lugar de competir en ciertas áreas, estamos celebrando campeonatos de programación unificada, donde simplemente hay pistas paralelas, incluida la interfaz.
No me arrepiento un poco del tiempo dedicado a preparar tareas. Fue interesante y divertido, poco convencional. En uno de los comentarios sobre Habré escribió que las condiciones fueron pensadas por entusiastas del negocio. Durante la competencia, fue genial darse cuenta de que los participantes están resolviendo las tareas que se les ocurrieron.
Referencias
- Análisis de la asignación frontend del año pasado, que preparamos
- Análisis de la pista en la interfaz en el primer campeonato de este año.