Telefone para um cavalo e orquestra sem pianista. Como elaborar tarefas esportivas no front-end

Oi Meu nome é Dmitry Andriyanov, trabalho como desenvolvedor de interfaces no Yandex. No ano passado, participei da preparação do nosso concurso de front-end online.



Alguns dias atrás, recebi uma carta dos organizadores perguntando se eu gostaria de participar novamente - para criar tarefas de front-end para o segundo campeonato de programação . Eu concordei - e pensei que era um tópico interessante para o artigo. Despeje o café, sente-se. Vou lhe contar como preparamos as tarefas há um ano.




Havia cerca de dez de nós, quase todos eles desenvolvedores front-end de vários serviços Yandex. Tivemos que fazer uma seleção de tarefas que seriam verificadas pelos autotestes.


Para competições de programação, há um serviço especial - Yandex.Contest . Lá você pode publicar tarefas, e os participantes se registram e resolvem. O teste das tarefas ocorre automaticamente, os resultados dos participantes são publicados em uma tabela especial. Assim, a infraestrutura já estava pronta. Tudo o que era necessário era criar tarefas. Mas aconteceu que há uma ressalva. Anteriormente, a Yandex realizava competições em algoritmos, aprendizado de máquina e outros tópicos, mas nunca em competições front-end. Ninguém entendeu em que consistia a concorrência e como automatizar a verificação.



Decidimos que, para desenvolvedores front-end, as tarefas que exigem layout, JavaScript e conhecimento da API do navegador são adequadas. O layout pode ser verificado através da comparação de capturas de tela. Tarefas algorítmicas podem ser executadas no Node.js e verificadas comparando o resultado com a resposta correta. Os programas que funcionam com a API do navegador podem ser iniciados pelo Puppeteer e o script pode verificar o status da página após a execução.


As competições consistem em duas rodadas - qualificação e final, com 6 tarefas em cada rodada. As tarefas de qualificação devem ser variadas para que diferentes participantes obtenham opções diferentes. Selecionamos o número e o tipo de tarefas para cada rodada, divididos em equipes de duas pessoas e distribuímos tarefas entre equipes. Cada grupo teve que apresentar dois problemas variados para qualificação e duas tarefas não-variacionais para as finais.



Vamos clicar nos elementos DOM ...


A idéia surgiu - dar um jogo de navegador, no qual você precisa clicar nos elementos DOM, como uma das tarefas variáveis. A tarefa do participante era escrever um programa que jogue esse jogo e vença. Inventou 4 opções:



Se você quiser, pode seguir os links e jogar. Se você tocar "telefone" ou "piano", não se esqueça de ligar o som.


Escreveu uma parte comum para todas as opções. Ele continha a lógica para exibir elementos clicáveis, bem como elementos com informações sobre onde clicar (notas, números manuscritos, cartões com figuras e cores). Conjuntos de informações e elementos clicáveis ​​são definidos através de parâmetros.


//   —   div    // targetClasses —      // keyClasses —    ,     function initGame(targetClasses, keyClasses) { //    for(let i = 0; i < targetClasses.length; i++) { document.body.insertAdjacentHTML('afterbegin', `<div class="${targetClasses[i]}" />`); } //    for(let i = 0; i < keyClasses.length; i++) { document.body.insertAdjacentHTML('beforeend', // data-index     `<div class="key ${keyClasses[i]}" data-index="${i}" />`); } //       ,     } 

A aparência foi controlada através de CSS. O resultado foi muito semelhante ao csszengarden.com - um layout com estilos diferentes parece diferente.






