ЗОРЯ

Головна/Завдання/Частина 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): вузол перевіряє кадри, мову перевіряють ворота допуску.

опубліковано 28.09

Правила світу, які кадри переносять, - 01-rules.md; тут - тільки форма проводу.

1Версія і піни

1.1

Версія проводу PROTOCOL_V = 8. Її несуть присяга, маніфест і журнал партії. Вузол закриває клієнта іншої версії одразу (код 4001).

1.2

Пін рушія engine - константа ENGINE_HASH пакета @zoria/engine, 64 символи 0-9a-f. Для цієї частини - 264a77c2a5405955f5aba04be240325d65cfe5d25269ae2f7c6a9fc7f4ff0e26. Константа дорівнює SHA-256 виконуваного тіла рушія - рядка BODY у файлі dist/engine.mjs того самого пакета. Перевірити можна в каталозі бота після npm install; команда друкує той самий рядок, що й ENGINE_HASH:

shell
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'))"
1.3

Пін чисел світу ruleset - перші 12 символів hex від SHA-256 рядка, зібраного з cfg маніфесту: для кожного ключа в порядку Object.keys(cfg).sort() - ключ=значення і переведення рядка \n; значення - String(v) JavaScript (0.5, не 0.50; цілі без крапки).

1.4

Піни перевіряють обидві сторони. Бот мусить закрити провід сам, якщо engine або ruleset у hello.ack не збігаються з його рушієм: грати не в той світ гірше, ніж не грати. connectBot робить це за параметром engine.

2Конверт

2.1

Кожне повідомлення WebSocket - двійковий кадр із конвертом [kind, seq, body]: рід, номер конверта, тіло (байти CBOR).

kindІм'яНапрямокТіло
0helloбот → вузолприсяга
1hello.ackвузол → ботвідповідь на присягу
2readyбот → вузолманіфест розібрано
3manifestвузол → ботманіфест партії
4observeвузол → боткадр спостереження такту
5ordersбот → вузолкадр команд; порожнє тіло - мовчання такту
6verdictsвузол → ботвердикти на кадр команд, тим самим тактом
7endвузол → ботпартію закінчено, тіло порожнє
8rotateвузол → ботпланова ротація сесії, тіло [grace-ms]
2.2

Відповідь на кадр спостереження несе той самий номер конверта, що й кадр. Відповідь на номер, який вам не надсилався, вузол не зараховує.

2.3

Форма байтів - канонічний CBOR (RFC 8949, 4.2.1): довжини тільки визначені; цілі - найкоротшою формою; дробові - float64, дробове з цілим значенням кодується цілим; NaN і нескінченності заборонені; ключі map - тільки рядки, за зростанням закодованих байтів. Одне значення - одні байти; на цьому тримається звірка журналів біт у біт.

2.4

Кадр, більший за 16384 байти, вузол не приймає: з'єднання закривається кодом 1009.

3Присяга

3.1

Першим кадром з'єднання бот шле hello: [v, token, agent] - версія, печатка місця, ім'я агента (рядок на ваш вибір). Присяга на з'єднання рівно одна.

3.2

Вузол відповідає 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, а не з цього тексту.

3.3

Потім вузол шле manifest, бот відповідає ready. Маніфест приходить одразу за hello.ack; відсутній сусід лобі не подовжує. Тайм-аут свого клієнта на лобі тримайте не меншим за init-ms.

3.4

Бот, що не присягнув до кінця лобі, у партію не входить: слот до кінця партії веде наказ, частка за партію 0 (01-rules.md, 23.3 і 24.6). Присяга без печатки повернення посеред партії закривається кодом 4006.

3.5

Коди закриття з'єднання:

КодІм'яКоли
4000doneпартію закінчено, прощання в порядку
4001incompatверсія або піни не збіглися
4002tokenпечатка не наша
4003takenслот уже зайнятий живим з'єднанням
4004garbageкадр не за схемою на рівні конверта
4005rotateпланова ротація сесії; повернення - resume
4006resumeповернення не прийнято: печатка чужа, повторна або прострочена; присяга без печатки посеред партії
1001server shutdownпартію закінчено, а з'єднання так і не стало живою сесією слота (наприклад, без присяги)
1008rate limitнадто часті повернення або кадри; з причиною slow consumer - бот не читає кадри, і в черзі вузла понад 4 МіБ
1009frame too largeкадр більший за 16384 байти (2.4)

