Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions packages/r/vik000/gnoplace/admin.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package gnoplace

import (
"chain/runtime"
"time"

"gno.land/p/nt/avl"
"gno.land/p/nt/ownable"
)

var (
OwnableMain = ownable.NewWithAddress("g13kytw9mpyutwmyg5eq7arqxqcszfl6uq4p89zg")
OwnableBackup = ownable.NewWithAddress("g1f699cfulem8jvq69pxlm5eq945dzusptzkdrgz")
)

// Resets gnoplace
func Reset(_ realm) {
CheckPermission()

users = avl.NewTree()
pixels = [300]int{}
}

func SetInterval(_ realm, interval int) {
CheckPermission()

interval_s = time.Duration(interval) * time.Second
}

func CheckPermission() {
addr := runtime.PreviousRealm().Address()
if addr == OwnableMain.Owner() || addr == OwnableBackup.Owner() {
return
} else {
panic("access restricted")
}
}
2 changes: 2 additions & 0 deletions packages/r/vik000/gnoplace/gnomod.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
module = "gno.land/r/vik000/gnoplace"
gno = "0.9"
101 changes: 101 additions & 0 deletions packages/r/vik000/gnoplace/gnoplace.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package gnoplace

import (
"chain/runtime"
"net/url"
"strconv"
"time"

"gno.land/p/moul/md"
"gno.land/p/nt/avl"
"gno.land/r/leon/hor"
)

// todo: versionning

var (
users = avl.NewTree()
pixels = [300]int{0}
interval_s = 30 * time.Second
colors = []string{"⬜", "⬛", "🟦", "🟪", "🟧", "🟫", "🟥", "🟨", "🟩"}
)

func init() {
hor.Register(cross, "GnoPlace", "a mini version of r/place \n🟥🟩🟦🟨")
}

// Sets a pixel to a color
func SetPixel(_ realm, pixel int, color int) {
// check for errors
if pixel < 0 || pixel >= len(pixels) {
panic("invalid pixel")
}
if color < 0 || color >= len(colors) {
panic("invalid color")
}

// check if user allowed to set pixel
lastEvent, ok := users.Get(runtime.PreviousRealm().Address().String())

if ok && time.Since(lastEvent.(time.Time)) < interval_s {
panic("you placed a pixel less than " + interval_s.String() + " ago")
}

pixels[pixel] = color

// record the SetPixel event time for this user
users.Set(runtime.PreviousRealm().Address().String(), time.Now())
}

// Render renders ui and pixel grid
func Render(path string) string {
u, _ := url.Parse(path)
query := u.Query()
color := atoiDefault(query.Get("color"), -1)

// show home
out := md.H1("GnoPlace")
out += md.HorizontalRule()
out += md.H2("1 - Select your pixel color")

for i, _ := range colors {
out += md.Link(colors[i], "?color="+strconv.Itoa(i)) + " "
}

out += " \n"

out += "## 2 - Click a pixel to paint it"
if color >= 0 {
out += " with color " + colors[color]
}
out += " \n"

// render pixels
for i, _ := range pixels {
out += renderPixel(i, color)
if (i+1)%20 == 0 {
out += " \n"
}
}

out += md.H2("3 - Wait " + interval_s.String() + " before placing again :)")
out += md.HorizontalRule()
out += "If you enjoy gnoplace, please upvote in "
out += md.Link("the Hall of Realms", "/r/leon/hor:hall?sort=creation") + "!\n"

return out
}

// helper to render a pixel
func renderPixel(pixel int, color int) string {
return md.Link(colors[pixels[pixel]], "gnoplace$help&func=SetPixel&.send=&pixel="+strconv.Itoa(pixel)+"&color="+strconv.Itoa(color))
Comment thread
vikbez marked this conversation as resolved.
}

// atoiDefault converts string to integer with a default fallback
func atoiDefault(s string, def int) int {
if s == "" {
return def
}
i, _ := strconv.Atoi(s)
return i
}
83 changes: 83 additions & 0 deletions packages/r/vik000/gnoplace/gnoplace_test.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package gnoplace

import (
"strings"
"testing"

"gno.land/p/nt/testutils"
"gno.land/p/nt/urequire"
)

var (
user1 = testutils.TestAddress("user1")
user2 = testutils.TestAddress("user2")
user3 = testutils.TestAddress("user3")
)

func TestGnoPlace(t *testing.T) {
// Test initial render
output := Render("")
urequire.True(t, strings.Contains(output, "GnoPlace"), "should contain title")

// Test render with color parameter
output = Render("?color=1")
urequire.True(t, strings.Contains(output, "with color"), "should show selected color")

// User 1 sets a pixel
testing.SetRealm(testing.NewUserRealm(user1))
urequire.NotPanics(t, func() {
SetPixel(cross, 0, 1)
}, "user1 should be able to set pixel")

// User 1 tries to set another pixel too soon (should panic)
urequire.AbortsWithMessage(t, "you placed a pixel less than 30s ago", func() {
SetPixel(cross, 2, 3)
}, "user1 should not be able to set pixel too soon")

// User 2 sets a different pixel (should work immediately)
testing.SetRealm(testing.NewUserRealm(user2))
urequire.NotPanics(t, func() {
SetPixel(cross, 1, 2)
}, "user2 should be able to set pixel")
}

func TestSetPixel_InvalidInputs(t *testing.T) {
testing.SetRealm(testing.NewUserRealm(user1))

// Test invalid pixel (negative)
urequire.AbortsWithMessage(t, "invalid pixel", func() {
SetPixel(cross, -1, 1)
})

// Test invalid pixel (out of bounds)
urequire.AbortsWithMessage(t, "invalid pixel", func() {
SetPixel(cross, 300, 1)
})

// Test invalid color (negative)
urequire.AbortsWithMessage(t, "invalid color", func() {
SetPixel(cross, 0, -1)
})

// Test invalid color (out of bounds)
urequire.AbortsWithMessage(t, "invalid color", func() {
SetPixel(cross, 0, 10)
})
}

func TestAtoiDefault(t *testing.T) {
urequire.Equal(t, 10, atoiDefault("", 10), "empty string should return default")
urequire.Equal(t, 5, atoiDefault("5", 10), "valid number should be parsed")
urequire.Equal(t, 0, atoiDefault("0", 10), "zero should be parsed")
urequire.Equal(t, 0, atoiDefault("invalid", 10), "invalid string should return 0")
}

func TestRenderPixel(t *testing.T) {
// Set a pixel to test rendering
testing.SetRealm(testing.NewUserRealm(user3))
SetPixel(cross, 0, 1)

// Test render pixel function
output := renderPixel(0, 1)
urequire.True(t, strings.Contains(output, "SetPixel"), "should contain SetPixel call")
}
Loading