Package accrual is stock that fills at a rate while nobody is playing: the resource field of an idle game, the warehouse of a 4X, anything whose state is a function of how long it has been left alone.
The shape is always the same. Store what you had and when, and compute the rest on demand; a player who does nothing for a month costs nothing, and the realm needs no cron, no keeper and no per-block hook. That much is obvious. What is not obvious is that the obvious implementation is wrong in three ways, each of which has been observed in this repository rather than reasoned about.
1. Re-anchoring to now makes acting more often pay
Written naively, a claim advances the anchor to now and computes elapsed/Period whole periods of production. The division truncates, so every claim silently forfeits the part-period it lands in, and a player who claims twice forfeits twice. Run in reverse on a decaying stat it is worse than unfair, it is free: r/moul/x/daily/tamagotchi decayed by elapsed/2 and elapsed/3, so at elapsed == 1 it decayed by nothing at all, and feeding once per block made the pet immortal while every other cadence died inside 240 blocks. It was fixed in #221.
The invariant that rules it out is worth stating on its own, because it is the whole contract of this package:
Example
1Advance(s, a, c) == Advance(Advance(s, a, b), b, c) for a <= b <= c
How often you call must not change where you end up. Rate.Advance gets there by advancing the anchor only by the WHOLE periods it paid for, leaving the remainder on the clock rather than throwing it away. TestSplitInvariant asserts it over pseudo-random partitions rather than over a handful of hand-picked spans, because the hand-picked spans are exactly the ones a wrong implementation already passes.
2. A view that does not share the write path's arithmetic drifts from it
The same realm had a second copy of the decay for its Render, so the page showed a pet the next call would not honour. Two implementations of one rule means one of them is the stale one somebody acts on. Here there is one function: Rate.At is Rate.Advance with the anchor discarded, so a view cannot disagree with a write even in principle.
Render runs under a query gas limit and may not write, so whatever it computes has to be bounded however long the player was away. Anything iterative, stepping a simulation once per block, is fine in a transaction and unusable in a page: the player who comes back after a month is exactly the one whose page times out. Everything here is O(1) in elapsed time, which is the property that makes a live-updating page possible, not an optimisation.
Times are int64 and the unit is the caller's, block heights or unix seconds, as long as it is consistent. Prefer a timestamp: a block-height rate drifts in wall-clock terms every time the chain's block time moves, and gno.land's has moved from about 4.1s to 3.405s inside one month. Nothing here reads the chain, so a realm can test a year of its own economy without one.
A game built on this package is at r/moul/x/games/idle(/r/moul/x/games/idle/v0).
r/moul/x/daily/tamagotchi: /r/moul/x/daily/tamagotchi/v0