asciiart.gno
6.39 Kb · 223 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/moul/kit/ui/v0"
16 "gno.land/p/moul/kit/store/v0"
17)
18
19// maxSize bounds every pattern's size parameter so a single Generate call
20// can't blow the block gas limit on a pathologically large grid.
21const maxSize = 32
22
23// galleryPageSize caps how many pieces Render shows on the home page.
24const galleryPageSize = 8
25
26// piece carries no ID field: the id belongs to the store, which hands it back
27// on lookup and iteration.
28type piece struct {
29 Pattern string
30 Size int
31 Author address
32 CreatedAt int64
33 Art string
34}
35
36// gallery assigns the piece ids. v1 kept its own nextID plus a padID() that
37// zero-padded to width 6, the narrowest ceiling in this repo: at the millionth
38// piece the padding stops and the gallery starts rendering out of order. The
39// store key is 8 fixed bytes with no width to outgrow.
40var gallery = store.Named("piece")
41
42// RenderDiamond draws a filled diamond of half-width n (2n-1 rows total),
43// each row's stars centered and padded with spaces.
44func RenderDiamond(n int) string {
45 if n < 1 {
46 panic("diamond size must be >= 1")
47 }
48 var b strings.Builder
49 for i := 0; i < 2*n-1; i++ {
50 d := i
51 if i >= n {
52 d = 2*n - 2 - i
53 }
54 b.WriteString(strings.Repeat(" ", n-1-d))
55 b.WriteString(strings.Repeat("*", 2*d+1))
56 if i < 2*n-2 {
57 b.WriteString("\n")
58 }
59 }
60 return b.String()
61}
62
63// RenderPyramid draws a solid triangle n rows tall, widening by two stars
64// per row and centered with leading spaces.
65func RenderPyramid(n int) string {
66 if n < 1 {
67 panic("pyramid size must be >= 1")
68 }
69 var b strings.Builder
70 for i := 0; i < n; i++ {
71 b.WriteString(strings.Repeat(" ", n-1-i))
72 b.WriteString(strings.Repeat("*", 2*i+1))
73 if i < n-1 {
74 b.WriteString("\n")
75 }
76 }
77 return b.String()
78}
79
80// RenderSierpinski draws a Sierpinski triangle in an n x n grid (n must be a
81// power of two): cell (row, col) is filled exactly when row&col == 0, the
82// classic bitwise identity for Pascal's-triangle-mod-2.
83func RenderSierpinski(n int) string {
84 if n < 1 || n&(n-1) != 0 {
85 panic("sierpinski size must be a power of two (1, 2, 4, 8, 16, 32)")
86 }
87 var b strings.Builder
88 for row := 0; row < n; row++ {
89 for col := 0; col < n; col++ {
90 if row&col == 0 {
91 b.WriteString("#")
92 } else {
93 b.WriteString(" ")
94 }
95 }
96 if row < n-1 {
97 b.WriteString("\n")
98 }
99 }
100 return b.String()
101}
102
103// RenderChecker draws an n x n checkerboard of # and . cells.
104func RenderChecker(n int) string {
105 if n < 1 {
106 panic("checker size must be >= 1")
107 }
108 var b strings.Builder
109 for row := 0; row < n; row++ {
110 for col := 0; col < n; col++ {
111 if (row+col)%2 == 0 {
112 b.WriteString("#")
113 } else {
114 b.WriteString(".")
115 }
116 }
117 if row < n-1 {
118 b.WriteString("\n")
119 }
120 }
121 return b.String()
122}
123
124func renderPattern(pattern string, size int) string {
125 switch pattern {
126 case "diamond":
127 return RenderDiamond(size)
128 case "pyramid":
129 return RenderPyramid(size)
130 case "sierpinski":
131 return RenderSierpinski(size)
132 case "checker":
133 return RenderChecker(size)
134 default:
135 panic(`unknown pattern: use "diamond", "pyramid", "sierpinski", or "checker"`)
136 }
137}
138
139// Generate renders a pattern, adds it to the on-chain gallery, and returns
140// the rendered art.
141func Generate(cur realm, pattern string, size int) string {
142 if size < 1 || size > maxSize {
143 panic("size must be between 1 and " + strconv.Itoa(maxSize))
144 }
145 art := renderPattern(pattern, size)
146
147 author := cur.Previous().Address()
148 id := gallery.Add(&piece{
149 Pattern: pattern,
150 Size: size,
151 Author: author,
152 CreatedAt: runtime.ChainHeight(),
153 Art: art,
154 })
155
156 chain.Emit("ArtGenerated", "id", id.String(), "pattern", pattern, "author", author.String())
157
158 return art
159}
160
161func renderHome() string {
162 var b strings.Builder
163 b.WriteString("# ASCII Art Generator\n\n")
164 b.WriteString("Four deterministic patterns, no font table required -- each shape " +
165 "falls out of a short formula over row/column indices. Pick a pattern and a " +
166 "size (1-" + strconv.Itoa(maxSize) + ", sierpinski wants a power of two) and " +
167 "`Generate` adds it to the gallery below.\n\n")
168 b.WriteString("## Patterns\n\n")
169 b.WriteString("- `diamond` -- filled diamond, half-width `size`\n")
170 b.WriteString("- `pyramid` -- solid triangle, `size` rows tall\n")
171 b.WriteString("- `sierpinski` -- Sierpinski gasket in an n×n grid " +
172 "(`row & col == 0`), `size` a power of two\n")
173 b.WriteString("- `checker` -- n×n checkerboard\n\n")
174 b.WriteString("Call `Generate(pattern, size)`. View a piece at this realm's path " +
175 "plus its ID (e.g. `.../asciiart:3`).\n\n")
176
177 b.WriteString("## Gallery (total: " + gallery.LastID().String() + ")\n\n")
178 if gallery.Len() == 0 {
179 b.WriteString("_nothing generated yet -- be the first_\n")
180 return b.String()
181 }
182
183 // The newest page, straight from the store: no counter, and only that
184 // window is walked.
185 for _, e := range gallery.PageReverse(1, galleryPageSize) {
186 p := e.Value.(*piece)
187 b.WriteString("### #" + e.ID.String() + " -- " + p.Pattern + " (size " + strconv.Itoa(p.Size) + ")\n\n")
188 b.WriteString("by `" + p.Author.String() + "` at block " + strconv.Itoa(int(p.CreatedAt)) + "\n\n")
189 b.WriteString("```\n" + p.Art + "\n```\n\n")
190 }
191 return b.String()
192}
193
194func renderPiece(idStr string) string {
195 safe := ui.Inline(idStr)
196 id, ok := store.ParseID(idStr)
197 if !ok {
198 return "# Piece " + safe + "\n\nNo such piece.\n"
199 }
200 v, ok := gallery.Get(id)
201 if !ok {
202 return "# Piece #" + safe + "\n\nNo such piece.\n"
203 }
204 p := v.(*piece)
205
206 var b strings.Builder
207 b.WriteString("# Piece #" + id.String() + "\n\n")
208 b.WriteString("- Pattern: " + p.Pattern + "\n")
209 b.WriteString("- Size: " + strconv.Itoa(p.Size) + "\n")
210 b.WriteString("- Author: `" + p.Author.String() + "`\n")
211 b.WriteString("- Block: " + strconv.Itoa(int(p.CreatedAt)) + "\n\n")
212 b.WriteString("```\n" + p.Art + "\n```\n")
213 return b.String()
214}
215
216// Render shows the pattern guide + gallery at "", or one piece by numeric ID.
217func Render(path string) string {
218 path = strings.TrimPrefix(strings.TrimSpace(path), "/")
219 if path == "" {
220 return renderHome()
221 }
222 return renderPiece(path)
223}