diff --git a/packages/r/vik000/gnoplace/admin.gno b/packages/r/vik000/gnoplace/admin.gno new file mode 100644 index 0000000..e71000e --- /dev/null +++ b/packages/r/vik000/gnoplace/admin.gno @@ -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") + } +} diff --git a/packages/r/vik000/gnoplace/gnomod.toml b/packages/r/vik000/gnoplace/gnomod.toml new file mode 100644 index 0000000..786630a --- /dev/null +++ b/packages/r/vik000/gnoplace/gnomod.toml @@ -0,0 +1,2 @@ +module = "gno.land/r/vik000/gnoplace" +gno = "0.9" diff --git a/packages/r/vik000/gnoplace/gnoplace.gno b/packages/r/vik000/gnoplace/gnoplace.gno new file mode 100644 index 0000000..dc38b44 --- /dev/null +++ b/packages/r/vik000/gnoplace/gnoplace.gno @@ -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)) +} + +// 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 +} diff --git a/packages/r/vik000/gnoplace/gnoplace_test.gno b/packages/r/vik000/gnoplace/gnoplace_test.gno new file mode 100644 index 0000000..a049912 --- /dev/null +++ b/packages/r/vik000/gnoplace/gnoplace_test.gno @@ -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") +}