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

pixelsandbox.gno

7.31 Kb · 276 lines
  1// Package pixelsandbox is a deliberately generic/throwaway practice
  2// deployment of the GNO Pixels design -- not the final intended realm
  3// name, so redeploying while the design is still changing (which it
  4// will be, more than once, on the way to something we're satisfied
  5// with) never collides with whatever the real, permanent realm ends
  6// up being called.
  7//
  8// Design itself matches this project's local pixelgame3 iteration:
  9// expandable bounds instead of a fixed size, per-pixel provenance (who
 10// placed it, at what block), and an owner-gated import hook for a
 11// possible future migration to a fresh, permanently-named deployment.
 12package pixelsandbox
 13
 14import (
 15	"encoding/base64"
 16	"strconv"
 17	"strings"
 18
 19	runtime "chain/runtime"
 20	unsaferealm "chain/runtime/unsafe"
 21
 22	"gno.land/p/nt/avl/v0"
 23	"gno.land/p/nt/ufmt/v0"
 24)
 25
 26const (
 27	InitialMinX, InitialMaxX = int64(0), int64(63)
 28	InitialMinY, InitialMaxY = int64(0), int64(63)
 29
 30	MaxBoardDim = 128
 31	ExpandStep  = 8
 32
 33	ExpandThresholdPercent = 80
 34
 35	CooldownBlocks = 1
 36	cellPx         = 8
 37)
 38
 39var palette = []string{
 40	"#000000", "#ffffff", "#808080", "#ef4444",
 41	"#fb923c", "#facc15", "#4ade80", "#3b82f6", "#a855f7",
 42}
 43
 44// collectionOwner is the address given for this project -- deploying
 45// account (whoever signs the addpkg transaction) and owner don't have
 46// to be the same address; this constant is what actually gates
 47// ForceExpand/ImportHistoricalPixel/CloseMigrationWindow regardless of
 48// who pays for the deploy itself.
 49const collectionOwner address = "g188mapat33awn7r9uk08l0jc9my0n07fpmspxel"
 50
 51var (
 52	minX, maxX = InitialMinX, InitialMaxX
 53	minY, maxY = InitialMinY, InitialMaxY
 54
 55	grid            avl.Tree
 56	placedBy        avl.Tree
 57	placedAtHeight  avl.Tree
 58	lastPlacedBlock avl.Tree
 59
 60	totalPlacements int64
 61	occupiedCells   int64
 62	expansionsCount int64
 63
 64	migrationOpen = true
 65)
 66
 67func key(x, y int64) string {
 68	return strconv.FormatInt(x, 10) + "," + strconv.FormatInt(y, 10)
 69}
 70
 71func callerAddress() address {
 72	return unsaferealm.PreviousRealm().Address()
 73}
 74
 75func assertOwner() {
 76	if callerAddress() != collectionOwner {
 77		panic("owner-only")
 78	}
 79}
 80
 81func getPixel(x, y int64) int64 {
 82	if c, ok := grid.Get(key(x, y)).(int64); ok {
 83		return c
 84	}
 85	return 0
 86}
 87
 88func boardArea() int64  { return (maxX - minX + 1) * (maxY - minY + 1) }
 89func boardWidth() int64 { return maxX - minX + 1 }
 90
 91func maybeExpand() {
 92	if boardWidth() >= MaxBoardDim {
 93		return
 94	}
 95	if occupiedCells*100 < boardArea()*ExpandThresholdPercent {
 96		return
 97	}
 98	expand()
 99}
