Skip to content

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

Bunker API

Реалтайм-бекенд для гри "Бункер". NestJS + Socket.IO, без бази даних — весь стан кімнат/гравців зберігається in-memory, а властивості/бункери/ катаклізми — у статичних конфіг-файлах.

Стек і чому саме такий

  • NestJS (TypeScript) — модульна DI-архітектура, звична для Angular-розробників (той самий стиль декораторів/сервісів), з нативною підтримкою WebSocket-гейтвеїв.
  • Socket.IO — двосторонній realtime-канал для стану кімнати (як просив автор ТЗ).
  • Без БД — RoomsService/GameService тримають усе у Map в пам'яті процесу. Якщо згодом знадобиться горизонтальне масштабування — це єдине місце, яке довелося б винести в Redis (adapter для Socket.IO + спільне сховище кімнат).
  • Фронтенд: Angular підійде без проблем — API суто REST+WS, жодних бекенд-специфічних вимог до клієнта немає.

Встановлення і запуск

npm install
npm run start:dev   # http://localhost:3000, WS namespace /game

Модель гри (коротко)

  • Кімната: 4–16 гравців, власник — той, хто створив кімнату.
  • Лобі: гравці задають ім'я, тиснуть "готовий"; власник опційно обирає бункер/катаклізм (інакше обираються випадково при старті).
  • Після старту кожен гравець отримує 9 випадкових властивостей (стать/вік, статура, характер, професія+стаж, здоров'я, хобі+рівень, фобія, багаж, факт) — приховані від усіх, крім власника картки.
  • Гра йде 5 раундів. У раунді: коло відкриття однієї властивості на гравця (у раунді 1 — примусово картка "Професія"), потім голосування (кількість "сесій" голосування в раунді залежить від кількості гравців на старті гри — див. ELIMINATION_SCHEDULE в src/game/constants/game.constants.ts).
  • Вигнаний гравець розкриває всі властивості одразу і більше не ходить/не голосує. Наступний раунд починається з гравця, який ходив другим у попередньому раунді.
  • Після 5-го раунду гра завершується, властивості всіх гравців стають видимими всім.

Особливі умови

На старті кожен гравець отримує 2 різні картки особливих умов (SPECIAL_CARDS_PER_PLAYER; колода — у src/config/special-cards.data.ts). Перша картка завжди має timing ANY, тож ніхто не залишиться лише з картками для голосування. Картку можна зіграти будь-коли в грі, не обов'язково у свій хід. Вигнані гравці карток не грають.

Тип Timing Ціль Ефект
SWAP_TRAIT ANY інший обмін характеристикою (професія / здоров'я / багаж / фобія)
REROLL_OWN ANY — своя характеристика замінюється випадковою
REROLL_TARGET ANY інший характеристика цілі замінюється випадковою
HEAL ANY будь-хто здоров'я → «Ідеально здоровий»
PEEK ANY інший граючий приватно бачить одну приховану характеристику цілі
FORCE_REVEAL ANY інший ціль публічно відкриває випадкову приховану характеристику
IMMUNITY VOTING — за гравця не голосують у поточній сесії (наявні голоси знімаються)
DOUBLE_VOTE VOTING — голос гравця рахується двічі в поточній сесії
REVOTE VOTING — усі голоси сесії скасовуються, таймер — заново

Якщо FORCE_REVEAL уже відкрив професію гравця до його ходу в 1-му раунді, він відкриває будь-яку іншу картку. Кожна зіграна картка пишеться в журнал подій room.log.

REST API

  • POST /rooms — створити кімнату. Body: { ownerName }. Відповідь: { playerId, room }
  • POST /rooms/:code/join — приєднатися по коду. Body: { name }. Відповідь: { playerId, room }
  • GET /rooms/:code — поточний стан кімнати (без маскування — сервісний перегляд)
  • POST /rooms/:code/leave — вийти з кімнати, поки вона в лобі. Body: { playerId }
  • GET /config/bunkers — список доступних бункерів
  • GET /config/cataclysms — список доступних катаклізмів

Код кімнати — 6-значний числовий код (легко продиктувати голосом).

WebSocket (/game namespace)

Підключення: io('http://host/game', { query: { code, playerId } }) — code і playerId отримуються через REST (create/join) заздалегідь. При невалідному code/playerId сокет отримує room:error і розривається.

Клієнт → сервер

  • player:rename — { name }. Будь-який гравець, лише в лобі.
  • player:ready — { ready }. Не-власник, лише в лобі.
  • owner:selectBunker — { bunkerId }. Власник, лише в лобі.
  • owner:selectCataclysm — { cataclysmId }. Власник, лише в лобі.
  • owner:kickPlayer — { playerId }. Власник, лише в лобі. Вигнаний гравець отримує room:kicked, і його сокет відключається.
  • owner:startGame — без payload. Власник, коли гравці готові (4–16).
  • owner:startRound — без payload. Власник.
  • player:revealTrait — { category }. Активний гравець (у раунді 1 категорія ігнорується — завжди PROFESSION).
  • owner:startVoting — без payload. Власник, коли є що голосувати.
  • player:castVote — { targetPlayerId }. Будь-який невигнаний гравець.
  • owner:endVoting — без payload. Власник, коли всі проголосували. Коли 60-секундний таймер голосування спливає, сервер сам віддає випадковий голос за кожного, хто не проголосував.
  • owner:restartVoting — без payload. Власник, лише після нічиєї.
  • player:playSpecialCard — { cardId, targetPlayerId? }. Будь-який невигнаний гравець під час гри; targetPlayerId обов'язковий для карток з ціллю.

category — одне з: GENDER_AGE, BODY, CHARACTER, PROFESSION, HEALTH, HOBBY, PHOBIA, BAGGAGE, FACT.

Сервер → клієнт

  • room:state — повний знімок кімнати, персоналізований під конкретного глядача: властивості інших гравців приховані (revealed: false, card: null), поки власник картки їх не відкрив; властивості вигнаних гравців і, після завершення гри, взагалі всіх гравців — видно повністю. Кожен гравець у players[] додатково несе publiclyRevealedCategories — список категорій, які він розкрив публічно, незалежно від того, хто дивиться. Це окремо від traits[].revealed (яке для власних карток гравця завжди true) і потрібне клієнту, щоб відрізнити "бачу лише я" від "уже бачать усі". Поле voting (коли є активна сесія) містить sessionIndex (зростає при кожному новому/перезапущеному голосуванні — сигнал для клієнтських таймерів) та lastResult/tiedPlayerIds для показу нічиєї, а також immunePlayerIds/doubleVoterIds; voteCounts уже враховують подвійні голоси. players[].specialCards — власні картки глядача завжди, чужі — лише зіграні (після завершення гри — всі); specialCardsLeft публічний. Характеристики, підглянуті через PEEK, приходять глядачу як revealed: true, але не потрапляють у publiclyRevealedCategories. log — останні 30 подій (раунди, зіграні картки, вигнання).
  • room:error — { message } при відхиленій дії.

Таймери

Ліміт 60 секунд на хід і 1 хвилина на обговорення при голосуванні — навмисно не реалізовані на бекенді (немає server-side таймаутів чи примусових дій). Легше й надійніше рахувати їх на фронтенді (як і пропонував автор ТЗ) — бекенд лишається єдиним джерелом істини щодо ігрового стану, а UI сам вирішує, коли підсвітити "час вийшов".

Структура проєкту

src/
  common/          enum'и та типи (Room, Player, TraitCategory, RoomPhase)
  config/          статичні пули властивостей, бункерів, катаклізмів + REST-контролер
  rooms/           RoomsService (лобі: створення/приєднання/готовність/вибір бункера)
  game/            GameService (раунди/голосування/елімінація), GameGateway (WS),
                    SpecialCardsService (ефекти особливих умов),
                    room-view.mapper (маскування властивостей під глядача)

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages