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}