Magic Vault 5×3 — Frontend DEV API

DEV ONLY · DEMO ONLY · DRAFT MATH · NOT FOR PRODUCTION · NOT CERTIFIED

Це короткий handoff для нового frontend engine/store. Канонічний і детальніший API contract: backend/doc/dev-api.md, §5. Якщо цей файл і backend-документація розходяться, пріоритет має backend-документація та актуальна OpenAPI schema.

Поточна доступність

ПолеЗначення
Base URLhttps://kiro-dev.chowchowhome.duckdns.org
Game IDmagic-vault-5x3
Published versionmagic-vault-5x3-draft-v0.1
Environmentтільки DEV
Operator / mode / walletтільки demo / demo / demo
Grid5 reels × 3 rows
Featurehold_and_win/v1
Frontend bundleвідсутній; цей документ описує API integration

Stage і production не публікують цю гру. Наявність гри в catalog не є авторизацією: backend повторно перевіряє environment, session, wallet, Game ID, config identity та DEV flags перед будь-яким рухом грошей.

Джерела істини

1. Bootstrap і повторне використання токена

POST /dev/v1/demo/session
x-api-key: <edge-key>
Authorization: Bearer <cached-demo-bearer>   # optional
Content-Type: application/json

{"game_id":"magic-vault-5x3","currency":"EUR","lang":"en"}

Перший успішний виклик повертає новий session_bearer і reused:false. Після reload frontend повторно надсилає кешований bearer; reused:true означає, що сервер продовжив ту саму demo session. Не створювати новий токен на кожне перезавантаження сторінки. Bearer — credential: не логувати, не комітити, не вставляти в telemetry.

Якщо кешований bearer отримав 403 DEMO_SESSION_UNAVAILABLE, видалити його та один раз повторити bootstrap без Authorization.

Початковий store заповнює:

  • sessionBearer з session_bearer;
  • balance з balance.real;
  • gameId з game.game_id;
  • debugAllowed з capabilities.debug_allowed;
  • feature = null.

2. Base spin

POST /game/v1/spin
x-api-key: <edge-key>
Authorization: Bearer <demo-bearer>
Idempotency-Key: <new UUID per logical paid spin>
Content-Type: application/json

{"bet":1}

Важливі поля відповіді:

{
  "round_id": "a852905b-be9f-474b-8404-db2bc310dbc2",
  "game_config_version": "magic-vault-5x3-draft-v0.1",
  "spin": {
    "grid": [["H3","L3","L4"],["H4","L1","L2"],["L2","L3","L4"],["L3","L4","L1"],["L2","L3","H2"]],
    "layout": [["H3","H4","L2","L3","L2"],["L3","L1","L3","L4","L3"],["L4","L2","L4","L1","H2"]],
    "wins": [],
    "payout": 0,
    "triggered_features": []
  },
  "wallet": {
    "bet": 1,
    "payout": 0,
    "balance_before": {"real":10000,"bonus":0},
    "balance_after": {"real":9999,"bonus":0}
  },
  "next_state": null
}
  • spin.grid має порядок [reel][row]: 5 зовнішніх масивів по 3 символи.
  • spin.layout — ті самі клітинки в порядку [row][reel].
  • Store має обрати один формат; рекомендовано зберігати canonical spin.grid.
  • Символи: WILD, COIN, H1H4, L1L4. Символу BONUS немає.
  • Загальний виграш — тільки spin.payout. Не сумувати spin.wins[].payout.
  • Баланс оновлювати з wallet.balance_after, а не обчислювати локально.

3. COIN trigger

  • 6 або більше видимих COIN запускають Hold & Win.
  • COIN не має line payout; WILD не замінює COIN.
  • Початкові COIN переходять у feature як locked cells у тих самих позиціях.
  • Trigger spin може мати spin.payout: 0; активну feature визначає next_state, а не payout.
  • Debug mock може показати 6+ COIN, але на поточному mock route не відкриває feature. Lifecycle тестувати через звичайний RNG або debug.force_seed.

Перевірений DEV seed: mv-runtime-357. Він відкриває feature із locked indexes [6,7,8,9,10,13,14].

4. Cell model

index = column * 3 + row

reel 0:  0,  1,  2
reel 1:  3,  4,  5
reel 2:  6,  7,  8
reel 3:  9, 10, 11
reel 4: 12, 13, 14

Backend повертає всі 15 cells на trigger, кожному step, replay і settlement. Це authoritative feature snapshot. newly_landed — лише список індексів для анімації; frontend не повинен відновлювати game state через накопичення animation deltas.

type PrizeKind = "regular" | "mini" | "minor" | "major";

type HoldAndWinCell = {
  index: number;
  column: number;
  row: number;
  locked: boolean;
  prize: null | {
    kind: PrizeKind;
    multiplier: number; // multiplier of total bet, not currency
  };
};

5. Feature step

POST /game/v1/spin
x-api-key: <edge-key>
Authorization: Bearer <demo-bearer>
Idempotency-Key: <new UUID per logical feature step>
Content-Type: application/json

{"bet":1,"expected_hold_and_win_step_index":0}

bet потрібен для validation, але feature step не робить нової ставки: відповідь має wallet.bet: 0.

Правила:

  1. Брати expected_hold_and_win_step_index тільки з попереднього next_state.expected_step_index.
  2. Не інкрементувати index локально до підтвердженого 200.
  3. Один логічний step — один Idempotency-Key.
  4. Retry того самого step повторює той самий key, body та expected index.
  5. Одночасно дозволений тільки один feature request.

Feature response містить:

type HoldAndWinState = {
  kind: "hold_and_win";
  mechanic_version: "hold_and_win/v1";
  status: "open";
  step_index: number;
  expected_step_index: number;
  respins_remaining: number;
  columns: 5;
  rows: 3;
  locked_cells: number;
  total_cells: 15;
  accumulated_multiplier: number;
  cells: HoldAndWinCell[];
  newly_landed: number[];
  parent_round_id: string;
  next_step_id: string; // opaque; never construct client-side
  settlement?: {
    reason: "no_respins_remaining" | "full_grid";
    raw_multiplier: number;
    award_multiplier: number;
    capped: boolean;
    full_grid: boolean;
    jackpot_counts: Record<PrizeKind, number>;
  };
};

Multiplier fields — множники total bet. wallet.* і spin.payout — currency major units.

6. Store contract

type FrontGamePhase =
  | "bootstrap"
  | "ready"
  | "base-spin-pending"
  | "base-result"
  | "feature-active"
  | "feature-step-pending"
  | "feature-animating"
  | "settled"
  | "ambiguous-mock"
  | "error";

type MagicVaultStore = {
  phase: FrontGamePhase;
  sessionBearer: string | null;
  gameId: "magic-vault-5x3";
  gameConfigVersion: string | null;
  balance: { real: number; bonus: number } | null;
  grid: string[][];
  wins: unknown[];
  roundPayout: number;
  lastConfirmedRoundId: string | null;
  feature: HoldAndWinState | null;
  pendingOperation: null | {
    idempotencyKey: string;
    expectedStepIndex: number | null;
    body: unknown;
  };
};

Store actions повинні бути атомарними на рівні response:

  • bootstrap() — створити/відновити demo session;
  • spin(bet) — один paid base request;
  • applyBaseResponse(response) — оновити grid, wins, payout, balance, round і feature;
  • stepFeature() — створити pending operation з поточним expected index;
  • retryPendingFeatureStep() — повторити ті самі key/body/index;
  • applyFeatureResponse(response) — замінити повний snapshot і balance;
  • settleFeature(response) — показати award один раз, очистити pending operation;
  • clearSettledFeature() — перед наступним base spin видалити feature та step index;
  • stopAmbiguousMock() — зупинити queue і показати останній підтверджений round_id.

7. Animation contract

  1. Отримати повну відповідь.
  2. Анімувати тільки indexes із newly_landed.
  3. Після анімації замінити UI state повним cells snapshot.
  4. Якщо newly_landed.length > 0, показати reset respins до 3; інакше показати decrement.
  5. Не надсилати наступний step до завершення animation.
  6. На settlement один раз анімувати award_multiplier.
  7. Баланс завжди брати з wallet.balance_after.

