From 9093fca29f131a2d20e9435de96a8c0f2444e36e Mon Sep 17 00:00:00 2001 From: Marc Vertes Date: Thu, 25 Jul 2024 11:28:54 +0200 Subject: [PATCH 1/4] feat(examples): a shifumi (rock, paper, scissors) smart contract A very simple game as a smart contract. It's possible to play against yourself, or another opponent. UI could be improved. No provision against cheating, no gain or automatic action. Related to #611. --- .../gno.land/r/demo/games/shifumi/gno.mod | 1 + .../gno.land/r/demo/games/shifumi/shifumi.gno | 100 ++++++++++++++++++ 2 files changed, 101 insertions(+) create mode 100644 examples/gno.land/r/demo/games/shifumi/gno.mod create mode 100644 examples/gno.land/r/demo/games/shifumi/shifumi.gno diff --git a/examples/gno.land/r/demo/games/shifumi/gno.mod b/examples/gno.land/r/demo/games/shifumi/gno.mod new file mode 100644 index 00000000000..e6a428090a9 --- /dev/null +++ b/examples/gno.land/r/demo/games/shifumi/gno.mod @@ -0,0 +1 @@ +module gno.land/r/demo/games/shifumi diff --git a/examples/gno.land/r/demo/games/shifumi/shifumi.gno b/examples/gno.land/r/demo/games/shifumi/shifumi.gno new file mode 100644 index 00000000000..0e24a4efc26 --- /dev/null +++ b/examples/gno.land/r/demo/games/shifumi/shifumi.gno @@ -0,0 +1,100 @@ +package shifumi + +import ( + "errors" + "std" + "strconv" +) + +const ( + empty = iota + rock + paper + scissors + last +) + +type game struct { + player1, player2 std.Address // shifumi is a 2 players game + move1, move2 int // can be empty, rock, paper, or scissors +} + +var games = []*game{} + +func (g *game) play(player std.Address, move int) error { + if !(move > empty && move < last) { + return errors.New("invalid move") + } + if player != g.player1 && player != g.player2 { + return errors.New("invalid player") + } + if player == g.player1 && g.move1 == empty { + g.move1 = move + return nil + } + if player == g.player2 && g.move2 == empty { + g.move2 = move + return nil + } + return errors.New("already played") +} + +func (g *game) winner() int { + if g.move1 == empty || g.move2 == empty { + return -1 + } + if g.move1 == g.move2 { + return 0 + } + if g.move1 == rock && g.move2 == scissors || + g.move1 == paper && g.move2 == rock || + g.move1 == scissors && g.move2 == paper { + return 1 + } + return 2 +} + +// NewGame creates a new game where player1 is the caller and player2 the argument. +// A new game index is returned. +func NewGame(player std.Address) int { + games = append(games, &game{player1: std.GetOrigCaller(), player2: player}) + return len(games)-1 +} + +// Play executes a move for the game at index idx, where move can be: +// 1 (rock), 2 (paper), 3 (scissors). +func Play(idx, move int) { + if err := games[idx].play(std.GetOrigCaller(), move); err != nil { + panic(err) + } +} + +func Render(path string) string { + mov1 := []string{ "", " 🤜 ", " 🫱 ", " 👉 "} + mov2 := []string{ "", " 🤛 ", " 🫲 ", " 👈 "} + win := []string{ "pending", "draw", "player1", "player2" } + + output := `# 👊 ✋ ✌️ Shifumi +Actions: +* [NewGame](r/demo/games/shifumi?help&__func=NewGame) opponentAddress +* [Play](r/demo/games/shifumi?help&__func=Play) gameIndex move (1=rock, 2=paper, 3=scissors) + + game | player1 | | player2 | | win + --- | --- | --- | --- | --- | --- +` + for i := len(games)-1; i >= 0; i-- { + g := games[i] + output += strconv.Itoa(i) + " | " + + short(g.player1) + " | " + mov1[g.move1] + " | " + + short(g.player2) + " | " + mov2[g.move2] + " | " + + win[g.winner()+1] + "\n" + } + return output +} + +func short(addr std.Address) string { + if len(addr) < 10 { + return string(addr) + } + return string(addr)[:10] + "..." +} From e531a2f3efb20b6529625127b954f39b3ad8733f Mon Sep 17 00:00:00 2001 From: Marc Vertes Date: Thu, 25 Jul 2024 12:58:50 +0200 Subject: [PATCH 2/4] fix formatting --- examples/gno.land/r/demo/games/shifumi/shifumi.gno | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/examples/gno.land/r/demo/games/shifumi/shifumi.gno b/examples/gno.land/r/demo/games/shifumi/shifumi.gno index 0e24a4efc26..c8578bb8c6f 100644 --- a/examples/gno.land/r/demo/games/shifumi/shifumi.gno +++ b/examples/gno.land/r/demo/games/shifumi/shifumi.gno @@ -15,8 +15,8 @@ const ( ) type game struct { - player1, player2 std.Address // shifumi is a 2 players game - move1, move2 int // can be empty, rock, paper, or scissors + player1, player2 std.Address // shifumi is a 2 players game + move1, move2 int // can be empty, rock, paper, or scissors } var games = []*game{} @@ -58,7 +58,7 @@ func (g *game) winner() int { // A new game index is returned. func NewGame(player std.Address) int { games = append(games, &game{player1: std.GetOrigCaller(), player2: player}) - return len(games)-1 + return len(games) - 1 } // Play executes a move for the game at index idx, where move can be: @@ -70,9 +70,9 @@ func Play(idx, move int) { } func Render(path string) string { - mov1 := []string{ "", " 🤜 ", " 🫱 ", " 👉 "} - mov2 := []string{ "", " 🤛 ", " 🫲 ", " 👈 "} - win := []string{ "pending", "draw", "player1", "player2" } + mov1 := []string{"", " 🤜 ", " 🫱 ", " 👉 "} + mov2 := []string{"", " 🤛 ", " 🫲 ", " 👈 "} + win := []string{"pending", "draw", "player1", "player2"} output := `# 👊 ✋ ✌️ Shifumi Actions: @@ -82,7 +82,7 @@ Actions: game | player1 | | player2 | | win --- | --- | --- | --- | --- | --- ` - for i := len(games)-1; i >= 0; i-- { + for i := len(games) - 1; i >= 0; i-- { g := games[i] output += strconv.Itoa(i) + " | " + short(g.player1) + " | " + mov1[g.move1] + " | " + From 947cc6a24fc8afa238702d45c9d4a7f3e22f1fe8 Mon Sep 17 00:00:00 2001 From: Marc Vertes Date: Tue, 27 Aug 2024 18:21:03 +0200 Subject: [PATCH 3/4] apply suggestions from review. --- .../gno.land/r/demo/games/shifumi/gno.mod | 6 +++ .../gno.land/r/demo/games/shifumi/shifumi.gno | 44 ++++++++++++++----- 2 files changed, 38 insertions(+), 12 deletions(-) diff --git a/examples/gno.land/r/demo/games/shifumi/gno.mod b/examples/gno.land/r/demo/games/shifumi/gno.mod index e6a428090a9..7a4fc173d3d 100644 --- a/examples/gno.land/r/demo/games/shifumi/gno.mod +++ b/examples/gno.land/r/demo/games/shifumi/gno.mod @@ -1 +1,7 @@ module gno.land/r/demo/games/shifumi + +require ( + gno.land/p/demo/avl v0.0.0-latest + gno.land/p/demo/seqid v0.0.0-latest + gno.land/r/demo/users v0.0.0-latest +) diff --git a/examples/gno.land/r/demo/games/shifumi/shifumi.gno b/examples/gno.land/r/demo/games/shifumi/shifumi.gno index c8578bb8c6f..f642b61842f 100644 --- a/examples/gno.land/r/demo/games/shifumi/shifumi.gno +++ b/examples/gno.land/r/demo/games/shifumi/shifumi.gno @@ -4,6 +4,11 @@ import ( "errors" "std" "strconv" + + "gno.land/p/demo/avl" + "gno.land/p/demo/seqid" + + "gno.land/r/demo/users" ) const ( @@ -19,7 +24,8 @@ type game struct { move1, move2 int // can be empty, rock, paper, or scissors } -var games = []*game{} +var games avl.Tree +var id seqid.ID func (g *game) play(player std.Address, move int) error { if !(move > empty && move < last) { @@ -57,14 +63,18 @@ func (g *game) winner() int { // NewGame creates a new game where player1 is the caller and player2 the argument. // A new game index is returned. func NewGame(player std.Address) int { - games = append(games, &game{player1: std.GetOrigCaller(), player2: player}) - return len(games) - 1 + games.Set(id.Next().String(), &game{player1: std.PrevRealm().Addr(), player2: player}) + return int(id) } // Play executes a move for the game at index idx, where move can be: // 1 (rock), 2 (paper), 3 (scissors). func Play(idx, move int) { - if err := games[idx].play(std.GetOrigCaller(), move); err != nil { + v, ok := games.Get(seqid.ID(idx).String()) + if !ok { + panic("game not found") + } + if err := v.(*game).play(std.PrevRealm().Addr(), move); err != nil { panic(err) } } @@ -76,23 +86,33 @@ func Render(path string) string { output := `# 👊 ✋ ✌️ Shifumi Actions: -* [NewGame](r/demo/games/shifumi?help&__func=NewGame) opponentAddress -* [Play](r/demo/games/shifumi?help&__func=Play) gameIndex move (1=rock, 2=paper, 3=scissors) +* [NewGame](shifumi?help&__func=NewGame) opponentAddress +* [Play](shifumi?help&__func=Play) gameIndex move (1=rock, 2=paper, 3=scissors) game | player1 | | player2 | | win --- | --- | --- | --- | --- | --- ` - for i := len(games) - 1; i >= 0; i-- { - g := games[i] - output += strconv.Itoa(i) + " | " + - short(g.player1) + " | " + mov1[g.move1] + " | " + - short(g.player2) + " | " + mov2[g.move2] + " | " + + // Output the 100 most recent games. + maxGames := 100 + for n := int(id); n > 0 && int(id) - n < maxGames; n-- { + v, ok := games.Get(seqid.ID(n).String()) + if !ok { + continue + } + g := v.(*game) + output += strconv.Itoa(n) + " | " + + shortName(g.player1) + " | " + mov1[g.move1] + " | " + + shortName(g.player2) + " | " + mov2[g.move2] + " | " + win[g.winner()+1] + "\n" } return output } -func short(addr std.Address) string { +func shortName(addr std.Address) string { + user := users.GetUserByAddress(addr) + if user != nil { + return user.Name + } if len(addr) < 10 { return string(addr) } From 45d0685ef4d56fe50f60874bf2b9c05813283b21 Mon Sep 17 00:00:00 2001 From: Marc Vertes Date: Tue, 27 Aug 2024 18:40:11 +0200 Subject: [PATCH 4/4] fix lint --- examples/gno.land/r/demo/games/shifumi/shifumi.gno | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/gno.land/r/demo/games/shifumi/shifumi.gno b/examples/gno.land/r/demo/games/shifumi/shifumi.gno index f642b61842f..9094cb8fd69 100644 --- a/examples/gno.land/r/demo/games/shifumi/shifumi.gno +++ b/examples/gno.land/r/demo/games/shifumi/shifumi.gno @@ -94,7 +94,7 @@ Actions: ` // Output the 100 most recent games. maxGames := 100 - for n := int(id); n > 0 && int(id) - n < maxGames; n-- { + for n := int(id); n > 0 && int(id)-n < maxGames; n-- { v, ok := games.Get(seqid.ID(n).String()) if !ok { continue