---
title: "Ігри з друзями"
description: "Безплатні мініігри для Member через Tunnel — маркетплейс, доступність у профілі, віджети game_request, ходи з перевіркою на сервері та P1-операції (завершення, ACL, FCM, підказки DataChannel)"
locale: "uk"
---
# Ігри з друзями

> **Info**
> Використовуйте вкладки **Founder** / **Developer** на бічній панелі документації, щоб фільтрувати цю сторінку. Видимість у бічній панелі налаштовується в `lib/docs/audience-curated-docs.ts`.

Ring постачає **мініігри для друзів**, які повторно використовують fan-out Tunnel, набір інтерактивних повідомлень і IA публічного профілю — без GameConductor, паралельного стека повідомлень чи ставок.

| Поверхня | Шлях |
|---------|------|
| Маркетплейс | `/games` |
| Мініапка | `/games/[slug]?session=` |
| Публічна доступність | `/{username}/games` |
| Керування власника | `/profile/games` (Member) |
| Віджет чату | тип повідомлення / `metadata.kind` = `game_request` |
| Вхідний банер | MessagesShell **і** layout `/games` |

**Доступні назви:** хрестики-нулики · шахи · шашки. Тексти каталогу локалізуються через `locales/*/modules/games.json` (`modules.games`); англійські рядки в `catalog.ts` залишаються резервним варіантом для FCM/історії.

```mermaid
flowchart LR
  invite[createInvite] --> session[peer_game_sessions]
  session --> msg[game_request message]
  invite --> inbox[games:incoming]
  inbox --> banner[IncomingGameBanner]
  session --> gameCh[game sessionId]
  gameCh --> miniApp[Mini-app hydrate]
  miniApp --> moves[validateMove applyMove]
  moves --> tunnel[game:move Tunnel]
```

### For founders

## Чому це важливо для вашого клону

Учасники швидше зближуються, коли можуть **грати**, не залишаючи ваш Ring. Виклики надходять як віджети чату та як глобальний **IncomingGameBanner** у Messages і `/games` — тож запрошення не губляться за межами гілки месенджера.

- Безплатна перевага **Member** — без ставок Stars / кредитів / PaymentConductor
- Лише **прямі** розмови (той самий gate, що й для дзвінків 1:1)
- **М’ютекс** дзвінка ↔ гри (між вкладками через BroadcastChannel): не можна прийняти гру під час активного дзвінка і навпаки
- Публічна дія **Грати зі мною** пропонує лише slug-и, які гравець увімкнув на `/{username}/games`
- Неприйняті запрошення спливають через 15 хвилин; активні ігри бездіяльні та очищуються через 2 години

### Чекліст оператора

1. Підтвердьте членство, щоб власники могли відкрити `/profile/games` і опублікувати доступність (шахи / шашки / хрестики-нулики).
2. Перевірте `/games` у кожній локалі — назви каталогу мають локалізуватися (EN/UK/RU/ES/DE).
3. У прямому чаті створіть ігровий виклик → співрозмовник бачить банер **і** віджет.
4. Прийміть → зробіть ходи → здайтеся; обидві сторони оновлюються без перезавантаження.
5. Переконайтеся, що телефонний/відеодзвінок блокує прийняття гри, поки накладка дзвінка активна (також спробуйте другу вкладку браузера).
6. Необов’язково: залиште неприйняте запрошення приблизно на 15 хвилин (або запустіть cron завершення сесій) → банер і мініапка очищуються.

  
- **[Повідомлення](/docs/features/messaging.md)** — Розмови та інтерактивні віджети (`game_request` у спільному наборі).

  
- **[Дзвінки WebRTC](/docs/features/webrtc-calls.md)** — Спільний м’ютекс дзвінка/гри; ICE-хелпер повторно використовується для оптимістичних підказок ходів DataChannel.

  
- **[Членство](/docs/features/subscriptions.md)** — Gate Member для публікації доступності та створення викликів.

  
- **[Публічний профіль](/docs/features/public-profile.md)** — `/{username}/games` розташований поруч із Player і Gallery у публічному профілі.

### For developers

## Архітектура