O resultado do programa do participante é um log de cliques nos elementos. Adicionado um manipulador que grava informações sobre itens clicados em uma variável global. Para que o participante, em vez de cliques honestos, não pudesse escrever imediatamente o resultado nessa variável, passamos o nome para fora.


 function initGame(targetClasses, keyClasses, resultName) { // ... const log = []; document.body.addEventListener('click', (e) => { if (e.target.classList.contains('key')) { //     , //       log.push(e.target.data.index); //    ,    //  ,       if (log.length === targetClasses.length) { window[resultName] = log; } } }); } 

O script para executar o programa participante era algo como isto:


 //     ,    , //     Node.js. //   Chrome  headless-,    //       . const puppeteer = require('puppeteer'); const { writeFileSync } = require('fs'); const htmlFilePath = require.resolve('./game.html'); //    const solutionJsPath = resolve(process.argv[2]); //   const data = require('input.json'); //    const resName = `RESULT${Date.now()}`; //     (async () => { const browser = await puppeteer.launch(); //   const page = await browser.newPage(); //    await page.goto(`file://${htmlFilePath}`); //       await page.evaluate(resName => initGame( //   data.target, data.keys, resName), resName); await page.addScriptTag({ path: solutionJsPath }); //    await page.waitForFunction(`!!window[${resName}]`) // ,     resName const result = await page.evaluate(`window[${resName}]`); //   writeFileSync('output.json', JSON.stringify(result)); //       await browser.close(); })(); 

Adicionar som


Decidimos que precisamos reviver o jogo com o telefone um pouco e adicionar o som das teclas. Esses sons são chamados de tons DTMF . Encontrei um artigo sobre como gerá-los. Em suma, é necessário tocar simultaneamente dois sons com frequências diferentes. Os sons de uma determinada frequência podem ser reproduzidos usando a API de áudio da Web . O resultado é algo como este código:


 function playSound(num) { //  audioContext const context = this.audioContext; const g = context.createGain() //     const o = context.createOscillator(); o.connect(g); o.type='sine'; o.frequency.value = [697, 697, 697, 770, 770, 770, 852, 852, 852, 941, 941][num]; g.connect(context.destination); //     const o2 = context.createOscillator(); o2.connect(g); o2.type='sine'; o2.frequency.value = [1209, 1336, 1477, 1209, 1336, 1477, 1209, 1336, 1477, 1209, 1336][num]; g.connect(context.destination); //   —      // .    //   o.start(0); o2.start(0); //   240  g.gain.value = 1; setTimeout(() => g.gain.value = 0, 240); } 

Também foram adicionados sons para tocar piano. Se algum dos participantes tentasse tocar as notas escritas na página, ele teria ouvido a marcha imperial de Guerra nas Estrelas.



Vamos complicar a tarefa


Nós nos regozijamos com a tarefa legal com os sons que fizemos, mas a alegria não durou muito. Durante o teste do jogo, verificou-se que o programa clica nos botões muito rapidamente e todos os nossos sons legais se fundem em uma confusão comum. Decidimos adicionar um atraso de 50 ms entre as teclas, para que os sons sejam reproduzidos por vez. Ao mesmo tempo, isso complicou um pouco a tarefa.


 function initGame(targetClasses, keyClasses, resultName) { //      //    let lastClick = 0; // ... document.body.addEventListener('click', (e) => { const now = Date.now(); //      //    50 ,    if (lastClick + 50 < now) { // ... //     lastClick = now; } }); } 

Mas isso não é tudo. Pensamos que os participantes pudessem ver facilmente o código-fonte e imediatamente o atraso. Para complicar sua tarefa, reduzimos todo o código JS da página usando o UglifyJS . Mas essa biblioteca não altera a API pública das classes. Portanto, as partes que o UglifyJS deixaram as mesmas (ou seja, os nomes dos métodos e campos da classe), substituímos por meio da replace .


O script para a ofuscação do jogo era mais ou menos assim:


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

Vamos escrever uma condição criativa


Preparamos a parte técnica do jogo, mas precisávamos de um texto criativo da condição - não apenas com os requisitos que precisam ser cumpridos, mas com algum tipo de história.


Meu tipo de humor favorito é o absurdo. É quando, com um olhar sério, você diz algumas bobagens ridículas. O absurdo geralmente soa inesperado e causa risadas. Queria tornar absurdas as condições das tarefas para agradar aos participantes. Então havia uma história sobre o cavalo de Adolf, que não pode ligar para um amigo, porque ele não coloca seus cascos grandes nas teclas do telefone.



Depois, houve uma história sobre uma garota que está envolvida no piano e quer automatizá-lo, de modo que, em vez de aulas, ela sai para passear. Havia a frase "Se uma garota para de brincar, a mãe sai da sala e dá um tapa na cara". Fomos informados de que isso é propaganda de abuso infantil e precisamos escrever outro texto. Então, inventamos uma história sobre uma orquestra na qual um pianista adoeceu antes de um show, e um dos músicos escreveu um programa em JS que faria sua parte.


Em geral, conseguimos o efeito desejado dos textos. Se você quiser, pode lê-los aqui .


Definindo tarefas no concurso


Portanto, tínhamos condições de tarefas prontas, scripts para verificar soluções e soluções de referência. Então foi necessário configurar tudo isso no concurso. Para qualquer tarefa, existem vários testes, cada um dos quais contém um conjunto de dados de entrada e a resposta correta. O diagrama abaixo mostra as etapas do concurso. A primeira etapa é a execução do programa, a segunda é a verificação do resultado:



Na entrada do primeiro estágio, um conjunto de dados de teste e um programa participante são recebidos. Por dentro, o script run.js funciona, cujo código escrevemos acima. Ele é responsável pela execução do programa do participante, recebendo e gravando o resultado do seu trabalho em um arquivo. O programa é executado em uma máquina virtual separada, que surge da imagem do Docker antes da execução. Esta máquina virtual é limitada em recursos, não tem acesso à rede.


O segundo estágio (verificação do resultado) é realizado em outra máquina virtual. Assim, o programa do participante não tem acesso físico ao ambiente em que a verificação ocorre. A entrada da segunda etapa é o resultado do trabalho do programa do participante (obtido na primeira etapa) e o arquivo com a resposta correta. A saída é o código de saída do script de verificação, de acordo com o qual o Concurso entende como a verificação terminou:


- OK = 0
- PE (erro de apresentação - formato de resultado incorreto) = 4
- WA (resposta errada) = 5
- CF (erro durante a verificação) = 6


O concurso foi mal adaptado às tarefas no front-end, incluindo Node.js. Resolvemos o problema compactando os scripts de validação em um arquivo binário usando o pkg junto com o Node.js e o node_modules. Agora, temos um conhecimento secreto sobre o concurso e temos muito menos dificuldades na preparação do campeonato atual.




Então, nós preparamos as tarefas. Depois disso, havia muito mais: testes públicos para calibrar a complexidade, publicação de tarefas, serviço de suporte técnico durante a competição e premiação dos vencedores no escritório da Yandex. Mas essas são histórias completamente diferentes.


Agora, em vez de competir em determinadas áreas, estamos realizando campeonatos de programação unificados, onde há simplesmente trilhas paralelas, incluindo o frontend.


Não me arrependo um pouco do tempo gasto na preparação de tarefas. Foi interessante e divertido, não convencional. Em um dos comentários sobre Habré, escreveu que as condições foram pensadas pelos entusiastas do negócio. Durante a competição, foi legal perceber que os participantes estão resolvendo as tarefas que você criou.


Referências:
- Análise da tarefa de frontend do ano passado, que preparamos
- Análise da pista no frontend no primeiro campeonato deste ano

Source: https://habr.com/ru/post/pt466647/


All Articles