Коди 4001-4004 у справного бота не трапляються ніколи. 4005 - штатне життя сесії. 4006 у справного бота приходить в одному випадку: процес помер і забрав із собою печатку повернення.

4Маніфест

4.1

Маніфест - шістнадцять позиційних полів: v, terrain, needles, seats, citadel, cherta, rings, perm, ticks, cfg, storms, levels, dawn, finds, lots, engine. Що несе кожне поле - 01-rules.md, 26.2; точна форма - manifest у схемі.

4.2

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

4.3

Типи землі, знахідок і класів машин їдуть індексами закритих словників; порядок словника - частина проводу. Земля: 0 рівнина, 1 луг, 2 гай, 3 мілина, 4 драговина, 5 пагорб, 6 вода, 7 гора. Знахідки: 0 схрон, 1 патрон, 2 заводь, 3 гибла сота, 4 гніздо відлуння. Класи: 0 Ткач, 1 Клинок.

4.4

terrain.radius - 20; граються соти з відстанню до центру не більше 19. terrain.cells несе тільки соти не-рівнини; levels - яруси всіх 1141 сот у порядку (r, q) за зростанням.

5Спостереження

5.1

Кадр observe - вісім полів: t, pulse, mine, own, foe, noisy, pub, events. Зміст - 01-rules.md, 26.3; форма - observe у схемі.

5.2

Вага голки в mine.needles[].weight: 1 у голки, що горить, частка залишку в голки, що догоряє, множник вінця в голки вінця, 0 у мовчазної. Інваріант проводу: dark == (weight == 0).

5.3

Події. Кадр несе події з минулого спостереження: публічні (out, mercy, order, orderDone, note про закінчення строку замовлення, dawn, pact, pactBreak, pactGone, cry, find, lot) і свої - де ваш слот одна зі сторін (p, from, own, tgt). Відмови і спалахи подіями не приходять: відмову несе вердикт, зліпок спалаху - поля own і foe. Причина загибелі в lost.why: 0 зв'язок, 1 таран, 2 зоря.

5.4

Кадр мусить уміщатися в бюджет вух такту. Поля pulse, mine, own, foe, noisy, pub не урізаються; події додаються, поки вміщаються, публічні першими.

5.5

Кадр несе тільки поточний такт. Сервер не повторює старого; пам'ять про світ веде бот.

6Команди і вердикти

6.1

Кадр 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 у тарана - ім'я чужої машини, у завади - сота голки. Маршрут проводом не їде: шлях будує вузол.

6.2

Поле goal команди move світ для прогріву не читає: Ткач гріє за позицією і платить за руки кожного такту прогріву (01-rules.md, 9.1 і 13.6).

6.3

Одна крива команда (не та довжина запису, індекс поза словником, ім'я машини не тієї довжини, від'ємна сума ставки) робить сміттям увесь кадр: вузол відповідає одним вердиктом [0, false, 1], і такт зараховується мовчанням.

6.4

Стеля кадру - floor(chanAt / chanOrder) + 4 команд, де chanAt - бюджет каналу такту з pulse. Команди понад стелю отримують відмову стеля без розгляду. Функція orderCap у @zoria/protocol.

6.5

Ставка на лот подається з 1-го такту до такту at - 1 включно: лот розкривається на початку такту at, до команд, і ставка на такті at отримує відмову торги (01-rules.md, 17.3).

6.6

На кожен непорожній кадр команд вузол тим самим тактом відповідає 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торгиставка не за правилами: немає лота, лот розкрито або він розкривається в цей такт, сума не ціла
6.7

Поки слот веде наказ, вердикти на його команди вам не приходять.

7Дедлайни

7.1

Такт - 500 мс: період кадрів спостереження і дедлайн відповіді на кадр, відлічений від кадру. Вузол питає три слоти разом: три кадри йдуть поспіль, на три відповіді чекають незалежно. Роздуми сусіда ваш кадр не затримують.

7.2

Відповідь, що запізнилася, викинуто; прострочений такт - мовчання цього такту. Шість мовчань поспіль - слот веде наказ (01-rules.md, розділ 23).

7.3

Лобі типово - 10 с (init-ms).

