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

pixelcanvas.gno

7.28 Kb Β· 278 lines
  1// Package pixelcanvas is a shared 16x16 pixel canvas: anyone can paint one
  2// cell at a time from a fixed 9-colour palette, and the canvas renders in
  3// gnoweb as a grid of coloured blocks.
  4//
  5// The canvas is a flat [Size*Size]int of palette indices held in realm state β€”
  6// a fixed-size array rather than a tree, because every cell exists from the
  7// start and the grid is walked in full on every render. Painting is
  8// last-write-wins; the realm keeps who painted each cell and a per-address
  9// tally, so the board doubles as a contribution leaderboard.
 10//
 11// A stateful app with nothing reusable to extract, so it ships as a lone realm
 12// rather than a p/ library + demo pair.
 13package pixelcanvas
 14
 15import (
 16	"sort"
 17	"strconv"
 18	"strings"
 19
 20	"chain"
 21	"chain/runtime"
 22
 23	"gno.land/p/nt/avl/v0"
 24)
 25
 26// Size is the canvas edge length; the canvas is Size x Size cells.
 27const Size = 16
 28
 29// palette maps a colour index to the block rendered for it. Index 0 is the
 30// blank canvas. Kept parallel to paletteNames.
 31var palette = []string{"⬜", "πŸŸ₯", "🟧", "🟨", "🟩", "🟦", "πŸŸͺ", "🟫", "⬛"}
 32
 33// paletteNames are the names Paint accepts, in palette order.
 34var paletteNames = []string{"white", "red", "orange", "yellow", "green", "blue", "purple", "brown", "black"}
 35
 36// Package-level persistent state.
 37var (
 38	cells    [Size * Size]int // palette index per cell, row-major
 39	painters [Size * Size]string
 40	tally    = avl.NewTree() // address string -> *int paint count
 41	strokes  int             // total paints ever
 42	lastAt   int64           // height of the most recent paint
 43)
 44
 45// Paint colours the cell at (x, y) and returns the total number of strokes.
 46//
 47// Coordinates are 0-based with (0,0) at the top-left. Colour is a palette name
 48// ("red", "blue", …) β€” see Palette. Painting over an existing cell is allowed:
 49// the canvas is last-write-wins, which is the whole point of a shared board.
 50func Paint(cur realm, x, y int, colour string) int {
 51	if !cur.IsCurrent() {
 52		panic("spoofed realm")
 53	}
 54	prev := cur.Previous()
 55	if !prev.IsUserCall() {
 56		panic("only an EOA via MsgCall can paint")
 57	}
 58	if x < 0 || x >= Size || y < 0 || y >= Size {
 59		panic("out of bounds: x and y must be in [0," + strconv.Itoa(Size-1) + "]")
 60	}
 61	idx := colourIndex(colour)
 62	if idx < 0 {
 63		panic("unknown colour " + strconv.Quote(colour) + "; see Palette()")
 64	}
 65
 66	addr := prev.Address()
 67	i := y*Size + x
 68	cells[i] = idx
 69	painters[i] = addr.String()
 70	strokes++
 71	lastAt = runtime.ChainHeight()
 72	bump(addr.String())
 73
 74	chain.Emit("Paint",
 75		"addr", addr.String(),
 76		"x", strconv.Itoa(x),
 77		"y", strconv.Itoa(y),
 78		"colour", paletteNames[idx],
 79	)
 80	return strokes
 81}
 82
 83// colourIndex resolves a palette name to its index, or -1.
 84func colourIndex(name string) int {
 85	n := strings.ToLower(strings.TrimSpace(name))
 86	for i, p := range paletteNames {
 87		if p == n {
 88			return i
 89		}
 90	}
 91	return -1
 92}
 93
 94// bump increments an address's paint tally.
 95func bump(addr string) {
 96	// avl/v0's Get returns a single `any`; a miss is a nil interface.
 97	if v := tally.Get(addr); v != nil {
 98		c := v.(*int)
 99		*c++
100		return
101	}
102	one := 1
103	tally.Set(addr, &one)
104}
105
106// At returns the palette name of the cell at (x, y), or "" when out of bounds.
107func At(x, y int) string {
108	if x < 0 || x >= Size || y < 0 || y >= Size {
109		return ""
110	}
111	return paletteNames[cells[y*Size+x]]
112}
113
114// PainterAt returns the address that last painted (x, y), or "" if untouched.
115func PainterAt(x, y int) string {
116	if x < 0 || x >= Size || y < 0 || y >= Size {
117		return ""
118	}
119	return painters[y*Size+x]
120}
121
122// Strokes returns how many paints the canvas has taken.
123func Strokes() int { return strokes }
124
125// Palette returns the accepted colour names, comma-separated.
126func Palette() string { return strings.Join(paletteNames, ", ") }
127
128// scorer is one row of the contribution leaderboard.
129type scorer struct {
130	addr  string
131	count int
132}
133
134// byCount ranks painters by strokes descending, ties broken on address so the
135// board is deterministic.
136type byCount []scorer
137
138func (s byCount) Len() int      { return len(s) }
139func (s byCount) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
140func (s byCount) Less(i, j int) bool {
141	if s[i].count != s[j].count {
142		return s[i].count > s[j].count
143	}
144	return s[i].addr < s[j].addr
145}
146
147// Render renders the canvas for gnoweb.
148//
149//	Render("")        / Render("/") -> the canvas, palette and leaderboard
150//	Render("/<x>,<y>")              -> who painted that cell, and what colour
151func Render(path string) string {
152	var b strings.Builder
153	b.WriteString("# Pixel Canvas\n\n")
154	b.WriteString("A shared ")
155	b.WriteString(strconv.Itoa(Size))
156	b.WriteString("x")
157	b.WriteString(strconv.Itoa(Size))
158	b.WriteString(" canvas. Anyone can `Paint(x, y, colour)` β€” last write wins.\n\n")
159
160	if q := parseArg(path); q != "" {
161		return b.String() + renderCell(q)
162	}
163
164	b.WriteString(grid())
165	b.WriteString("\n**")
166	b.WriteString(strconv.Itoa(strokes))
167	b.WriteString("** stroke")
168	if strokes != 1 {
169		b.WriteString("s")
170	}
171	b.WriteString(" Β· palette: ")
172	b.WriteString(Palette())
173	b.WriteString("\n")
174
175	if board := leaderboard(); board != "" {
176		b.WriteString("\n## Top painters\n\n")
177		b.WriteString(board)
178	}
179	return b.String()
180}
181
182// grid renders the canvas as rows of coloured blocks.
183func grid() string {
184	var b strings.Builder
185	for y := 0; y < Size; y++ {
186		for x := 0; x < Size; x++ {
187			b.WriteString(palette[cells[y*Size+x]])
188		}
189		b.WriteString("\n")
190	}
191	return b.String()
192}
193
194// leaderboard renders the top painters, or "" when nobody has painted.
195func leaderboard() string {
196	scores := []scorer{}
197	tally.Iterate("", "", func(k string, v any) bool {
198		scores = append(scores, scorer{addr: k, count: *(v.(*int))})
199		return false
200	})
201	if len(scores) == 0 {
202		return ""
203	}
204	sort.Sort(byCount(scores))
205	if len(scores) > 5 {
206		scores = scores[:5]
207	}
208
209	var b strings.Builder
210	b.WriteString("| # | address | strokes |\n|---|---|---|\n")
211	for i, s := range scores {
212		b.WriteString("| ")
213		b.WriteString(strconv.Itoa(i + 1))
214		b.WriteString(" | `")
215		b.WriteString(s.addr)
216		b.WriteString("` | ")
217		b.WriteString(strconv.Itoa(s.count))
218		b.WriteString(" |\n")
219	}
220	return b.String()
221}
222
223// renderCell renders a single "x,y" lookup.
224func renderCell(q string) string {
225	var b strings.Builder
226	x, y, ok := parseCoord(q)
227	if !ok {
228		b.WriteString("_Expected `x,y` β€” e.g. `/3,7`._\n")
229		return b.String()
230	}
231	b.WriteString("## Cell (")
232	b.WriteString(strconv.Itoa(x))
233	b.WriteString(", ")
234	b.WriteString(strconv.Itoa(y))
235	b.WriteString(")\n\n")
236	b.WriteString(palette[cells[y*Size+x]])
237	b.WriteString(" **")
238	b.WriteString(At(x, y))
239	b.WriteString("**\n\n")
240	if p := PainterAt(x, y); p != "" {
241		b.WriteString("Painted by `")
242		b.WriteString(p)
243		b.WriteString("`.\n")
244	} else {
245		b.WriteString("_Never painted._\n")
246	}
247	return b.String()
248}
249
250// parseArg extracts the first path segment.
251func parseArg(path string) string {
252	s := strings.TrimSpace(path)
253	s = strings.TrimPrefix(s, "/")
254	if i := strings.IndexByte(s, '/'); i >= 0 {
255		s = s[:i]
256	}
257	return s
258}
259
260// parseCoord parses "x,y" and reports whether it is in bounds.
261func parseCoord(s string) (int, int, bool) {
262	i := strings.IndexByte(s, ',')
263	if i < 0 {
264		return 0, 0, false
265	}
266	x, err := strconv.Atoi(strings.TrimSpace(s[:i]))
267	if err != nil {
268		return 0, 0, false
269	}
270	y, err2 := strconv.Atoi(strings.TrimSpace(s[i+1:]))
271	if err2 != nil {
272		return 0, 0, false
273	}
274	if x < 0 || x >= Size || y < 0 || y >= Size {
275		return 0, 0, false
276	}
277	return x, y, true
278}