Animation state ніколи не є game state. Якщо animation і backend snapshot розходяться, правильний backend snapshot.

8. Replay, reconnect і відомі ризики

Feature replay idempotent: той самий logical request із тим самим key/index повертає той самий round_id і state без нового RNG, wager, payout intent або credit.

Debug mock route

Idempotency-Key приймається, але ігнорується. Після timeout не робити auto-retry: зупинити scenario queue та показати останній підтверджений round_id.

Reconnect

restore.next_state зараз null навіть при відкритій feature. Після reload:

  1. повторно використати cached bearer;
  2. повторити останній підтверджений feature request із тим самим key та expected index;
  3. отримати PostgreSQL-backed replay;
  4. перемалювати всі 15 клітинок;
  5. продовжити з повернутого expected_step_index.

Stale index — money warning

Якщо feature вже завершена, але request builder залишив expected_hold_and_win_step_index, backend наразі може виконати звичайний paid base spin. Тому при отриманні settlement store зобов'язаний очистити feature state, pending operation і step index до дозволу наступної ставки.

Error messages

Частина orchestrator errors містить internal diagnostics, включно з parent round ID. Не показувати message гравцю. Мапити відомі code/prefix на локалізоване frontend повідомлення, а повну деталь залишати тільки в захищеному DEV logging.

9. Observed lifecycle

Seed mv-runtime-357:

trigger: 7 initial COIN, respins 3, wallet.bet 1
step 0: landed 1, respins 3, wallet.bet 0
step 1: landed 0, respins 2, wallet.bet 0
step 2: landed 1, respins 3, wallet.bet 0
step 3: landed 2, respins 3, wallet.bet 0
step 4: landed 0, respins 2, wallet.bet 0
step 5: landed 0, respins 1, wallet.bet 0
step 6: landed 0, respins 0, wallet.bet 0
settlement: no_respins_remaining, 60×

Money reconciliation from live DEV smoke:

10000 − 6 paid spins × 1 + 60 settlement = 10054

Replay і reconnect не створили нових rounds, intents або credits. Settlement створив рівно один credit intent і один wallet credit.

10. Debug inputs

Deterministic runtime trigger:

{"bet":1,"debug":{"force_seed":"mv-runtime-357"}}

Mock grid — тільки grid/payout test, не feature trigger:

{
  "bet": 1,
  "debug": {
    "mock": [
      ["COIN","COIN","COIN"],
      ["L1","L2","L3"],
      ["H1","L2","L3"],
      ["L4","L1","WILD"],
      ["L2","L3","L4"]
    ]
  }
}

Не використовувати неіснуючі поля force_grid, force_feature або force_win_band.

11. Publication update

Стандартний Math Studio from-pack flow виправлено: internal storage ID лишається version-scoped, а executable runtime ID зберігає канонічні WILD, COIN, H1H4, L1L4.

Рекомендований DEV publication command:

MATH_STUDIO_URL=... EDGE_API_KEY=... bash scripts/seed-magic-vault.sh

mvpublish deprecated і зберігається тимчасово як recovery tool. Frontend не викликає жоден із publication endpoints.

Три різні identities не можна змішувати:

  • a42ae358… — SHA-256 canonical YAML bytes у Math Spec/evidence;
  • ee67c5ee… — recursively canonicalized config для Studio evidence;
  • 61dc7d68… — runtime gate comparison identity.

12. Definition of done для frontend store

  • Cached demo bearer переживає reload.
  • Base spin блокує повторний click, поки request pending.
  • Grid рендериться як 5×3 без transposition bug.
  • Total win береться з spin.payout.
  • 6+ COIN відкривають feature через runtime response.
  • Trigger locked positions збігаються з base grid.
  • Store зберігає повний 15-cell snapshot.
  • newly_landed використовується тільки для animation.
  • Feature steps виконуються строго послідовно.
  • Runtime retry використовує той самий Idempotency-Key.
  • Mock timeout зупиняє queue без retry.
  • Reload відновлює feature через replay останнього step.
  • Settlement очищає stale step index до наступного base spin.
  • Wallet оновлюється з wallet.balance_after.
  • Raw backend diagnostics не показуються гравцю.