DocsCore concepts

Round Lifecycle

The order of events for one single-player bet, naming who acts, which library function runs, what the server stores and what the player can see at each point.

How it works tells this story in plain language. This page is the same story for someone writing the server: every step in order, with the function that runs and the state it leaves behind. "Demo" below means Galabet's demo API in apps/api, which is not deployed; it's cited because it is real code that does each step, and where it does something the library doesn't require, the table says so.

The library's part is smaller than the sequence suggests. Of the nine steps, five call @galabet/fair, or six if you sign records. Locking, nonce reservation, storage and money are the application's.

Sequence for a One-Shot Bet

Dice, Limbo, Roulette, Wheel, Plinko and Keno finish in one request, and the result can go straight back to the player.

#EventWho actsLibrary functionStored afterwardsPlayer can see
1Seed made and committedServercreateServerSeed, commitServer seed, commitment, bet count 0The commitment
2Client seed setServer by default, the player whenever they likecreateClientSeed, or assertClientSeed on the player's textClient seedClient seed, commitment, next nonce
3Bet arrivesPlayer sends, server locks the sessionNoneA lock with a 30 second leaseNothing new. A second action sent meanwhile gets 409
4Nonce reservedServerNone. The demo uses a Redis script that checks the commitment and runs INCRCounter raised by oneNothing yet
5Outcome derivedServerplay, which runs deriveFloats and then the game's mapperNothingNothing yet
6Record built and publishedServerNone, a record is a plain object. signRecord here if you signRecord at the head of the history list, bet count raised by oneThe whole record, result included, no serverSeed
7Bet settledApplicationNoneBalancePayout, and a receipt if you issue one
8Seed rotatedThe player asks, the server does itcreateServerSeed, commitOld seed filed under its commitment, new seed and commitment, counter set to 0 in the same write, bet count 0Old seed, its commitment, how many bets it covered, the next commitment
9Record checkedAnyoneverifyRecord or inspectRecordNothingok, or the reasons it isn't

Steps 3 to 7 repeat for every bet, with the nonce one higher each time. Steps 1 and 2 happen once per seed, and step 8 leads back to step 1 inside the same request.

Some of those cells need a sentence more.

Step 1 has to be complete, commitment on the player's screen, before step 3 can happen even once. The demo does both at session creation and returns a view of the session with serverSeed removed and the current nonce added.

Step 2 is not as separate from step 1 as the table makes it look. In the demo a changed client seed throws the server seed away and starts a new one, revealing the old one if any bets were made under it. Client seeds explains why.

The demo debits the stake between steps 3 and 4, after the lock and before the nonce, for reasons Order of Operations gives. The nonce is reserved before play runs, so a play that throws leaves a nonce with no record. That gap is harmless and expected.

At step 5 play returns three things: result, cursor and the floats it consumed. The first two go into the record. The floats don't, because anyone with the seeds can regenerate them.

The demo doesn't sign records, so its step 6 is only a push onto a Redis list trimmed to the last 100. If you do sign at step 6, that signature won't verify against the record as it looks after step 8. The Reveal Problem has the two ways round it.

Step 8 has preconditions of its own, and Rotation and reveal lists them. The record stored at step 6 is never rewritten. The demo attaches serverSeed on the way out of its history endpoint, by looking up each record's commitment among the revealed seeds.

Step 9 calls play again. It's the same function as step 5 with the same inputs, run by somebody else, and the whole scheme is the claim that the two calls agree. verifyRecord runs verifyCommitment first and then compares result and cursor. Before step 8 it can't do either and says so: server seed not revealed yet; verify after rotation.

Concealed Rounds

Mines can't follow the table above, because its result is the board. The derivation is identical. What moves is publication: step 6 splits in three, and everything between the first part and the last is answered from stored state with no library call at all.

#EventLibrary functionStored afterwardsPlayer can see
6aRecord built and withheldNoneRecord, stake, mine count, empty pick list and over: false, in one value per session. Nothing in historyThe record without result
6bTile picked, any number of timesNone. The tile is looked up in the stored record.resultPick list. A mine goes straight to 6cHit or safe, current multiplier, next multiplier
6cRound ends: a mine, a cash-out, every safe tile opened, or a forfeitNoneRecord pushed to history, then over: true with outcome and payout. A cash-out is credited once, before eitherThe full record, board included, and how the round ended

Each of 6a, 6b and 6c takes the session lock, the same one as step 3. From 6a until 6c the demo refuses step 8 with a 400, since a revealed seed plus the public client seed and nonce is the board. It refuses a client seed change for the same reason and a second board with finish the current game first. A cash-out with no picks is refused too.

A forfeit is the way to reach 6c without playing on. POST /api/demo/games/mines/forfeit ends the round as lost, with outcome: "forfeit" and a payout of 0, publishes the record like any other ending, and lets step 8 through again. It works with no picks at all. The cash-out ending has one extra state. Once its credit is written, 6b and a forfeit are both refused with round already cashed out; repeat the cash-out to close the board, even if the board never got saved as over. The repeated cash-out pays nothing more and reports the payout stored with the credit.

play runs once, at step 5, and never again for that round on the server. A pick can't be answered from a different board than the one in the record, because there is no second derivation to disagree with.

After 6c the round rejoins the main sequence at step 8, and the board becomes checkable against the commitment at step 9 like any other result. Concealed games covers the bookkeeping, including the leak that the withheld publication fixed, and says what a Blackjack or Hi-Lo engine would add. The demo has neither: it returns those records with the whole deck at step 6.

Record Fields at Three Points

The "Player can see" column, for one Mines round under the public inputs.

visible.mjs
import { commit, play, verifyRecord } from '@galabet/fair';

const serverSeed = '5c1f7d3e8a2b4c6d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d';
const { commitment } = await commit(serverSeed);
const { result, cursor } = await play({ game: 'mines', params: { mines: 3 }, serverSeed, clientSeed: 'galabet', nonce: 42 });

const stored = { spec: 'GFS/1.0', profile: 'single-player', game: 'mines', params: { mines: 3 }, commitment, clientSeed: 'galabet', nonce: 42, cursor, result, at: 1790000000000 };
const { result: _board, ...live } = stored;

console.log('6a', Object.keys(live).join(' '));
console.log('6c', Object.keys(stored).join(' '));
console.log('   ', (await verifyRecord(stored)).reasons[0]);
console.log('8 ', Object.keys({ ...stored, serverSeed }).join(' '));
console.log('9 ', (await verifyRecord({ ...stored, serverSeed })).ok, stored.result);
Output
6a spec profile game params commitment clientSeed nonce cursor at
6c spec profile game params commitment clientSeed nonce cursor result at
    server seed not revealed yet; verify after rotation
8  spec profile game params commitment clientSeed nonce cursor result at serverSeed
9  true [ 9, 17, 22 ]

For a one-shot game there is no 6a line. The player goes straight to the second one.

Interrupted Rounds

A process can die between any two rows. What that costs depends on the row: after the debit and before the record, a stake is gone with nothing to show for it; after the credit and before the state is saved, a retry must not pay twice. A Process That Dies Mid-Bet goes through each case, and the repair rests on one property of step 5. play is a pure function of values you can write down at step 4, so a sweeper that finds a half-finished bet can derive the identical outcome and finish it.

An abandoned Mines board waits for the player. Nothing closes it on a timer and step 8 stays refused while it's open, but a forfeit takes it to 6c at any time. Its key expires no earlier than the session's. If the player never comes back, the board goes with the session, the seed is never revealed and that nonce's record is never published.