100
101func expand() {
102	step := int64(ExpandStep)
103	if boardWidth()+2*step > MaxBoardDim {
104		step = (MaxBoardDim - boardWidth()) / 2
105	}
106	if step <= 0 {
107		return
108	}
109	minX -= step
110	maxX += step
111	minY -= step
112	maxY += step
113	expansionsCount++
114}
115
116func ForceExpand(cur realm) {
117	assertOwner()
118	if boardWidth() >= MaxBoardDim {
119		panic("already at max board size")
120	}
121	expand()
122}
123
124func inBounds(x, y int64) bool {
125	return x >= minX && x <= maxX && y >= minY && y <= maxY
126}
127
128func recordPlacement(x, y, colorIndex int64, placer address, height int64) {
129	k := key(x, y)
130	wasDefault := grid.Get(k) == nil
131	if colorIndex == 0 {
132		grid.Remove(k)
133		if !wasDefault {
134			occupiedCells--
135		}
136	} else {
137		grid.Set(k, colorIndex)
138		if wasDefault {
139			occupiedCells++
140		}
141	}
142	placedBy.Set(k, placer.String())
143	placedAtHeight.Set(k, height)
144}
145
146func SetPixel(cur realm, x, y, colorIndex int64) {
147	maybeExpand()
148	if !inBounds(x, y) {
149		panic("out of bounds")
150	}
151	if colorIndex < 0 || colorIndex >= int64(len(palette)) {
152		panic("invalid color index")
153	}
154	caller := callerAddress()
155	callerKey := caller.String()
156	if last, ok := lastPlacedBlock.Get(callerKey).(int64); ok {
157		elapsed := runtime.ChainHeight() - last
158		if elapsed < CooldownBlocks {
159			panic(ufmt.Sprintf("cooldown active: %d more block(s) to wait", CooldownBlocks-elapsed))
160		}
161	}
162	recordPlacement(x, y, colorIndex, caller, runtime.ChainHeight())
163	lastPlacedBlock.Set(callerKey, runtime.ChainHeight())
164	totalPlacements++
165}
166
167func ImportHistoricalPixel(cur realm, x, y, colorIndex int64, originalPlacer address, originalHeight int64) {
168	assertOwner()
169	if !migrationOpen {
170		panic("migration window is closed")
171	}
172	if colorIndex < 0 || colorIndex >= int64(len(palette)) {
173		panic("invalid color index")
174	}
175	for x < minX || x > maxX || y < minY || y > maxY {
176		if boardWidth() >= MaxBoardDim {
177			panic("historical coordinate exceeds MaxBoardDim -- cannot import")
178		}
179		expand()
180	}
181	recordPlacement(x, y, colorIndex, originalPlacer, originalHeight)
182	totalPlacements++
183}
184
185func CloseMigrationWindow(cur realm) {
186	assertOwner()
187	migrationOpen = false
188}
189
190func MigrationOpen() bool { return migrationOpen }
191
192func GetPixel(x, y int64) int64 { return getPixel(x, y) }
193
194func PlacedBy(x, y int64) (address, int64) {
195	k := key(x, y)
196	addr, _ := placedBy.Get(k).(string)
197	height, _ := placedAtHeight.Get(k).(int64)
198	return address(addr), height
199}
200
201func Bounds() (int64, int64, int64, int64) { return minX, maxX, minY, maxY }
202
203func BoardWidth() int64  { return boardWidth() }
204func BoardHeight() int64 { return maxY - minY + 1 }
205
206func TotalPlacements() int64 { return totalPlacements }
207func OccupiedCells() int64   { return occupiedCells }
208func ExpansionsCount() int64 { return expansionsCount }
209
210func PaletteCSV() string { return strings.Join(palette, ",") }
211
212func CooldownRemaining(addr address) int64 {
213	last, ok := lastPlacedBlock.Get(addr.String()).(int64)
214	if !ok {
215		return 0
216	}
217	elapsed := runtime.ChainHeight() - last
218	if elapsed >= CooldownBlocks {
219		return 0
220	}
221	return CooldownBlocks - elapsed
222}
223
224func Snapshot() string {
225	var b strings.Builder
226	for y := minY; y <= maxY; y++ {
227		for x := minX; x <= maxX; x++ {
228			b.WriteString(strconv.FormatInt(getPixel(x, y), 10))
229		}
230	}
231	return b.String()
232}
233
234func canvasDataURI() string {
235	w := (maxX - minX + 1) * cellPx
236	h := (maxY - minY + 1) * cellPx
237	var svg strings.Builder
238	svg.WriteString(ufmt.Sprintf(
239		`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 %d %d" shape-rendering="crispEdges">`,
240		w, h,
241	))
242	svg.WriteString(ufmt.Sprintf(`<rect width="100%%" height="100%%" fill="%s"/>`, palette[0]))
243	grid.Iterate("", "", func(k string, v any) bool {
244		parts := strings.SplitN(k, ",", 2)
245		x, _ := strconv.ParseInt(parts[0], 10, 64)
246		y, _ := strconv.ParseInt(parts[1], 10, 64)
247		c := v.(int64)
248		svg.WriteString(ufmt.Sprintf(
249			`<rect x="%d" y="%d" width="%d" height="%d" fill="%s"/>`,
250			(x-minX)*cellPx, (y-minY)*cellPx, cellPx, cellPx, palette[c],
251		))
252		return false
253	})
254	svg.WriteString("</svg>")
255	return "data:image/svg+xml;base64," + base64.StdEncoding.EncodeToString([]byte(svg.String()))
256}
257
258func Render(path string) string {
259	var b strings.Builder
260	b.WriteString("# GNO Pixels (sandbox)\n\n")
261	b.WriteString(ufmt.Sprintf(
262		"A %dx%d collaborative on-chain canvas (grows toward a %dx%d max as it fills). `SetPixel(x, y, colorIndex)` takes no address -- the pixel always belongs to whoever signs, and there's a %d-block cooldown per address between placements.\n\n",
263		boardWidth(), maxY-minY+1, MaxBoardDim, MaxBoardDim, CooldownBlocks,
264	))
265	b.WriteString(ufmt.Sprintf(
266		"**Total placements:** %d | **Cells painted:** %d / %d | **Expansions so far:** %d\n\n",
267		totalPlacements, occupiedCells, boardArea(), expansionsCount,
268	))
269	b.WriteString(ufmt.Sprintf("![canvas](%s)\n\n", canvasDataURI()))
270	b.WriteString("## Palette\n\n")
271	b.WriteString("| Index | Color |\n|---|---|\n")
272	for i, hex := range palette {
273		b.WriteString(ufmt.Sprintf("| %d | %s |\n", i, hex))
274	}
275	return b.String()
276}