АЛМАНАХ ⚡ — almanac.bg
Куиз битки в реално време за много играчи: играй на almanac.bg. Тук е пълният изходен код на играта, свободен софтуер под AGPL-3.0.

Какво има:
- Режими: соло тренировка, въпросите на деня, класирани дуели 1 на 1 и отбори 2 на 2, мачове всеки срещу всеки за 3 и 5 играчи, Роял за 8 играчи с отпадане, частни стаи за приятели и линкове с предизвикателство, които всеки може да изиграе.
- Прогрес: профилите качват нива, отключват аватари, рамки и титли, печелят постижения, изпълняват дневни задачи, трупат дневна серия (с щитове), изкачват се в месечни сезони и пазят подробна статистика.
- Приятели и общност: играчите могат да се добавят за приятели, да се канят и предизвикват, да искат реванш и да предлагат свои въпроси за одобрение. Посетителите могат да играят веднага като гости и да запазят прогреса си по-късно.
Бърз старт
Нужен е Node 24+. Играта ползва вградения node:sqlite, така че няма база данни за инсталиране.
npm install
npm run dev
Отвори http://localhost:5173, регистрирай профил и играй. При разработка първият профил, който регистрираш, става администратор, за да може да одобрява въпроси от общността. В продукция никой не става администратор с регистрация; виж стъпка 7 от Внедряване.
Истинската банка с въпроси на играта (около 2000 въпроса) не е част от публичния код, за да не могат отговорите да се
търсят по време на мач. Без нея се играе с примерните 120 въпроса от apps/server/src/data/questions.sample.json —
публична извадка от истинската банка, която стига за разработка и за всички тестове. Виж Въпроси.
Когато никой друг не чака за мач, след няколко секунди ботове заемат празните места, така че всеки режим може да се играе и без други хора. За да тестваш истински мултиплейър локално, отвори втори браузър (или частен прозорец) с друг профил.
| Скрипт | Какво прави |
|---|---|
npm run dev |
API + realtime сървър на :3001 и Vite на :5173 (през proxy) |
npm run build |
Билдва уеб приложението и пакетира сървъра в apps/server/dist |
npm start |
Продукционен сървър на :3001 (чист Node), който сервира и уеб приложението |
npm run typecheck |
Проверява типовете във всички пакети |
npm test |
Всички тестове (около минута и половина), виж по-долу |
npm run lint |
Форматиране, правила и подредба на импортите (Biome), както в CI |
npm run format |
Поправя автоматично каквото може от горното |
Тестове
npm test пуска тези тестове с вградения test runner на Node:
e2e.test.tsстартира истински сървър с временна база данни и минава през всичко с истински сокети. Двама играчи и ботове покриват: соло, класиран дуел с повторно свързване, отпадания в Роял, частни стаи и отбори, въпросите на деня и линковете към тях, приятели, покани и предизвикателства, всеки коз, въпроси от общността с модерация, сезонни награди, сигнали и скриване на грешни въпроси, гости, линкове с предизвикателство, реванши, отказ от напомнянията за серията, резултати, показани отново след прекъсната връзка, невалидни съобщения по сокета (които никога не бива да сриват сървъра) и кеширането на файловете на сайта.game.test.tsуправлява енджина на мача с подменен часовник: напускане и служебни загуби, прекъснати връзки, изоставени мачове, „Двойно или нищо“, равенства и силата на ботовете.rewards.test.tsпроверява какво носи класиран мач, върху истинска база: мач, напуснат преди третия въпрос, не се зачита, по-късното напускане носи победа според изиграното, гостите играят срещу хора без рейтинг, а лимитът от три мача на ден между двама казва защо рейтингът не се е променил.restart.test.tsпокрива плавните рестарти (текущите мачове първо завършват) и опит в дневното предизвикателство, прекъснат от срив.database.test.tsпроверява историята на въпросите за всеки играч и че честите заявки използват индекси.production.test.tsпуска сървъра като в продукция: администратори се добавят само от командния ред, security хедъри за браузъра, health check, отчети за сривове, заглавията и адресите на страниците, „noindex“ и 404 за грешни адреси.maintenance.test.tsпокрива почистването на изтекли сесии, дневните копия на базата данни и изтриването на профили, неизползвани три години (с писмо месец по-рано).progress.test.tsпокрива дневните задачи, щитовете за серията, вечерното напомняне за серията, почистването на гост-профили и статистиката за старта.monitoring.test.tsпроверява отчетите за грешки към Sentry, ограничението на броя им и че тунелът от браузъра препраща само към собствения проект на сайта.questions.test.tsвалидира банката с въпроси. Новите въпроси трябва да минават проверката.rules.test.tsпроверява правилата за точките и прогреса, включително границите на дните и сезоните по софийско време при смяна на лятното часово време.
npm run test:dist -w @quiz/server пуска същите тестове срещу продукционния bundle. Колко играчи носи един сървър
показва npm run build && caffeinate -i npm run load -w @quiz/server (няколко минути; ulimit -n 30000 преди
големи стъпки): хиляди симулирани играчи с реалните времена на играта и колко бързо отговаря сървърът междувременно.
На MacBook с M4 3000 души наведнъж минаха без нито една грешка, с най-много 35 ms забавяне. GitHub Actions
(.github/workflows/ci.yml) при всеки push към main и всеки pull request пуска lint, typecheck, build, двата
варианта на тестовете и браузърните тестове, на Node 24 и 26; после билдва Docker образа, проверява, че стартира, и
прави пълен бекъп и възстановяване с Litestream. На main, щом всичко това мине, билдва образа за сървъра (ARM) и го
публикува в GitHub Container Registry като ghcr.io/almanac-bg/almanac:<commit> — продукцията пуска точно него.
Конфигурация (променливи на средата)
| Променлива | По подразбиране | Значение |
|---|---|---|
PORT |
3001 |
Порт на сървъра |
DB_PATH |
apps/server/data/quiz.db |
SQLite файл (създава се и се мигрира автоматично) |
BOT_FILL_SECONDS |
8 |
Изчакване в опашката, преди ботове да заемат празните места; off изключва ботовете |
BOT_FILL_BUSY_SECONDS |
20 |
Същото, докато друг човек е онлайн и свободен (не е в мач, опашка или стая): тогава си струва да го изчакаме |
PUBLIC_QUEUES |
all |
Класирани опашки, в които играчите могат да влизат (all или списък от duel, teams, squad, royale) |
COOKIE_SECURE |
false |
Задай true, когато сайтът се сервира през HTTPS |
TRUST_PROXY |
loopback |
Express trust proxy: на кои reverse proxy сървъри да се вярва |
APP_URL |
http://localhost:5173 |
Публичният адрес (origin) в линковете в имейлите |
SMTP_URL |
Напр. smtp://user:pass@host:587. Без него → имейлите се печатат в конзолата |
|
EMAIL_EVENTS_SECRET |
Тайната в адреса за отчети от Scaleway (/api/email-events/<тайна>). Без нея → изключено |
|
MAIL_FROM |
АЛМАНАХ <no-reply@almanac.bg> |
Адрес на подателя |
GAME_TIME_SCALE |
1 |
Умножава всички таймери в играта. Само за автоматични тестове |
BACKUP_DIR |
backups до базата данни |
Дневни копия на базата; off ги изключва |
BACKUP_KEEP_DAYS |
3 |
Колко дневни копия да се пазят |
DRAIN_SECONDS |
330 в продукция, иначе 0 |
При спиране: колко време имат текущите мачове да завършат |
SENTRY_DSN |
Sentry проект за отчети за грешки; без него → грешките отиват само в лога | |
APP_RELEASE |
Версия в отчетите за грешки (командата за деплой задава git commit-а) | |
OPERATOR_NAME, OPERATOR_ADDRESS |
Кой поддържа сайта; показва се в политиката за поверителност и условията (името е задължително, адресът — не) | |
CONTACT_EMAIL |
privacy@<domain> |
Адрес за контакт в правните страници и имейлите |
EMAIL_PROVIDER, HOSTING_PROVIDER |
Посочени в политиката за поверителност като обработващи данни | |
BACKUP_PROVIDER, INBOX_PROVIDER |
Също в политиката, ако са зададени: бекъпите извън сървъра и пренасочването на писмата до CONTACT_EMAIL |
|
SOURCE_URL |
Публичното хранилище, към което води линкът във футъра („Изходен код“) | |
QUESTIONS_PATH |
private/questions.json, иначе примерната |
Вградената банка с въпроси (виж Добавяне на въпроси) |
Името на марката е в packages/shared/src/brand.ts.
Структура на кода
packages/shared Правила и каталог, общи за клиента и сървъра: режими, крива на нивата, сезони,
козметика, козове, постижения, типове за сокетите и REST
apps/server Express + Socket.IO + SQLite
src/game/Game.ts Енджинът на мача, изцяло на сървъра (таймери, точки, отбори, козове, отпадане, ботове)
src/game/matchmaking.ts Опашки за бърз мач, попълване с ботове и балансиране на отборите
src/game/rooms.ts Частни стаи (6-цифрени кодове, линкове /join/123456, отбори, покани към приятели)
src/game/rewards.ts Записва мачовете: XP, статистика, сезонно Elo, дневни резултати, постижения
src/seasons.ts Месечни сезони: частично нулиране, награди в края на сезона (изчисляват се при нужда)
src/daily.ts Въпросите на деня: един набор на ден (от seed), един опит, линкове към деня
src/skill.ts Колко труден е всеки въпрос и колко силен всеки играч, мерено по отговорите (като Elo)
src/runs.ts Всеки изигран мач въпрос по въпрос (пази се година): за съперници-записи и анализ
src/friends.ts Покани за приятелство и кой е онлайн
src/submissions.ts Въпроси от общността и модерация
src/data/questions.sample.json Публична извадка от банката с въпроси (истинската е частна, в private/)
test/ Тестовете на сървъра (виж „Тестове“)
packages/client Ядрото на клиента: API заявки, състояние на играта в реално време (zustand), форматиране.
Сайтът добавя звуците си; отделено е, за да могат и други клиенти да го ползват
apps/web React 19 + Vite + Tailwind 4 + Motion
e2e/ Браузърните тестове (Playwright)
deploy/ Caddyfile (HTTPS и страницата при обновяване) и litestream.yml (бекъпите)
scripts/ github-setup.sh: настройките на хранилището в GitHub
Как се играе
- Режими:
- Соло: 10 въпроса от избрана категория.
- Въпросите на деня: едни и същи 10 въпроса за всички (всеки играч получава свой ред на отговорите), един опит на
ден, без козове, собствена класация. Изтеглят се първо от въпросите с „Знаеше ли?“, а от тях — от най-малко
играните. Гост или профил, направен днес, играе същите въпроси, но извън класацията, и вижда къде щеше да е в нея
(„Щеше да си на 7-мо място от 24“): иначе профил за секунди би бил класиран заедно с основния. Гост от предишен ден,
който запази профила си, влиза в класацията веднага, с днешния си резултат. Изиграл деня, играчът може да прати
свой линк към него (
/daily/<код>, важи до полунощ): който го отвори, играе същите въпроси, вижда резултата на пратилия и се сравнява с него, а пратилият получава известие и вижда на/dailyвсички изиграли по линка му. Опит, прекъснат от срив или обновяване след разкрит отговор, приключва с резултата дотук (иначе същите въпроси щяха да дойдат отново с известни отговори), а преди първия разкрит отговор може да се изиграе отново; напуснат на отброяването (преди първия въпрос) не е опит. Денят се брои за серията, ако е изиграна поне половината. - Дуел: 1 на 1, в три рунда по три въпроса, всеки от една категория. Първият рунд е изтеглен; преди втория и третия изоставащият по точки (при равенство — с по-нисък рейтинг) избира категорията от три неизиграни, за 5 секунди (иначе тя се изтегля; ботът избира силна своя). Трудността пак расте: лесни, средни, после трудни. Последният, десети въпрос е финалът с число, от категория извън рундовете.
- Отбори: двама приятели в един отбор срещу друга двойка (или два бота). Влиза се само по двама: единият кани
приятел от картата на режима (
duo:invite), другият приема (duo:accept) и двамата чакат заедно; „Реванш“ пази двойките. - Арена (3–5): 10 въпроса в рундове като Дуел (категорията избира последният) и финал с число. Започва с петима
веднага, а щом времето за чакане изтече — с колкото са дошли; ботове допълват само до трима. Замени Трио (3) и
старата Арена (5); мачовете „Трио“ остават в историята, а
PUBLIC_QUEUES=trioзначи Арена. - Роял: 8 играчи. Играчът с най-малко точки отпада след въпроси 4, 6, 8, 9, 11 и 13 (не след 3-ия: един неуспешен първи въпрос не стига, за да изпрати някого у дома); последните двама играят 14-ия и финала с число.
- Предизвикателство (линк с предизвикателство,
/c/<id>): играч създава линк с 10 фиксирани въпроса и го изиграва, преди да го сподели; всеки с линка (и гостите) играе същите въпроси веднъж, със свой ред на отговорите и без козове, и влиза в класацията на самия линк. Създателят получава известие за всеки резултат. Линковете важат 7 дни. - Дуел с приятел (същият
/c/<id>, но само за двама): играеш сега, приятелят ти има 24 часа за същите въпроси — без да е нужно двамата да са онлайн. Въпросите са в рундове като в Дуел (без избор по време на мача) и с финал с число; без козове и без рейтинг. Приятелят получава известие с резултата за биене, а който е играл пръв — известие как е свършил дуелът (с XP за победата, колкото за победа 1 на 1). Всяка двойка има общ резултат от дуелите си (на профила и в резултатите). „Реванш“: категориите на трите рунда избира загубилият последния дуел. Най-много 3 неизиграни дуела на двойка; изтекъл неизигран дуел носи известие на изиграл своя ход. - Частна стая: 2–40 играчи (с елиминации — до 8), с настройки за въпросите, таймера, категориите („Микс“, „Избира последният“ — рундове, или една), финала с число, отпадането и отборите. „Само водя“: домакинът не играе, а показва мача на голям екран (въпросът, колко са отговорили, после колко са избрали всеки отговор, фактът и първите петима; накрая подиумът), докато играчите отговарят от телефоните. Гост, който влиза в стая, си избира име за нея (2–20 знака, уникално в стаята). В голям мач класирането показва първите и теб.
- Моите въпроси (
/sets): собствен набор до 50 въпроса — с избор от четири, вярно/невярно или число (верният отговор, мерна единица и отклонение, при което точките стават нула) — играе се в частните стаи на автора, в реда на набора. При вярно/невярно 50/50 и „Втори шанс“ не важат. Линкът (/sets/shared/<код>) дава на друг домакин да вземе копие; сигналът отива при модераторите като доклад за автора, а модератор може да скрие набора. Въпрос с избор може да се предложи в общата банка. Мачът на собствен набор плаща само класирането в стаята и не мери знание (без сила, статистика, алманах, задачи и постижения за мача). - Вечерен Роял: всеки ден в 21:00 софийско време (
EVENING_AT,offго спира), вход от 20:55. До 50 души в мач (повече правят няколко мача), ботове само до осем. След 3-ти, 6-и, 9-и и 12-и въпрос отпадат последните 30% (поне един), след 13-и — всички освен двама, които играят 14-и и финала с число. Без рейтинг; XP като в Роял за осем. Победителят носи „Шампион на вечерта“ 24 часа, три победи дават рамка, а първите десет са на началната страница до следващата вечер. - Всички класирани опашки са отворени;
PUBLIC_QUEUES(списък) може да затвори някои. - Поправителен: сгрешените въпроси (от всеки режим) се връщат 3 дни след грешката, после — познати — след 7 и след 21 дни; сгрешен отново, въпросът започва отначало, а познат и на третата стъпка излиза. Играе се, щом поне 3 са се върнали, по до 10 наведнъж (въпросите с число накрая), без козове и без финал. Верните отговори носят XP, но отговорите (вече показани веднъж) не влизат никъде, където се мери знание: в силата и рейтинга на въпросите, в общите числа за тях, в дневника на отговорите, в записите за повторение, в точността и рекордите, в задачите и в постиженията за мача.
- Моят алманах (
/almanac): „Знаеше ли?“ на всеки въпрос, познат в мач (или спрян за четене, когато играеш сам), остава на играча — за всяка категория страница с търсене и брой („48 от 116“). Пазят се всички познати въпроси, затова факт, написан по-късно, също се появява. Постижения при 100, 500 и 1000 факта; броят е в профила. - Колко знаят: след 30 отговора на въпрос разкриването показва „62% знаят“ (на въпрос с число — „бяха близо“), а
прегледът след мача — колко са избрали всеки отговор. Изборите се броят в
question_statsпо реда на отговорите в банката и се показват в реда на мача; въпрос, променен оттогава, не показва нищо. Във въпросите на деня, щом са играли поне 20: „По-добър от 73% днес“ и стълбчета с броя верни на всички. - Майсторство: за всяка категория степен Познавач, Експерт или Майстор според
