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

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}