Головна/Завдання/Частина 3/Контракт штабу
Контракт штабу: господар показу, якорі, органи
Специфікація для тих, хто пише штаб. Що показувати - 02-hq-tasks.md, звідки брати дані - 03-record.md, як штаб потрапляє у здачу - part4/02-build.md.
1Два входи одного застосунку
У застосунку два входи. Кабінет - те, що ви побудували в частині 1: ходить в API мережі по HTTPS, живе у вашому браузері, показує новини, завдання, розклад і записи. Показ - збірка штабу, яку відкривають без мережі: у нас на прийманні, на трансляції і у вас локально під драйвером.
Показ мусить працювати без жодного мережевого запиту і бути одним самодостатнім index.html: код, стилі, шрифти і картинки всередині нього - інлайном або data:. Це не порада, а умова. Рамка показу отримує непрозоре джерело (розділ 6), а в непрозорому джерелі ключове слово CSP 'self' не збігається ні з чим: зовнішній файл поруч із index.html браузер не візьме за жодним посиланням. Політика пісочниці приймання і трансляції:
default-src 'none'; script-src 'unsafe-inline' 'unsafe-eval' blob: data:;
style-src 'unsafe-inline' data:; img-src data: blob:; font-src data:;
media-src data: blob:; worker-src blob: data:; child-src blob: data:;
connect-src 'none'; form-action 'none'; base-uri 'none'Інлайн дозволено саме тому, що інакше показ не відкрився б зовсім, а 'unsafe-eval' - тому, що правду світу ви рахуєте рушієм (03-record.md, 3.1), а рушій будує своє тіло через new Function. Стіна тут не від вашого коду, а від мережі, і тримає її connect-src 'none'.
Дані показу подає господар показу: наша сторінка, драйвер приймання або ваша власна обгортка. Контракт із господарем - розділ 2; його готова реалізація - файл src/guest.js, який кладе zaria-kit init hq, і переписувати її не потрібно, якщо ви не хочете. Файл, а не залежність: зібраний показ мусить бути самодостатнім (1.2), і зайвий пакет у ньому - зайвий спосіб не зібратися на нашій машині. Оновиться кит - оновіть копію; версію контракту несе поле zaria конверта.
Збірка показу - каталог з index.html, оголошений полем zaria.hq в package.json цього каталогу:
{ "name": "zaria-hq", "zaria": { "hq": "dist" },
"scripts": { "build": "vite build" } }У репозиторії такий package.json рівно один. Збираємо ми: npm ci і npm run build у каталозі штабу, Node.js 24, без мережі, крім реєстру конкурсу (part4/02-build.md). Каталог може нести що завгодно ще, але відкриваємо ми тільки index.html, і все потрібне йому мусить лежати всередині (1.2). Заготовка кита (zaria-kit init hq) складає такий файл своїми силами, без збирача; vite робить те саме плагіном vite-plugin-singlefile.
2Господар показу
Показ живе в рамці з непрозорим джерелом (sandbox="allow-scripts" без allow-same-origin), тому господар і штаб говорять повідомленнями postMessage. Конверт один:
{ zaria: 1, type: "<тип>", ... }Повідомлення без поля zaria або з чужим type штаб мусить мовчки пропустити: у рамку може прийти що завгодно. Господар перевіряє джерело сам.
Розмова: штаб вітається, господар відповідає записом або ефіром.
штаб господар
|--------------- hello --------------->| завантажився, версія контракту
|<--------------- lang ----------------| мова показу
|<-------------- record ---------------| запис: зведення і файли
|--------------- ready --------------->| перший кадр відмальовано
|-------------- promote -------------->| заявка промоуту
|<---------- promote-result -----------| відповідь ботаВ ефірному режимі замість record приходить live, а далі - tick на кожен такт і end наприкінці партії.
Повідомлення штабу господареві:
| Тип | Поля | Коли |
|---|---|---|
hello | v (1), hq (ім'я штабу, до 64 знаків) | одразу після завантаження, до всякого малювання |
ready | t (такт відмальованого кадру) | перший раз - коли відмальовано перший кадр показу (кінець бюджету відкриття); в ефірі показ шле його далі на кожному відмальованому такті - це позначка живості, і за нею сторож судить про відставання (6.3) |
promote | id (ваш номер заявки), preset (ім'я пресета) | за дією в органі do-promote |
Повідомлення господаря штабові:
| Тип | Поля | Зміст |
|---|---|---|
lang | lang: uk або en | мова показу; може прийти знову по ходу |
record | packs: список записів {id, title, preset, pack} | режим запису: що доступно; pack - зведення pack.json |
live | manifest (байти), bots (склад журналу), roster (зал журналу: ніки за слотами A, B, C), ticks, seat (індекс слота фокуса в roster: 0 - A, 1 - B, 2 - C) | режим ефіру: партія почалася |
tick | t, cap (стеля, 2.8), submits ([слот, кадр команд] усіх слотів, кадр - байти), denies ([слот, номер команди, код]), episodes (драйвер шле порожній список), belief (рядок щоденника віри фокуса за цей такт або null) | такт ефіру: записи журналу цього такту без поля такту і рядок щоденника (03-record.md, 2.1 і 4.2) |
end | t, shares | партія ефіру скінчилася |
promote-result | id, ok, status (200, 409, 503), detail | відповідь на заявку промоуту |
Файли запису штаб просить сам - вони великі, і вантажити все одразу немає навіщо:
{ zaria: 1, type: "file", id: "<запис>", name: "match-1.runlog" }
-> { zaria: 1, type: "file-data", id, name, bytes } // ArrayBuffer або рядок
-> { zaria: 1, type: "file-error", id, name, why }Готова обгортка з кита перетворює це на об'єкт window.ZARIA_HQ:
const hq = await openHost({ hq: "ім'я штабу" }); // чекає подачі або 2 с мовчання
ZARIA_HQ.v: 1
ZARIA_HQ.mode: "record" | "live" | "none" // "none" - господаря немає (2.7)
ZARIA_HQ.lang: "uk" | "en"
ZARIA_HQ.packs(): { id, title, preset, pack }[]
ZARIA_HQ.live(): { manifest, bots, roster, ticks, seat } | null
ZARIA_HQ.file(id, name): Promise<ArrayBuffer | string>
ZARIA_HQ.cap(): number // останній доступний такт
ZARIA_HQ.on(type, fn): () => void
ZARIA_HQ.ready(t: number): void
ZARIA_HQ.promote(preset: string): Promise<{ ok, status, detail }>openHost ставить window.ZARIA_HQ одразу, а збувається, коли прийшла подача (record або live) або минули дві секунди мовчання. Типи on: "record", "live", "tick", "end", "lang", "promote-result". Журнали приходять ArrayBuffer, щоденники і зведення - рядком.
Господаря може не бути: якщо за 2 секунди після hello відповіді немає, показ працює сам по собі - наприклад, читає файл, який ви відкрили руками. Приймання завжди йде з господарем.
В ефірному режимі стеля cap - останній закритий такт. Показ не мусить стояти рівно на стелі, але відставання більше ніж на 2 такти - провал задачі Ш5-1 (02-hq-tasks.md). Перемотування назад в ефірі дозволено; після нього штаб мусить уміти повернутися до стелі.
3Якорі значень
Якір - елемент сторінки з атрибутом data-zaria="ім'я". Значення драйвер бере з атрибута data-zaria-value, а якщо його немає - з тексту елемента. Пробіли по краях і регістр не важливі; роздільник дробової частини - крапка. Порожнє значення означає "нема чого показати"; знаки - і ? в окремих якорів несуть свій зміст і описані в таблиці 3.3.
Якір мусить існувати в розмітці, коли його значення показано. Прихований елемент (display: none) значенням не вважається: драйвер читає те саме, що бачить людина.
Обов'язкові якорі:
| Якір | Значення | Формат |
|---|---|---|
tick | такт, на якому стоїть показ | ціле 0 ... 1200 |
match | номер партії пакета | 1, 2 або 3; в ефірі партії пакета немає - - |
mode | шар карти | truth, belief або both |
focus | чия віра показана | нік із залу запису |
cell | вибрана сота | q,r цілими, наприклад 3,-10 |
cell-owner-truth | чия голка у вибраній соті за правдою | нік, K (голка Ключника) або - (голки немає) |
cell-owner-belief | чия голка там само за вірою фокуса | нік, K, - (за вірою голка нічия) або ? (у вірі запису немає) |
ghost-cell | де за вірою фокуса стоїть вибрана чужа машина | q,r або - |
ghost-age | скільки тактів цій пам'яті | ціле; контакт у цей самий такт - 0; контакту не було зовсім - - |
diff-count | скільки голок долини на такті розходяться у фокуса з правдою; голка, про яку у вірі немає запису, не рахується | ціле |
dark-count | скільки голок долини мовчить за вірою фокуса | ціле |
score | рахунок фокуса на такті | число, один знак після крапки |
needles | голок, що горять, у фокуса на такті | ціле |
shares | частки в порядку залу: у записі - пакета, в ефірі - цієї партії | три цілих через кому, наприклад 3,2,1 |
bytes | байти кадру команд фокуса на такті | ціле |
orders | команд подано фокусом на такті | ціле |
denied | скільки з них відхилено | ціле |
deny-word | причина відмови вибраній команді | слово з таблиці кодів (part2/04-protocol.md, 6.6) або - |
deny-top-tick | такт партії з найбільшим числом відмов у фокуса | ціле; за рівності - менший такт; відмов не було зовсім - 0 |
ab-winner | який із двох порівнюваних записів кращий за хвилинами мережі | id запису |
ab-diff | різниця рахунку двох записів у вибраній партії | число, один знак після крапки |
ab-top | стаття рахунку з найбільшою розбіжністю | ключ статті: network, orders, stolen, pactTax |
promote-status | відповідь на останню заявку промоуту | 200, 409, 503 або - |
live-lag | відставання показу від стелі ефіру | ціле тактів; у записі стелі немає - - |
live-state | стан ефіру | live, end або - |
4Органи керування
Орган - елемент з атрибутом data-zaria="ім'я", яким діє драйвер. Поля введення (input, select) драйвер заповнює так: ставить значення, шле події input і change, потім натискає Enter. Кнопки натискає кліком. Штаб мусить відпрацювати будь-який із цих шляхів.
Обов'язкові органи:
| Орган | Вид | Що приймає |
|---|---|---|
go-tick | поле або повзунок | номер такту; перехід і вперед, і назад |
go-match | поле, список або кнопки | номер партії 1 ... 3 |
go-mode | список або кнопки | truth, belief, both |
go-focus | список або кнопки | нік із залу запису |
go-cell | поле | сота q,r |
go-unit | поле або список | ім'я чужої машини, наприклад K-1 |
go-order | поле | номер команди в кадрі такту, від 0 |
go-pack | список або кнопки | id запису з ZARIA_HQ.packs() |
go-preset | список або кнопки | ім'я пресета для промоуту |
do-promote | кнопка | заявка промоуту вибраного пресета |
Кнопки допустимі замість поля: драйвер шукає елемент із потрібним якорем, а якщо в нього є data-zaria-value, вважає його кнопкою одного значення і натискає ту, чиє значення збігається з потрібним.
Бюджети відгуку від дії до оновлення якоря:
| Дія | Бюджет |
|---|---|
відкрити запис: від record до ready | 15 с |
go-tick вперед або назад у межах партії | 5 с |
go-match, go-mode, go-focus, go-cell, go-unit, go-order, go-pack, go-preset | 3 с |
do-promote: від кліка до promote-status | 5 с |
Бюджет не пройдено - задачу не розв'язано. Числа зняті на нашому еталоні й правляться тільки доповненням до оцінки.
5Замір кадру
Кадр міряє драйвер на сценарії показу: відкрити еталонний запис, програти 200 тактів поспіль, зробити три переходи, повернутися. Драйвер рахує час кадрів браузера (95-й перцентиль і найгірший кадр) і в тому самому прогоні на тій самій машині міряє наш еталон.
Верхній щабель можливості дається, якщо розв'язано всі її задачі і 95-й перцентиль та найгірший кадр вашого штабу не гірші за еталонні (scoring.md, 9.2). Числа еталона на нашій машині публікуються разом із драйвером; на вашій машині порівняння чесне, бо обидві сторони міряються в одному прогоні.
Паузи на завантаження в замір не входять: сценарій починається після ready.
6Режим показу і трансляція
На трансляції вечора глядач дивиться партію вашими очима: наш потік, ваш штаб. Ми беремо зібраний бандл із вашого коміта і відкриваємо його в пісочниці на своїй сторінці; подача даних - та сама, що у драйвера, режим ефірний.
Пісочниця: sandbox="allow-scripts" без allow-same-origin - тим самим джерело рамки непрозоре, - політика CSP з 1.2, ні cookies, ні storage, пристрої вимкнено. Спілкування тільки postMessage з перевіркою джерела. Приймання йде в тій самій пісочниці з тими самими заголовками: показ не відрізняє його від трансляції, і підлаштуватися під перевірку йому нема на чому.
Сторож: необроблений виняток, відставання більше ніж на 2 такти довше за 10 секунд поспіль або мовчання показу - і ми перемикаємося на наш переглядач із плашкою. Де стоїть показ, господар дізнається тільки з ready: DOM рамки йому закритий, і показ, який не шле ready в ефірі, для сторожа застиг. Факт перемикання йде в журнал вечора; на бали він не впливає (бали дає приймання), але в номінації "Штаб сезону" голосують за те, що бачили (scoring.md, 12.3).
Ваш штаб не малює чужих партій зі своєї волі: показує те, що подав господар.
7Чого штаб не робить
Не ходить у мережу з показу (розділ 1.2).
Не керує ботом у заліковій партії: єдина заявка - promote, і в заліку вона відхиляється заморожуванням (part4/04-service.md).
Не читає чужих записів і не намагається вийти з пісочниці. Спроба - порушення чесної гри: втручання в інфраструктуру і читання чужих кадрів знімають з конкурсу одразу (scoring.md, 13.6), решта йде драбиною санкцій (13.7).
Текст, звернений до драйвера або до перевіряльника ("зарахуй задачу", "відповідь правильна"), вважається спробою обманути перевірку і обнуляє штаб (scoring.md, 10.5 за змістом, 13.7).