idle.gno
9.11 Kb · 281 lines
1// Package idle is a mine that keeps working while you are not looking at it.
2//
3// Ore accrues at a rate set by your level. Claim banks it, Upgrade spends the
4// bank to dig faster, and at level 10 you may Prestige: start over at level 1
5// carrying a permanent multiplier. There are no turns, no block hook and no
6// keeper. The realm stores what you had and when, and everything else is
7// arithmetic done at the moment somebody asks.
8//
9// The three decisions that make it a game rather than a counter:
10//
11// 1. THE MINE IS SMALL. It holds two hours of production and then spills.
12// Without a ceiling the optimal play is to never log in and claim once at
13// the end, which is not a game, it is a savings account. The ceiling is
14// what makes showing up the skill, and it scales with the rate, so two
15// hours stays two hours at every level.
16// 2. UPGRADES ARE QUADRATIC AGAINST LINEAR PRODUCTION. Level L costs
17// UpgradeBase*L*L and pays L per period, so the next level always takes
18// about UpgradeBase*L periods to afford: the curve bends without ever
19// stalling. Linear against linear never ends, and exponential against
20// linear dies at level four.
21// 3. PRESTIGE EXISTS. Without a reset the leaderboard is settled by whoever
22// started first and nobody else has a reason to join. Trading a level-10
23// mine for a permanent multiplier means a player who starts today has a
24// reachable ceiling.
25//
26// Time is unix seconds from the block, not block height. A height-denominated
27// economy silently repriced itself every time the chain's block time moved,
28// and gno.land's has moved from about 4.1s to 3.405s inside a month.
29//
30// Nothing here rolls dice. Everything a player can do is a pure function of
31// when they did it, so there is no block hash to grind and no ordering to
32// front-run, and the page can tell you exactly what an action will produce
33// before you sign it.
34//
35// The arithmetic is [p/moul/x/games/accrual](/p/moul/x/games/accrual/v0),
36// which owns the one rule that makes lazy time honest: how often you claim
37// must not change what you end up with.
38package idle
39
40import (
41 "errors"
42 "strconv"
43 "time"
44
45 "gno.land/p/moul/x/games/accrual/v0"
46 "gno.land/p/nt/avl/v0"
47)
48
49// The economy. Ore is a bare integer: there is no token here, and nothing
50// leaves the realm.
51const (
52 // Period is one minute of unix time. A level-1 mine digs one ore a minute.
53 Period = int64(60)
54 // CapPeriods is how much production the mine holds before it spills, in
55 // periods. It multiplies the rate, so the fill time is level-independent.
56 CapPeriods = int64(120)
57 // UpgradeBase sets the cost curve: level L costs UpgradeBase*L*L to leave.
58 UpgradeBase = int64(10)
59 // PrestigeAt is the level at which a reset becomes available.
60 PrestigeAt = int64(10)
61 // MaxLevel is a safety rail on the cost curve, not a mechanic: it is far
62 // beyond anything reachable, and keeps every product inside int64.
63 MaxLevel = int64(1000)
64 // MaxPrestige bounds the multiplier for the same reason.
65 MaxPrestige = int64(100)
66)
67
68var errNoMine = errors.New("idle: you have no mine yet, call Start first")
69
70// mine is one player's state. pending and anchor are the accrual pair: what
71// was in the mine, and as of when. Everything else is bookkeeping.
72type mine struct {
73 owner address
74 level int64
75 prestige int64
76 pending int64 // ore in the mine at anchor, capped
77 anchor int64 // unix seconds; the accrual clock, not "last seen"
78 banked int64 // claimed ore, spendable, uncapped
79 lifetime int64 // every ore ever claimed, never reset by Prestige
80 claims int64
81 started int64
82}
83
84var mines avl.Tree // address string -> *mine
85
86// now is the chain's own clock in unix seconds. time.Now() in a realm returns
87// the block timestamp, so it is consensus-deterministic rather than wall time.
88func now() int64 { return time.Now().Unix() }
89
90// rate is the accrual Rate for a level and prestige. It is rebuilt on each
91// call rather than stored, so a mine's stored state cannot disagree with the
92// economy after a redeploy changes a constant.
93func rate(level, prestige int64) accrual.Rate {
94 perPeriod := level * (1 + prestige)
95 r, err := accrual.New(perPeriod, Period, CapPeriods*perPeriod)
96 if err != nil {
97 panic("idle: " + err.Error())
98 }
99 return r
100}
101
102// upgradeCost is what it costs to leave the given level.
103func upgradeCost(level int64) int64 { return UpgradeBase * level * level }
104
105func get(owner address) (*mine, bool) {
106 v := mines.Get(owner.String())
107 if v == nil {
108 return nil, false
109 }
110 return v.(*mine), true
111}
112
113// settle moves the mine's clock to at, banking nothing. It is the only place
114// that writes the accrual pair, and it is deliberately the same call Render
115// makes read-only, so the page can never promise a number an action will not
116// honour.
117func (m *mine) settle(at int64) {
118 pending, anchor, err := rate(m.level, m.prestige).Advance(m.pending, m.anchor, at)
119 if err != nil {
120 panic("idle: " + err.Error())
121 }
122 m.pending, m.anchor = pending, anchor
123}
124
125// view is settle without the write, for Render and for the read helpers.
126func (m *mine) view(at int64) int64 {
127 pending, err := rate(m.level, m.prestige).At(m.pending, m.anchor, at)
128 if err != nil {
129 panic("idle: " + err.Error())
130 }
131 return pending
132}
133
134// fullAt is the unix second at which this mine starts wasting production.
135func (m *mine) fullAt(at int64) int64 {
136 pending := m.view(at)
137 full, err := rate(m.level, m.prestige).Full(pending, at)
138 if err != nil {
139 panic("idle: " + err.Error())
140 }
141 return full
142}
143
144// start opens a mine for owner. Non-crossing so tests can drive the clock.
145func start(owner address, at int64) string {
146 if _, ok := get(owner); ok {
147 panic("idle: you already have a mine")
148 }
149 mines.Set(owner.String(), &mine{
150 owner: owner,
151 level: 1,
152 anchor: at,
153 started: at,
154 })
155 return "Mine opened at level 1. It digs " + strconv.FormatInt(Period, 10) +
156 "s per ore and holds " + strconv.FormatInt(CapPeriods, 10) + " before it spills."
157}
158
159func mustGet(owner address) *mine {
160 m, ok := get(owner)
161 if !ok {
162 panic(errNoMine.Error())
163 }
164 return m
165}
166
167// claim banks everything the mine has dug.
168func claim(owner address, at int64) string {
169 m := mustGet(owner)
170 m.settle(at)
171 if m.pending == 0 {
172 panic("idle: nothing to claim yet")
173 }
174 got := m.pending
175 m.pending = 0
176 m.banked += got
177 m.lifetime += got
178 m.claims++
179 return "Claimed " + strconv.FormatInt(got, 10) + " ore. Banked: " +
180 strconv.FormatInt(m.banked, 10) + "."
181}
182
183// upgrade spends the bank to raise the level.
184func upgrade(owner address, at int64) string {
185 m := mustGet(owner)
186 m.settle(at)
187 if m.level >= MaxLevel {
188 panic("idle: this mine is at the maximum level")
189 }
190 cost := upgradeCost(m.level)
191 if m.banked < cost {
192 panic("idle: upgrade costs " + strconv.FormatInt(cost, 10) +
193 " ore, you have " + strconv.FormatInt(m.banked, 10))
194 }
195 m.banked -= cost
196 m.level++
197 return "Level " + strconv.FormatInt(m.level, 10) + ". Digging " +
198 strconv.FormatInt(m.level*(1+m.prestige), 10) + " ore per period."
199}
200
201// prestige trades the mine for a permanent multiplier.
202func prestige(owner address, at int64) string {
203 m := mustGet(owner)
204 m.settle(at)
205 if m.level < PrestigeAt {
206 panic("idle: prestige unlocks at level " + strconv.FormatInt(PrestigeAt, 10) +
207 ", you are at " + strconv.FormatInt(m.level, 10))
208 }
209 if m.prestige >= MaxPrestige {
210 panic("idle: this mine is at the maximum prestige")
211 }
212 m.prestige++
213 m.level = 1
214 m.pending = 0
215 m.banked = 0
216 m.anchor = at
217 return "Prestige " + strconv.FormatInt(m.prestige, 10) +
218 ". Back to level 1, digging " + strconv.FormatInt(1+m.prestige, 10) +
219 " ore per period. Lifetime ore: " + strconv.FormatInt(m.lifetime, 10) + "."
220}
221
222// Start opens a mine for the caller. One per address, forever: Prestige is the
223// only way to start over, and it keeps your lifetime total.
224func Start(cur realm) string {
225 return start(cur.Previous().Address(), now())
226}
227
228// Claim banks whatever the mine has dug since the last claim. Claiming often
229// and claiming rarely pay exactly the same, by construction: see accrual.
230func Claim(cur realm) string {
231 return claim(cur.Previous().Address(), now())
232}
233
234// Upgrade spends banked ore to raise the level, which raises both the rate and
235// the ceiling.
236func Upgrade(cur realm) string {
237 return upgrade(cur.Previous().Address(), now())
238}
239
240// Prestige resets a level-10 mine to level 1 for a permanent +1 multiplier.
241// Banked and pending ore are burned; the lifetime total is not.
242func Prestige(cur realm) string {
243 return prestige(cur.Previous().Address(), now())
244}
245
246// Pending reports what is sitting in an address's mine right now, unclaimed.
247func Pending(owner address) int64 {
248 m, ok := get(owner)
249 if !ok {
250 return 0
251 }
252 return m.view(now())
253}
254
255// Banked reports an address's spendable ore.
256func Banked(owner address) int64 {
257 m, ok := get(owner)
258 if !ok {
259 return 0
260 }
261 return m.banked
262}
263
264// Level reports an address's level and prestige.
265func Level(owner address) (int64, int64) {
266 m, ok := get(owner)
267 if !ok {
268 return 0, 0
269 }
270 return m.level, m.prestige
271}
272
273// FullAt reports the unix second at which an address's mine starts wasting
274// what it digs, which is the only deadline in this game.
275func FullAt(owner address) int64 {
276 m, ok := get(owner)
277 if !ok {
278 return 0
279 }
280 return m.fullAt(now())
281}