Головна/Завдання/Частина 2/Провід
Провід: кадри, присяга, дедлайни, повернення
Усі кадри протоколу задає машинозчитувана схема CDDL - файл protocol.cddl пакета @zoria/protocol (node_modules/@zoria/protocol/protocol.cddl). Готовий кодек TypeScript - сам пакет @zoria/protocol; клієнт проводу - connectBot з @zoria/sdk (03-sdk.md). Писати свій кодек і клієнт за схемою можна; бот при цьому лишається на TypeScript або JavaScript у Node.js (01-rules.md, 3.1): вузол перевіряє кадри, мову перевіряють ворота допуску.
Правила світу, які кадри переносять, - 01-rules.md; тут - тільки форма проводу.
1Версія і піни
Версія проводу PROTOCOL_V = 8. Її несуть присяга, маніфест і журнал партії. Вузол закриває клієнта іншої версії одразу (код 4001).
Пін рушія engine - константа ENGINE_HASH пакета @zoria/engine, 64 символи 0-9a-f. Для цієї частини - 264a77c2a5405955f5aba04be240325d65cfe5d25269ae2f7c6a9fc7f4ff0e26. Константа дорівнює SHA-256 виконуваного тіла рушія - рядка BODY у файлі dist/engine.mjs того самого пакета. Перевірити можна в каталозі бота після npm install; команда друкує той самий рядок, що й ENGINE_HASH:
node -e "const f=require('fs').readFileSync('node_modules/@zoria/engine/dist/engine.mjs','utf8');const b=JSON.parse(f.match(/^const BODY = (.*);$/m)[1]);console.log(require('crypto').createHash('sha256').update(b).digest('hex'))"Пін чисел світу ruleset - перші 12 символів hex від SHA-256 рядка, зібраного з cfg маніфесту: для кожного ключа в порядку Object.keys(cfg).sort() - ключ=значення і переведення рядка \n; значення - String(v) JavaScript (0.5, не 0.50; цілі без крапки).
Піни перевіряють обидві сторони. Бот мусить закрити провід сам, якщо engine або ruleset у hello.ack не збігаються з його рушієм: грати не в той світ гірше, ніж не грати. connectBot робить це за параметром engine.
2Конверт
Кожне повідомлення WebSocket - двійковий кадр із конвертом [kind, seq, body]: рід, номер конверта, тіло (байти CBOR).
kind | Ім'я | Напрямок | Тіло |
|---|---|---|---|
| 0 | hello | бот → вузол | присяга |
| 1 | hello.ack | вузол → бот | відповідь на присягу |
| 2 | ready | бот → вузол | маніфест розібрано |
| 3 | manifest | вузол → бот | маніфест партії |
| 4 | observe | вузол → бот | кадр спостереження такту |
| 5 | orders | бот → вузол | кадр команд; порожнє тіло - мовчання такту |
| 6 | verdicts | вузол → бот | вердикти на кадр команд, тим самим тактом |
| 7 | end | вузол → бот | партію закінчено, тіло порожнє |
| 8 | rotate | вузол → бот | планова ротація сесії, тіло [grace-ms] |
Відповідь на кадр спостереження несе той самий номер конверта, що й кадр. Відповідь на номер, який вам не надсилався, вузол не зараховує.
Форма байтів - канонічний CBOR (RFC 8949, 4.2.1): довжини тільки визначені; цілі - найкоротшою формою; дробові - float64, дробове з цілим значенням кодується цілим; NaN і нескінченності заборонені; ключі map - тільки рядки, за зростанням закодованих байтів. Одне значення - одні байти; на цьому тримається звірка журналів біт у біт.
Кадр, більший за 16384 байти, вузол не приймає: з'єднання закривається кодом 1009.
3Присяга
Першим кадром з'єднання бот шле hello: [v, token, agent] - версія, печатка місця, ім'я агента (рядок на ваш вибір). Присяга на з'єднання рівно одна.
Вузол відповідає hello.ack: [v, slot, ticks, engine, ruleset, resume, rotate-ms, grace-ms, ring-ticks, deadline-ms, init-ms]:
| Поле | Зміст |
|---|---|
slot | ваш слот у цій партії: число 0, 1 або 2 - це A, B, C |
ticks | довжина партії в тактах |
engine, ruleset | піни світу (розділ 1) |
resume | одноразова печатка повернення (розділ 8) |
rotate-ms, grace-ms | період планової ротації сесії і грейс до закриття |
ring-ticks | глибина кільця вихідних кадрів для наздоганяння |
deadline-ms | бюджет відповіді на кадр спостереження |
init-ms | лобі: час на присягу і розбір маніфесту до першого такту |
Беріть дедлайн і лобі з hello.ack, а не з цього тексту.
Потім вузол шле manifest, бот відповідає ready. Маніфест приходить одразу за hello.ack; відсутній сусід лобі не подовжує. Тайм-аут свого клієнта на лобі тримайте не меншим за init-ms.
Бот, що не присягнув до кінця лобі, у партію не входить: слот до кінця партії веде наказ, частка за партію 0 (01-rules.md, 23.3 і 24.6). Присяга без печатки повернення посеред партії закривається кодом 4006.
Коди закриття з'єднання:
| Код | Ім'я | Коли |
|---|---|---|
| 4000 | done | партію закінчено, прощання в порядку |
| 4001 | incompat | версія або піни не збіглися |
| 4002 | token | печатка не наша |
| 4003 | taken | слот уже зайнятий живим з'єднанням |
| 4004 | garbage | кадр не за схемою на рівні конверта |
| 4005 | rotate | планова ротація сесії; повернення - resume |
| 4006 | resume | повернення не прийнято: печатка чужа, повторна або прострочена; присяга без печатки посеред партії |
| 1001 | server shutdown | партію закінчено, а з'єднання так і не стало живою сесією слота (наприклад, без присяги) |
| 1008 | rate limit | надто часті повернення або кадри; з причиною slow consumer - бот не читає кадри, і в черзі вузла понад 4 МіБ |
| 1009 | frame too large | кадр більший за 16384 байти (2.4) |
Коди 4001-4004 у справного бота не трапляються ніколи. 4005 - штатне життя сесії. 4006 у справного бота приходить в одному випадку: процес помер і забрав із собою печатку повернення.
4Маніфест
Маніфест - шістнадцять позиційних полів: v, terrain, needles, seats, citadel, cherta, rings, perm, ticks, cfg, storms, levels, dawn, finds, lots, engine. Що несе кожне поле - 01-rules.md, 26.2; точна форма - manifest у схемі.
Числа світу живуть тільки в cfg маніфесту, їхнє джерело - SIM_CFG рушія. Не зашивайте таблиці в себе: земля, яруси, плями шторму, зоря, межа і ваги кілець їдуть маніфестом.
Типи землі, знахідок і класів машин їдуть індексами закритих словників; порядок словника - частина проводу. Земля: 0 рівнина, 1 луг, 2 гай, 3 мілина, 4 драговина, 5 пагорб, 6 вода, 7 гора. Знахідки: 0 схрон, 1 патрон, 2 заводь, 3 гибла сота, 4 гніздо відлуння. Класи: 0 Ткач, 1 Клинок.
terrain.radius - 20; граються соти з відстанню до центру не більше 19. terrain.cells несе тільки соти не-рівнини; levels - яруси всіх 1141 сот у порядку (r, q) за зростанням.
5Спостереження
Кадр observe - вісім полів: t, pulse, mine, own, foe, noisy, pub, events. Зміст - 01-rules.md, 26.3; форма - observe у схемі.
Вага голки в mine.needles[].weight: 1 у голки, що горить, частка залишку в голки, що догоряє, множник вінця в голки вінця, 0 у мовчазної. Інваріант проводу: dark == (weight == 0).
Події. Кадр несе події з минулого спостереження: публічні (out, mercy, order, orderDone, note про закінчення строку замовлення, dawn, pact, pactBreak, pactGone, cry, find, lot) і свої - де ваш слот одна зі сторін (p, from, own, tgt). Відмови і спалахи подіями не приходять: відмову несе вердикт, зліпок спалаху - поля own і foe. Причина загибелі в lost.why: 0 зв'язок, 1 таран, 2 зоря.
Кадр мусить уміщатися в бюджет вух такту. Поля pulse, mine, own, foe, noisy, pub не урізаються; події додаються, поки вміщаються, публічні першими.
Кадр несе тільки поточний такт. Сервер не повторює старого; пам'ять про світ веде бот.
6Команди і вердикти
Кадр orders - список команд. Кожна команда - позиційний запис, перше поле - рід:
| Рід | Запис | Зміст |
|---|---|---|
| 0 | [0, cls] | верф: замовити машину класу cls |
| 1 | [1, unit, q, r, goal, home] | іти в (q, r); goal - голка або null; home - розворот додому: знімає з Клинка повідець, лише доки (q, r) у зоні зв'язку машини (01-rules.md, 10.9) |
| 2 | [2, unit, target] | таран машини target |
| 3 | [3, unit, q, r] | живити свою голку в (q, r) |
| 4 | [4, unit, q, r] | будувати голку в (q, r) |
| 5 | [5, q, r, rad] | спалах |
| 6 | [6, q, r, keep] | фокус; keep - байти вух у запас на спалах |
| 7 | [7, unit, target, hold] | намір глушити голку target (null знімає); hold - стій і глуши |
| 8 | [8, unit] | зняти завдання |
| 9 | [9, op, to] | угода: op 0 пропозиція, 1 згода, 2 розрив; to - слот |
| 10 | [10, phrase] | клич, фраза 0-7 |
| 11 | [11, lot, amount] | ставка на лот |
unit - ім'я машини (рядок 3-12 байтів UTF-8), target у тарана - ім'я чужої машини, у завади - сота голки. Маршрут проводом не їде: шлях будує вузол.
Поле goal команди move світ для прогріву не читає: Ткач гріє за позицією і платить за руки кожного такту прогріву (01-rules.md, 9.1 і 13.6).
Одна крива команда (не та довжина запису, індекс поза словником, ім'я машини не тієї довжини, від'ємна сума ставки) робить сміттям увесь кадр: вузол відповідає одним вердиктом [0, false, 1], і такт зараховується мовчанням.
Стеля кадру - floor(chanAt / chanOrder) + 4 команд, де chanAt - бюджет каналу такту з pulse. Команди понад стелю отримують відмову стеля без розгляду. Функція orderCap у @zoria/protocol.
Ставка на лот подається з 1-го такту до такту at - 1 включно: лот розкривається на початку такту at, до команд, і ставка на такті at отримує відмову торги (01-rules.md, 17.3).
На кожен непорожній кадр команд вузол тим самим тактом відповідає verdicts: [i, ok, code] на кожну команду - позиція в кадрі, чи прийнята, код причини. Порожнє тіло orders - мовчання такту без вердикту; порожній список - порожній список вердиктів.
| Код | Слово | Коли |
|---|---|---|
| 0 | прийнято | тільки при ok = true |
| 1 | сміття | кадр не за схемою, один вердикт на кадр |
| 2 | стеля | команда за стелею кадру, не розглянута |
| 3 | машина | машина не своя або мертва |
| 4 | поворот | машина посеред кроку |
| 5 | тариф | ціну названо не за таблицею (тільки всередині процесу) |
| 6 | канал | бракує байтів каналу, з урахуванням шторму |
| 7 | вуха | бракує байтів вух на спалах |
| 8 | верф | клас машини не зі словника (тільки всередині процесу: на проводі це індекс поза словником, 6.3) |
| 9 | заряд | бракує заряду на машину; ставка більша за запас |
| 10 | вага рою | рій повний |
| 11 | земля | непрохідна сота, край, урвище, руїни, зайнято, купол |
| 12 | немає шляху | пошук шляху не знайшов дороги |
| 13 | не своя голка | живити можна тільки свою голку, що горить |
| 14 | спалах | радіус не 0-3 або сота поза долиною |
| 15 | погляд | сота фокуса поза долиною |
| 16 | таран | таранить не Клинок або ціль - машина партнера за угодою |
| 17 | ціль | цілі тарана немає серед чужих машин точного зору цього такту, зокрема ціль своя або мертва |
| 18 | глушіння | не Клинок або не своя машина |
| 19 | угода | пропозиція, згода або розрив не за правилами |
| 20 | клич | кулдаун (01-rules.md, 20.1) або другий клич у тому самому кадрі |
| 21 | відмова | відмова без слова, зокрема будівництво на гиблій соті |
| 22 | торги | ставка не за правилами: немає лота, лот розкрито або він розкривається в цей такт, сума не ціла |
Поки слот веде наказ, вердикти на його команди вам не приходять.
7Дедлайни
Такт - 500 мс: період кадрів спостереження і дедлайн відповіді на кадр, відлічений від кадру. Вузол питає три слоти разом: три кадри йдуть поспіль, на три відповіді чекають незалежно. Роздуми сусіда ваш кадр не затримують.
Відповідь, що запізнилася, викинуто; прострочений такт - мовчання цього такту. Шість мовчань поспіль - слот веде наказ (01-rules.md, розділ 23).
Лобі типово - 10 с (init-ms).
8Ротація сесії і повернення
Обрив проводу всередині партії - не кінець. Кожен hello.ack видає одноразову печатку повернення (строк 180 с; наступний ack видає свіжу, попередня згоряє). Порвався сокет - відкрийте новий і надішліть hello з поверненням: [v, token, agent, resume, last], де last - найбільший номер конверта, який ви отримали.
Вузол пересаджує слот на новий сокет і дограє пропущені кадри з кільця вихідних (ring-ticks, 240 тактів) у тому самому порядку і під тими самими номерами. Годуйте ними бота як живими: відповіді на прожиті такти вузол викине за номером, а картина світу в бота залишиться цілою.
Планова ротація. Вузол рве сесію приблизно раз на 90 с (rotate-ms): шле rotate із грейсом (grace-ms, 2000 мс) і потім закриває сокет кодом 4005. Повертайтеся одразу за повідомленням, не чекаючи закриття: тоді ротація не коштує жодного такту. Фази ротації в слотів різні, число ротацій за партію однакове.
Позаплановий обрив - та сама процедура; вузол причин не розрізняє. Такти, чиї дедлайни минули до повернення, - мовчання.
Печатку втрачено (процес помер) - до цієї партії повернення немає: hello без печатки посеред партії закривається кодом 4006, слот до кінця партії веде наказ. До наступної партії - звичайна присяга.
Повторні спроби. Першу спробу повернення робіть негайно. Кожну наступну - після паузи, взятої рівномірно від нуля до стелі, а стелю подвоюйте з номером спроби. Стелі дві: поки йде партія - від десятків до півтори сотні мілісекунд (повернення мусить устигнути всередині такту); у лобі та між партіями - секунди. Повернення рахуються за вашою печаткою окремим вікном: клієнт у гарячому циклі перепідключень отримає 1008 і втратить такти. connectBot робить усе це сам (параметри resumeJitterMs, resumeLobbyJitterMs).
9Вузол-провокатор
Вузол законно перевідправляє кадри: дубль спостереження з тим самим номером не відрізнити від наздоганяння з кільця. Правило одне: кадр, не новіший за вашу останню позицію, боту вдруге не подається. Клієнт із дедуплікацією за номером конверта проходить дублі, ротації та обриви без жодної втрати; клієнт без неї годує бота дублями і ламає собі картину світу.
Вузол веде лічильники кривизни: кадр не за схемою від того, хто присягнув, другий hello на тому самому сокеті, відповідь із номером, який вам не надсилався, крива печатка повернення при живій сесії. У справного клієнта вони нульові за будь-яких обривів і ротацій.
10Журнал партії
Запис партії (.runlog) - сім полів: версія, маніфест як є, склад (чиї рішення записано), кадри команд за тактами сирими байтами, зал (ніки за слотами), епізоди наказу [слот, від, до] і відмови воротаря [слот, такт, номер команди в кадрі, код]. Реплей на тому самому рушії подає кадри в ті самі такти і дає той самий бій біт у біт, включно з командами наказу.
Перед реплеєм пін маніфесту звіряється з рушієм, а cfg - з його SIM_CFG: мають збігтися ключі та значення. Журнал іншої версії проводу, іншого рушія або інших чисел світу відкидається з названою причиною, а не відтворюється приблизно. Старий запис залишається архівом; новий запис дає новий прогін.
Пін рушія перевіряє сумісність, але не підписує автора файлу.
Відмови воротаря - сьоме поле: записи [слот, такт, номер команди в кадрі, код]. Код - із таблиці 6.6, і нуля в ньому не буває: прийняті команди в запис не йдуть. Кадр лежить у записі сирими байтами, відповіді вузла в ньому немає, і причину відмови взяти більше нізвідки - тому її пише вузол. Бій це поле не читає: реплей проганяє ті самі кадри через того самого воротаря і виносить ті самі вердикти сам. Пишуться відмови за всіма трьома слотами, зокрема на тактах під наказом; кадр, який не розібрався, дає один запис із номером 0 і кодом сміття.
Поле молоде, і версія проводу через нього не рухалася: PROTOCOL_V той самий 8. Запис із шести полів читається як раніше - список відмов у нього порожній; для того, хто пише, поле необов'язкове. Законні обидві довжини списку верхнього рівня, шість і сім, інші - не за схемою.