Skip to content

Commit cbbdcda

Browse files
authored
feat: add r/vik000/gnoplace
feat: add r/vik000/gnoplace
2 parents dde7f68 + 3073783 commit cbbdcda

File tree

4 files changed

+223
-0
lines changed

4 files changed

+223
-0
lines changed
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package gnoplace
2+
3+
import (
4+
"chain/runtime"
5+
"time"
6+
7+
"gno.land/p/nt/avl"
8+
"gno.land/p/nt/ownable"
9+
)
10+
11+
var (
12+
OwnableMain = ownable.NewWithAddress("g13kytw9mpyutwmyg5eq7arqxqcszfl6uq4p89zg")
13+
OwnableBackup = ownable.NewWithAddress("g1f699cfulem8jvq69pxlm5eq945dzusptzkdrgz")
14+
)
15+
16+
// Resets gnoplace
17+
func Reset(_ realm) {
18+
CheckPermission()
19+
20+
users = avl.NewTree()
21+
pixels = [300]int{}
22+
}
23+
24+
func SetInterval(_ realm, interval int) {
25+
CheckPermission()
26+
27+
interval_s = time.Duration(interval) * time.Second
28+
}
29+
30+
func CheckPermission() {
31+
addr := runtime.PreviousRealm().Address()
32+
if addr == OwnableMain.Owner() || addr == OwnableBackup.Owner() {
33+
return
34+
} else {
35+
panic("access restricted")
36+
}
37+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
module = "gno.land/r/vik000/gnoplace"
2+
gno = "0.9"
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
package gnoplace
2+
3+
import (
4+
"chain/runtime"
5+
"net/url"
6+
"strconv"
7+
"time"
8+
9+
"gno.land/p/moul/md"
10+
"gno.land/p/nt/avl"
11+
"gno.land/r/leon/hor"
12+
)
13+
14+
// todo: versionning
15+
16+
var (
17+
users = avl.NewTree()
18+
pixels = [300]int{0}
19+
interval_s = 30 * time.Second
20+
colors = []string{"⬜", "⬛", "🟦", "🟪", "🟧", "🟫", "🟥", "🟨", "🟩"}
21+
)
22+
23+
func init() {
24+
hor.Register(cross, "GnoPlace", "a mini version of r/place \n🟥🟩🟦🟨")
25+
}
26+
27+
// Sets a pixel to a color
28+
func SetPixel(_ realm, pixel int, color int) {
29+
// check for errors
30+
if pixel < 0 || pixel >= len(pixels) {
31+
panic("invalid pixel")
32+
}
33+
if color < 0 || color >= len(colors) {
34+
panic("invalid color")
35+
}
36+
37+
// check if user allowed to set pixel
38+
lastEvent, ok := users.Get(runtime.PreviousRealm().Address().String())
39+
40+
if ok && time.Since(lastEvent.(time.Time)) < interval_s {
41+
panic("you placed a pixel less than " + interval_s.String() + " ago")
42+
}
43+
44+
pixels[pixel] = color
45+
46+
// record the SetPixel event time for this user
47+
users.Set(runtime.PreviousRealm().Address().String(), time.Now())
48+
}
49+
50+
// Render renders ui and pixel grid
51+
func Render(path string) string {
52+
u, _ := url.Parse(path)
53+
query := u.Query()
54+
color := atoiDefault(query.Get("color"), -1)
55+
56+
// show home
57+
out := md.H1("GnoPlace")
58+
out += md.HorizontalRule()
59+
out += md.H2("1 - Select your pixel color")
60+
61+
for i, _ := range colors {
62+
out += md.Link(colors[i], "?color="+strconv.Itoa(i)) + " "
63+
}
64+
65+
out += " \n"
66+
67+
out += "## 2 - Click a pixel to paint it"
68+
if color >= 0 {
69+
out += " with color " + colors[color]
70+
}
71+
out += " \n"
72+
73+
// render pixels
74+
for i, _ := range pixels {
75+
out += renderPixel(i, color)
76+
if (i+1)%20 == 0 {
77+
out += " \n"
78+
}
79+
}
80+
81+
out += md.H2("3 - Wait " + interval_s.String() + " before placing again :)")
82+
out += md.HorizontalRule()
83+
out += "If you enjoy gnoplace, please upvote in "
84+
out += md.Link("the Hall of Realms", "/r/leon/hor:hall?sort=creation") + "!\n"
85+
86+
return out
87+
}
88+
89+
// helper to render a pixel
90+
func renderPixel(pixel int, color int) string {
91+
return md.Link(colors[pixels[pixel]], "gnoplace$help&func=SetPixel&.send=&pixel="+strconv.Itoa(pixel)+"&color="+strconv.Itoa(color))
92+
}
93+
94+
// atoiDefault converts string to integer with a default fallback
95+
func atoiDefault(s string, def int) int {
96+
if s == "" {
97+
return def
98+
}
99+
i, _ := strconv.Atoi(s)
100+
return i
101+
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
package gnoplace
2+
3+
import (
4+
"strings"
5+
"testing"
6+
7+
"gno.land/p/nt/testutils"
8+
"gno.land/p/nt/urequire"
9+
)
10+
11+
var (
12+
user1 = testutils.TestAddress("user1")
13+
user2 = testutils.TestAddress("user2")
14+
user3 = testutils.TestAddress("user3")
15+
)
16+
17+
func TestGnoPlace(t *testing.T) {
18+
// Test initial render
19+
output := Render("")
20+
urequire.True(t, strings.Contains(output, "GnoPlace"), "should contain title")
21+
22+
// Test render with color parameter
23+
output = Render("?color=1")
24+
urequire.True(t, strings.Contains(output, "with color"), "should show selected color")
25+
26+
// User 1 sets a pixel
27+
testing.SetRealm(testing.NewUserRealm(user1))
28+
urequire.NotPanics(t, func() {
29+
SetPixel(cross, 0, 1)
30+
}, "user1 should be able to set pixel")
31+
32+
// User 1 tries to set another pixel too soon (should panic)
33+
urequire.AbortsWithMessage(t, "you placed a pixel less than 30s ago", func() {
34+
SetPixel(cross, 2, 3)
35+
}, "user1 should not be able to set pixel too soon")
36+
37+
// User 2 sets a different pixel (should work immediately)
38+
testing.SetRealm(testing.NewUserRealm(user2))
39+
urequire.NotPanics(t, func() {
40+
SetPixel(cross, 1, 2)
41+
}, "user2 should be able to set pixel")
42+
}
43+
44+
func TestSetPixel_InvalidInputs(t *testing.T) {
45+
testing.SetRealm(testing.NewUserRealm(user1))
46+
47+
// Test invalid pixel (negative)
48+
urequire.AbortsWithMessage(t, "invalid pixel", func() {
49+
SetPixel(cross, -1, 1)
50+
})
51+
52+
// Test invalid pixel (out of bounds)
53+
urequire.AbortsWithMessage(t, "invalid pixel", func() {
54+
SetPixel(cross, 300, 1)
55+
})
56+
57+
// Test invalid color (negative)
58+
urequire.AbortsWithMessage(t, "invalid color", func() {
59+
SetPixel(cross, 0, -1)
60+
})
61+
62+
// Test invalid color (out of bounds)
63+
urequire.AbortsWithMessage(t, "invalid color", func() {
64+
SetPixel(cross, 0, 10)
65+
})
66+
}
67+
68+
func TestAtoiDefault(t *testing.T) {
69+
urequire.Equal(t, 10, atoiDefault("", 10), "empty string should return default")
70+
urequire.Equal(t, 5, atoiDefault("5", 10), "valid number should be parsed")
71+
urequire.Equal(t, 0, atoiDefault("0", 10), "zero should be parsed")
72+
urequire.Equal(t, 0, atoiDefault("invalid", 10), "invalid string should return 0")
73+
}
74+
75+
func TestRenderPixel(t *testing.T) {
76+
// Set a pixel to test rendering
77+
testing.SetRealm(testing.NewUserRealm(user3))
78+
SetPixel(cross, 0, 1)
79+
80+
// Test render pixel function
81+
output := renderPixel(0, 1)
82+
urequire.True(t, strings.Contains(output, "SetPixel"), "should contain SetPixel call")
83+
}

0 commit comments

Comments
 (0)