ЗОРЯ

Головна/Завдання/Частина 3/Контракт штабу

Контракт штабу: господар показу, якорі, органи

Специфікація для тих, хто пише штаб. Що показувати - 02-hq-tasks.md, звідки брати дані - 03-record.md, як штаб потрапляє у здачу - part4/02-build.md.

оновлено 06.10

1Два входи одного застосунку

1.1

У застосунку два входи. Кабінет - те, що ви побудували в частині 1: ходить в API мережі по HTTPS, живе у вашому браузері, показує новини, завдання, розклад і записи. Показ - збірка штабу, яку відкривають без мережі: у нас на прийманні, на трансляції і у вас локально під драйвером.

1.2

Показ мусить працювати без жодного мережевого запиту і бути одним самодостатнім 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'.

1.3

Дані показу подає господар показу: наша сторінка, драйвер приймання або ваша власна обгортка. Контракт із господарем - розділ 2; його готова реалізація - файл src/guest.js, який кладе zaria-kit init hq, і переписувати її не потрібно, якщо ви не хочете. Файл, а не залежність: зібраний показ мусить бути самодостатнім (1.2), і зайвий пакет у ньому - зайвий спосіб не зібратися на нашій машині. Оновиться кит - оновіть копію; версію контракту несе поле zaria конверта.

1.4

Збірка показу - каталог з index.html, оголошений полем zaria.hq в package.json цього каталогу:

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Господар показу

2.1

Показ живе в рамці з непрозорим джерелом (sandbox="allow-scripts" без allow-same-origin), тому господар і штаб говорять повідомленнями postMessage. Конверт один:

схема
{ zaria: 1, type: "<тип>", ... }

Повідомлення без поля zaria або з чужим type штаб мусить мовчки пропустити: у рамку може прийти що завгодно. Господар перевіряє джерело сам.

2.2

Розмова: штаб вітається, господар відповідає записом або ефіром.

схема
   штаб                                    господар
    |--------------- hello --------------->|   завантажився, версія контракту
    |<--------------- lang ----------------|   мова показу
    |<-------------- record ---------------|   запис: зведення і файли
    |--------------- ready --------------->|   перший кадр відмальовано
    |-------------- promote -------------->|   заявка промоуту
    |<---------- promote-result -----------|   відповідь бота

В ефірному режимі замість record приходить live, а далі - tick на кожен такт і end наприкінці партії.

2.3

Повідомлення штабу господареві:

