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.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}