تحية! اسمي ديمتري أندريانوف ، أعمل كمطور واجهة في ياندكس. شاركت في العام الماضي في التحضير لمسابقتنا الأمامية على الإنترنت.

منذ يومين تلقيت رسالة من المنظمين تسألني عما إذا كنت أرغب في المشاركة مرة أخرى - للشروع في مهام الواجهة لبطولة البرمجة الثانية . وافقت - وأعتقد أنه كان موضوعًا مثيرًا للاهتمام لهذه المقالة. صب في القهوة ، والجلوس. سوف أخبركم كيف أعددنا المهام قبل عام.
كان هناك حوالي عشرة منا ، وجميعهم تقريبا كانوا مطورين أماميين من مختلف خدمات ياندكس. كان علينا أن نجعل مجموعة مختارة من المهام التي سيتم فحصها من قبل autotests.
بالنسبة لمسابقات البرمجة ، هناك خدمة خاصة - Yandex.Contest . هناك يمكنك نشر المهام ، والمشاركين تسجيل وحلها. يتم اختبار المهام تلقائيًا ، ويتم نشر نتائج المشاركين في جدول خاص. وبالتالي ، كانت البنية التحتية جاهزة بالفعل. كل ما هو مطلوب هو الخروج بالمهام. ولكن اتضح أن هناك تحذير واحد. سابقا ، عقدت ياندكس مسابقات في الخوارزميات ، والتعلم الآلي ، وغيرها من المواضيع ، ولكن لم يحدث في المسابقات الأمامية. لم يكن لدى أي شخص فهم لما يجب أن تتكون عليه المنافسة وكيفية أتمتة عملية التحقق.

