asciiart.gno
6.57 Kb · 253 lines
1// Package asciiart is a small on-chain ASCII art generator. Instead of a
2// hand-authored font table, each pattern is produced by a short, pure,
3// deterministic algorithm -- e.g. the Sierpinski triangle falls out of a
4// single bitwise AND over row/column indices. Callers pick a pattern and a
5// size; the realm renders it and keeps a gallery of everyone's generations.
6package asciiart
7
8import (
9 "strconv"
10 "strings"
11
12 "chain"
13 "chain/runtime"
14
15 "gno.land/p/nt/avl/v0"
16)
17
18// maxSize bounds every pattern's size parameter so a single Generate call
19// can't blow the block gas limit on a pathologically large grid.
20const maxSize = 32
21
22// galleryPageSize caps how many pieces Render shows on the home page.
23const galleryPageSize = 8
24
25type piece struct {
26 ID string
27 Pattern string
28 Size int
29 Author address
30 CreatedAt int64
31 Art string
32}
33
34var (
35 gallery avl.Tree // zero-padded ID -> *piece
36 nextID int
37)
38
39// RenderDiamond draws a filled diamond of half-width n (2n-1 rows total),
40// each row's stars centered and padded with spaces.
41func RenderDiamond(n int) string {
42 if n < 1 {
43 panic("diamond size must be >= 1")
44 }
45 var b strings.Builder
46 for i := 0; i < 2*n-1; i++ {
47 d := i
48 if i >= n {
49 d = 2*n - 2 - i
50 }
51 b.WriteString(strings.Repeat(" ", n-1-d))
52 b.WriteString(strings.Repeat("*", 2*d+1))
53 if i < 2*n-2 {
54 b.WriteString("\n")
55 }
56 }
57 return b.String()
58}
59
60// RenderPyramid draws a solid triangle n rows tall, widening by two stars
61// per row and centered with leading spaces.
62func RenderPyramid(n int) string {
63 if n < 1 {
64 panic("pyramid size must be >= 1")
65 }
66 var b strings.Builder
67 for i := 0; i < n; i++ {
68 b.WriteString(strings.Repeat(" ", n-1-i))
69 b.WriteString(strings.Repeat("*", 2*i+1))
70 if i < n-1 {
71 b.WriteString("\n")
72 }
73 }
74 return b.String()
75}
76
77// RenderSierpinski draws a Sierpinski triangle in an n x n grid (n must be a
78// power of two): cell (row, col) is filled exactly when row&col == 0, the
79// classic bitwise identity for Pascal's-triangle-mod-2.
80func RenderSierpinski(n int) string {
81 if n < 1 || n&(n-1) != 0 {
82 panic("sierpinski size must be a power of two (1, 2, 4, 8, 16, 32)")
83 }
84 var b strings.Builder
85 for row := 0; row < n; row++ {
86 for col := 0; col < n; col++ {
87 if row&col == 0 {
88 b.WriteString("#")
89 } else {
90 b.WriteString(" ")
91 }
92 }
93 if row < n-1 {
94 b.WriteString("\n")
95 }
96 }
97 return b.String()
98}
99
100// RenderChecker draws an n x n checkerboard of # and . cells.
101func RenderChecker(n int) string {
102 if n < 1 {
103 panic("checker size must be >= 1")
104 }
105 var b strings.Builder
106 for row := 0; row < n; row++ {
107 for col := 0; col < n; col++ {
108 if (row+col)%2 == 0 {
109 b.WriteString("#")
110 } else {
111 b.WriteString(".")
112 }
113 }
114 if row < n-1 {
115 b.WriteString("\n")
116 }
117 }
118 return b.String()
119}
120
121func renderPattern(pattern string, size int) string {
122 switch pattern {
123 case "diamond":
124 return RenderDiamond(size)
125 case "pyramid":
126 return RenderPyramid(size)
127 case "sierpinski":
128 return RenderSierpinski(size)
129 case "checker":
130 return RenderChecker(size)
131 default:
132 panic(`unknown pattern: use "diamond", "pyramid", "sierpinski", or "checker"`)
133 }
134}
135
136// padID zero-pads an ID so avl.Tree's key-ordered iteration matches numeric
137// order.
138func padID(n int) string {
139 s := strconv.Itoa(n)
140 for len(s) < 6 {
141 s = "0" + s
142 }
143 return s
144}
145
146// Generate renders a pattern, adds it to the on-chain gallery, and returns
147// the rendered art.
148func Generate(cur realm, pattern string, size int) string {
149 if size < 1 || size > maxSize {
150 panic("size must be between 1 and " + strconv.Itoa(maxSize))
151 }
152 art := renderPattern(pattern, size)
153
154 nextID++
155 id := strconv.Itoa(nextID)
156 author := cur.Previous().Address()
157 p := &piece{
158 ID: id,
159 Pattern: pattern,
160 Size: size,
161 Author: author,
162 CreatedAt: runtime.ChainHeight(),
163 Art: art,
164 }
165 gallery.Set(padID(nextID), p)
166
167 chain.Emit("ArtGenerated", "id", id, "pattern", pattern, "author", author.String())
168
169 return art
170}
171
172// escapeInline neutralizes markdown-active characters in untrusted text
173// before it's embedded inline in Render output.
174func escapeInline(s string) string {
175 r := strings.NewReplacer(
176 "\\", "\\\\",
177 "`", "\\`",
178 "*", "\\*",
179 "_", "\\_",
180 "[", "\\[",
181 "]", "\\]",
182 "|", "\\|",
183 )
184 return r.Replace(s)
185}
186
187func renderHome() string {
188 var b strings.Builder
189 b.WriteString("# ASCII Art Generator\n\n")
190 b.WriteString("Four deterministic patterns, no font table required -- each shape " +
191 "falls out of a short formula over row/column indices. Pick a pattern and a " +
192 "size (1-" + strconv.Itoa(maxSize) + ", sierpinski wants a power of two) and " +
193 "`Generate` adds it to the gallery below.\n\n")
194 b.WriteString("## Patterns\n\n")
195 b.WriteString("- `diamond` -- filled diamond, half-width `size`\n")
196 b.WriteString("- `pyramid` -- solid triangle, `size` rows tall\n")
197 b.WriteString("- `sierpinski` -- Sierpinski gasket in an n×n grid " +
198 "(`row & col == 0`), `size` a power of two\n")
199 b.WriteString("- `checker` -- n×n checkerboard\n\n")
200 b.WriteString("Call `Generate(pattern, size)`. View a piece at this realm's path " +
201 "plus its ID (e.g. `.../asciiart:3`).\n\n")
202
203 b.WriteString("## Gallery (total: " + strconv.Itoa(nextID) + ")\n\n")
204 if nextID == 0 {
205 b.WriteString("_nothing generated yet -- be the first_\n")
206 return b.String()
207 }
208
209 shown := 0
210 gallery.ReverseIterate("", "", func(key string, value interface{}) bool {
211 if shown >= galleryPageSize {
212 return true
213 }
214 p := value.(*piece)
215 b.WriteString("### #" + p.ID + " -- " + p.Pattern + " (size " + strconv.Itoa(p.Size) + ")\n\n")
216 b.WriteString("by `" + p.Author.String() + "` at block " + strconv.Itoa(int(p.CreatedAt)) + "\n\n")
217 b.WriteString("```\n" + p.Art + "\n```\n\n")
218 shown++
219 return false
220 })
221 return b.String()
222}
223
224func renderPiece(idStr string) string {
225 safe := escapeInline(idStr)
226 n, err := strconv.Atoi(idStr)
227 if err != nil || n < 1 {
228 return "# Piece " + safe + "\n\nNo such piece.\n"
229 }
230 v := gallery.Get(padID(n))
231 if v == nil {
232 return "# Piece #" + safe + "\n\nNo such piece.\n"
233 }
234 p := v.(*piece)
235
236 var b strings.Builder
237 b.WriteString("# Piece #" + p.ID + "\n\n")
238 b.WriteString("- Pattern: " + p.Pattern + "\n")
239 b.WriteString("- Size: " + strconv.Itoa(p.Size) + "\n")
240 b.WriteString("- Author: `" + p.Author.String() + "`\n")
241 b.WriteString("- Block: " + strconv.Itoa(int(p.CreatedAt)) + "\n\n")
242 b.WriteString("```\n" + p.Art + "\n```\n")
243 return b.String()
244}
245
246// Render shows the pattern guide + gallery at "", or one piece by numeric ID.
247func Render(path string) string {
248 path = strings.TrimPrefix(strings.TrimSpace(path), "/")
249 if path == "" {
250 return renderHome()
251 }
252 return renderPiece(path)
253}