Téléphone pour cheval et orchestre sans pianiste. Comment proposer des tùches sportives en premiÚre ligne

Salut Je m'appelle Dmitry Andriyanov, je travaille en tant que développeur d'interface dans Yandex. L'année derniÚre, j'ai participé à la préparation de notre concours front end en ligne.



Il y a quelques jours, j'ai reçu une lettre des organisateurs me demandant si je voulais participer à nouveau - pour proposer des tùches frontales pour le deuxiÚme championnat de programmation . J'ai accepté - et j'ai pensé que c'était un sujet intéressant pour l'article. Versez le café, asseyez-vous. Je vais vous dire comment nous avons préparé les tùches il y a un an.




Nous étions une dizaine, presque tous des développeurs front-end de divers services Yandex. Nous avons dû faire une sélection de tùches qui seraient vérifiées par des autotests.


Pour les compĂ©titions de programmation, il existe un service spĂ©cial - Yandex.Contest . LĂ , vous pouvez publier des tĂąches et les participants s'enregistrent et les rĂ©solvent. Le test des tĂąches a lieu automatiquement, les rĂ©sultats des participants sont publiĂ©s dans un tableau spĂ©cial. Ainsi, l'infrastructure Ă©tait dĂ©jĂ  prĂȘte. Il suffisait de trouver des tĂąches. Mais il s'est avĂ©rĂ© qu'il y avait une mise en garde. Auparavant, Yandex a organisĂ© des concours d'algorithmes, d'apprentissage automatique et d'autres sujets, mais jamais de concours frontaux. Personne ne savait en quoi le concours devrait consister et comment automatiser la vĂ©rification.



Nous avons dĂ©cidĂ© que pour les dĂ©veloppeurs front-end, les tĂąches nĂ©cessitant une mise en page, JavaScript et la connaissance de l'API du navigateur conviennent. La mise en page peut ĂȘtre vĂ©rifiĂ©e en comparant les captures d'Ă©cran. Les tĂąches algorithmiques peuvent ĂȘtre exĂ©cutĂ©es dans Node.js et vĂ©rifiĂ©es en comparant le rĂ©sultat avec la bonne rĂ©ponse. Les programmes qui fonctionnent avec l'API du navigateur peuvent ĂȘtre lancĂ©s via Puppeteer et le script peut vĂ©rifier l'Ă©tat de la page aprĂšs l'exĂ©cution.


Les compĂ©titions se composent de deux tours - qualification et finale, avec 6 tĂąches dans chaque tour. Les tĂąches de qualification doivent ĂȘtre variĂ©es afin que les diffĂ©rents participants aient diffĂ©rentes options. Nous avons sĂ©lectionnĂ© le nombre et le type de tĂąches pour chaque tour, divisĂ© en Ă©quipes de deux personnes et rĂ©parti les tĂąches entre les Ă©quipes. Chaque groupe a dĂ» trouver deux problĂšmes variĂ©s pour la qualification et deux tĂąches non variationnelles pour les finales.



Cliquons sur les éléments DOM ...


Il y avait une idée - comme l'une des tùches variées pour donner un jeu par navigateur dans lequel vous devez cliquer sur les éléments DOM. La tùche du participant était d'écrire un programme qui joue à ce jeu et gagne. Inventé 4 options:



Si vous le souhaitez, vous pouvez suivre les liens et jouer. Si vous jouez «téléphone» ou «piano», n'oubliez pas d'activer le son.


A Ă©crit une partie commune pour toutes les options. Il contenait la logique d'affichage des Ă©lĂ©ments cliquables, ainsi que des Ă©lĂ©ments contenant des informations sur l'endroit oĂč cliquer (notes, numĂ©ros manuscrits, cartes avec images et couleurs). Les ensembles d'informations et les Ă©lĂ©ments cliquables sont dĂ©finis via des paramĂštres.


//   —   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}" />`); } //       ,     } 

L'apparence était contrÎlée par CSS. Il s'est avéré trÚs similaire à csszengarden.com - une mise en page avec des styles différents est différente.






Le rĂ©sultat du programme du participant est un journal des clics sur les Ă©lĂ©ments. Ajout d'un gestionnaire qui Ă©crit des informations sur les Ă©lĂ©ments cliquĂ©s dans une variable globale. Pour que le participant, au lieu de clics honnĂȘtes, ne puisse pas immĂ©diatement Ă©crire le rĂ©sultat dans cette variable, nous lui transmettons son nom Ă  l'extĂ©rieur.


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

Le script pour exécuter le programme participant était quelque chose comme ceci:


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

Ajouter du son


Nous avons dĂ©cidĂ© que nous devions relancer un peu le jeu avec le tĂ©lĂ©phone et ajouter le son des touches. Ces sons sont appelĂ©s tons DTMF . TrouvĂ© un article sur la façon de les gĂ©nĂ©rer. Bref, il faut jouer simultanĂ©ment deux sons de frĂ©quences diffĂ©rentes. Les sons d'une frĂ©quence donnĂ©e peuvent ĂȘtre lus Ă  l'aide de l' API Web Audio . Le rĂ©sultat est quelque chose comme ce code:


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

Des sons ont également été ajoutés pour jouer du piano. Si l'un des participants avait essayé de jouer les notes écrites sur la page, il aurait entendu la marche impériale de Star Wars.



Compliquons la tĂąche


Nous nous sommes rĂ©jouis de la tĂąche cool que nous avons faite avec les sons, mais la joie n'a pas durĂ© longtemps. Lors des tests du jeu, il s'est avĂ©rĂ© que le programme cliquait sur les boutons trĂšs rapidement et tous nos sons sympas fusionnaient en un gĂąchis commun. Nous avons dĂ©cidĂ© d'ajouter un dĂ©lai de 50 ms entre les frappes, afin que les sons soient jouĂ©s tour Ă  tour. En mĂȘme temps, cela compliquait un peu la tĂąche.


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