قررنا أنه بالنسبة للمطورين المتقدمين ، فإن المهام التي تتطلب التخطيط وجافا سكريبت ومعرفة واجهة برمجة تطبيقات المتصفح مناسبة. يمكن التحقق من التخطيط من خلال مقارنة لقطات الشاشة. يمكن تشغيل مهام الخوارزمية في Node.js والتحقق منها من خلال مقارنة النتيجة بالإجابة الصحيحة. يمكن تشغيل البرامج التي تعمل مع المستعرض API من خلال Puppeteer ويمكن للبرنامج النصي التحقق من حالة الصفحة بعد التنفيذ.
تتكون المسابقات من جولتين - التأهيل والنهائي ، مع 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) {
لكن هذا ليس كل شيء. لقد اعتقدنا أن المشاركين يمكنهم بسهولة رؤية الكود المصدري ورؤية التأخير على الفور. لتعقيد مهمتهم ، قمنا بتصغير كل كود JS على الصفحة باستخدام UglifyJS . لكن هذه المكتبة لا تغير API العامة للفصول. لذلك ، الأجزاء التي تركها UglifyJS هي نفسها (وهي أسماء الطرق وحقول الفصل) ، 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');
دعنا نكتب حالة الإبداعية
لقد أعددنا الجزء الفني من اللعبة ، لكننا كنا بحاجة إلى نص مبتكر للحالة - ليس فقط مع المتطلبات التي يجب الوفاء بها ، ولكن مع نوع من القصة.
النوع المفضل لدي من الفكاهة هو العبثية. هذا عندما تقول بنظرة جدية بعض الهراء السخيف. الهراء عادة ما يبدو غير متوقع ويسبب الضحك. أردت أن أجعل شروط المهام سخيفة لإرضاء المشاركين. لذلك كانت هناك قصة عن حصان Adolf ، الذي لا يمكنه الاتصال بصديق ، لأنه لا يحمل حوافره الكبيرة على مفاتيح الهاتف.

ثم كانت هناك قصة عن فتاة تعمل في البيانو وتريد تشغيلها تلقائيًا ، بحيث تذهب بدلاً من الفصول في نزهة على الأقدام. كانت هناك عبارة "إذا توقفت الفتاة عن اللعب ، خرجت أمي من الغرفة وأعطت صفعة في الوجه". قيل لنا إن هذه دعاية لإساءة معاملة الأطفال ونحن بحاجة إلى كتابة نص آخر. ثم توصلنا إلى قصة عن أوركسترا أصيب فيها عازف البيانو بالمرض قبل حفل موسيقي ، وكتب أحد الموسيقيين برنامجًا على JS يلعب دوره.
بشكل عام ، تمكنا من تحقيق التأثير المطلوب للنصوص. إذا كنت تريد ، يمكنك قراءتها هنا .
تحديد المهام في المسابقة
لذلك ، كان لدينا شروط مهمة جاهزة ، ومخطوطات لفحص الحلول والحلول المرجعية. ثم كان من الضروري تكوين كل هذا في المسابقة. لأي مهمة ، هناك العديد من الاختبارات ، يحتوي كل منها على مجموعة من بيانات الإدخال والإجابة الصحيحة. يوضح الرسم البياني أدناه مراحل المسابقة. المرحلة الأولى هي تنفيذ البرنامج ، والثانية هي التحقق من النتيجة:

عند إدخال المرحلة الأولى ، يتم استلام مجموعة من بيانات الاختبار وبرنامج مشارك. في الداخل ، يعمل البرنامج النصي run.js ، الذي كتبناه أعلاه. إنه مسؤول عن تشغيل برنامج المشارك ، وتلقي وكتابة نتيجة عمله في ملف. يعمل البرنامج في جهاز افتراضي منفصل ، يرتفع من صورة Docker قبل التشغيل. يقتصر هذا الجهاز الظاهري في الموارد ، وليس لديه إمكانية الوصول إلى الشبكة.
يتم تنفيذ المرحلة الثانية (التحقق من النتيجة) في جهاز افتراضي آخر. وبالتالي ، لا يتمتع برنامج المشارك فعليًا بالوصول إلى البيئة التي يتم فيها التحقق. مدخلات المرحلة الثانية هي نتيجة عمل برنامج المشارك (تم الحصول عليه في المرحلة الأولى) والملف مع الإجابة الصحيحة. الإخراج هو رمز الخروج من البرنامج النصي للتحقق ، والتي بموجبها يفهم المسابقة كيف انتهت عملية التحقق:
- OK
= 0 ،
- PE
(خطأ في العرض - تنسيق نتائج غير صحيح) = 4
- WA
(إجابة خاطئة) = 5
- CF
(خطأ أثناء التحقق) = 6
تم تكييف المسابقة بشكل سيء مع المهام في الواجهة الأمامية ، بما في ذلك Node.js. لقد قمنا بحل المشكلة عن طريق حزم البرامج النصية للتحقق من الصحة في ملف ثنائي باستخدام pkg مع Node.js و node_modules. الآن لدينا معرفة سرية حول المسابقة وتجربة أقل صعوبة بكثير في إعداد البطولة الحالية.
لذلك ، أعددنا المهام. بعد ذلك ، كان هناك أكثر من ذلك بكثير: اختبارات عامة لمعايرة التعقيد ونشر المهام وواجب الدعم الفني أثناء المنافسة ومنح الفائزين في مكتب ياندكس. لكن هذه قصص مختلفة تماما.
الآن ، بدلاً من التنافس في مجالات معينة ، نقيم بطولات برمجة موحدة ، حيث توجد مسارات موازية ، بما في ذلك الواجهة الأمامية.
أنا لست نادما بعض الشيء على الوقت الذي يقضيه في إعداد المهام. لقد كانت ممتعة وممتعة وغير تقليدية. في أحد التعليقات على Habré كتب أن الظروف قد تم التفكير فيها من قبل عشاق العمل. خلال المسابقة ، كان من الرائع إدراك أن المشاركين يقومون بحل المهام التي توصلت إليها.
المراجع:
- تحليل للواجهة الأمامية للعام الماضي ، والتي أعددناها
- تحليل المسار على الواجهة الأمامية في البطولة الأولى من هذا العام