Skip to content Search Apps Documentation Source Content File Folder Download Copy Actions State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

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}