```text
createGameRequest / POST …/game-invite / challengeUserToGameAction
  → PeerGameService.createInvite (SSOT)
  → Redis SET NX claim peer-game:invite:{from}:{to}:{slug} (Map fallback)
  → create session (peer_game_sessions) FIRST
  → send game_request message → link messageId
  → on failure: delete session + releaseNx
  → publishToChannel(conversation:…, game:invite)
  → publishToUserTunnel(peer, games:incoming)
  → if !deliveredLive: sleep ~500ms; FCM GAME_REQUEST only if still !isUserConnected
accept / decline / resign / submitMove
  → participant ACL + plugin validateMove/applyMove
  → updateMessageLocked on linked widget metadata
  → game:* on game:{sessionId} AND conversation:{id}
  → decline/expire also publish terminal on games:incoming (banner clear)
client mini-app
  → hydrate DB SSOT → useTunnelChannel(game:{id})
  → optional RTCDataChannel optimistic hints (fetchIceServers); server remains SSOT
```

### Ключові модулі

| Шлях | Роль |
|------|------|
| `features/peer-games/service.ts` | Сесії + доступність + SSOT createInvite |
| `features/peer-games/session-expiry.ts` | Повернення сиріт + завершення неприйнятих/неактивних сесій |
| `features/peer-games/plugins/*-logic.ts` | Безпечні для сервера validate/apply (ttt, chess, checkers) |
| `features/peer-games/catalog.ts` | Реєстр slug-ів (резервний EN) |
| `locales/*/modules/games.json` | i18n каталогу + маркетплейсу (`modGames`) |
| `features/peer-games/lib/peer-game-mutex.ts` | Зайнятість дзвінка/гри + BroadcastChannel / Web Locks |
| `features/peer-games/lib/catalog-i18n.ts` | Хелпери локалізованих назв/описів |
| `features/peer-games/hooks/use-peer-game-datachannel.ts` | Оптимістичні підказки ходів (`ring-peer-game-moves`) |
| `app/_actions/peer-games.ts` | Серверні дії |
| `app/api/conversations/[id]/game-invite/route.ts` | Тонкий HTTP-аналог → `createInvite` |
| `app/api/cron/peer-game-session-expiry/route.ts` | Конвеєр ProcessConductor |
| `app/api/tunnel/subscribe/route.ts` | ACL учасників HTTP `game:*` |
| `lib/tunnel/native-ws/attach.ts` | ACL учасників WS `game:*` (віддзеркалює HTTP) |
| `IncomingGameBanner` | Очищення життєвого циклу `games:incoming` + `game:{id}` |

### Збереження даних

Міграція **`data/migrations/043_peer_games.sql`**:

- `peer_game_sessions` — індекси за conversation / challenger / peer / status / slug / messageId / `updated_at`
- `user_peer_games` — `enabledSlugs` для кожного власника (унікальне поле `ownerId`)

### Канали Tunnel (перевірено)

| Канал | Події |
|---------|--------|
| `conversation:{id}` | `game:invite`, `game:accept`, `game:decline`, `game:resign`, `game:move`, `game:expire` |
| `game:{sessionId}` | `game:accept`, `game:decline`, `game:resign`, `game:move`, `game:expire`, `game:dc-signal` |
| інбокс користувача | `games:incoming` (invite, `accepted`, `terminal`) |

**ACL підписки:** для `game:{sessionId}` за замовчуванням доступ заборонено і через HTTP, і через native WS — лише учасники з БД (`getSessionForParticipant`). Спостереження відкладено.

**Очищення банера:** `game:expire` / `game:decline` / `game:accept` на каналі сесії, а також `games:incoming` із `terminal` або `accepted` (охоплює гонку підписки після запрошення).

### Сповіщення та FCM

`NotificationType.GAME_REQUEST` / `GAME_UPDATED`. Офлайн-push: після `!deliveredLive` зачекати приблизно 500 мс і повторно перевірити `getTunnelHub().isUserConnected(peer)` перед `createNotification` + **PUSH dual-stack** (FCM Admin і RFC `web-push`). На банері — лише м’який сигнал Web Audio; ніколи не викликайте ringtone / `setPeerCallBusy`. На пристроях, якими керує Chrome FCM, `push_subscriptions` порожній (RFC — no-op). Див. [Push-сповіщення (FCM)](/docs/features/push-notifications-fcm.md).