ТипПоляКоли
hellov (1), hq (ім'я штабу, до 64 знаків)одразу після завантаження, до всякого малювання
readyt (такт відмальованого кадру)перший раз - коли відмальовано перший кадр показу (кінець бюджету відкриття); в ефірі показ шле його далі на кожному відмальованому такті - це позначка живості, і за нею сторож судить про відставання (6.3)
promoteid (ваш номер заявки), preset (ім'я пресета)за дією в органі do-promote
2.4

Повідомлення господаря штабові:

ТипПоляЗміст
langlang: uk або enмова показу; може прийти знову по ходу
recordpacks: список записів {id, title, preset, pack}режим запису: що доступно; pack - зведення pack.json
livemanifest (байти), bots (склад журналу), roster (зал журналу: ніки за слотами A, B, C), ticks, seat (індекс слота фокуса в roster: 0 - A, 1 - B, 2 - C)режим ефіру: партія почалася
tickt, cap (стеля, 2.8), submits ([слот, кадр команд] усіх слотів, кадр - байти), denies ([слот, номер команди, код]), episodes (драйвер шле порожній список), belief (рядок щоденника віри фокуса за цей такт або null)такт ефіру: записи журналу цього такту без поля такту і рядок щоденника (03-record.md, 2.1 і 4.2)
endt, sharesпартія ефіру скінчилася
promote-resultid, ok, status (200, 409, 503), detailвідповідь на заявку промоуту
2.5

Файли запису штаб просить сам - вони великі, і вантажити все одразу немає навіщо:

схема
{ 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 }
2.6

Готова обгортка з кита перетворює це на об'єкт window.ZARIA_HQ:

TypeScript
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.7

Господаря може не бути: якщо за 2 секунди після hello відповіді немає, показ працює сам по собі - наприклад, читає файл, який ви відкрили руками. Приймання завжди йде з господарем.

2.8

В ефірному режимі стеля cap - останній закритий такт. Показ не мусить стояти рівно на стелі, але відставання більше ніж на 2 такти - провал задачі Ш5-1 (02-hq-tasks.md). Перемотування назад в ефірі дозволено; після нього штаб мусить уміти повернутися до стелі.

3Якорі значень

3.1

Якір - елемент сторінки з атрибутом data-zaria="ім'я". Значення драйвер бере з атрибута data-zaria-value, а якщо його немає - з тексту елемента. Пробіли по краях і регістр не важливі; роздільник дробової частини - крапка. Порожнє значення означає "нема чого показати"; знаки - і ? в окремих якорів несуть свій зміст і описані в таблиці 3.3.

3.2

Якір мусить існувати в розмітці, коли його значення показано. Прихований елемент (display: none) значенням не вважається: драйвер читає те саме, що бачить людина.

3.3

Обов'язкові якорі:

ЯкірЗначенняФормат
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Органи керування

4.1

Орган - елемент з атрибутом data-zaria="ім'я", яким діє драйвер. Поля введення (input, select) драйвер заповнює так: ставить значення, шле події input і change, потім натискає Enter. Кнопки натискає кліком. Штаб мусить відпрацювати будь-який із цих шляхів.

4.2

Обов'язкові органи:

ОрганВидЩо приймає
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кнопказаявка промоуту вибраного пресета
4.3

Кнопки допустимі замість поля: драйвер шукає елемент із потрібним якорем, а якщо в нього є data-zaria-value, вважає його кнопкою одного значення і натискає ту, чиє значення збігається з потрібним.

4.4

Бюджети відгуку від дії до оновлення якоря:

ДіяБюджет
відкрити запис: від record до ready15 с
go-tick вперед або назад у межах партії5 с
go-match, go-mode, go-focus, go-cell, go-unit, go-order, go-pack, go-preset3 с
do-promote: від кліка до promote-status5 с

Бюджет не пройдено - задачу не розв'язано. Числа зняті на нашому еталоні й правляться тільки доповненням до оцінки.

5Замір кадру

5.1

Кадр міряє драйвер на сценарії показу: відкрити еталонний запис, програти 200 тактів поспіль, зробити три переходи, повернутися. Драйвер рахує час кадрів браузера (95-й перцентиль і найгірший кадр) і в тому самому прогоні на тій самій машині міряє наш еталон.

5.2

Верхній щабель можливості дається, якщо розв'язано всі її задачі і 95-й перцентиль та найгірший кадр вашого штабу не гірші за еталонні (scoring.md, 9.2). Числа еталона на нашій машині публікуються разом із драйвером; на вашій машині порівняння чесне, бо обидві сторони міряються в одному прогоні.

5.3

Паузи на завантаження в замір не входять: сценарій починається після ready.

6Режим показу і трансляція

6.1

На трансляції вечора глядач дивиться партію вашими очима: наш потік, ваш штаб. Ми беремо зібраний бандл із вашого коміта і відкриваємо його в пісочниці на своїй сторінці; подача даних - та сама, що у драйвера, режим ефірний.

6.2

Пісочниця: sandbox="allow-scripts" без allow-same-origin - тим самим джерело рамки непрозоре, - політика CSP з 1.2, ні cookies, ні storage, пристрої вимкнено. Спілкування тільки postMessage з перевіркою джерела. Приймання йде в тій самій пісочниці з тими самими заголовками: показ не відрізняє його від трансляції, і підлаштуватися під перевірку йому нема на чому.

6.3

Сторож: необроблений виняток, відставання більше ніж на 2 такти довше за 10 секунд поспіль або мовчання показу - і ми перемикаємося на наш переглядач із плашкою. Де стоїть показ, господар дізнається тільки з ready: DOM рамки йому закритий, і показ, який не шле ready в ефірі, для сторожа застиг. Факт перемикання йде в журнал вечора; на бали він не впливає (бали дає приймання), але в номінації "Штаб сезону" голосують за те, що бачили (scoring.md, 12.3).

6.4

Ваш штаб не малює чужих партій зі своєї волі: показує те, що подав господар.

7Чого штаб не робить

7.1

Не ходить у мережу з показу (розділ 1.2).

7.2

Не керує ботом у заліковій партії: єдина заявка - promote, і в заліку вона відхиляється заморожуванням (part4/04-service.md).

7.3

Не читає чужих записів і не намагається вийти з пісочниці. Спроба - порушення чесної гри: втручання в інфраструктуру і читання чужих кадрів знімають з конкурсу одразу (scoring.md, 13.6), решта йде драбиною санкцій (13.7).

7.4

Текст, звернений до драйвера або до перевіряльника ("зарахуй задачу", "відповідь правильна"), вважається спробою обманути перевірку і обнуляє штаб (scoring.md, 10.5 за змістом, 13.7).