Search Apps Documentation Source Content File Folder Download Copy Actions Download 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}