> **Warning**
> `isUserConnected` хаба **локальний для процесу**. Хибний офлайн FCM у multi-pod — відома залишкова проблема; спільна presence ще в беклозі.

### М’ютекс, дедуплікація, дошки

- М’ютекс: `setPeerCallBusy` / `setPeerGameBusy` з BroadcastChannel (+ опційно `navigator.locks`)
- Дедуплікація запрошень: Redis `SET NX PX` **усередині** `createInvite` (спершу claim; у разі помилки — `releaseNx`) — Server Action + HTTP + Play-with-me спільно використовують SSOT
- Шахи: FEN SSOT (`key={fen}`); невалідні ходи повертають `false`
- Шашки: англійські шашки, обов’язкове взяття, **один стрибок за хід** (ланцюжок стрибків — у беклозі)

### Завершення сесій

Конвеєр `peer-game-session-expiry`:

| Випадок | TTL | Результат |
|------|-----|--------|
| Осиротіла неприйнята сесія (без `messageId`) | ~2m | Повернення |
| Неприйняте запрошення | 15m | Відхилено + `updateMessageLocked` |
| Активна бездіяльність | 2h | Здано/завершено + сповіщення Tunnel |

**Планувальники:** Vercel `*/5` (`vercel.json`) · k3s-or CronJobs `infrastructure/k3s-or/ring-platform-org/cronjob-peer-game-session-expiry.yaml` (+ сусідній `cronjob-close-expired-polls.yaml`, `/bin/sh`, щоб розгортати `CRON_SECRET`). Застосуйте в кластері під час переведення k8s prod.

### DataChannel (лише оптимістичний режим)

`RTCDataChannel` із міткою `ring-peer-game-moves` (ordered) + `fetchIceServers`. Конверти peer — це **підказки**; `validateMove` / Tunnel `game:move` / БД залишаються SSOT. Відхилення → відкат до останнього авторитетного hydrate. Перезапуск ICE у нестабільних мережах — у беклозі; шлях Tunnel залишається резервним.

### Серверні дії

`createGameRequest` · `acceptGameRequest` · `declineGameRequest` · `resignPeerGameAction` · `submitPeerGameMoveAction` · `getPeerGameSessionAction` · `updateEnabledGamesAction` · `challengeUserToGameAction`

### Smoke / soak

Структурна перевірка: `npx tsx scripts/smoke-peer-games-soak.cts`. Модульні тести плагінів у `__tests__/features/peer-games/plugins.test.ts`. Чекліст live E2E для Member: `AI-CONTEXT/concepts/peer-games-live-e2e-soak-2026-07-24.json` (поширення empire Reggie заблоковане до погодження).

## Пов’язана документація

  
- [features/messaging](/docs/features/messaging.md) — Залежить від: game_request живе в наборі інтерактивних повідомлень і прямих розмовах.

  
- [features/webrtc-calls](/docs/features/webrtc-calls.md) — Той самий процес: спільний м’ютекс дзвінка/гри; маршрут ICE повторно використовується для підказок ходів DataChannel.

  
- [features/tunnel-protocol](/docs/features/tunnel-protocol.md) — Залежить від: ACL conversation:*, game:{sessionId} і fan-out games:incoming.

  
- [features/subscriptions](/docs/features/subscriptions.md) — Передумова: привілеї Member керують /profile/games і створенням викликів.

  
- [features/public-profile](/docs/features/public-profile.md) — Наступний крок: публічний /{username}/games поруч із Player і Gallery.

  
- [features/push-notifications-fcm](/docs/features/push-notifications-fcm.md) — Також див.: createInvite PUSH GAME_REQUEST (FCM + RFC fan-out) після grace-періоду присутності Tunnel.

  
- [features/notifications](/docs/features/notifications.md) — Також див.: типи налаштувань GAME_REQUEST і GAME_UPDATED.
