没有钢琴家的马和乐团的电话。 如何在前端提出运动任务

你好 我叫Dmitry Andriyanov,我是Yandex的界面开发人员。 去年,我参加了在线前端竞赛的准备工作。



几天前,我收到了组织者的来信,询问我是否想再次参加-提出第二届编程冠军的前端任务。 我同意-并认为这是本文的一个有趣主题。 倒入咖啡,坐下。 我将告诉您一年前我们是如何准备任务的。




我们大约有十个人,几乎所有人都是来自各种Yandex服务的前端开发人员。 我们必须选择将由自动测试检查的任务。


对于编程比赛,有一个特殊的服务-Yandex.Contest 。 在那里您可以发布任务,参与者可以注册并解决它们。 任务测试自动进行;参与者的结果发布在一个特殊的表中。 因此,基础架构已经准备就绪。 需要做的只是提出任务。 但事实证明,有一个警告。 以前,Yandex举办过算法,机器学习和其他主题的比赛,但从未参加过前端比赛。 没有人知道比赛应该包括什么以及如何使验证自动化。



我们认为对于前端开发人员来说,需要布局,JavaScript和浏览器API知识的任务是合适的。 可以通过比较屏幕截图来检查布局。 算法任务可以在Node.js中运行,并通过将结果与正确答案进行比较来进行验证。 可以通过Puppeteer启动与浏览器API配合使用的程序,脚本可以在执行后检查页面状态。


比赛分为资格赛和决赛两轮,每轮有6项任务。 资格任务必须不同,以便不同的参与者获得不同的选择。 我们选择了每轮任务的数量和类型,分为两个人组成的团队,并在团队之间分配任务。 每个小组必须提出两个变数问题以进行资格鉴定,并提出两个非变数任务以进行决赛。



让我们点击DOM元素...


这个想法浮出水面-提供一个浏览器游戏,您需要单击DOM元素作为可变任务之一。 参与者的任务是编写一个可以玩这个游戏并获胜的程序。 发明了4种选择:



如果需要,您可以点击链接并播放。 如果您播放“电话”或“钢琴”,请不要忘记打开声音。


为所有选项编写了公用部分。 它包含用于显示可单击元素的逻辑,以及包含有关单击位置信息的元素(注释,手写数字,带有图片和颜色的卡片)。 信息集和可点击元素是通过参数设置的。


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

通过CSS控制外观。 事实证明,它与csszengarden.com非常相似-一种样式不同的布局看起来有所不同。






参与者计划的结果是点击元素的日志。 添加了一个处理程序,该处理程序将有关单击项的信息写入全局变量。 为了使参与者无法直接点击结果,而不能立即将结果写入此变量,我们将其名称传递给外部。


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

运行参与者程序的脚本是这样的:


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

添加声音


我们决定需要稍微用手机使游戏恢复活力并增加按键的声音。 这种声音称为DTMF音调 。 找到有关如何生成它们的文章 。 简而言之,有必要同时播放两个不同频率的声音。 可以使用Web Audio API播放给定频率的声音 。 结果是这样的代码:


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

还添加了声音来弹钢琴。 如果有任何参与者想演奏页面上写的笔记,那么他会听过《星球大战》的皇家游行。



让任务复杂化


我们为自己完成的声音任务感到很高兴,但是这种欢乐并没有持续很长时间。 在游戏测试期间,事实证明该程序非常迅速地单击按钮,并且我们所有的酷声音都合并为一个常见的混乱。 我们决定在按键之间增加50毫秒的延迟,以便依次播放声音。 同时,这使任务有些复杂。


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

但这还不是全部。 我们认为参与者可以轻松查看源代码并立即看到延迟。 为了使他们的任务复杂化,我们使用UglifyJS缩小了页面上的所有JS代码。 但是该库不会更改类的公共API。 因此,UglifyJS保留的部分相同(即方法的名称和类字段),我们通过replace进行了replace


游戏迷惑的脚本如下所示:


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

让我们写一个创造性的条件


我们准备了游戏的技术部分,但是我们需要一个具有条件的创意文字-不仅需要满足要求,而且还需要一些故事。


我最喜欢的幽默是荒唐的。 这是当你认真看时说一些荒谬的废话。 废话通常听起来是意料之外的,会引起笑声。 我想使任务的条件荒唐,以取悦参与者。 因此,有一个关于阿道夫的马的故事,他无法打电话给朋友,因为他的大蹄子没有放在电话的按键上。



然后是一个关于一个女孩的故事,这个女孩正在从事钢琴演奏并想使它自动化,因此她去上课而不是上课。 有一句话是“如果一个女孩停止玩耍,妈妈走出房间,一巴掌。” 有人告诉我们,这是对虐待儿童的宣传,我们需要再写一段文字。 然后我们想出了一个关于乐团的故事,其中一位钢琴家在音乐会前生病了,其中一位音乐家在JS上编写了一个程序,将发挥他的作用。


总的来说,我们设法达到了预期的效果。 如果需要,可以在这里阅读


在比赛中设置任务


因此,我们已经准备好任务条件,检查解决方案的脚本和参考解决方案。 然后有必要在竞赛中配置所有这些内容。 对于任何任务,都有多个测试,每个测试都包含一组输入数据和正确答案。 下图显示了比赛的各个阶段。 第一阶段是程序的执行,第二阶段是结果验证:



在第一阶段的输入处,将接收一组测试数据和一个参与者程序。 在内部,run.js脚本有效,我们在上面编写了代码。 他负责运行参与者的程序,接收其工作结果并将其写入文件。 该程序在单独的虚拟机中运行,该虚拟机从Docker映像开始运行。 该虚拟机的资源有限,它无权访问网络。


第二阶段(检查结果)在另一个虚拟机中执行。 因此,参与者的程序实际上无法访问进行验证的环境。 第二阶段的输入是参与者程序(在第一阶段获得)和具有正确答案的文件的工作结果。 输出是验证脚本的退出代码,竞赛根据该退出代码了解验证如何结束:


OK = 0,
PE (显示错误-错误的结果格式)= 4
WA (错误答案)= 5
CF (验证错误)= 6


竞赛不适用于前端任务,包括Node.js。 我们通过使用pkg以及Node.js和node_modules将验证脚本打包到一个二进制文件中来解决了这个问题。 现在,我们已经掌握了有关比赛的秘密知识,并且在准备当前冠军时所遇到的困难也大大减少了。




因此,我们准备了任务。 此后,还有更多的事情:公开测试以校准复杂性,任务发布,比赛期间的技术支持职责以及在Yandex办公室授予获奖者。 但这是完全不同的故事。


现在,我们不再在某些领域竞争,而是举办统一的编程冠军赛,那里只有平行的赛道,包括前端。


对于准备任务所花费的时间,我一点都不后悔。 这很有趣,很有趣,与众不同。 在对哈布雷(Habré)的评论中,其中一项条件是,该行业的发烧友都在考虑条件。 在比赛中,很高兴意识到参赛者正在解决您提出的任务。


参考文献:
- 分析了我们准备的去年的前端分配
- 分析今年首届冠军赛的前端赛道

Source: https://habr.com/ru/post/zh-CN466647/


All Articles