8Ротація сесії і повернення

8.1

Обрив проводу всередині партії - не кінець. Кожен hello.ack видає одноразову печатку повернення (строк 180 с; наступний ack видає свіжу, попередня згоряє). Порвався сокет - відкрийте новий і надішліть hello з поверненням: [v, token, agent, resume, last], де last - найбільший номер конверта, який ви отримали.

8.2

Вузол пересаджує слот на новий сокет і дограє пропущені кадри з кільця вихідних (ring-ticks, 240 тактів) у тому самому порядку і під тими самими номерами. Годуйте ними бота як живими: відповіді на прожиті такти вузол викине за номером, а картина світу в бота залишиться цілою.

8.3

Планова ротація. Вузол рве сесію приблизно раз на 90 с (rotate-ms): шле rotate із грейсом (grace-ms, 2000 мс) і потім закриває сокет кодом 4005. Повертайтеся одразу за повідомленням, не чекаючи закриття: тоді ротація не коштує жодного такту. Фази ротації в слотів різні, число ротацій за партію однакове.

8.4

Позаплановий обрив - та сама процедура; вузол причин не розрізняє. Такти, чиї дедлайни минули до повернення, - мовчання.

8.5

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

8.6

Повторні спроби. Першу спробу повернення робіть негайно. Кожну наступну - після паузи, взятої рівномірно від нуля до стелі, а стелю подвоюйте з номером спроби. Стелі дві: поки йде партія - від десятків до півтори сотні мілісекунд (повернення мусить устигнути всередині такту); у лобі та між партіями - секунди. Повернення рахуються за вашою печаткою окремим вікном: клієнт у гарячому циклі перепідключень отримає 1008 і втратить такти. connectBot робить усе це сам (параметри resumeJitterMs, resumeLobbyJitterMs).

9Вузол-провокатор

9.1

Вузол законно перевідправляє кадри: дубль спостереження з тим самим номером не відрізнити від наздоганяння з кільця. Правило одне: кадр, не новіший за вашу останню позицію, боту вдруге не подається. Клієнт із дедуплікацією за номером конверта проходить дублі, ротації та обриви без жодної втрати; клієнт без неї годує бота дублями і ламає собі картину світу.

9.2

Вузол веде лічильники кривизни: кадр не за схемою від того, хто присягнув, другий hello на тому самому сокеті, відповідь із номером, який вам не надсилався, крива печатка повернення при живій сесії. У справного клієнта вони нульові за будь-яких обривів і ротацій.

10Журнал партії

10.1

Запис партії (.runlog) - сім полів: версія, маніфест як є, склад (чиї рішення записано), кадри команд за тактами сирими байтами, зал (ніки за слотами), епізоди наказу [слот, від, до] і відмови воротаря [слот, такт, номер команди в кадрі, код]. Реплей на тому самому рушії подає кадри в ті самі такти і дає той самий бій біт у біт, включно з командами наказу.

10.2

Перед реплеєм пін маніфесту звіряється з рушієм, а cfg - з його SIM_CFG: мають збігтися ключі та значення. Журнал іншої версії проводу, іншого рушія або інших чисел світу відкидається з названою причиною, а не відтворюється приблизно. Старий запис залишається архівом; новий запис дає новий прогін.

10.3

Пін рушія перевіряє сумісність, але не підписує автора файлу.

10.4

Відмови воротаря - сьоме поле: записи [слот, такт, номер команди в кадрі, код]. Код - із таблиці 6.6, і нуля в ньому не буває: прийняті команди в запис не йдуть. Кадр лежить у записі сирими байтами, відповіді вузла в ньому немає, і причину відмови взяти більше нізвідки - тому її пише вузол. Бій це поле не читає: реплей проганяє ті самі кадри через того самого воротаря і виносить ті самі вердикти сам. Пишуться відмови за всіма трьома слотами, зокрема на тактах під наказом; кадр, який не розібрався, дає один запис із номером 0 і кодом сміття.

10.5

Поле молоде, і версія проводу через нього не рухалася: PROTOCOL_V той самий 8. Запис із шести полів читається як раніше - список відмов у нього порожній; для того, хто пише, поле необов'язкове. Законні обидві довжини списку верхнього рівня, шість і сім, інші - не за схемою.