1// Package pixelcanvas is a shared 16x16 pixel canvas: anyone can paint one2// cell at a time from a fixed 9-colour palette, and the canvas renders in3// 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 the7// start and the grid is walked in full on every render. Painting is8// last-write-wins; the realm keeps who painted each cell and a per-address9// 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 realm12// rather than a p/ library + demo pair.13package pixelcanvas1415import (16 "sort"17 "strconv"18 "strings"1920 "chain"21 "chain/runtime"2223 "gno.land/p/nt/avl/v0"24)2526// Size is the canvas edge length; the canvas is Size x Size cells.27const Size = 162829// palette maps a colour index to the block rendered for it. Index 0 is the30// blank canvas. Kept parallel to paletteNames.31var palette = []string{"⬜", "🟥", "🟧", "🟨", "🟩", "🟦", "🟪", "🟫", "⬛"}3233// paletteNames are the names Paint accepts, in palette order.34var paletteNames = []string{"white", "red", "orange", "yellow", "green", "blue", "purple", "brown", "black"}3536// Package-level persistent state.37var (38 cells [Size * Size]int // palette index per cell, row-major39 painters [Size * Size]string40 tally = avl.NewTree() // address string -> *int paint count41 strokes int // total paints ever42 lastAt int64 // height of the most recent paint43)4445// 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 name48// ("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 }6566 addr := prev.Address()67 i := y*Size + x68 cells[i] = idx69 painters[i] = addr.String()70 strokes++71 lastAt = runtime.ChainHeight()72 bump(addr.String())7374 chain.Emit("Paint",75 "addr", addr.String(),76 "x", strconv.Itoa(x),77 "y", strconv.Itoa(y),78 "colour", paletteNames[idx],79 )80 return strokes81}8283// 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 i89 }90 }91 return -192}9394// 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 return101 }102 one := 1103 tally.Set(addr, &one)104}105106// 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}113114// 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}121122// Strokes returns how many paints the canvas has taken.123func Strokes() int { return strokes }124125// Palette returns the accepted colour names, comma-separated.126func Palette() string { return strings.Join(paletteNames, ", ") }127128// scorer is one row of the contribution leaderboard.129type scorer struct {130 addr string131 count int132}133134// byCount ranks painters by strokes descending, ties broken on address so the135// board is deterministic.136type byCount []scorer137138func (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].count143 }144 return s[i].addr < s[j].addr145}146147// Render renders the canvas for gnoweb.148//149// Render("") / Render("/") -> the canvas, palette and leaderboard150// Render("/<x>,<y>") -> who painted that cell, and what colour151func Render(path string) string {152 var b strings.Builder153 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")159160 if q := parseArg(path); q != "" {161 return b.String() + renderCell(q)162 }163164 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")174175 if board := leaderboard(); board != "" {176 b.WriteString("\n## Top painters\n\n")177 b.WriteString(board)178 }179 return b.String()180}181182// grid renders the canvas as rows of coloured blocks.183func grid() string {184 var b strings.Builder185 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}193194// 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 false200 })201 if len(scores) == 0 {202 return ""203 }204 sort.Sort(byCount(scores))205 if len(scores) > 5 {206 scores = scores[:5]207 }208209 var b strings.Builder210 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}222223// renderCell renders a single "x,y" lookup.224func renderCell(q string) string {225 var b strings.Builder226 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}249250// 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 s258}259260// 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, false265 }266 x, err := strconv.Atoi(strings.TrimSpace(s[:i]))267 if err != nil {268 return 0, 0, false269 }270 y, err2 := strconv.Atoi(strings.TrimSpace(s[i+1:]))271 if err2 != nil {272 return 0, 0, false273 }274 if x < 0 || x >= Size || y < 0 || y >= Size {275 return 0, 0, false276 }277 return x, y, true278}279At(x int, y int) string
Paint(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, x int, y int, colour string) int
PainterAt(x int, y int) string
Palette() string
Render(path string) string
Strokes() int
Signatures reconstructed verbatim from vm/qfuncs — interface params keep their inline definitions.
vm/qrender output, sanitized (docs/render-security.md) and displayed in an empty-sandbox iframe — scripts, forms and popups cannot run. Links stay inert in-preview; right-click to open.