Skip to content

Reward wheel R1 ​

The server supports versioned R1 tables with ticket and resource prizes. The browser client displays the server-selected outcome. client_ready requires both the mechanic descriptor and a built release supporting the rule version. A project can enable the wheel only when this check passes and its table is published. Publishing a table does not activate it. Material prizes, campaign reservations and server ticket grants remain unavailable.

Session API ​

All calls require a session bearer token. Anonymous tokens are accepted only for free_play. Writes require the API Origin, as described in game runs. Reads use a separate 30-per-minute limit.

MethodPathPurpose
GET/v1/wheel?wheel_id=dailySafe sectors, current odds, rules, published ticket_cost, entitlement and unrevealed result
POST/v1/wheel/spinsAtomically consume one right and grant the selected R1 prize
GET/v1/wheel/spins/{spin_id}Recover the caller's saved result, including after disabling the wheel
POST/v1/wheel/spins/{spin_id}/seenIdempotently acknowledge the result
GET/v1/game-runs/{run_id}/rewardCommon reward status; committed R1 grants are confirmed

The spin body is at most 512 bytes and contains exactly:

json
{"spin_id":"<fresh UUID>","wheel_id":"daily","wheel_version":1,"source":"daily_free"}

wheel_id is daily for an enabled lobby wheel or site for wheel_only. source is daily_free or ticket. The server calculates the prize and animation stop; clients cannot supply weights or an outcome. Successful responses include spin_id, run_id, run_ref, pinned wheel_version, segment_index, segment_id, stop_fraction, prize, reward, server_time, segments_state and the remaining entitlement.

Retry the same UUID and body after transport failure. Exact durable repeats return the saved response before rate checks; changing the body returns 409 idempotency_conflict. New attempts are limited to six per minute and 30 per hour per project/player. Insufficient rights return 409 spin_unavailable; a stale version returns 409 wheel_changed with the new safe view. A foreign result returns 404.

One free right is available per project-local calendar day. Defaults are five total spins per wheel/day and one ticket per paid spin. Calendar changes take effect at the next midnight in the previous IANA timezone. The stored day and timezone remain attached to each result.

ticket_cost is configurable. Use the value returned by GET /v1/wheel to compare the player's ticket balance; do not assume that one ticket is enough. The server still checks the current table, daily cap and balance on every spin.

New server-calculated game wins earn one wheel_ticket in the same command transaction, with key ticket:{run_id} and a shared limit of three per project/player/local day across game mechanics. Command repeats, replayed levels, losses and wheel outcomes earn none. Tickets work even when the ordinary economy is disabled. wheel.spun counts as a play for account usage; anonymous players remain excluded from the paid MAU quota.

wheel_ticket is reserved: it cannot be configured as an ordinary resource or booster price, or granted through the legacy owner/server economy APIs. Changing the economy cannot disable or remove resources used by a published wheel. The configuration change and its audit entry commit together.

The database transaction commits the right, balance changes, immutable result, closed game run, fraud flags and events together. Infrastructure, entropy or balance failures roll the entire transaction back. Successful flags do not expose risk data to players or cancel an R1 grant.

Player client ​

The daily wheel opens as a sheet over the lobby and refreshes lobby rights when closed. In wheel_only, the site wheel occupies the whole iframe and loads without the lobby or Phaser engine. Both use the same server API and show the table's odds and rules.

Before sending a spin, the client saves its UUID and exact request body for recovery. An interrupted request repeats that attempt; it does not create a second paid spin. A pending server result is recovered on reopening. If its table version differs from the current view, the client shows the saved prize without animating a stop against the wrong sectors. Acknowledgement happens after the result is displayed.

The result supports pending, checking, confirmed, delayed, unavailable and none. R1 ticket/resource grants are committed by the server and return confirmed; the other states share the common reward UI contract. Pending checks stop when the view closes or after 60 seconds. Sound and reduced motion follow the shared widget settings. Closing or destroying the view ignores late replies and releases its animations, timers and listeners.

The spin and rules controls are buttons usable with Enter or Space. Escape closes the rules view and returns focus to its opener; closing the daily sheet returns focus to the lobby wheel button. Reopening fetches the current server entitlement and recovers an unrevealed result. Closing a view does not undo a committed spin, refund a spent ticket or reset the daily allowance.

Owner API ​

Owner endpoints use the existing admin session, CSRF origin and two-factor policy. All paths begin /v1/admin/projects/{project_id}/wheels.

MethodSuffixPurpose
GET / PUT/{wheel_id}/draftLoad or validate/save the editable table
POST/{wheel_id}/publishRevalidate and publish a new immutable version with audit
GET/{wheel_id}/statsAggregate counts by local date, version and sector; up to 1000 rows
POST/{wheel_id}/simulateSample 10,000 draws from the saved draft/current table, without grants
GET / PUT/settingsRead the calendar and supported zones or schedule {"timezone":"Europe/Berlin"}

Draft fields are free_spins_per_day (R1 requires 1), max_spins_per_day, ticket_cost, segments and rules:{text,organizer:null}. Budget fields remain null. Tables contain 6–12 sectors with a positive fallback. Rules are plain text. Resource prizes require an enabled configured resource. Public views omit raw weights; owner views include them and server-computed percentages. Preview and simulation never call the public spin endpoint.

Repeated publication without a new draft returns the current version and does not duplicate the audit entry. Published content is immutable; changes must go through a new draft. Project display switches continue to require a ready browser client and a published configuration.

Admin editor ​

Open Engagement → Колесо наград (/wheel) and select the daily or site table. Edit 6–12 sectors, their labels, standard icons/colors, weights, ticket/resource prizes and one positive consolation fallback. Drag rows or use the up/down buttons to change their order without changing sector IDs. Set daily limits and plain-text rules, then save a draft or publish it. Publish saves the current form first and waits for server validation. The draft response includes draft.hash; publication requires {"expected_draft_hash":"<hash returned by save>"}. The server compares the content hash under the publication lock. Another editor's intervening save returns 409 wheel_draft_changed without publishing or adding an audit entry; reload and review the table before trying again.

Percentages come from the server and disappear while the form has unsaved changes. The local demonstration is labelled as a preview: it shows motion without consuming rights or granting prizes, and does not represent the weight distribution. The separate 10,000-spin server simulation uses the saved table and creates no real spins. Statistics group actual results by date, published version and sector.

The project calendar panel shows the effective zone, next local day and any pending change. Scheduling a zone affects both tables at the next midnight in the previous zone. Switching project or table discards late responses from the previous editor. Publication remains separate from activating the player-facing client. The display switches require client readiness and the corresponding published daily or site table.

Local verification ​

The canonical build.sh → swarm-start.sh stack smoke saves/publishes an owner table and checks simulation/calendar readiness. Release gate #28 creates and cleans up its own wheel fixture, checks real HTTP spins, byte-identical replay, UUID namespace, recovery, player isolation and stale/disabled/anonymous gates. Its 20-request HTTP race expects one success, five unavailable responses and 14 rate-limit responses, preserving the six-per-minute ingress limit. The SQL integration suite separately checks the atomic 20-spin race (one success, 19 unavailable), rollback, flags, erasure and actual free slots across midnight, DST and deferred timezone changes. Server checks and client unit tests do not replace browser, device and visual acceptance of the player-facing client.

Internal & integration documentation