collatz.gno
5.74 Kb · 222 lines
1package collatz
2
3import (
4 "chain"
5 "sort"
6 "strconv"
7 "strings"
8
9 "gno.land/p/moul/md/v0"
10 "gno.land/p/nt/avl/v0"
11)
12
13// selfPath is this realm's own page. A root-relative link resolves against
14// the domain, not the realm, so every link back into it names it in full.
15const selfPath = "/r/moul/x/daily/collatz/v1"
16
17// maxIters caps the Collatz walk so a hostile / huge input cannot loop
18// forever on-chain. No known n stays below this bound and fails to reach 1.
19const maxIters = 1000000
20
21// MaxResults bounds the leaderboard's state: anyone may Compute, and each new
22// n is a new entry the root page sorts.
23const MaxResults = 1000
24
25// maxSeqLen caps how many terms Render("/<n>") will print.
26const maxSeqLen = 500
27
28// Result is one recorded submission.
29type Result struct {
30 N int64
31 Steps int
32 Peak int64
33 Who address
34}
35
36// results maps a decimal-string key of N -> Result. avl.Tree keeps a
37// deterministic order for iteration inside Render.
38var results avl.Tree
39
40// maxOdd is the largest odd term whose successor 3n+1 still fits in an
41// int64. Past it the step wraps: 6148914691236517205 maps to exactly 2^64,
42// which is 0, and the walk then spins on 0 until maxIters.
43const maxOdd = (1<<63 - 1 - 1) / 3
44
45// stopping returns the number of steps to reach 1 and the peak value seen,
46// walking the Collatz (hailstone) map. ok is false when a term would overflow
47// int64, in which case steps and peak describe the walk up to that term. It
48// never mutates state.
49func stopping(n int64) (steps int, peak int64, ok bool) {
50 peak = n
51 cur := n
52 for cur != 1 {
53 if steps >= maxIters {
54 break
55 }
56 if cur%2 == 0 {
57 cur = cur / 2
58 } else {
59 if cur > maxOdd {
60 return steps, peak, false
61 }
62 cur = 3*cur + 1
63 }
64 if cur > peak {
65 peak = cur
66 }
67 steps++
68 }
69 return steps, peak, true
70}
71
72// Compute walks the hailstone sequence for n and records the result
73// attributed to the caller. It is a crossing (state-mutating) func.
74func Compute(cur realm, n int) {
75 if !cur.IsCurrent() {
76 panic("collatz: spoofed realm")
77 }
78 caller := cur.Previous().Address()
79 if n <= 0 {
80 panic("n must be > 0")
81 }
82 nn := int64(n)
83 steps, peak, ok := stopping(nn)
84 if !ok {
85 // Recording it would put a wrapped walk on the leaderboard, forever.
86 panic("collatz: the walk from " + strconv.FormatInt(nn, 10) + " overflows int64")
87 }
88
89 res := Result{N: nn, Steps: steps, Peak: peak, Who: caller}
90 key := strconv.FormatInt(nn, 10)
91 if !results.Has(key) && results.Size() >= MaxResults {
92 panic("collatz: the leaderboard is full at " + strconv.Itoa(MaxResults) + " entries")
93 }
94 results.Set(key, res)
95
96 chain.Emit(
97 "Computed",
98 "n", key,
99 "steps", strconv.Itoa(steps),
100 "peak", strconv.FormatInt(peak, 10),
101 "who", caller.String(),
102 )
103}
104
105// byLongest implements sort.Interface, ordering results by steps desc,
106// then peak desc, then n asc for stability.
107type byLongest []Result
108
109func (b byLongest) Len() int { return len(b) }
110func (b byLongest) Swap(i, j int) { b[i], b[j] = b[j], b[i] }
111func (b byLongest) Less(i, j int) bool {
112 if b[i].Steps != b[j].Steps {
113 return b[i].Steps > b[j].Steps
114 }
115 if b[i].Peak != b[j].Peak {
116 return b[i].Peak > b[j].Peak
117 }
118 return b[i].N < b[j].N
119}
120
121// Render shows the leaderboard at root, or the full hailstone sequence for a
122// given n at path "/<n>".
123func Render(path string) string {
124 p := strings.TrimPrefix(path, "/")
125 if p == "" {
126 return renderLeaderboard()
127 }
128 return renderSequence(p)
129}
130
131func renderLeaderboard() string {
132 all := make([]Result, 0, results.Size())
133 results.Iterate("", "", func(_ string, v interface{}) bool {
134 all = append(all, v.(Result))
135 return false
136 })
137
138 var sb strings.Builder
139 sb.WriteString("# Collatz Explorer\n\n")
140 sb.WriteString("Hailstone-sequence explorer. Call `Compute(n)` to record a run, ")
141 sb.WriteString("or view a full sequence at `/<n>`.\n\n")
142
143 if len(all) == 0 {
144 sb.WriteString("_No sequences submitted yet._\n")
145 return sb.String()
146 }
147
148 sort.Stable(byLongest(all))
149
150 sb.WriteString("## Leaderboard — longest sequences\n\n")
151 sb.WriteString("| # | n | steps | peak | who |\n")
152 sb.WriteString("|---|---|-------|------|-----|\n")
153 limit := len(all)
154 if limit > 20 {
155 limit = 20
156 }
157 for i := 0; i < limit; i++ {
158 r := all[i]
159 sb.WriteString("| " + strconv.Itoa(i+1) + " | ")
160 sb.WriteString("[" + strconv.FormatInt(r.N, 10) + "](" + selfPath + ":" + strconv.FormatInt(r.N, 10) + ") | ")
161 sb.WriteString(strconv.Itoa(r.Steps) + " | ")
162 sb.WriteString(strconv.FormatInt(r.Peak, 10) + " | ")
163 sb.WriteString(r.Who.String() + " |\n")
164 }
165 sb.WriteString("\n_" + strconv.Itoa(len(all)) + " sequence(s) recorded._\n")
166 return sb.String()
167}
168
169func renderSequence(p string) string {
170 n, err := strconv.ParseInt(p, 10, 64)
171 var sb strings.Builder
172 if err != nil || n <= 0 {
173 sb.WriteString("# Invalid n\n\n" + md.InlineCode(p) + " is not a positive integer.\n")
174 return sb.String()
175 }
176
177 sb.WriteString("# Hailstone sequence for " + strconv.FormatInt(n, 10) + "\n\n")
178
179 seq := make([]int64, 0, 64)
180 cur := n
181 var peak int64 = n
182 truncated := false
183 overflow := false
184 for {
185 seq = append(seq, cur)
186 if cur == 1 {
187 break
188 }
189 if len(seq) >= maxSeqLen {
190 truncated = true
191 break
192 }
193 if cur%2 == 0 {
194 cur = cur / 2
195 } else {
196 if cur > maxOdd {
197 overflow = true
198 break
199 }
200 cur = 3*cur + 1
201 }
202 if cur > peak {
203 peak = cur
204 }
205 }
206
207 parts := make([]string, len(seq))
208 for i, v := range seq {
209 parts[i] = strconv.FormatInt(v, 10)
210 }
211 sb.WriteString(strings.Join(parts, " → "))
212 sb.WriteString("\n\n")
213 sb.WriteString("- steps: " + strconv.Itoa(len(seq)-1) + "\n")
214 sb.WriteString("- peak: " + strconv.FormatInt(peak, 10) + "\n")
215 if overflow {
216 sb.WriteString("\n_Stopped: the next term would overflow int64._\n")
217 }
218 if truncated {
219 sb.WriteString("\n_Sequence truncated at " + strconv.Itoa(maxSeqLen) + " terms._\n")
220 }
221 return sb.String()
222}