Mais ce n'est pas tout. Nous pensions que les participants pouvaient facilement voir le code source et voir immĂ©diatement le retard. Pour compliquer leur tĂąche, nous avons minimisĂ© tout le code JS sur la page en utilisant UglifyJS . Mais cette bibliothĂšque ne modifie pas l'API publique des classes. Par consĂ©quent, les parties que UglifyJS a laissĂ©es les mĂȘmes (Ă  savoir, les noms des mĂ©thodes et des champs de classe), nous avons remplacĂ© par replace .


Le script pour l'obscurcissement du jeu ressemblait Ă  ceci:


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

Écrivons une condition crĂ©ative


Nous avons prĂ©parĂ© la partie technique du jeu, mais nous avions besoin d'un texte crĂ©atif de la condition - non seulement avec les exigences qui doivent ĂȘtre remplies, mais avec une sorte d'histoire.


Mon genre d'humour prĂ©fĂ©rĂ© est l'absurditĂ©. C'est quand avec un regard sĂ©rieux vous dites des bĂȘtises ridicules. Les bĂȘtises semblent gĂ©nĂ©ralement inattendues et provoquent des rires. Je voulais rendre les conditions des tĂąches absurdes afin de plaire aux participants. Il y avait donc une histoire sur le cheval d'Adolf, qui ne peut pas appeler un ami, car il ne met pas ses gros sabots sur les touches du tĂ©lĂ©phone.



Puis il y a eu une histoire sur une fille qui est engagĂ©e dans le piano et veut l'automatiser, de sorte qu'au lieu des cours, elle se promĂšne. Il y avait la phrase "Si une fille arrĂȘte de jouer, maman sort de la piĂšce et donne une gifle au visage." On nous a dit que c'Ă©tait de la propagande de maltraitance d'enfants et nous devons Ă©crire un autre texte. Puis nous sommes arrivĂ©s avec une histoire d'un orchestre dans lequel un pianiste est tombĂ© malade avant un concert, et l'un des musiciens a Ă©crit un programme sur JS qui jouerait son rĂŽle.


En général, nous avons réussi à obtenir l'effet souhaité des textes. Si vous le souhaitez, vous pouvez les lire ici .


Définition des tùches dans le concours


Nous avions donc des conditions de tĂąche prĂȘtes, des scripts de vĂ©rification des solutions et des solutions de rĂ©fĂ©rence. Il a ensuite fallu configurer tout cela dans le cadre du concours. Pour toute tĂąche, il existe plusieurs tests, chacun contenant un ensemble de donnĂ©es d'entrĂ©e et la bonne rĂ©ponse. Le diagramme ci-dessous montre les Ă©tapes du concours. La premiĂšre Ă©tape est l'exĂ©cution du programme, la seconde est la vĂ©rification du rĂ©sultat:



A l'entrée de la premiÚre étape, un ensemble de données de test et un programme participant sont reçus. A l'intérieur, le script run.js fonctionne, dont nous avons écrit le code ci-dessus. Il est responsable de l'exécution du programme du participant, de la réception et de l'écriture du résultat de son travail dans un fichier. Le programme s'exécute sur une machine virtuelle distincte, qui découle de l'image Docker avant de s'exécuter. Cette machine virtuelle est limitée en ressources, elle n'a pas accÚs au réseau.


La deuxiĂšme Ă©tape (vĂ©rification du rĂ©sultat) est effectuĂ©e dans une autre machine virtuelle. Ainsi, le programme du participant n’a pas physiquement accĂšs Ă  l’environnement oĂč la vĂ©rification a lieu. L'entrĂ©e de la deuxiĂšme Ă©tape est le rĂ©sultat du travail du programme du participant (obtenu Ă  la premiĂšre Ă©tape) et du dossier avec la bonne rĂ©ponse. Le rĂ©sultat est le code de sortie du script de vĂ©rification, selon lequel le concours comprend comment la vĂ©rification s'est terminĂ©e:


- OK = 0,
- PE (erreur de présentation - format de résultat incorrect) = 4
- WA (mauvaise réponse) = 5
- CF (erreur lors de la vérification) = 6


Le concours était mal adapté aux tùches sur le front-end, dont Node.js. Nous avons résolu le problÚme en compressant les scripts de validation dans un fichier binaire à l'aide de pkg avec Node.js et node_modules. Maintenant, nous avons des connaissances secrÚtes sur le concours et éprouvons beaucoup moins de difficultés à préparer le championnat actuel.




Nous avons donc préparé les tùches. AprÚs cela, il y avait beaucoup plus: tests publics pour calibrer la complexité, publication des tùches, devoir de support technique pendant le concours et attribution des gagnants au bureau Yandex. Mais ce sont des histoires complÚtement différentes.


Maintenant, au lieu de concourir dans certains domaines, nous organisons des championnats de programmation unifiĂ©s, oĂč il y a simplement des pistes parallĂšles, y compris le frontend.


Je ne regrette pas un peu le temps passé à préparer les tùches. C'était intéressant et amusant, non conventionnel. Dans un des commentaires sur Habré écrit que les conditions ont été imaginées par des passionnés de l'entreprise. Pendant la compétition, c'était cool de réaliser que les participants résolvent les tùches que vous avez imaginées.


Références:
- Analyse de l'affectation frontend de l'année derniÚre, que nous avons préparée
- Analyse de la piste en frontend dans le premier championnat de cette année

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


All Articles