Search Apps Documentation Source Content File Folder Download Copy Actions Download 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}