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("\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}