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

几天前,我收到了组织者的来信,询问我是否想再次参加-提出第二届编程冠军的前端任务。 我同意-并认为这是本文的一个有趣主题。 倒入咖啡,坐下。 我将告诉您一年前我们是如何准备任务的。
我们大约有十个人,几乎所有人都是来自各种Yandex服务的前端开发人员。 我们必须选择将由自动测试检查的任务。
对于编程比赛,有一个特殊的服务-Yandex.Contest 。 在那里您可以发布任务,参与者可以注册并解决它们。 任务测试自动进行;参与者的结果发布在一个特殊的表中。 因此,基础架构已经准备就绪。 需要做的只是提出任务。 但事实证明,有一个警告。 以前,Yandex举办过算法,机器学习和其他主题的比赛,但从未参加过前端比赛。 没有人知道比赛应该包括什么以及如何使验证自动化。

我们认为对于前端开发人员来说,需要布局,JavaScript和浏览器API知识的任务是合适的。 可以通过比较屏幕截图来检查布局。 算法任务可以在Node.js中运行,并通过将结果与正确答案进行比较来进行验证。 可以通过Puppeteer启动与浏览器API配合使用的程序,脚本可以在执行后检查页面状态。
比赛分为资格赛和决赛两轮,每轮有6项任务。 资格任务必须不同,以便不同的参与者获得不同的选择。 我们选择了每轮任务的数量和类型,分为两个人组成的团队,并在团队之间分配任务。 每个小组必须提出两个变数问题以进行资格鉴定,并提出两个非变数任务以进行决赛。

让我们点击DOM元素...
这个想法浮出水面-提供一个浏览器游戏,您需要单击DOM元素作为可变任务之一。 参与者的任务是编写一个可以玩这个游戏并获胜的程序。 发明了4种选择:
如果需要,您可以点击链接并播放。 如果您播放“电话”或“钢琴”,请不要忘记打开声音。
为所有选项编写了公用部分。 它包含用于显示可单击元素的逻辑,以及包含有关单击位置信息的元素(注释,手写数字,带有图片和颜色的卡片)。 信息集和可点击元素是通过参数设置的。
通过CSS控制外观。 事实证明,它与csszengarden.com非常相似-一种样式不同的布局看起来有所不同。




参与者计划的结果是点击元素的日志。 添加了一个处理程序,该处理程序将有关单击项的信息写入全局变量。 为了使参与者无法直接点击结果,而不能立即将结果写入此变量,我们将其名称传递给外部。
function initGame(targetClasses, keyClasses, resultName) {
运行参与者程序的脚本是这样的:
添加声音
我们决定需要稍微用手机使游戏恢复活力并增加按键的声音。 这种声音称为DTMF音调 。 找到了有关如何生成它们的文章 。 简而言之,有必要同时播放两个不同频率的声音。 可以使用Web Audio API播放给定频率的声音 。 结果是这样的代码:
function playSound(num) {
还添加了声音来弹钢琴。 如果有任何参与者想演奏页面上写的笔记,那么他会听过《星球大战》的皇家游行。

让任务复杂化
我们为自己完成的声音任务感到很高兴,但是这种欢乐并没有持续很长时间。 在游戏测试期间,事实证明该程序非常迅速地单击按钮,并且我们所有的酷声音都合并为一个常见的混乱。 我们决定在按键之间增加50毫秒的延迟,以便依次播放声音。 同时,这使任务有些复杂。
function initGame(targetClasses, keyClasses, resultName) {
但这还不是全部。 我们认为参与者可以轻松查看源代码并立即看到延迟。 为了使他们的任务复杂化,我们使用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é)的评论中,其中一项条件是,该行业的发烧友都在考虑条件。 在比赛中,很高兴意识到参赛者正在解决您提出的任务。
参考文献:
- 分析了我们准备的去年的前端分配
- 分析今年首届冠军赛的前端赛道