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

render.gno

4.83 Kb · 174 lines
  1package idle
  2
  3import (
  4	"sort"
  5	"strconv"
  6	"strings"
  7
  8	"gno.land/p/moul/kit/ui/v0"
  9)
 10
 11// Render shows every mine, or one mine's card at /r/.../idle:<address>.
 12//
 13// The numbers move on their own: the same URL renders differently a minute
 14// later because the mine kept digging, and nothing wrote to the chain in
 15// between. That is the whole point of the accrual pair, and it is why every
 16// figure here comes from the read-only view rather than from stored state.
 17func Render(path string) string {
 18	at := now()
 19	if addr := strings.TrimSpace(path); addr != "" {
 20		return card(address(addr), at)
 21	}
 22	return board(at)
 23}
 24
 25// byLifetimeThenOwner ranks by lifetime ore, then by address so the order is
 26// identical on every node.
 27type byLifetimeThenOwner []*mine
 28
 29func (r byLifetimeThenOwner) Len() int      { return len(r) }
 30func (r byLifetimeThenOwner) Swap(i, j int) { r[i], r[j] = r[j], r[i] }
 31func (r byLifetimeThenOwner) Less(i, j int) bool {
 32	if r[i].prestige != r[j].prestige {
 33		return r[i].prestige > r[j].prestige
 34	}
 35	if r[i].lifetime != r[j].lifetime {
 36		return r[i].lifetime > r[j].lifetime
 37	}
 38	return r[i].owner.String() < r[j].owner.String()
 39}
 40
 41func board(at int64) string {
 42	var b strings.Builder
 43	b.WriteString("# The Mine\n\n")
 44	b.WriteString("Ore accrues while you are away. The mine holds " +
 45		strconv.FormatInt(CapPeriods, 10) + " periods of production and then spills, so the only thing that costs you anything is not showing up.\n\n")
 46
 47	all := []*mine{}
 48	mines.Iterate("", "", func(_ string, v any) bool {
 49		all = append(all, v.(*mine))
 50		return false
 51	})
 52	sort.Sort(byLifetimeThenOwner(all))
 53
 54	t := ui.NewTable("#", "Miner", "Lv", "Prestige", "In the mine", "Banked", "Lifetime")
 55	for i, m := range all {
 56		pending := m.view(at)
 57		full := ""
 58		if r := m.fullAt(at); r > at {
 59			full = " (" + span(r-at) + " to spill)"
 60		} else {
 61			full = " (**full**)"
 62		}
 63		t.Row(
 64			ui.Podium(i)+strconv.Itoa(i+1),
 65			ui.Addr(m.owner),
 66			strconv.FormatInt(m.level, 10),
 67			strconv.FormatInt(m.prestige, 10),
 68			strconv.FormatInt(pending, 10)+full,
 69			strconv.FormatInt(m.banked, 10),
 70			strconv.FormatInt(m.lifetime, 10),
 71		)
 72	}
 73	b.WriteString(t.OrEmpty("Nobody is digging yet."))
 74	b.WriteString("\n")
 75	b.WriteString(ui.Join(" | ",
 76		ui.Action("Open a mine", "Start"),
 77		ui.Action("Claim", "Claim"),
 78		ui.Action("Upgrade", "Upgrade"),
 79		ui.Action("Prestige", "Prestige"),
 80	))
 81	b.WriteString("\n")
 82	return b.String()
 83}
 84
 85func card(owner address, at int64) string {
 86	m, ok := get(owner)
 87	if !ok {
 88		return "# The Mine\n\n" + ui.Empty("No mine at "+ui.Addr(owner)+".")
 89	}
 90
 91	pending := m.view(at)
 92	r := rate(m.level, m.prestige)
 93	var b strings.Builder
 94
 95	b.WriteString("# " + ui.Addr(m.owner) + "'s mine\n\n")
 96	b.WriteString("Level **" + strconv.FormatInt(m.level, 10) + "**")
 97	if m.prestige > 0 {
 98		b.WriteString(", prestige **" + strconv.FormatInt(m.prestige, 10) + "**")
 99	}
100	b.WriteString(", digging **" + strconv.FormatInt(r.Amount, 10) + "** ore every " +
101		span(Period) + ".\n\n")
102
103	t := ui.NewTable("", "")
104	t.Row("In the mine", strconv.FormatInt(pending, 10)+" / "+strconv.FormatInt(r.Cap, 10))
105	if full := m.fullAt(at); full > at {
106		t.Row("Spills in", span(full-at))
107	} else {
108		t.Row("Spills in", "**already full, every period is being wasted**")
109	}
110	t.Row("Banked", strconv.FormatInt(m.banked, 10))
111	t.Row("Lifetime ore", strconv.FormatInt(m.lifetime, 10))
112	t.Row("Claims", strconv.FormatInt(m.claims, 10))
113	t.Row("Digging for", span(at-m.started))
114	if m.level < MaxLevel {
115		cost := upgradeCost(m.level)
116		short := cost - m.banked
117		next := strconv.FormatInt(cost, 10) + " ore"
118		if short > 0 {
119			next += " (" + strconv.FormatInt(short, 10) + " short)"
120		} else {
121			next += " (**affordable now**)"
122		}
123		t.Row("Next level", next)
124	}
125	if m.prestige < MaxPrestige {
126		if m.level >= PrestigeAt {
127			t.Row("Prestige", "**available**, and it burns "+
128				strconv.FormatInt(m.banked+pending, 10)+" ore")
129		} else {
130			t.Row("Prestige", "at level "+strconv.FormatInt(PrestigeAt, 10)+
131				", "+strconv.FormatInt(PrestigeAt-m.level, 10)+" to go")
132		}
133	}
134	b.WriteString(t.String())
135	b.WriteString("\n")
136	b.WriteString(ui.Join(" | ",
137		ui.Action("Claim", "Claim"),
138		ui.Action("Upgrade", "Upgrade"),
139		ui.Action("Prestige", "Prestige"),
140	))
141	b.WriteString("\n")
142	return b.String()
143}
144
145// span renders a number of seconds the way a player reads it. It rounds down
146// and stops at two units, because "2h 3m 11s to spill" is noise when the
147// question is whether to claim now or after lunch.
148func span(secs int64) string {
149	if secs <= 0 {
150		return "0s"
151	}
152	units := []struct {
153		size int64
154		name string
155	}{
156		{86400, "d"},
157		{3600, "h"},
158		{60, "m"},
159		{1, "s"},
160	}
161	parts := []string{}
162	for i, u := range units {
163		if secs < u.size {
164			continue
165		}
166		n := secs / u.size
167		secs -= n * u.size
168		parts = append(parts, strconv.FormatInt(n, 10)+u.name)
169		if len(parts) == 2 || i == len(units)-1 {
170			break
171		}
172	}
173	return strings.Join(parts, " ")
174}