Carry
The one thing a round leaves behind.
A round is a pure function of its state and its randomness. Carry is how the next round gets the state: an opaque string the math returns at the end of a round and receives at the start of the following one. Progress meters, a bonus counter, a gamble pot, "the next spin is a free one" all live in it, and nothing else in the engine looks inside.
The loop
The math writes it, the engine passes it to the wallet with the money, the wallet stores it, and the engine reads it back when the session next opens.
// the math returns it with the outcome
play(prev, ctx) {
const meter = prev ? JSON.parse(prev).meter : 0;
return {
multiplier: 0,
ops: [],
type: "loss",
carry: JSON.stringify({ meter: meter + 1 }),
};
}
// the engine hands the previous one back on the next round
play(prev, ctx) // prev === '{"meter":1}' Two rules make that work. The math never stores anything itself, because a math instance is shared by every player on the server: state kept in a module variable belongs to whichever session touched it last. And the format is the math's own business, since the engine treats it as an opaque string.
The wallet stores it, not the engine
The orchestrator keeps carry in memory for a live session, but that copy is a cache. The durable one goes to the wallet inside the same call that moves the money, and comes back on openSession.
// simple round: roundState IS the carry, since it opens and closes at once
settleSimple({ sessionId, bet, win, roundState, mathVersion });
// complex round: carry is separate from the round's own final state
closeComplex({ sessionId, roundId, finalState, carry, mathVersion });
// and it comes back next time the session opens
openSession(sid) -> { balance, carry, mathVersion, ... } That is ADR-004, and it is what makes the RGS stateless: any pod can serve the next round, because the state arrives with the session rather than living on the machine that dealt the last one. It also means carry rides the same write as the balance, so a round that never paid never leaves progress behind. That is Guarantee 1, and it is why "just cache it" is not an alternative.
An adapter that drops carry produces a game that works perfectly until a player reconnects, and then quietly restarts their progress. The conformance suite checks the round trip for exactly that reason.
Reconnects
A player who drops mid-session and comes back gets their carry from the wallet on the fresh INIT. A player who drops mid-round is a different case: the round is still open, so the engine resumes it from the round's own state and the carry is untouched until that round closes.
A new math version discards it
Carry is written by one version of a math and read by another only if you let it. The engine stores mathVersion beside the carry, and on INIT it compares that against the loaded math:
// SessionInfo comes back with carry + mathVersion
// if mathVersion !== the loaded math's version, the carry is dropped
// and the session starts fresh, with a warn-level log line Dropping it is the conservative choice, and the alternative is worse than it sounds: a meter written as { level: 3 } by yesterday's math and read as { level, multiplier } by today's is not a crash, it is a game paying the wrong thing to one player in a way nothing else will notice. Bump math.version whenever the carry's shape changes, and the engine handles the rest.
Keep it small, and keep it yours
- Small. It crosses the wire on every settle and is stored per session. A JSON blob of a few fields is right; a board snapshot is not.
- Yours. The engine never parses it. Version it inside if you want migrations rather than discards.
- Not a ledger. Money lives in the wallet. Carry is what the math needs to keep playing, not a record of what it paid.
- Not a session. A promo pool, a balance and an open round are engine concepts with their own fields.
A simple round has no separate carry field on the wire: roundState doubles as it, because a simple round opens and closes in the same call, so this round's final state is what the next one starts from. A complex round has both, and they are different things: finalState is what this round ended as, carry is what the next one begins with.