Skip to content Search Apps Documentation Source Content File Folder Download Copy Actions State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

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}