From 38a1074943b3e572c4cddddea4e3edaefda8d264 Mon Sep 17 00:00:00 2001 From: linhpn99 Date: Wed, 28 Aug 2024 21:46:50 +0700 Subject: [PATCH 01/22] update --- tm2/pkg/std/fee.go | 28 ++++++ tm2/pkg/std/gasprice_test.go | 95 +++++++++++++++++++ tm2/pkg/std/tx.go | 27 ------ tm2/pkg/std/tx_test.go | 177 +++++++++++++++++++++++++++++++++++ 4 files changed, 300 insertions(+), 27 deletions(-) create mode 100644 tm2/pkg/std/fee.go create mode 100644 tm2/pkg/std/gasprice_test.go create mode 100644 tm2/pkg/std/tx_test.go diff --git a/tm2/pkg/std/fee.go b/tm2/pkg/std/fee.go new file mode 100644 index 00000000000..14cf6128117 --- /dev/null +++ b/tm2/pkg/std/fee.go @@ -0,0 +1,28 @@ +package std + +import "github.com/gnolang/gno/tm2/pkg/amino" + +// Fee includes the amount of coins paid in fees and the maximum +// gas to be used by the transaction. The ratio yields an effective "gasprice", +// which must be above some miminum to be accepted into the mempool. +type Fee struct { + GasWanted int64 `json:"gas_wanted" yaml:"gas_wanted"` + GasFee Coin `json:"gas_fee" yaml:"gas_fee"` +} + +// NewFee returns a new instance of Fee +func NewFee(gasWanted int64, gasFee Coin) Fee { + return Fee{ + GasWanted: gasWanted, + GasFee: gasFee, + } +} + +// Bytes for signing later +func (fee Fee) Bytes() []byte { + bz, err := amino.MarshalJSON(fee) // TODO + if err != nil { + panic(err) + } + return bz +} diff --git a/tm2/pkg/std/gasprice_test.go b/tm2/pkg/std/gasprice_test.go new file mode 100644 index 00000000000..7671c124716 --- /dev/null +++ b/tm2/pkg/std/gasprice_test.go @@ -0,0 +1,95 @@ +package std + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestParseGasPrice(t *testing.T) { + tests := []struct { + input string + expected GasPrice + hasError bool + }{ + { + input: "10foo/1gas", + expected: GasPrice{Gas: 1, Price: Coin{Denom: "foo", Amount: 10}}, + hasError: false, + }, + { + input: "100bar/2gas", + expected: GasPrice{Gas: 2, Price: Coin{Denom: "bar", Amount: 100}}, + hasError: false, + }, + { + input: "5coin/0gas", + expected: GasPrice{Gas: 0, Price: Coin{Denom: "coin", Amount: 5}}, + hasError: false, + }, + { + input: "invalid", + expected: GasPrice{}, + hasError: true, + }, + { + input: "10foo/1coin", + expected: GasPrice{}, + hasError: true, + }, + } + + for _, test := range tests { + result, err := ParseGasPrice(test.input) + if test.hasError { + assert.True(t, err != nil, "expected error but got none") + } else { + assert.Nil(t, err, "unexpected error") + assert.Equal(t, result, test.expected) + } + } +} + +func TestParseGasPrices(t *testing.T) { + tests := []struct { + input string + expected []GasPrice + hasError bool + }{ + { + input: "10foo/1gas;5bar/2gas", + expected: []GasPrice{ + {Gas: 1, Price: Coin{Denom: "foo", Amount: 10}}, + {Gas: 2, Price: Coin{Denom: "bar", Amount: 5}}, + }, + hasError: false, + }, + { + input: "5coin/0gas", + expected: []GasPrice{ + {Gas: 0, Price: Coin{Denom: "coin", Amount: 5}}, + }, + hasError: false, + }, + { + input: "invalid", + expected: nil, + hasError: true, + }, + { + input: "10foo/1coin;5bar/2gas", + expected: nil, + hasError: true, + }, + } + + for _, test := range tests { + result, err := ParseGasPrices(test.input) + if test.hasError { + assert.True(t, err != nil, "expected error but got none") + } else { + assert.Nil(t, err, "unexpected error") + assert.Equal(t, result, test.expected) + } + } +} diff --git a/tm2/pkg/std/tx.go b/tm2/pkg/std/tx.go index 4fd7af4a641..c2ba4b94af4 100644 --- a/tm2/pkg/std/tx.go +++ b/tm2/pkg/std/tx.go @@ -118,33 +118,6 @@ func (tx Tx) GetSignBytes(chainID string, accountNumber uint64, sequence uint64) }) } -// __________________________________________________________ - -// Fee includes the amount of coins paid in fees and the maximum -// gas to be used by the transaction. The ratio yields an effective "gasprice", -// which must be above some miminum to be accepted into the mempool. -type Fee struct { - GasWanted int64 `json:"gas_wanted" yaml:"gas_wanted"` - GasFee Coin `json:"gas_fee" yaml:"gas_fee"` -} - -// NewFee returns a new instance of Fee -func NewFee(gasWanted int64, gasFee Coin) Fee { - return Fee{ - GasWanted: gasWanted, - GasFee: gasFee, - } -} - -// Bytes for signing later -func (fee Fee) Bytes() []byte { - bz, err := amino.MarshalJSON(fee) // TODO - if err != nil { - panic(err) - } - return bz -} - func ParseTxs(ctx context.Context, reader io.Reader) ([]Tx, error) { var txs []Tx scanner := bufio.NewScanner(reader) diff --git a/tm2/pkg/std/tx_test.go b/tm2/pkg/std/tx_test.go new file mode 100644 index 00000000000..2f662512954 --- /dev/null +++ b/tm2/pkg/std/tx_test.go @@ -0,0 +1,177 @@ +package std + +import ( + "testing" + + "github.com/gnolang/gno/tm2/pkg/crypto" + "github.com/gnolang/gno/tm2/pkg/crypto/ed25519" + "github.com/gnolang/gno/tm2/pkg/crypto/multisig" + "github.com/stretchr/testify/require" +) + +// Mock implementations of Msg interfaces +type mockMsg struct { + caller crypto.Address + msgType string +} + +func (m mockMsg) ValidateBasic() error { + return nil +} + +func (m mockMsg) GetSignBytes() []byte { + return nil +} + +func (m mockMsg) GetSigners() []crypto.Address { + return []crypto.Address{m.caller} +} + +func (m mockMsg) Route() string { + return "" +} + +func (m mockMsg) Type() string { + return m.msgType +} + +func TestNewTx(t *testing.T) { + addr, _ := crypto.AddressFromBech32("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") + msgs := []Msg{ + mockMsg{ + caller: addr, + }, + } + + fee := NewFee(1000, Coin{Denom: "atom", Amount: 10}) + + sigs := []Signature{ + { + Signature: []byte{0x00}, + }, + } + + memo := "test memo" + + tx := NewTx(msgs, fee, sigs, memo) + require.Equal(t, msgs, tx.GetMsgs()) + require.Equal(t, fee, tx.Fee) + require.Equal(t, sigs, tx.GetSignatures()) + require.Equal(t, memo, tx.GetMemo()) +} + +func Test_ValidateBasic(t *testing.T) { + addr, _ := crypto.AddressFromBech32("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") + msgs := []Msg{ + mockMsg{ + caller: addr, + }, + } + + fee := NewFee(maxGasWanted, Coin{Denom: "atom", Amount: 10}) + sigs := []Signature{ + { + Signature: []byte{0x00}, + }, + } + + tx := NewTx(msgs, fee, sigs, "test memo") + + // Valid case + require.NoError(t, tx.ValidateBasic()) + + // Invalid gas case + invalidFee := NewFee(maxGasWanted+1, Coin{Denom: "atom", Amount: 10}) + txInvalidGas := NewTx(msgs, invalidFee, sigs, "test memo") + require.Error(t, txInvalidGas.ValidateBasic(), "expected gas overflow error") + + // Invalid fee case + invalidFeeAmount := NewFee(1000, Coin{Denom: "atom", Amount: -10}) + txInvalidFee := NewTx(msgs, invalidFeeAmount, sigs, "test memo") + require.Error(t, txInvalidFee.ValidateBasic(), "expected insufficient fee error") + + // No signatures case + txNoSigs := NewTx(msgs, fee, []Signature{}, "test memo") + require.Error(t, txNoSigs.ValidateBasic(), "expected no signatures error") + + // Wrong number of signers case + wrongSigs := []Signature{ + { + Signature: []byte{0x00}, + }, + { + Signature: []byte{0x01}, + }, + } + txWrongSigs := NewTx(msgs, fee, wrongSigs, "test memo") + require.Error(t, txWrongSigs.ValidateBasic(), "expected wrong number of signers error") +} + +func Test_CountSubKeys(t *testing.T) { + // Single key case + pubKey := ed25519.GenPrivKey().PubKey() + require.Equal(t, 1, CountSubKeys(pubKey)) + + // Multi-sig case + // Assuming multisig.PubKeyMultisigThreshold is correctly implemented for testing purposes + pubKeys := []crypto.PubKey{ed25519.GenPrivKey().PubKey(), ed25519.GenPrivKey().PubKey()} + multisigPubKey := multisig.NewPubKeyMultisigThreshold(2, pubKeys) + require.Equal(t, len(pubKeys), CountSubKeys(multisigPubKey)) +} + +func Test_GetSigners(t *testing.T) { + // Single signer case + addr, _ := crypto.AddressFromBech32("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") + msgs := []Msg{ + mockMsg{ + caller: addr, + msgType: "call", + }, + } + tx := NewTx(msgs, Fee{}, []Signature{}, "") + require.Equal(t, []crypto.Address{addr}, tx.GetSigners()) + + // Duplicate signers case + msgs = []Msg{ + mockMsg{ + caller: addr, + msgType: "send", + }, + mockMsg{ + caller: addr, + msgType: "send", + }, + } + + tx = NewTx(msgs, Fee{}, []Signature{}, "") + require.Equal(t, []crypto.Address{addr}, tx.GetSigners()) + + // Multiple unique signers case + addr2, _ := crypto.AddressFromBech32("g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj") + msgs = []Msg{ + mockMsg{ + caller: addr, + msgType: "call", + }, + mockMsg{ + caller: addr2, + msgType: "run", + }, + } + tx = NewTx(msgs, Fee{}, []Signature{}, "") + require.Equal(t, []crypto.Address{addr, addr2}, tx.GetSigners()) +} + +func Test_GetSignBytes(t *testing.T) { + msgs := []Msg{} + fee := NewFee(1000, Coin{Denom: "atom", Amount: 10}) + sigs := []Signature{} + tx := NewTx(msgs, fee, sigs, "test memo") + chainID := "test-chain" + accountNumber := uint64(1) + sequence := uint64(1) + + signBytes, err := tx.GetSignBytes(chainID, accountNumber, sequence) + require.NoError(t, err) + require.NotEmpty(t, signBytes) +} From 73fbf9d63f5888a83503836a764149c14ad9b2fb Mon Sep 17 00:00:00 2001 From: linhpn99 Date: Sun, 8 Sep 2024 20:06:17 +0700 Subject: [PATCH 02/22] first commit --- .../r/demo/games/diceduel/dice_test.gno | 182 +++++++++++++++++ .../r/demo/games/diceduel/diceduel.gno | 191 ++++++++++++++++++ .../gno.land/r/demo/games/diceduel/gno.mod | 10 + 3 files changed, 383 insertions(+) create mode 100644 examples/gno.land/r/demo/games/diceduel/dice_test.gno create mode 100644 examples/gno.land/r/demo/games/diceduel/diceduel.gno create mode 100644 examples/gno.land/r/demo/games/diceduel/gno.mod diff --git a/examples/gno.land/r/demo/games/diceduel/dice_test.gno b/examples/gno.land/r/demo/games/diceduel/dice_test.gno new file mode 100644 index 00000000000..be44c99180a --- /dev/null +++ b/examples/gno.land/r/demo/games/diceduel/dice_test.gno @@ -0,0 +1,182 @@ +package diceduel + +import ( + "std" + "testing" + + "gno.land/p/demo/avl" + "gno.land/p/demo/seqid" + "gno.land/p/demo/testutils" + "gno.land/p/demo/urequire" +) + +var ( + player1 = testutils.TestAddress("alice") + player2 = testutils.TestAddress("bob") + unknownPlayer = testutils.TestAddress("unknown") +) + +// resetGameState resets the game state for testing +func resetGameState() { + games = avl.Tree{} + id = seqid.ID(0) +} + +// TestNewGame tests the initialization of a new game +func TestNewGame(t *testing.T) { + resetGameState() + + std.TestSetOrigCaller(player1) + gameID := NewGame(player2, 3) + + // Verify that the game has been correctly initialized + g, err := gameByID(gameID) + urequire.NoError(t, err) + urequire.Equal(t, player1.String(), g.player1.String()) + urequire.Equal(t, player2.String(), g.player2.String()) + urequire.Equal(t, 0, g.roll1) + urequire.Equal(t, 0, g.roll2) + urequire.Equal(t, 0, g.score1) + urequire.Equal(t, 0, g.score2) + urequire.Equal(t, 0, g.round) + urequire.Equal(t, 3, g.maxRounds) +} + +// TestPlay tests the dice rolling functionality for both players +func TestPlay(t *testing.T) { + resetGameState() + + std.TestSetOrigCaller(player1) + gameID := NewGame(player2, 3) + + g, err := gameByID(gameID) + urequire.NoError(t, err) + + // Simulate rolling dice for player 1 + roll1 := Play(gameID) + + // Verify player 1's roll and score + urequire.NotEqual(t, 0, g.roll1) + urequire.Equal(t, g.score1, roll1) + urequire.Equal(t, 0, g.roll2) // Player 2 hasn't rolled yet + urequire.Equal(t, 0, g.round) + + // Simulate rolling dice for player 2 + std.TestSetOrigCaller(player2) + roll2 := Play(gameID) + + // Verify player 2's roll and score + urequire.Equal(t, 0, g.roll1) + urequire.Equal(t, 0, g.roll2) + urequire.Equal(t, g.score1, roll1) + urequire.Equal(t, g.score2, roll2) + urequire.Equal(t, 1, g.round) // Round should be incremented now +} + +// TestPlayAgainstSelf tests the scenario where a player plays against themselves +func TestPlayAgainstSelf(t *testing.T) { + resetGameState() + + std.TestSetOrigCaller(player1) + gameID := NewGame(player1, 1) + + // Simulate rolling dice twice by the same player + roll1 := Play(gameID) + roll2 := Play(gameID) + + g, err := gameByID(gameID) + urequire.NoError(t, err) + urequire.Equal(t, 0, g.roll1) + urequire.Equal(t, g.score1, roll1) + urequire.Equal(t, 0, g.roll2) + urequire.Equal(t, g.score2, roll2) +} + +// TestPlayInvalidPlayer tests the scenario where an invalid player tries to play +func TestPlayInvalidPlayer(t *testing.T) { + resetGameState() + + std.TestSetOrigCaller(player1) + gameID := NewGame(player1, 3) + + // Attempt to play as an invalid player + std.TestSetOrigCaller(unknownPlayer) + urequire.PanicsWithMessage(t, "invalid player", func() { + Play(gameID) + }) +} + +// TestPlayAlreadyPlayed tests the scenario where a player tries to play again in the same round +func TestPlayAlreadyPlayed(t *testing.T) { + resetGameState() + + std.TestSetOrigCaller(player1) + gameID := NewGame(player2, 3) + + // Player 1 rolls + std.TestSetOrigCaller(player1) + Play(gameID) + + // Player 1 tries to roll again + urequire.PanicsWithMessage(t, "already played", func() { + Play(gameID) + }) +} + +// TestPlayUntilGameEnds tests the scenario where the game is played until it ends +func TestPlayUntilGameEnds(t *testing.T) { + resetGameState() + + std.TestSetOrigCaller(player1) + gameID := NewGame(player2, 10) // Set the game to have 10 rounds + + for round := 0; round < 10; round++ { + // Play each round + std.TestSetOrigCaller(player1) + Play(gameID) + std.TestSetOrigCaller(player2) + Play(gameID) + } + + // Check if the game is over + g, err := gameByID(gameID) + urequire.NoError(t, err) + urequire.Equal(t, 10, g.round) + urequire.NotEqual(t, "Game still in progress", g.winner()) + + // Verify the winner is correctly determined + winner := g.winner() + if winner == "Player 1 wins!" { + urequire.True(t, g.score1 > g.score2) + } else if winner == "Player 2 wins!" { + urequire.True(t, g.score2 > g.score1) + } else if winner == "It's a draw!" { + urequire.True(t, g.score1 == g.score2) + } +} + +// TestPlayBeyondMaxRounds tests that playing beyond the maximum number of rounds fails +func TestPlayBeyondMaxRounds(t *testing.T) { + resetGameState() + + std.TestSetOrigCaller(player1) + gameID := NewGame(player2, 1) // Only 1 round + + // Play the single round + std.TestSetOrigCaller(player1) + Play(gameID) + std.TestSetOrigCaller(player2) + Play(gameID) + + // Check if the game is over + g, err := gameByID(gameID) + urequire.NoError(t, err) + urequire.Equal(t, 1, g.round) + urequire.NotEqual(t, "Game still in progress", g.winner()) + + // Attempt to play more rounds should fail + std.TestSetOrigCaller(player1) + urequire.PanicsWithMessage(t, "game over", func() { + Play(gameID) + }) +} diff --git a/examples/gno.land/r/demo/games/diceduel/diceduel.gno b/examples/gno.land/r/demo/games/diceduel/diceduel.gno new file mode 100644 index 00000000000..19a27a9e313 --- /dev/null +++ b/examples/gno.land/r/demo/games/diceduel/diceduel.gno @@ -0,0 +1,191 @@ +package diceduel + +import ( + "errors" + "math/rand" + "std" + "strconv" + + "gno.land/p/demo/avl" + "gno.land/p/demo/entropy" + "gno.land/p/demo/seqid" + "gno.land/r/demo/users" +) + +type game struct { + player1, player2 std.Address + roll1, roll2 int + score1, score2 int + round, maxRounds int +} + +var ( + games avl.Tree + id seqid.ID + seed = uint64(entropy.New().Seed()) +) + +// Roll a dice between 1 and 6 +func rollDice() int { + r := rand.New(rand.NewPCG(seed, 0xdeadbeef)) + + return r.IntN(6) + 1 +} + +// NewGame initializes a new Dice Duel game with the specified number of rounds +func NewGame(player std.Address, rounds int) int { + if rounds <= 0 { + panic("invalid number of rounds") + } + + gameID := id.Next() + games.Set(gameID.String(), &game{ + player1: std.PrevRealm().Addr(), + player2: player, + maxRounds: rounds, + }) + return int(gameID) +} + +// Play simulates a dice roll for the current player and updates the game state +func Play(idx int) int { + g, err := gameByID(idx) + if err != nil { + panic(err) + } + + roll := rollDice() + + if err := g.play(std.PrevRealm().Addr(), roll); err != nil { + panic(err) + } + + // Check if both players have played, then advance the round + if g.roll1 != 0 && g.roll2 != 0 { + g.round++ + g.roll1, g.roll2 = 0, 0 // Reset rolls for the next round + } + + return roll +} + +// play handles a player's dice roll and updates their score +func (g *game) play(player std.Address, roll int) error { + if player != g.player1 && player != g.player2 { + return errors.New("invalid player") + } + + if g.round >= g.maxRounds { + return errors.New("game over") + } + + if player == g.player1 && g.roll1 == 0 { + g.roll1 = roll + g.score1 += roll + return nil + } + + if player == g.player2 && g.roll2 == 0 { + g.roll2 = roll + g.score2 += roll + return nil + } + + return errors.New("already played") +} + +// winner determines the winner of the game based on scores +func (g *game) winner() string { + if g.round < g.maxRounds { + return "Game still in progress" + } + + if g.score1 > g.score2 { + return "Player 1 wins!" + } else if g.score2 > g.score1 { + return "Player 2 wins!" + } else { + return "It's a draw!" + } +} + +// diceIcon returns a string representation of the dice roll +func diceIcon(roll int) string { + switch roll { + case 1: + return "🎲1" + case 2: + return "🎲2" + case 3: + return "🎲3" + case 4: + return "🎲4" + case 5: + return "🎲5" + case 6: + return "🎲6" + default: + return strconv.Itoa(roll) + } +} + +// Render provides a summary of the current state of all games +func Render(path string) string { + output := `# 🎲 Dice Duel + +## Game Rules: +Dice Duel is a two-player game where each player rolls a dice over a specified number of rounds +1. **Create a game** with an opponent using [NewGame](dice?help&__func=NewGame) +2. **Play rounds** using [Play](dice?help&__func=Play), where each player rolls a dice + - Dice values range from 1 to 6 for each roll + - Each player rolls the dice in turn, and the rolls are recorded for each round +## Actions: +* [NewGame](dice?help&__func=NewGame) opponentAddress rounds + - **opponentAddress**: The address of the opponent player + - **rounds**: The number of rounds for the game. Must be a positive integer +* [Play](dice?help&__func=Play) gameIndex + - **gameIndex**: The unique index of the game you want to play +## Recent Games: +Displays a summary of the most recent games. Up to 10 recent games are shown + +| Game | Player 1 | Roll 1 | Score 1 | Player 2 | Roll 2 | Score 2 | Round | Max Rounds | Winner | +|------|----------|--------|---------|----------|--------|---------|-------|------------|--------| +` + + // Display up to 10 recent games + maxGames := 10 + for n := int(id); n > 0 && int(id)-n < maxGames; n-- { + g, err := gameByID(n) + if err != nil { + continue + } + + output += strconv.Itoa(n) + " | " + + shortName(g.player1) + " | " + diceIcon(g.roll1) + " | " + strconv.Itoa(g.score1) + " | " + + shortName(g.player2) + " | " + diceIcon(g.roll2) + " | " + strconv.Itoa(g.score2) + " | " + + strconv.Itoa(g.round) + " | " + strconv.Itoa(g.maxRounds) + " | " + + g.winner() + "\n" + } + return output +} + +// shortName returns a shortened name for the given address +func shortName(addr std.Address) string { + user := users.GetUserByAddress(addr) + if user != nil { + return user.Name + } + if len(addr) < 10 { + return string(addr) + } + return string(addr)[:10] + "..." +} + +// gameByID retrieves the game state by its ID +func gameByID(idx int) (*game, error) { + v, ok := games.Get(seqid.ID(idx).String()) + if !ok { + return nil, errors.New("game not found") + } + return v.(*game), nil +} diff --git a/examples/gno.land/r/demo/games/diceduel/gno.mod b/examples/gno.land/r/demo/games/diceduel/gno.mod new file mode 100644 index 00000000000..0b2b10ac459 --- /dev/null +++ b/examples/gno.land/r/demo/games/diceduel/gno.mod @@ -0,0 +1,10 @@ +module gno.land/r/demo/games/diceduel + +require ( + gno.land/p/demo/avl v0.0.0-latest + gno.land/p/demo/entropy v0.0.0-latest + gno.land/p/demo/seqid v0.0.0-latest + gno.land/p/demo/testutils v0.0.0-latest + gno.land/p/demo/urequire v0.0.0-latest + gno.land/r/demo/users v0.0.0-latest +) From b9f86d677f657ae6fa1b0437946855b110377a0f Mon Sep 17 00:00:00 2001 From: linhpn99 Date: Sun, 8 Sep 2024 20:09:53 +0700 Subject: [PATCH 03/22] rename game --- .../{diceduel/diceduel.gno => dice_roller/dice_roller.gno} | 2 +- .../dice_test.gno => dice_roller/dice_roller_test.gno} | 2 +- .../gno.land/r/demo/games/{diceduel => dice_roller}/gno.mod | 0 3 files changed, 2 insertions(+), 2 deletions(-) rename examples/gno.land/r/demo/games/{diceduel/diceduel.gno => dice_roller/dice_roller.gno} (99%) rename examples/gno.land/r/demo/games/{diceduel/dice_test.gno => dice_roller/dice_roller_test.gno} (99%) rename examples/gno.land/r/demo/games/{diceduel => dice_roller}/gno.mod (100%) diff --git a/examples/gno.land/r/demo/games/diceduel/diceduel.gno b/examples/gno.land/r/demo/games/dice_roller/dice_roller.gno similarity index 99% rename from examples/gno.land/r/demo/games/diceduel/diceduel.gno rename to examples/gno.land/r/demo/games/dice_roller/dice_roller.gno index 19a27a9e313..a366bceb9e2 100644 --- a/examples/gno.land/r/demo/games/diceduel/diceduel.gno +++ b/examples/gno.land/r/demo/games/dice_roller/dice_roller.gno @@ -1,4 +1,4 @@ -package diceduel +package diceroller import ( "errors" diff --git a/examples/gno.land/r/demo/games/diceduel/dice_test.gno b/examples/gno.land/r/demo/games/dice_roller/dice_roller_test.gno similarity index 99% rename from examples/gno.land/r/demo/games/diceduel/dice_test.gno rename to examples/gno.land/r/demo/games/dice_roller/dice_roller_test.gno index be44c99180a..37777874706 100644 --- a/examples/gno.land/r/demo/games/diceduel/dice_test.gno +++ b/examples/gno.land/r/demo/games/dice_roller/dice_roller_test.gno @@ -1,4 +1,4 @@ -package diceduel +package diceroller import ( "std" diff --git a/examples/gno.land/r/demo/games/diceduel/gno.mod b/examples/gno.land/r/demo/games/dice_roller/gno.mod similarity index 100% rename from examples/gno.land/r/demo/games/diceduel/gno.mod rename to examples/gno.land/r/demo/games/dice_roller/gno.mod From b058967df7378bcf337790113b4d90215308372f Mon Sep 17 00:00:00 2001 From: linhpn99 Date: Sun, 8 Sep 2024 20:36:34 +0700 Subject: [PATCH 04/22] wrong adding --- .../games/dice_roller/dice_roller_test.gno | 11 +- tm2/pkg/std/fee.go | 28 --- tm2/pkg/std/gasprice_test.go | 95 ---------- tm2/pkg/std/tx.go | 27 +++ tm2/pkg/std/tx_test.go | 177 ------------------ 5 files changed, 36 insertions(+), 302 deletions(-) delete mode 100644 tm2/pkg/std/fee.go delete mode 100644 tm2/pkg/std/gasprice_test.go delete mode 100644 tm2/pkg/std/tx_test.go diff --git a/examples/gno.land/r/demo/games/dice_roller/dice_roller_test.gno b/examples/gno.land/r/demo/games/dice_roller/dice_roller_test.gno index 37777874706..186755546f6 100644 --- a/examples/gno.land/r/demo/games/dice_roller/dice_roller_test.gno +++ b/examples/gno.land/r/demo/games/dice_roller/dice_roller_test.gno @@ -26,8 +26,10 @@ func resetGameState() { func TestNewGame(t *testing.T) { resetGameState() + maxRounds := 100 + std.TestSetOrigCaller(player1) - gameID := NewGame(player2, 3) + gameID := NewGame(player2, maxRounds) // Verify that the game has been correctly initialized g, err := gameByID(gameID) @@ -39,7 +41,12 @@ func TestNewGame(t *testing.T) { urequire.Equal(t, 0, g.score1) urequire.Equal(t, 0, g.score2) urequire.Equal(t, 0, g.round) - urequire.Equal(t, 3, g.maxRounds) + urequire.Equal(t, maxRounds, g.maxRounds) + + // Invalid game with negative rounds + urequire.PanicsWithMessage(t, "invalid number of rounds", func() { + NewGame(player2, -1) + }) } // TestPlay tests the dice rolling functionality for both players diff --git a/tm2/pkg/std/fee.go b/tm2/pkg/std/fee.go deleted file mode 100644 index 14cf6128117..00000000000 --- a/tm2/pkg/std/fee.go +++ /dev/null @@ -1,28 +0,0 @@ -package std - -import "github.com/gnolang/gno/tm2/pkg/amino" - -// Fee includes the amount of coins paid in fees and the maximum -// gas to be used by the transaction. The ratio yields an effective "gasprice", -// which must be above some miminum to be accepted into the mempool. -type Fee struct { - GasWanted int64 `json:"gas_wanted" yaml:"gas_wanted"` - GasFee Coin `json:"gas_fee" yaml:"gas_fee"` -} - -// NewFee returns a new instance of Fee -func NewFee(gasWanted int64, gasFee Coin) Fee { - return Fee{ - GasWanted: gasWanted, - GasFee: gasFee, - } -} - -// Bytes for signing later -func (fee Fee) Bytes() []byte { - bz, err := amino.MarshalJSON(fee) // TODO - if err != nil { - panic(err) - } - return bz -} diff --git a/tm2/pkg/std/gasprice_test.go b/tm2/pkg/std/gasprice_test.go deleted file mode 100644 index 7671c124716..00000000000 --- a/tm2/pkg/std/gasprice_test.go +++ /dev/null @@ -1,95 +0,0 @@ -package std - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestParseGasPrice(t *testing.T) { - tests := []struct { - input string - expected GasPrice - hasError bool - }{ - { - input: "10foo/1gas", - expected: GasPrice{Gas: 1, Price: Coin{Denom: "foo", Amount: 10}}, - hasError: false, - }, - { - input: "100bar/2gas", - expected: GasPrice{Gas: 2, Price: Coin{Denom: "bar", Amount: 100}}, - hasError: false, - }, - { - input: "5coin/0gas", - expected: GasPrice{Gas: 0, Price: Coin{Denom: "coin", Amount: 5}}, - hasError: false, - }, - { - input: "invalid", - expected: GasPrice{}, - hasError: true, - }, - { - input: "10foo/1coin", - expected: GasPrice{}, - hasError: true, - }, - } - - for _, test := range tests { - result, err := ParseGasPrice(test.input) - if test.hasError { - assert.True(t, err != nil, "expected error but got none") - } else { - assert.Nil(t, err, "unexpected error") - assert.Equal(t, result, test.expected) - } - } -} - -func TestParseGasPrices(t *testing.T) { - tests := []struct { - input string - expected []GasPrice - hasError bool - }{ - { - input: "10foo/1gas;5bar/2gas", - expected: []GasPrice{ - {Gas: 1, Price: Coin{Denom: "foo", Amount: 10}}, - {Gas: 2, Price: Coin{Denom: "bar", Amount: 5}}, - }, - hasError: false, - }, - { - input: "5coin/0gas", - expected: []GasPrice{ - {Gas: 0, Price: Coin{Denom: "coin", Amount: 5}}, - }, - hasError: false, - }, - { - input: "invalid", - expected: nil, - hasError: true, - }, - { - input: "10foo/1coin;5bar/2gas", - expected: nil, - hasError: true, - }, - } - - for _, test := range tests { - result, err := ParseGasPrices(test.input) - if test.hasError { - assert.True(t, err != nil, "expected error but got none") - } else { - assert.Nil(t, err, "unexpected error") - assert.Equal(t, result, test.expected) - } - } -} diff --git a/tm2/pkg/std/tx.go b/tm2/pkg/std/tx.go index c2ba4b94af4..4fd7af4a641 100644 --- a/tm2/pkg/std/tx.go +++ b/tm2/pkg/std/tx.go @@ -118,6 +118,33 @@ func (tx Tx) GetSignBytes(chainID string, accountNumber uint64, sequence uint64) }) } +// __________________________________________________________ + +// Fee includes the amount of coins paid in fees and the maximum +// gas to be used by the transaction. The ratio yields an effective "gasprice", +// which must be above some miminum to be accepted into the mempool. +type Fee struct { + GasWanted int64 `json:"gas_wanted" yaml:"gas_wanted"` + GasFee Coin `json:"gas_fee" yaml:"gas_fee"` +} + +// NewFee returns a new instance of Fee +func NewFee(gasWanted int64, gasFee Coin) Fee { + return Fee{ + GasWanted: gasWanted, + GasFee: gasFee, + } +} + +// Bytes for signing later +func (fee Fee) Bytes() []byte { + bz, err := amino.MarshalJSON(fee) // TODO + if err != nil { + panic(err) + } + return bz +} + func ParseTxs(ctx context.Context, reader io.Reader) ([]Tx, error) { var txs []Tx scanner := bufio.NewScanner(reader) diff --git a/tm2/pkg/std/tx_test.go b/tm2/pkg/std/tx_test.go deleted file mode 100644 index 2f662512954..00000000000 --- a/tm2/pkg/std/tx_test.go +++ /dev/null @@ -1,177 +0,0 @@ -package std - -import ( - "testing" - - "github.com/gnolang/gno/tm2/pkg/crypto" - "github.com/gnolang/gno/tm2/pkg/crypto/ed25519" - "github.com/gnolang/gno/tm2/pkg/crypto/multisig" - "github.com/stretchr/testify/require" -) - -// Mock implementations of Msg interfaces -type mockMsg struct { - caller crypto.Address - msgType string -} - -func (m mockMsg) ValidateBasic() error { - return nil -} - -func (m mockMsg) GetSignBytes() []byte { - return nil -} - -func (m mockMsg) GetSigners() []crypto.Address { - return []crypto.Address{m.caller} -} - -func (m mockMsg) Route() string { - return "" -} - -func (m mockMsg) Type() string { - return m.msgType -} - -func TestNewTx(t *testing.T) { - addr, _ := crypto.AddressFromBech32("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") - msgs := []Msg{ - mockMsg{ - caller: addr, - }, - } - - fee := NewFee(1000, Coin{Denom: "atom", Amount: 10}) - - sigs := []Signature{ - { - Signature: []byte{0x00}, - }, - } - - memo := "test memo" - - tx := NewTx(msgs, fee, sigs, memo) - require.Equal(t, msgs, tx.GetMsgs()) - require.Equal(t, fee, tx.Fee) - require.Equal(t, sigs, tx.GetSignatures()) - require.Equal(t, memo, tx.GetMemo()) -} - -func Test_ValidateBasic(t *testing.T) { - addr, _ := crypto.AddressFromBech32("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") - msgs := []Msg{ - mockMsg{ - caller: addr, - }, - } - - fee := NewFee(maxGasWanted, Coin{Denom: "atom", Amount: 10}) - sigs := []Signature{ - { - Signature: []byte{0x00}, - }, - } - - tx := NewTx(msgs, fee, sigs, "test memo") - - // Valid case - require.NoError(t, tx.ValidateBasic()) - - // Invalid gas case - invalidFee := NewFee(maxGasWanted+1, Coin{Denom: "atom", Amount: 10}) - txInvalidGas := NewTx(msgs, invalidFee, sigs, "test memo") - require.Error(t, txInvalidGas.ValidateBasic(), "expected gas overflow error") - - // Invalid fee case - invalidFeeAmount := NewFee(1000, Coin{Denom: "atom", Amount: -10}) - txInvalidFee := NewTx(msgs, invalidFeeAmount, sigs, "test memo") - require.Error(t, txInvalidFee.ValidateBasic(), "expected insufficient fee error") - - // No signatures case - txNoSigs := NewTx(msgs, fee, []Signature{}, "test memo") - require.Error(t, txNoSigs.ValidateBasic(), "expected no signatures error") - - // Wrong number of signers case - wrongSigs := []Signature{ - { - Signature: []byte{0x00}, - }, - { - Signature: []byte{0x01}, - }, - } - txWrongSigs := NewTx(msgs, fee, wrongSigs, "test memo") - require.Error(t, txWrongSigs.ValidateBasic(), "expected wrong number of signers error") -} - -func Test_CountSubKeys(t *testing.T) { - // Single key case - pubKey := ed25519.GenPrivKey().PubKey() - require.Equal(t, 1, CountSubKeys(pubKey)) - - // Multi-sig case - // Assuming multisig.PubKeyMultisigThreshold is correctly implemented for testing purposes - pubKeys := []crypto.PubKey{ed25519.GenPrivKey().PubKey(), ed25519.GenPrivKey().PubKey()} - multisigPubKey := multisig.NewPubKeyMultisigThreshold(2, pubKeys) - require.Equal(t, len(pubKeys), CountSubKeys(multisigPubKey)) -} - -func Test_GetSigners(t *testing.T) { - // Single signer case - addr, _ := crypto.AddressFromBech32("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") - msgs := []Msg{ - mockMsg{ - caller: addr, - msgType: "call", - }, - } - tx := NewTx(msgs, Fee{}, []Signature{}, "") - require.Equal(t, []crypto.Address{addr}, tx.GetSigners()) - - // Duplicate signers case - msgs = []Msg{ - mockMsg{ - caller: addr, - msgType: "send", - }, - mockMsg{ - caller: addr, - msgType: "send", - }, - } - - tx = NewTx(msgs, Fee{}, []Signature{}, "") - require.Equal(t, []crypto.Address{addr}, tx.GetSigners()) - - // Multiple unique signers case - addr2, _ := crypto.AddressFromBech32("g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj") - msgs = []Msg{ - mockMsg{ - caller: addr, - msgType: "call", - }, - mockMsg{ - caller: addr2, - msgType: "run", - }, - } - tx = NewTx(msgs, Fee{}, []Signature{}, "") - require.Equal(t, []crypto.Address{addr, addr2}, tx.GetSigners()) -} - -func Test_GetSignBytes(t *testing.T) { - msgs := []Msg{} - fee := NewFee(1000, Coin{Denom: "atom", Amount: 10}) - sigs := []Signature{} - tx := NewTx(msgs, fee, sigs, "test memo") - chainID := "test-chain" - accountNumber := uint64(1) - sequence := uint64(1) - - signBytes, err := tx.GetSignBytes(chainID, accountNumber, sequence) - require.NoError(t, err) - require.NotEmpty(t, signBytes) -} From a1f48dfe1f4ecec48982d5a400c860cea69021ef Mon Sep 17 00:00:00 2001 From: linhpn99 Date: Sun, 8 Sep 2024 23:47:45 +0700 Subject: [PATCH 05/22] tmp --- .../r/demo/games/diceroller/diceroller.gno | 325 ++++++++++++++++++ .../demo/games/diceroller/diceroller_test.gno | 191 ++++++++++ .../gno.land/r/demo/games/diceroller/gno.mod | 10 + 3 files changed, 526 insertions(+) create mode 100644 examples/gno.land/r/demo/games/diceroller/diceroller.gno create mode 100644 examples/gno.land/r/demo/games/diceroller/diceroller_test.gno create mode 100644 examples/gno.land/r/demo/games/diceroller/gno.mod diff --git a/examples/gno.land/r/demo/games/diceroller/diceroller.gno b/examples/gno.land/r/demo/games/diceroller/diceroller.gno new file mode 100644 index 00000000000..ad9c85f5381 --- /dev/null +++ b/examples/gno.land/r/demo/games/diceroller/diceroller.gno @@ -0,0 +1,325 @@ +package diceroller + +import ( + "errors" + "math/rand" + "sort" + "std" + "strconv" + + "gno.land/p/demo/avl" + "gno.land/p/demo/entropy" + "gno.land/p/demo/seqid" + "gno.land/p/demo/testutils" + "gno.land/p/demo/ufmt" + "gno.land/r/demo/users" +) + +type game struct { + player1, player2 *player + roll1, roll2 int + score1, score2 int + round, maxRounds int +} + +type player struct { + addr std.Address + wins, losses, draws, points, rank int +} + +type leaderBoard []player + +const ( + empty = iota + win + draw + lose +) + +var ( + games avl.Tree + gameId seqid.ID + + players avl.Tree + playerId seqid.ID + + seed = uint64(entropy.New().Seed()) +) + +// Roll a dice between 1 and 6 +func rollDice() int { + r := rand.New(rand.NewPCG(seed, 0xdeadbeef)) + + return r.IntN(6) + 1 +} + +// NewGame initializes a new Dice Duel game with the specified number of rounds +func NewGame(addr std.Address, rounds int) int { + if rounds <= 0 { + panic("invalid number of rounds") + } + + for i := 0; i <= 10; i++ { + addr := testutils.TestAddress(ufmt.Sprintf("%d", i)) + players.Set(playerId.Next().String(), &player{ + addr: addr, + points: i + 1, + }) + } + + games.Set(gameId.Next().String(), &game{ + player1: &player{addr: std.PrevRealm().Addr()}, + player2: &player{addr: addr}, + maxRounds: rounds, + }) + return int(gameId) +} + +// Play simulates a dice roll for the current player and updates the game state +func Play(idx int) int { + g, err := getGame(idx) + if err != nil { + panic(err) + } + + roll := rollDice() + + if err := g.play(std.PrevRealm().Addr(), roll); err != nil { + panic(err) + } + + // Check if both players have played, then advance the round + if g.roll1 != 0 && g.roll2 != 0 { + g.round++ + g.roll1, g.roll2 = 0, 0 // Reset rolls for the next round + } + + // Check if the game is over and update the player's stats + if g.round >= g.maxRounds { + if g.score1 > g.score2 { + g.player1.updateResult(win) + g.player2.updateResult(lose) + } else if g.score2 > g.score1 { + g.player2.updateResult(win) + g.player1.updateResult(lose) + } else { + g.player1.updateResult(draw) + g.player2.updateResult(draw) + } + } + + return roll +} + +// play handles a player's dice roll and updates their score +func (g *game) play(player std.Address, roll int) error { + if player != g.player1.addr && player != g.player2.addr { + return errors.New("invalid player") + } + + if g.round >= g.maxRounds { + return errors.New("game over") + } + + if player == g.player1.addr && g.roll1 == 0 { + g.roll1 = roll + g.score1 += roll + return nil + } + + if player == g.player2.addr && g.roll2 == 0 { + g.roll2 = roll + g.score2 += roll + return nil + } + + return errors.New("already played") +} + +// winner determines the winner of the game based on scores +func (g *game) winner() string { + if g.round < g.maxRounds { + return "Game still in progress" + } + + if g.score1 > g.score2 { + return "Player 1 wins!" + } else if g.score2 > g.score1 { + return "Player 2 wins!" + } else { + return "It's a draw!" + } +} + +// diceIcon returns a string representation of the dice roll +func diceIcon(roll int) string { + switch roll { + case 1: + return "🎲1" + case 2: + return "🎲2" + case 3: + return "🎲3" + case 4: + return "🎲4" + case 5: + return "🎲5" + case 6: + return "🎲6" + default: + return strconv.Itoa(roll) + } +} + +// Render provides a summary of the current state of all games and player rankings. +func Render(path string) string { + // Create the output for the games + output := `# 🎲 Dice Roller + +## Game Rules: +Dice Roller is a two-player game where each player rolls a dice over a specified number of rounds +1. **Create a game** with an opponent using [NewGame](diceroller?help&__func=NewGame) +2. **Play rounds** using [Play](diceroller?help&__func=Play), where each player rolls a dice + - Dice values range from 1 to 6 for each roll + - Each player rolls the dice in turn, and the rolls are recorded for each round +## Actions: +* [NewGame](diceroller?help&__func=NewGame) opponentAddress rounds + - **opponentAddress**: The address of the opponent player + - **rounds**: The number of rounds for the game. Must be a positive integer +* [Play](diceroller?help&__func=Play) gameIndex + - **gameIndex**: The unique index of the game you want to play +## Recent Games: +Displays a summary of the most recent games. Up to 10 recent games are shown + +| Game | Player 1 | Roll 1 | Score 1 | Player 2 | Roll 2 | Score 2 | Round | Max Rounds | Winner | +|------|----------|--------|---------|----------|--------|---------|-------|------------|--------| +` + + // Display up to 10 recent games + maxGames := 10 + for n := int(gameId); n > 0 && int(gameId)-n < maxGames; n-- { + g, err := getGame(n) + if err != nil { + continue + } + + output += strconv.Itoa(n) + " | " + + shortName(g.player1.addr) + " | " + diceIcon(g.roll1) + " | " + strconv.Itoa(g.score1) + " | " + + shortName(g.player2.addr) + " | " + diceIcon(g.roll2) + " | " + strconv.Itoa(g.score2) + " | " + + strconv.Itoa(g.round) + " | " + strconv.Itoa(g.maxRounds) + " | " + + g.winner() + "\n" + } + + // Add ranking board to the output + // Display the ranking board + output += ` +## Player Rankings: +Displays the current ranking of players based on their performance in Dice Roller. + +| Rank | Player | Wins | Losses | Draws | Points | +|------|-----------------------|------|--------|-------|--------| +` + + for i, player := range getRankingBoard() { + // Determine the icon based on the rank + rankIcon := strconv.Itoa(i) + + if i == 0 { + rankIcon = "πŸ₯‡" + } else if i == 1 { + rankIcon = "πŸ₯ˆ" + } else if i == 2 { + rankIcon = "πŸ₯‰" + } + + // Format the player address to ensure it's aligned + playerName := shortName(player.addr) + if len(playerName) > 20 { + playerName = playerName[:20] + "..." + } + + // Add each player's rank information + output += ufmt.Sprintf("| %s | %s | %d | %d | %d | %d |\n", + rankIcon, + playerName, + player.wins, + player.losses, + player.draws, + player.points, + ) + } + + return output +} + +// shortName returns a shortened name for the given address +func shortName(addr std.Address) string { + user := users.GetUserByAddress(addr) + if user != nil { + return user.Name + } + if len(addr) < 10 { + return string(addr) + } + return string(addr)[:10] + "..." +} + +// getGame retrieves the game state by its ID +func getGame(idx int) (*game, error) { + v, ok := games.Get(seqid.ID(idx).String()) + if !ok { + return nil, errors.New("game not found") + } + return v.(*game), nil +} + +func (p *player) updateResult(result int) { + switch result { + case win: + p.wins++ + p.points += 3 + case lose: + p.losses++ + case draw: + p.draws++ + p.points++ + } +} + +func getPlayer(addr std.Address) *player { + v, ok := players.Get(addr.String()) + if !ok { + player := &player{ + addr: addr, + } + players.Set(playerId.Next().String(), player) + return player + } + + return v.(*player) +} + +func getRankingBoard() leaderBoard { + board := leaderBoard{} + players.Iterate("", "", func(key string, value interface{}) bool { + player := value.(*player) + board = append(board, *player) + return false + }) + + sort.Sort(board) + + return board +} + +func (r leaderBoard) Len() int { + return len(r) +} + +func (r leaderBoard) Less(i, j int) bool { + return r[i].points > r[j].points +} + +func (r leaderBoard) Swap(i, j int) { + r[i].points, r[j].points = r[j].points, r[i].points +} diff --git a/examples/gno.land/r/demo/games/diceroller/diceroller_test.gno b/examples/gno.land/r/demo/games/diceroller/diceroller_test.gno new file mode 100644 index 00000000000..99fbe7d735b --- /dev/null +++ b/examples/gno.land/r/demo/games/diceroller/diceroller_test.gno @@ -0,0 +1,191 @@ +package diceroller + +import ( + "std" + "testing" + + "gno.land/p/demo/avl" + "gno.land/p/demo/seqid" + "gno.land/p/demo/testutils" + "gno.land/p/demo/urequire" +) + +var ( + player1 = testutils.TestAddress("alice") + player2 = testutils.TestAddress("bob") + unknownPlayer = testutils.TestAddress("unknown") +) + +// resetGameState resets the game state for testing +func resetGameState() { + games = avl.Tree{} + gameId = seqid.ID(0) + players = avl.Tree{} + playerId = seqid.ID(0) +} + +// TestNewGame tests the initialization of a new game +func TestNewGame(t *testing.T) { + resetGameState() + + maxRounds := 100 + + std.TestSetOrigCaller(player1) + gameID := NewGame(player2, maxRounds) + + // Verify that the game has been correctly initialized + g, err := getGame(gameID) + urequire.NoError(t, err) + urequire.Equal(t, player1.String(), g.player1.addr.String()) + urequire.Equal(t, player2.String(), g.player2.addr.String()) + urequire.Equal(t, 0, g.roll1) + urequire.Equal(t, 0, g.roll2) + urequire.Equal(t, 0, g.score1) + urequire.Equal(t, 0, g.score2) + urequire.Equal(t, 0, g.round) + urequire.Equal(t, maxRounds, g.maxRounds) + + // Invalid game with negative rounds + urequire.PanicsWithMessage(t, "invalid number of rounds", func() { + NewGame(player2, -1) + }) +} + +// TestPlay tests the dice rolling functionality for both players +func TestPlay(t *testing.T) { + resetGameState() + + std.TestSetOrigCaller(player1) + gameID := NewGame(player2, 3) + + g, err := getGame(gameID) + urequire.NoError(t, err) + + // Simulate rolling dice for player 1 + roll1 := Play(gameID) + + // Verify player 1's roll and score + urequire.NotEqual(t, 0, g.roll1) + urequire.Equal(t, g.score1, roll1) + urequire.Equal(t, 0, g.roll2) // Player 2 hasn't rolled yet + urequire.Equal(t, 0, g.round) + + // Simulate rolling dice for player 2 + std.TestSetOrigCaller(player2) + roll2 := Play(gameID) + + // Verify player 2's roll and score + urequire.Equal(t, 0, g.roll1) + urequire.Equal(t, 0, g.roll2) + urequire.Equal(t, g.score1, roll1) + urequire.Equal(t, g.score2, roll2) + urequire.Equal(t, 1, g.round) // Round should be incremented now +} + +// TestPlayAgainstSelf tests the scenario where a player plays against themselves +func TestPlayAgainstSelf(t *testing.T) { + resetGameState() + + std.TestSetOrigCaller(player1) + gameID := NewGame(player1, 1) + + // Simulate rolling dice twice by the same player + roll1 := Play(gameID) + roll2 := Play(gameID) + + g, err := getGame(gameID) + urequire.NoError(t, err) + urequire.Equal(t, 0, g.roll1) + urequire.Equal(t, g.score1, roll1) + urequire.Equal(t, 0, g.roll2) + urequire.Equal(t, g.score2, roll2) +} + +// TestPlayInvalidPlayer tests the scenario where an invalid player tries to play +func TestPlayInvalidPlayer(t *testing.T) { + resetGameState() + + std.TestSetOrigCaller(player1) + gameID := NewGame(player1, 3) + + // Attempt to play as an invalid player + std.TestSetOrigCaller(unknownPlayer) + urequire.PanicsWithMessage(t, "invalid player", func() { + Play(gameID) + }) +} + +// TestPlayAlreadyPlayed tests the scenario where a player tries to play again in the same round +func TestPlayAlreadyPlayed(t *testing.T) { + resetGameState() + + std.TestSetOrigCaller(player1) + gameID := NewGame(player2, 3) + + // Player 1 rolls + std.TestSetOrigCaller(player1) + Play(gameID) + + // Player 1 tries to roll again + urequire.PanicsWithMessage(t, "already played", func() { + Play(gameID) + }) +} + +// TestPlayUntilGameEnds tests the scenario where the game is played until it ends +func TestPlayUntilGameEnds(t *testing.T) { + resetGameState() + + std.TestSetOrigCaller(player1) + gameID := NewGame(player2, 10) // Set the game to have 10 rounds + + for round := 0; round < 10; round++ { + // Play each round + std.TestSetOrigCaller(player1) + Play(gameID) + std.TestSetOrigCaller(player2) + Play(gameID) + } + + // Check if the game is over + g, err := getGame(gameID) + urequire.NoError(t, err) + urequire.Equal(t, 10, g.round) + urequire.NotEqual(t, "Game still in progress", g.winner()) + + // Verify the winner is correctly determined + winner := g.winner() + if winner == "Player 1 wins!" { + urequire.True(t, g.score1 > g.score2) + } else if winner == "Player 2 wins!" { + urequire.True(t, g.score2 > g.score1) + } else if winner == "It's a draw!" { + urequire.True(t, g.score1 == g.score2) + } +} + +// TestPlayBeyondMaxRounds tests that playing beyond the maximum number of rounds fails +func TestPlayBeyondMaxRounds(t *testing.T) { + resetGameState() + + std.TestSetOrigCaller(player1) + gameID := NewGame(player2, 1) // Only 1 round + + // Play the single round + std.TestSetOrigCaller(player1) + Play(gameID) + std.TestSetOrigCaller(player2) + Play(gameID) + + // Check if the game is over + g, err := getGame(gameID) + urequire.NoError(t, err) + urequire.Equal(t, 1, g.round) + urequire.NotEqual(t, "Game still in progress", g.winner()) + + // Attempt to play more rounds should fail + std.TestSetOrigCaller(player1) + urequire.PanicsWithMessage(t, "game over", func() { + Play(gameID) + }) +} diff --git a/examples/gno.land/r/demo/games/diceroller/gno.mod b/examples/gno.land/r/demo/games/diceroller/gno.mod new file mode 100644 index 00000000000..da687c76641 --- /dev/null +++ b/examples/gno.land/r/demo/games/diceroller/gno.mod @@ -0,0 +1,10 @@ +module gno.land/r/demo/games/diceroller + +require ( + gno.land/p/demo/avl v0.0.0-latest + gno.land/p/demo/entropy v0.0.0-latest + gno.land/p/demo/seqid v0.0.0-latest + gno.land/p/demo/testutils v0.0.0-latest + gno.land/p/demo/urequire v0.0.0-latest + gno.land/r/demo/users v0.0.0-latest +) From 58262294a27fbfc8a2569d8542c0b754f8b00016 Mon Sep 17 00:00:00 2001 From: linhpn99 Date: Mon, 9 Sep 2024 22:51:24 +0700 Subject: [PATCH 06/22] update gameplay --- .../r/demo/games/dice_roller/dice_roller.gno | 262 +++++++++++--- .../games/dice_roller/dice_roller_test.gno | 92 ++--- .../gno.land/r/demo/games/dice_roller/gno.mod | 3 +- .../r/demo/games/diceroller/diceroller.gno | 325 ------------------ .../demo/games/diceroller/diceroller_test.gno | 191 ---------- .../gno.land/r/demo/games/diceroller/gno.mod | 10 - 6 files changed, 245 insertions(+), 638 deletions(-) delete mode 100644 examples/gno.land/r/demo/games/diceroller/diceroller.gno delete mode 100644 examples/gno.land/r/demo/games/diceroller/diceroller_test.gno delete mode 100644 examples/gno.land/r/demo/games/diceroller/gno.mod diff --git a/examples/gno.land/r/demo/games/dice_roller/dice_roller.gno b/examples/gno.land/r/demo/games/dice_roller/dice_roller.gno index a366bceb9e2..d787b9492c1 100644 --- a/examples/gno.land/r/demo/games/dice_roller/dice_roller.gno +++ b/examples/gno.land/r/demo/games/dice_roller/dice_roller.gno @@ -3,113 +3,175 @@ package diceroller import ( "errors" "math/rand" + "sort" "std" "strconv" "gno.land/p/demo/avl" "gno.land/p/demo/entropy" "gno.land/p/demo/seqid" + "gno.land/p/demo/ufmt" "gno.land/r/demo/users" ) -type game struct { - player1, player2 std.Address - roll1, roll2 int - score1, score2 int - round, maxRounds int -} +type ( + // game represents a Dice Roller game between two players + game struct { + player1, player2 std.Address + roll1, roll2 int + } + + // player holds the information about each player including their stats + player struct { + addr std.Address + wins, losses, draws, points, rank int + } + + // leaderBoard is a slice of players, used to sort players by rank + leaderBoard []player +) + +const ( + // Constants to represent game result outcomes + empty = iota + win + draw + lose +) var ( - games avl.Tree - id seqid.ID - seed = uint64(entropy.New().Seed()) + games avl.Tree // AVL tree for storing game states + gameId seqid.ID // Sequence ID for games + + players avl.Tree // AVL tree for storing player data + + seed = uint64(entropy.New().Seed()) ) -// Roll a dice between 1 and 6 +func init() { + // Pre-define two players for initial setup + addr1 := std.Address("g1y490harzjqmdngmmaw4tyc4qdm5ma5k0fv3w6p") + addr2 := std.Address("g1z4dfj9dvknvahykg3rhjrpsexhp4vl8gnde27q") + + // Initialize two players and their initial game stats + players.Set(addr1.String(), &player{ + addr: addr1, + wins: 1, + points: 3, // Wins give 3 points + }) + + players.Set(addr2.String(), &player{ + addr: addr2, + losses: 1, + points: 0, // Losses give 0 points + }) + + games.Set(gameId.Next().String(), &game{ + player1: addr1, + player2: addr2, + roll1: 6, + roll2: 5, + }) +} + +// rollDice generates a random dice roll between 1 and 6 func rollDice() int { r := rand.New(rand.NewPCG(seed, 0xdeadbeef)) - return r.IntN(6) + 1 } -// NewGame initializes a new Dice Duel game with the specified number of rounds -func NewGame(player std.Address, rounds int) int { - if rounds <= 0 { - panic("invalid number of rounds") +// NewGame initializes a new game with the provided opponent's address +func NewGame(addr std.Address) int { + if !addr.IsValid() { + panic("invalid opponent's address") } - gameID := id.Next() - games.Set(gameID.String(), &game{ - player1: std.PrevRealm().Addr(), - player2: player, - maxRounds: rounds, + games.Set(gameId.Next().String(), &game{ + player1: std.PrevRealm().Addr(), + player2: addr, }) - return int(gameID) + + return int(gameId) } -// Play simulates a dice roll for the current player and updates the game state +// Play allows a player to roll the dice and updates the game state accordingly func Play(idx int) int { - g, err := gameByID(idx) + g, err := getGame(idx) if err != nil { panic(err) } - roll := rollDice() + roll := rollDice() // Random the player's dice roll + // Play the game and update the player's roll if err := g.play(std.PrevRealm().Addr(), roll); err != nil { panic(err) } - // Check if both players have played, then advance the round + // If both players have rolled, update the results and leaderboard if g.roll1 != 0 && g.roll2 != 0 { - g.round++ - g.roll1, g.roll2 = 0, 0 // Reset rolls for the next round + // If the player is playing against themselves, no points are awarded + if g.player1 == g.player2 { + return roll + } + + player1 := getPlayer(g.player1) + player2 := getPlayer(g.player2) + + if g.roll1 > g.roll2 { + player1.updateResult(win) + player2.updateResult(lose) + } else if g.roll2 > g.roll1 { + player2.updateResult(win) + player1.updateResult(lose) + } else { + player1.updateResult(draw) + player2.updateResult(draw) + } } return roll } -// play handles a player's dice roll and updates their score +// play processes a player's roll and updates their score func (g *game) play(player std.Address, roll int) error { if player != g.player1 && player != g.player2 { return errors.New("invalid player") } - if g.round >= g.maxRounds { + if g.roll1 != 0 && g.roll2 != 0 { return errors.New("game over") } if player == g.player1 && g.roll1 == 0 { g.roll1 = roll - g.score1 += roll return nil } if player == g.player2 && g.roll2 == 0 { g.roll2 = roll - g.score2 += roll return nil } return errors.New("already played") } -// winner determines the winner of the game based on scores +// winner determines the winner based on scores func (g *game) winner() string { - if g.round < g.maxRounds { + if g.roll1 == 0 || g.roll2 == 0 { return "Game still in progress" } - if g.score1 > g.score2 { + if g.roll1 > g.roll2 { return "Player 1 wins!" - } else if g.score2 > g.score1 { + } else if g.roll2 > g.roll1 { return "Player 2 wins!" } else { return "It's a draw!" } } -// diceIcon returns a string representation of the dice roll +// diceIcon returns an icon of the dice roll func diceIcon(roll int) string { switch roll { case 1: @@ -129,43 +191,78 @@ func diceIcon(roll int) string { } } -// Render provides a summary of the current state of all games +// Render provides a summary of the current state of games and leader board func Render(path string) string { - output := `# 🎲 Dice Duel + output := `# 🎲 Dice Roller ## Game Rules: -Dice Duel is a two-player game where each player rolls a dice over a specified number of rounds -1. **Create a game** with an opponent using [NewGame](dice?help&__func=NewGame) -2. **Play rounds** using [Play](dice?help&__func=Play), where each player rolls a dice - - Dice values range from 1 to 6 for each roll - - Each player rolls the dice in turn, and the rolls are recorded for each round +Dice Roller is a two-player game where each player rolls a dice once. +1. **Create a game** with an opponent using [NewGame](diceroller?help&__func=NewGame) +2. **Play** using [Play](diceroller?help&__func=Play), where each player rolls a dice + +## Scoring: +- Playing against yourself? No points or stats changes for you +- **Win:** 3 points +- **Loss:** 0 points +- **Draw:** 1 point each + ## Actions: -* [NewGame](dice?help&__func=NewGame) opponentAddress rounds +* [NewGame](diceroller?help&__func=NewGame) opponentAddress - **opponentAddress**: The address of the opponent player - - **rounds**: The number of rounds for the game. Must be a positive integer -* [Play](dice?help&__func=Play) gameIndex +* [Play](diceroller?help&__func=Play) gameIndex - **gameIndex**: The unique index of the game you want to play + ## Recent Games: Displays a summary of the most recent games. Up to 10 recent games are shown -| Game | Player 1 | Roll 1 | Score 1 | Player 2 | Roll 2 | Score 2 | Round | Max Rounds | Winner | -|------|----------|--------|---------|----------|--------|---------|-------|------------|--------| +| Game | Player 1 | Roll 1 | Player 2 | Roll 2 | Winner | +|------|----------|--------|----------|--------|--------| ` // Display up to 10 recent games maxGames := 10 - for n := int(id); n > 0 && int(id)-n < maxGames; n-- { - g, err := gameByID(n) + for n := int(gameId); n > 0 && int(gameId)-n < maxGames; n-- { + g, err := getGame(n) if err != nil { continue } output += strconv.Itoa(n) + " | " + - shortName(g.player1) + " | " + diceIcon(g.roll1) + " | " + strconv.Itoa(g.score1) + " | " + - shortName(g.player2) + " | " + diceIcon(g.roll2) + " | " + strconv.Itoa(g.score2) + " | " + - strconv.Itoa(g.round) + " | " + strconv.Itoa(g.maxRounds) + " | " + + shortName(g.player1) + " | " + diceIcon(g.roll1) + " | " + + shortName(g.player2) + " | " + diceIcon(g.roll2) + " | " + g.winner() + "\n" } + + // Display the leader board + output += ` +## Leader board: +Displays the current ranking of players based on their performance in Dice Roller + +| Rank | Player | Wins | Losses | Draws | Points | +|------|-----------------------|------|--------|-------|--------| +` + + for i, player := range getLeaderBoard() { + rankIcon := strconv.Itoa(i) + + if i == 0 { + rankIcon = "πŸ₯‡" + } else if i == 1 { + rankIcon = "πŸ₯ˆ" + } else if i == 2 { + rankIcon = "πŸ₯‰" + } + + output += ufmt.Sprintf("| %s | %s | %d | %d | %d | %d |\n", + rankIcon, + shortName(player.addr), + player.wins, + player.losses, + player.draws, + player.points, + ) + } + return output } @@ -181,11 +278,66 @@ func shortName(addr std.Address) string { return string(addr)[:10] + "..." } -// gameByID retrieves the game state by its ID -func gameByID(idx int) (*game, error) { +// getGame retrieves the game state by its ID +func getGame(idx int) (*game, error) { v, ok := games.Get(seqid.ID(idx).String()) if !ok { return nil, errors.New("game not found") } return v.(*game), nil } + +// updateResult updates the player's result and points based on the game outcome +func (p *player) updateResult(result int) { + switch result { + case win: + p.wins++ + p.points += 3 + case lose: + p.losses++ + case draw: + p.draws++ + p.points++ + } +} + +// getPlayer retrieves a player or initializes a new one if they don't exist +func getPlayer(addr std.Address) *player { + v, ok := players.Get(addr.String()) + if !ok { + player := &player{ + addr: addr, + } + players.Set(addr.String(), player) + return player + } + + return v.(*player) +} + +// getLeaderBoard generates a leaderboard sorted by points +func getLeaderBoard() leaderBoard { + board := leaderBoard{} + players.Iterate("", "", func(key string, value interface{}) bool { + player := value.(*player) + board = append(board, *player) + return false + }) + + sort.Sort(board) + + return board +} + +// Methods for sorting the leaderboard +func (r leaderBoard) Len() int { + return len(r) +} + +func (r leaderBoard) Less(i, j int) bool { + return r[i].points > r[j].points +} + +func (r leaderBoard) Swap(i, j int) { + r[i].points, r[j].points = r[j].points, r[i].points +} diff --git a/examples/gno.land/r/demo/games/dice_roller/dice_roller_test.gno b/examples/gno.land/r/demo/games/dice_roller/dice_roller_test.gno index 186755546f6..24946175741 100644 --- a/examples/gno.land/r/demo/games/dice_roller/dice_roller_test.gno +++ b/examples/gno.land/r/demo/games/dice_roller/dice_roller_test.gno @@ -19,34 +19,24 @@ var ( // resetGameState resets the game state for testing func resetGameState() { games = avl.Tree{} - id = seqid.ID(0) + gameId = seqid.ID(0) + players = avl.Tree{} } // TestNewGame tests the initialization of a new game func TestNewGame(t *testing.T) { resetGameState() - maxRounds := 100 - std.TestSetOrigCaller(player1) - gameID := NewGame(player2, maxRounds) + gameID := NewGame(player2) // Verify that the game has been correctly initialized - g, err := gameByID(gameID) + g, err := getGame(gameID) urequire.NoError(t, err) urequire.Equal(t, player1.String(), g.player1.String()) urequire.Equal(t, player2.String(), g.player2.String()) urequire.Equal(t, 0, g.roll1) urequire.Equal(t, 0, g.roll2) - urequire.Equal(t, 0, g.score1) - urequire.Equal(t, 0, g.score2) - urequire.Equal(t, 0, g.round) - urequire.Equal(t, maxRounds, g.maxRounds) - - // Invalid game with negative rounds - urequire.PanicsWithMessage(t, "invalid number of rounds", func() { - NewGame(player2, -1) - }) } // TestPlay tests the dice rolling functionality for both players @@ -54,30 +44,27 @@ func TestPlay(t *testing.T) { resetGameState() std.TestSetOrigCaller(player1) - gameID := NewGame(player2, 3) + gameID := NewGame(player2) - g, err := gameByID(gameID) + g, err := getGame(gameID) urequire.NoError(t, err) // Simulate rolling dice for player 1 roll1 := Play(gameID) - // Verify player 1's roll and score + // Verify player 1's roll urequire.NotEqual(t, 0, g.roll1) - urequire.Equal(t, g.score1, roll1) + urequire.Equal(t, g.roll1, roll1) urequire.Equal(t, 0, g.roll2) // Player 2 hasn't rolled yet - urequire.Equal(t, 0, g.round) // Simulate rolling dice for player 2 std.TestSetOrigCaller(player2) roll2 := Play(gameID) - // Verify player 2's roll and score - urequire.Equal(t, 0, g.roll1) - urequire.Equal(t, 0, g.roll2) - urequire.Equal(t, g.score1, roll1) - urequire.Equal(t, g.score2, roll2) - urequire.Equal(t, 1, g.round) // Round should be incremented now + // Verify player 2's roll + urequire.NotEqual(t, 0, g.roll2) + urequire.Equal(t, g.roll1, roll1) + urequire.Equal(t, g.roll2, roll2) } // TestPlayAgainstSelf tests the scenario where a player plays against themselves @@ -85,18 +72,16 @@ func TestPlayAgainstSelf(t *testing.T) { resetGameState() std.TestSetOrigCaller(player1) - gameID := NewGame(player1, 1) + gameID := NewGame(player1) // Simulate rolling dice twice by the same player roll1 := Play(gameID) roll2 := Play(gameID) - g, err := gameByID(gameID) + g, err := getGame(gameID) urequire.NoError(t, err) - urequire.Equal(t, 0, g.roll1) - urequire.Equal(t, g.score1, roll1) - urequire.Equal(t, 0, g.roll2) - urequire.Equal(t, g.score2, roll2) + urequire.Equal(t, g.roll1, roll1) + urequire.Equal(t, g.roll2, roll2) } // TestPlayInvalidPlayer tests the scenario where an invalid player tries to play @@ -104,7 +89,7 @@ func TestPlayInvalidPlayer(t *testing.T) { resetGameState() std.TestSetOrigCaller(player1) - gameID := NewGame(player1, 3) + gameID := NewGame(player1) // Attempt to play as an invalid player std.TestSetOrigCaller(unknownPlayer) @@ -113,15 +98,14 @@ func TestPlayInvalidPlayer(t *testing.T) { }) } -// TestPlayAlreadyPlayed tests the scenario where a player tries to play again in the same round +// TestPlayAlreadyPlayed tests the scenario where a player tries to play again after already playing func TestPlayAlreadyPlayed(t *testing.T) { resetGameState() std.TestSetOrigCaller(player1) - gameID := NewGame(player2, 3) + gameID := NewGame(player2) // Player 1 rolls - std.TestSetOrigCaller(player1) Play(gameID) // Player 1 tries to roll again @@ -130,58 +114,54 @@ func TestPlayAlreadyPlayed(t *testing.T) { }) } -// TestPlayUntilGameEnds tests the scenario where the game is played until it ends +// TestPlayUntilGameEnds tests the scenario where both players finish their rolls and the game ends func TestPlayUntilGameEnds(t *testing.T) { resetGameState() std.TestSetOrigCaller(player1) - gameID := NewGame(player2, 10) // Set the game to have 10 rounds + gameID := NewGame(player2) - for round := 0; round < 10; round++ { - // Play each round - std.TestSetOrigCaller(player1) - Play(gameID) - std.TestSetOrigCaller(player2) - Play(gameID) - } + // Play for both players + std.TestSetOrigCaller(player1) + Play(gameID) + std.TestSetOrigCaller(player2) + Play(gameID) // Check if the game is over - g, err := gameByID(gameID) + g, err := getGame(gameID) urequire.NoError(t, err) - urequire.Equal(t, 10, g.round) urequire.NotEqual(t, "Game still in progress", g.winner()) // Verify the winner is correctly determined winner := g.winner() if winner == "Player 1 wins!" { - urequire.True(t, g.score1 > g.score2) + urequire.True(t, g.roll1 > g.roll2) } else if winner == "Player 2 wins!" { - urequire.True(t, g.score2 > g.score1) + urequire.True(t, g.roll2 > g.roll1) } else if winner == "It's a draw!" { - urequire.True(t, g.score1 == g.score2) + urequire.True(t, g.roll1 == g.roll2) } } -// TestPlayBeyondMaxRounds tests that playing beyond the maximum number of rounds fails -func TestPlayBeyondMaxRounds(t *testing.T) { +// TestPlayBeyondGameEnd tests that playing after both players have finished their rolls fails +func TestPlayBeyondGameEnd(t *testing.T) { resetGameState() std.TestSetOrigCaller(player1) - gameID := NewGame(player2, 1) // Only 1 round + gameID := NewGame(player2) - // Play the single round + // Play for both players std.TestSetOrigCaller(player1) Play(gameID) std.TestSetOrigCaller(player2) Play(gameID) // Check if the game is over - g, err := gameByID(gameID) + g, err := getGame(gameID) urequire.NoError(t, err) - urequire.Equal(t, 1, g.round) urequire.NotEqual(t, "Game still in progress", g.winner()) - // Attempt to play more rounds should fail + // Attempt to play more should fail std.TestSetOrigCaller(player1) urequire.PanicsWithMessage(t, "game over", func() { Play(gameID) diff --git a/examples/gno.land/r/demo/games/dice_roller/gno.mod b/examples/gno.land/r/demo/games/dice_roller/gno.mod index 0b2b10ac459..4f1016ad8d4 100644 --- a/examples/gno.land/r/demo/games/dice_roller/gno.mod +++ b/examples/gno.land/r/demo/games/dice_roller/gno.mod @@ -1,10 +1,11 @@ -module gno.land/r/demo/games/diceduel +module gno.land/r/demo/games/diceroller require ( gno.land/p/demo/avl v0.0.0-latest gno.land/p/demo/entropy v0.0.0-latest gno.land/p/demo/seqid v0.0.0-latest gno.land/p/demo/testutils v0.0.0-latest + gno.land/p/demo/ufmt v0.0.0-latest gno.land/p/demo/urequire v0.0.0-latest gno.land/r/demo/users v0.0.0-latest ) diff --git a/examples/gno.land/r/demo/games/diceroller/diceroller.gno b/examples/gno.land/r/demo/games/diceroller/diceroller.gno deleted file mode 100644 index ad9c85f5381..00000000000 --- a/examples/gno.land/r/demo/games/diceroller/diceroller.gno +++ /dev/null @@ -1,325 +0,0 @@ -package diceroller - -import ( - "errors" - "math/rand" - "sort" - "std" - "strconv" - - "gno.land/p/demo/avl" - "gno.land/p/demo/entropy" - "gno.land/p/demo/seqid" - "gno.land/p/demo/testutils" - "gno.land/p/demo/ufmt" - "gno.land/r/demo/users" -) - -type game struct { - player1, player2 *player - roll1, roll2 int - score1, score2 int - round, maxRounds int -} - -type player struct { - addr std.Address - wins, losses, draws, points, rank int -} - -type leaderBoard []player - -const ( - empty = iota - win - draw - lose -) - -var ( - games avl.Tree - gameId seqid.ID - - players avl.Tree - playerId seqid.ID - - seed = uint64(entropy.New().Seed()) -) - -// Roll a dice between 1 and 6 -func rollDice() int { - r := rand.New(rand.NewPCG(seed, 0xdeadbeef)) - - return r.IntN(6) + 1 -} - -// NewGame initializes a new Dice Duel game with the specified number of rounds -func NewGame(addr std.Address, rounds int) int { - if rounds <= 0 { - panic("invalid number of rounds") - } - - for i := 0; i <= 10; i++ { - addr := testutils.TestAddress(ufmt.Sprintf("%d", i)) - players.Set(playerId.Next().String(), &player{ - addr: addr, - points: i + 1, - }) - } - - games.Set(gameId.Next().String(), &game{ - player1: &player{addr: std.PrevRealm().Addr()}, - player2: &player{addr: addr}, - maxRounds: rounds, - }) - return int(gameId) -} - -// Play simulates a dice roll for the current player and updates the game state -func Play(idx int) int { - g, err := getGame(idx) - if err != nil { - panic(err) - } - - roll := rollDice() - - if err := g.play(std.PrevRealm().Addr(), roll); err != nil { - panic(err) - } - - // Check if both players have played, then advance the round - if g.roll1 != 0 && g.roll2 != 0 { - g.round++ - g.roll1, g.roll2 = 0, 0 // Reset rolls for the next round - } - - // Check if the game is over and update the player's stats - if g.round >= g.maxRounds { - if g.score1 > g.score2 { - g.player1.updateResult(win) - g.player2.updateResult(lose) - } else if g.score2 > g.score1 { - g.player2.updateResult(win) - g.player1.updateResult(lose) - } else { - g.player1.updateResult(draw) - g.player2.updateResult(draw) - } - } - - return roll -} - -// play handles a player's dice roll and updates their score -func (g *game) play(player std.Address, roll int) error { - if player != g.player1.addr && player != g.player2.addr { - return errors.New("invalid player") - } - - if g.round >= g.maxRounds { - return errors.New("game over") - } - - if player == g.player1.addr && g.roll1 == 0 { - g.roll1 = roll - g.score1 += roll - return nil - } - - if player == g.player2.addr && g.roll2 == 0 { - g.roll2 = roll - g.score2 += roll - return nil - } - - return errors.New("already played") -} - -// winner determines the winner of the game based on scores -func (g *game) winner() string { - if g.round < g.maxRounds { - return "Game still in progress" - } - - if g.score1 > g.score2 { - return "Player 1 wins!" - } else if g.score2 > g.score1 { - return "Player 2 wins!" - } else { - return "It's a draw!" - } -} - -// diceIcon returns a string representation of the dice roll -func diceIcon(roll int) string { - switch roll { - case 1: - return "🎲1" - case 2: - return "🎲2" - case 3: - return "🎲3" - case 4: - return "🎲4" - case 5: - return "🎲5" - case 6: - return "🎲6" - default: - return strconv.Itoa(roll) - } -} - -// Render provides a summary of the current state of all games and player rankings. -func Render(path string) string { - // Create the output for the games - output := `# 🎲 Dice Roller - -## Game Rules: -Dice Roller is a two-player game where each player rolls a dice over a specified number of rounds -1. **Create a game** with an opponent using [NewGame](diceroller?help&__func=NewGame) -2. **Play rounds** using [Play](diceroller?help&__func=Play), where each player rolls a dice - - Dice values range from 1 to 6 for each roll - - Each player rolls the dice in turn, and the rolls are recorded for each round -## Actions: -* [NewGame](diceroller?help&__func=NewGame) opponentAddress rounds - - **opponentAddress**: The address of the opponent player - - **rounds**: The number of rounds for the game. Must be a positive integer -* [Play](diceroller?help&__func=Play) gameIndex - - **gameIndex**: The unique index of the game you want to play -## Recent Games: -Displays a summary of the most recent games. Up to 10 recent games are shown - -| Game | Player 1 | Roll 1 | Score 1 | Player 2 | Roll 2 | Score 2 | Round | Max Rounds | Winner | -|------|----------|--------|---------|----------|--------|---------|-------|------------|--------| -` - - // Display up to 10 recent games - maxGames := 10 - for n := int(gameId); n > 0 && int(gameId)-n < maxGames; n-- { - g, err := getGame(n) - if err != nil { - continue - } - - output += strconv.Itoa(n) + " | " + - shortName(g.player1.addr) + " | " + diceIcon(g.roll1) + " | " + strconv.Itoa(g.score1) + " | " + - shortName(g.player2.addr) + " | " + diceIcon(g.roll2) + " | " + strconv.Itoa(g.score2) + " | " + - strconv.Itoa(g.round) + " | " + strconv.Itoa(g.maxRounds) + " | " + - g.winner() + "\n" - } - - // Add ranking board to the output - // Display the ranking board - output += ` -## Player Rankings: -Displays the current ranking of players based on their performance in Dice Roller. - -| Rank | Player | Wins | Losses | Draws | Points | -|------|-----------------------|------|--------|-------|--------| -` - - for i, player := range getRankingBoard() { - // Determine the icon based on the rank - rankIcon := strconv.Itoa(i) - - if i == 0 { - rankIcon = "πŸ₯‡" - } else if i == 1 { - rankIcon = "πŸ₯ˆ" - } else if i == 2 { - rankIcon = "πŸ₯‰" - } - - // Format the player address to ensure it's aligned - playerName := shortName(player.addr) - if len(playerName) > 20 { - playerName = playerName[:20] + "..." - } - - // Add each player's rank information - output += ufmt.Sprintf("| %s | %s | %d | %d | %d | %d |\n", - rankIcon, - playerName, - player.wins, - player.losses, - player.draws, - player.points, - ) - } - - return output -} - -// shortName returns a shortened name for the given address -func shortName(addr std.Address) string { - user := users.GetUserByAddress(addr) - if user != nil { - return user.Name - } - if len(addr) < 10 { - return string(addr) - } - return string(addr)[:10] + "..." -} - -// getGame retrieves the game state by its ID -func getGame(idx int) (*game, error) { - v, ok := games.Get(seqid.ID(idx).String()) - if !ok { - return nil, errors.New("game not found") - } - return v.(*game), nil -} - -func (p *player) updateResult(result int) { - switch result { - case win: - p.wins++ - p.points += 3 - case lose: - p.losses++ - case draw: - p.draws++ - p.points++ - } -} - -func getPlayer(addr std.Address) *player { - v, ok := players.Get(addr.String()) - if !ok { - player := &player{ - addr: addr, - } - players.Set(playerId.Next().String(), player) - return player - } - - return v.(*player) -} - -func getRankingBoard() leaderBoard { - board := leaderBoard{} - players.Iterate("", "", func(key string, value interface{}) bool { - player := value.(*player) - board = append(board, *player) - return false - }) - - sort.Sort(board) - - return board -} - -func (r leaderBoard) Len() int { - return len(r) -} - -func (r leaderBoard) Less(i, j int) bool { - return r[i].points > r[j].points -} - -func (r leaderBoard) Swap(i, j int) { - r[i].points, r[j].points = r[j].points, r[i].points -} diff --git a/examples/gno.land/r/demo/games/diceroller/diceroller_test.gno b/examples/gno.land/r/demo/games/diceroller/diceroller_test.gno deleted file mode 100644 index 99fbe7d735b..00000000000 --- a/examples/gno.land/r/demo/games/diceroller/diceroller_test.gno +++ /dev/null @@ -1,191 +0,0 @@ -package diceroller - -import ( - "std" - "testing" - - "gno.land/p/demo/avl" - "gno.land/p/demo/seqid" - "gno.land/p/demo/testutils" - "gno.land/p/demo/urequire" -) - -var ( - player1 = testutils.TestAddress("alice") - player2 = testutils.TestAddress("bob") - unknownPlayer = testutils.TestAddress("unknown") -) - -// resetGameState resets the game state for testing -func resetGameState() { - games = avl.Tree{} - gameId = seqid.ID(0) - players = avl.Tree{} - playerId = seqid.ID(0) -} - -// TestNewGame tests the initialization of a new game -func TestNewGame(t *testing.T) { - resetGameState() - - maxRounds := 100 - - std.TestSetOrigCaller(player1) - gameID := NewGame(player2, maxRounds) - - // Verify that the game has been correctly initialized - g, err := getGame(gameID) - urequire.NoError(t, err) - urequire.Equal(t, player1.String(), g.player1.addr.String()) - urequire.Equal(t, player2.String(), g.player2.addr.String()) - urequire.Equal(t, 0, g.roll1) - urequire.Equal(t, 0, g.roll2) - urequire.Equal(t, 0, g.score1) - urequire.Equal(t, 0, g.score2) - urequire.Equal(t, 0, g.round) - urequire.Equal(t, maxRounds, g.maxRounds) - - // Invalid game with negative rounds - urequire.PanicsWithMessage(t, "invalid number of rounds", func() { - NewGame(player2, -1) - }) -} - -// TestPlay tests the dice rolling functionality for both players -func TestPlay(t *testing.T) { - resetGameState() - - std.TestSetOrigCaller(player1) - gameID := NewGame(player2, 3) - - g, err := getGame(gameID) - urequire.NoError(t, err) - - // Simulate rolling dice for player 1 - roll1 := Play(gameID) - - // Verify player 1's roll and score - urequire.NotEqual(t, 0, g.roll1) - urequire.Equal(t, g.score1, roll1) - urequire.Equal(t, 0, g.roll2) // Player 2 hasn't rolled yet - urequire.Equal(t, 0, g.round) - - // Simulate rolling dice for player 2 - std.TestSetOrigCaller(player2) - roll2 := Play(gameID) - - // Verify player 2's roll and score - urequire.Equal(t, 0, g.roll1) - urequire.Equal(t, 0, g.roll2) - urequire.Equal(t, g.score1, roll1) - urequire.Equal(t, g.score2, roll2) - urequire.Equal(t, 1, g.round) // Round should be incremented now -} - -// TestPlayAgainstSelf tests the scenario where a player plays against themselves -func TestPlayAgainstSelf(t *testing.T) { - resetGameState() - - std.TestSetOrigCaller(player1) - gameID := NewGame(player1, 1) - - // Simulate rolling dice twice by the same player - roll1 := Play(gameID) - roll2 := Play(gameID) - - g, err := getGame(gameID) - urequire.NoError(t, err) - urequire.Equal(t, 0, g.roll1) - urequire.Equal(t, g.score1, roll1) - urequire.Equal(t, 0, g.roll2) - urequire.Equal(t, g.score2, roll2) -} - -// TestPlayInvalidPlayer tests the scenario where an invalid player tries to play -func TestPlayInvalidPlayer(t *testing.T) { - resetGameState() - - std.TestSetOrigCaller(player1) - gameID := NewGame(player1, 3) - - // Attempt to play as an invalid player - std.TestSetOrigCaller(unknownPlayer) - urequire.PanicsWithMessage(t, "invalid player", func() { - Play(gameID) - }) -} - -// TestPlayAlreadyPlayed tests the scenario where a player tries to play again in the same round -func TestPlayAlreadyPlayed(t *testing.T) { - resetGameState() - - std.TestSetOrigCaller(player1) - gameID := NewGame(player2, 3) - - // Player 1 rolls - std.TestSetOrigCaller(player1) - Play(gameID) - - // Player 1 tries to roll again - urequire.PanicsWithMessage(t, "already played", func() { - Play(gameID) - }) -} - -// TestPlayUntilGameEnds tests the scenario where the game is played until it ends -func TestPlayUntilGameEnds(t *testing.T) { - resetGameState() - - std.TestSetOrigCaller(player1) - gameID := NewGame(player2, 10) // Set the game to have 10 rounds - - for round := 0; round < 10; round++ { - // Play each round - std.TestSetOrigCaller(player1) - Play(gameID) - std.TestSetOrigCaller(player2) - Play(gameID) - } - - // Check if the game is over - g, err := getGame(gameID) - urequire.NoError(t, err) - urequire.Equal(t, 10, g.round) - urequire.NotEqual(t, "Game still in progress", g.winner()) - - // Verify the winner is correctly determined - winner := g.winner() - if winner == "Player 1 wins!" { - urequire.True(t, g.score1 > g.score2) - } else if winner == "Player 2 wins!" { - urequire.True(t, g.score2 > g.score1) - } else if winner == "It's a draw!" { - urequire.True(t, g.score1 == g.score2) - } -} - -// TestPlayBeyondMaxRounds tests that playing beyond the maximum number of rounds fails -func TestPlayBeyondMaxRounds(t *testing.T) { - resetGameState() - - std.TestSetOrigCaller(player1) - gameID := NewGame(player2, 1) // Only 1 round - - // Play the single round - std.TestSetOrigCaller(player1) - Play(gameID) - std.TestSetOrigCaller(player2) - Play(gameID) - - // Check if the game is over - g, err := getGame(gameID) - urequire.NoError(t, err) - urequire.Equal(t, 1, g.round) - urequire.NotEqual(t, "Game still in progress", g.winner()) - - // Attempt to play more rounds should fail - std.TestSetOrigCaller(player1) - urequire.PanicsWithMessage(t, "game over", func() { - Play(gameID) - }) -} diff --git a/examples/gno.land/r/demo/games/diceroller/gno.mod b/examples/gno.land/r/demo/games/diceroller/gno.mod deleted file mode 100644 index da687c76641..00000000000 --- a/examples/gno.land/r/demo/games/diceroller/gno.mod +++ /dev/null @@ -1,10 +0,0 @@ -module gno.land/r/demo/games/diceroller - -require ( - gno.land/p/demo/avl v0.0.0-latest - gno.land/p/demo/entropy v0.0.0-latest - gno.land/p/demo/seqid v0.0.0-latest - gno.land/p/demo/testutils v0.0.0-latest - gno.land/p/demo/urequire v0.0.0-latest - gno.land/r/demo/users v0.0.0-latest -) From 8d3e520797b523234c59c596e2c53326e15d5ee5 Mon Sep 17 00:00:00 2001 From: linhpn99 Date: Mon, 9 Sep 2024 23:08:29 +0700 Subject: [PATCH 07/22] ignore init --- .../r/demo/games/dice_roller/dice_roller.gno | 26 ------------------- 1 file changed, 26 deletions(-) diff --git a/examples/gno.land/r/demo/games/dice_roller/dice_roller.gno b/examples/gno.land/r/demo/games/dice_roller/dice_roller.gno index d787b9492c1..b2682d517bc 100644 --- a/examples/gno.land/r/demo/games/dice_roller/dice_roller.gno +++ b/examples/gno.land/r/demo/games/dice_roller/dice_roller.gno @@ -48,32 +48,6 @@ var ( seed = uint64(entropy.New().Seed()) ) -func init() { - // Pre-define two players for initial setup - addr1 := std.Address("g1y490harzjqmdngmmaw4tyc4qdm5ma5k0fv3w6p") - addr2 := std.Address("g1z4dfj9dvknvahykg3rhjrpsexhp4vl8gnde27q") - - // Initialize two players and their initial game stats - players.Set(addr1.String(), &player{ - addr: addr1, - wins: 1, - points: 3, // Wins give 3 points - }) - - players.Set(addr2.String(), &player{ - addr: addr2, - losses: 1, - points: 0, // Losses give 0 points - }) - - games.Set(gameId.Next().String(), &game{ - player1: addr1, - player2: addr2, - roll1: 6, - roll2: 5, - }) -} - // rollDice generates a random dice roll between 1 and 6 func rollDice() int { r := rand.New(rand.NewPCG(seed, 0xdeadbeef)) From 38531d060f5b0c5eac8b906f356c7a81ca0c9dc9 Mon Sep 17 00:00:00 2001 From: linhpn99 Date: Mon, 9 Sep 2024 23:14:34 +0700 Subject: [PATCH 08/22] update --- examples/gno.land/r/demo/games/dice_roller/dice_roller.gno | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/gno.land/r/demo/games/dice_roller/dice_roller.gno b/examples/gno.land/r/demo/games/dice_roller/dice_roller.gno index b2682d517bc..0c8ae454479 100644 --- a/examples/gno.land/r/demo/games/dice_roller/dice_roller.gno +++ b/examples/gno.land/r/demo/games/dice_roller/dice_roller.gno @@ -170,7 +170,7 @@ func Render(path string) string { output := `# 🎲 Dice Roller ## Game Rules: -Dice Roller is a two-player game where each player rolls a dice once. +Dice Roller is a two-player game where each player rolls a dice once 1. **Create a game** with an opponent using [NewGame](diceroller?help&__func=NewGame) 2. **Play** using [Play](diceroller?help&__func=Play), where each player rolls a dice From fa8bcf2157f088e9bf0100b43107e8b803a823a8 Mon Sep 17 00:00:00 2001 From: linhpn99 Date: Mon, 9 Sep 2024 23:22:30 +0700 Subject: [PATCH 09/22] fixup --- examples/gno.land/r/demo/games/dice_roller/dice_roller.gno | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/gno.land/r/demo/games/dice_roller/dice_roller.gno b/examples/gno.land/r/demo/games/dice_roller/dice_roller.gno index 0c8ae454479..f9e63902efb 100644 --- a/examples/gno.land/r/demo/games/dice_roller/dice_roller.gno +++ b/examples/gno.land/r/demo/games/dice_roller/dice_roller.gno @@ -46,11 +46,11 @@ var ( players avl.Tree // AVL tree for storing player data seed = uint64(entropy.New().Seed()) + r = rand.New(rand.NewPCG(seed, 0xdeadbeef)) ) // rollDice generates a random dice roll between 1 and 6 func rollDice() int { - r := rand.New(rand.NewPCG(seed, 0xdeadbeef)) return r.IntN(6) + 1 } From e4f9fa51a7b5a78bf9b7130246c6583526401e64 Mon Sep 17 00:00:00 2001 From: linhpn99 Date: Tue, 10 Sep 2024 20:27:12 +0700 Subject: [PATCH 10/22] improve Render --- .../r/demo/games/dice_roller/dice_roller.gno | 147 ++++++++---------- .../games/dice_roller/dice_roller_test.gno | 30 ---- .../r/demo/games/dice_roller/icon.gno | 55 +++++++ 3 files changed, 122 insertions(+), 110 deletions(-) create mode 100644 examples/gno.land/r/demo/games/dice_roller/icon.gno diff --git a/examples/gno.land/r/demo/games/dice_roller/dice_roller.gno b/examples/gno.land/r/demo/games/dice_roller/dice_roller.gno index f9e63902efb..6c91b7f922b 100644 --- a/examples/gno.land/r/demo/games/dice_roller/dice_roller.gno +++ b/examples/gno.land/r/demo/games/dice_roller/dice_roller.gno @@ -6,6 +6,7 @@ import ( "sort" "std" "strconv" + "strings" "gno.land/p/demo/avl" "gno.land/p/demo/entropy" @@ -33,10 +34,11 @@ type ( const ( // Constants to represent game result outcomes - empty = iota + selfPlay = iota win draw lose + ongoing ) var ( @@ -83,7 +85,7 @@ func Play(idx int) int { } // If both players have rolled, update the results and leaderboard - if g.roll1 != 0 && g.roll2 != 0 { + if g.isFinished() { // If the player is playing against themselves, no points are awarded if g.player1 == g.player2 { return roll @@ -93,14 +95,14 @@ func Play(idx int) int { player2 := getPlayer(g.player2) if g.roll1 > g.roll2 { - player1.updateResult(win) - player2.updateResult(lose) + player1.updateStats(win) + player2.updateStats(lose) } else if g.roll2 > g.roll1 { - player2.updateResult(win) - player1.updateResult(lose) + player2.updateStats(win) + player1.updateStats(lose) } else { - player1.updateResult(draw) - player2.updateResult(draw) + player1.updateStats(draw) + player2.updateStats(draw) } } @@ -113,7 +115,7 @@ func (g *game) play(player std.Address, roll int) error { return errors.New("invalid player") } - if g.roll1 != 0 && g.roll2 != 0 { + if g.isFinished() { return errors.New("game over") } @@ -130,68 +132,60 @@ func (g *game) play(player std.Address, roll int) error { return errors.New("already played") } -// winner determines the winner based on scores -func (g *game) winner() string { - if g.roll1 == 0 || g.roll2 == 0 { - return "Game still in progress" +// isFinished checks if the game has ended +func (g *game) isFinished() bool { + return g.roll1 != 0 && g.roll2 != 0 +} + +// checkResult returns the game status as a formatted string +func (g *game) status() string { + if g.player1 == g.player2 { + return resultIcon(selfPlay) + " No results for self-play" } - if g.roll1 > g.roll2 { - return "Player 1 wins!" - } else if g.roll2 > g.roll1 { - return "Player 2 wins!" - } else { - return "It's a draw!" + if !g.isFinished() { + return resultIcon(ongoing) + " Game still in progress" } -} -// diceIcon returns an icon of the dice roll -func diceIcon(roll int) string { - switch roll { - case 1: - return "🎲1" - case 2: - return "🎲2" - case 3: - return "🎲3" - case 4: - return "🎲4" - case 5: - return "🎲5" - case 6: - return "🎲6" - default: - return strconv.Itoa(roll) + if g.roll1 > g.roll2 { + return resultIcon(win) + shortName(g.player1) + " Wins!" + } else if g.roll1 < g.roll2 { + return resultIcon(lose) + shortName(g.player2) + " Wins!" + } else { + return resultIcon(draw) + " It's a Draw!" } } // Render provides a summary of the current state of games and leader board func Render(path string) string { - output := `# 🎲 Dice Roller + var sb strings.Builder -## Game Rules: -Dice Roller is a two-player game where each player rolls a dice once -1. **Create a game** with an opponent using [NewGame](diceroller?help&__func=NewGame) -2. **Play** using [Play](diceroller?help&__func=Play), where each player rolls a dice + sb.WriteString(`# 🎲 **Dice Roller Game** -## Scoring: -- Playing against yourself? No points or stats changes for you -- **Win:** 3 points -- **Loss:** 0 points -- **Draw:** 1 point each +Welcome to Dice Roller! Challenge your friends to a simple yet exciting dice rolling game. Roll the dice and see who gets the highest score! -## Actions: -* [NewGame](diceroller?help&__func=NewGame) opponentAddress - - **opponentAddress**: The address of the opponent player -* [Play](diceroller?help&__func=Play) gameIndex - - **gameIndex**: The unique index of the game you want to play +--- -## Recent Games: -Displays a summary of the most recent games. Up to 10 recent games are shown +## **How to Play**: +1. **Create a game**: Challenge an opponent using [NewGame](diceroller?help&__func=NewGame) +2. **Roll the dice**: Play your turn by rolling a dice using [Play](diceroller?help&__func=Play) -| Game | Player 1 | Roll 1 | Player 2 | Roll 2 | Winner | -|------|----------|--------|----------|--------|--------| -` +--- + +## **Scoring Rules**: +- **Win** πŸ†: +3 points +- **Draw** 🀝: +1 point each +- **Loss**: No points +- **Playing against yourself**: No points or stats changes for you + +--- + +## **Recent Games**: +Below are the results from the most recent games. Up to 10 recent games are displayed. Games where both players are the same (playing against oneself) are not counted in the leaderboard. + +| Game | Player 1 | 🎲 Roll 1 | Player 2 | 🎲 Roll 2 | πŸ† Winner | +|------|----------|-----------|----------|-----------|-----------| +`) // Display up to 10 recent games maxGames := 10 @@ -201,43 +195,36 @@ Displays a summary of the most recent games. Up to 10 recent games are shown continue } - output += strconv.Itoa(n) + " | " + + sb.WriteString(strconv.Itoa(n) + " | " + shortName(g.player1) + " | " + diceIcon(g.roll1) + " | " + shortName(g.player2) + " | " + diceIcon(g.roll2) + " | " + - g.winner() + "\n" + g.status() + "\n") } - // Display the leader board - output += ` -## Leader board: -Displays the current ranking of players based on their performance in Dice Roller + // Leaderboard + sb.WriteString(` +--- + +## **Leaderboard**: +The top players are ranked by performance. Games played against oneself are not counted in the leaderboard | Rank | Player | Wins | Losses | Draws | Points | |------|-----------------------|------|--------|-------|--------| -` +`) for i, player := range getLeaderBoard() { - rankIcon := strconv.Itoa(i) - - if i == 0 { - rankIcon = "πŸ₯‡" - } else if i == 1 { - rankIcon = "πŸ₯ˆ" - } else if i == 2 { - rankIcon = "πŸ₯‰" - } - - output += ufmt.Sprintf("| %s | %s | %d | %d | %d | %d |\n", - rankIcon, + sb.WriteString(ufmt.Sprintf("| %s | %s | %d | %d | %d | %d |\n", + rankIcon(i+1), shortName(player.addr), player.wins, player.losses, player.draws, player.points, - ) + )) } - return output + sb.WriteString("\n---\n**Good luck and have fun!** πŸŽ‰") + return sb.String() } // shortName returns a shortened name for the given address @@ -261,8 +248,8 @@ func getGame(idx int) (*game, error) { return v.(*game), nil } -// updateResult updates the player's result and points based on the game outcome -func (p *player) updateResult(result int) { +// updateResult updates the player's stats and points based on the game outcome +func (p *player) updateStats(result int) { switch result { case win: p.wins++ diff --git a/examples/gno.land/r/demo/games/dice_roller/dice_roller_test.gno b/examples/gno.land/r/demo/games/dice_roller/dice_roller_test.gno index 24946175741..fe6f5400f57 100644 --- a/examples/gno.land/r/demo/games/dice_roller/dice_roller_test.gno +++ b/examples/gno.land/r/demo/games/dice_roller/dice_roller_test.gno @@ -114,35 +114,6 @@ func TestPlayAlreadyPlayed(t *testing.T) { }) } -// TestPlayUntilGameEnds tests the scenario where both players finish their rolls and the game ends -func TestPlayUntilGameEnds(t *testing.T) { - resetGameState() - - std.TestSetOrigCaller(player1) - gameID := NewGame(player2) - - // Play for both players - std.TestSetOrigCaller(player1) - Play(gameID) - std.TestSetOrigCaller(player2) - Play(gameID) - - // Check if the game is over - g, err := getGame(gameID) - urequire.NoError(t, err) - urequire.NotEqual(t, "Game still in progress", g.winner()) - - // Verify the winner is correctly determined - winner := g.winner() - if winner == "Player 1 wins!" { - urequire.True(t, g.roll1 > g.roll2) - } else if winner == "Player 2 wins!" { - urequire.True(t, g.roll2 > g.roll1) - } else if winner == "It's a draw!" { - urequire.True(t, g.roll1 == g.roll2) - } -} - // TestPlayBeyondGameEnd tests that playing after both players have finished their rolls fails func TestPlayBeyondGameEnd(t *testing.T) { resetGameState() @@ -159,7 +130,6 @@ func TestPlayBeyondGameEnd(t *testing.T) { // Check if the game is over g, err := getGame(gameID) urequire.NoError(t, err) - urequire.NotEqual(t, "Game still in progress", g.winner()) // Attempt to play more should fail std.TestSetOrigCaller(player1) diff --git a/examples/gno.land/r/demo/games/dice_roller/icon.gno b/examples/gno.land/r/demo/games/dice_roller/icon.gno new file mode 100644 index 00000000000..7ccbe61f1a3 --- /dev/null +++ b/examples/gno.land/r/demo/games/dice_roller/icon.gno @@ -0,0 +1,55 @@ +package diceroller + +import ( + "strconv" +) + +// diceIcon returns an icon of the dice roll +func diceIcon(roll int) string { + switch roll { + case 1: + return "🎲1" + case 2: + return "🎲2" + case 3: + return "🎲3" + case 4: + return "🎲4" + case 5: + return "🎲5" + case 6: + return "🎲6" + default: + return strconv.Itoa(roll) + } +} + +// resultIcon returns the icon representing the result of a game +func resultIcon(result int) string { + switch result { + case selfPlay: + return "❌" + case win: + return "πŸ†" + case lose: + return "πŸ†" + case draw: + return "🀝" + default: + return "πŸ”„" + } +} + +// rankIcon returns the icon for a player's rank +func rankIcon(rank int) string { + switch rank { + case 1: + return "πŸ₯‡" + case 2: + return "πŸ₯ˆ" + case 3: + return "πŸ₯‰" + default: + return strconv.Itoa(rank) + } +} From 33bc82bc2db6d2437dcfe5b1db7740c7579966f9 Mon Sep 17 00:00:00 2001 From: linhpn99 Date: Tue, 10 Sep 2024 20:31:31 +0700 Subject: [PATCH 11/22] remove unused field --- examples/gno.land/r/demo/games/dice_roller/dice_roller.gno | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/gno.land/r/demo/games/dice_roller/dice_roller.gno b/examples/gno.land/r/demo/games/dice_roller/dice_roller.gno index 6c91b7f922b..76a81845436 100644 --- a/examples/gno.land/r/demo/games/dice_roller/dice_roller.gno +++ b/examples/gno.land/r/demo/games/dice_roller/dice_roller.gno @@ -24,8 +24,8 @@ type ( // player holds the information about each player including their stats player struct { - addr std.Address - wins, losses, draws, points, rank int + addr std.Address + wins, losses, draws, points int } // leaderBoard is a slice of players, used to sort players by rank From da318b3fe3a6e2f02e08eda2c5f518ef6aab787e Mon Sep 17 00:00:00 2001 From: linhpn99 Date: Tue, 10 Sep 2024 21:05:59 +0700 Subject: [PATCH 12/22] update --- .../r/demo/games/dice_roller/dice_roller.gno | 25 +++++++------------ .../r/demo/games/dice_roller/icon.gno | 4 +-- 2 files changed, 10 insertions(+), 19 deletions(-) diff --git a/examples/gno.land/r/demo/games/dice_roller/dice_roller.gno b/examples/gno.land/r/demo/games/dice_roller/dice_roller.gno index 76a81845436..c039f2e86ac 100644 --- a/examples/gno.land/r/demo/games/dice_roller/dice_roller.gno +++ b/examples/gno.land/r/demo/games/dice_roller/dice_roller.gno @@ -34,11 +34,10 @@ type ( const ( // Constants to represent game result outcomes - selfPlay = iota + ongoing = iota win draw lose - ongoing ) var ( @@ -139,20 +138,16 @@ func (g *game) isFinished() bool { // checkResult returns the game status as a formatted string func (g *game) status() string { - if g.player1 == g.player2 { - return resultIcon(selfPlay) + " No results for self-play" - } - if !g.isFinished() { return resultIcon(ongoing) + " Game still in progress" } if g.roll1 > g.roll2 { - return resultIcon(win) + shortName(g.player1) + " Wins!" + return resultIcon(win) + " Player1 Wins !" } else if g.roll1 < g.roll2 { - return resultIcon(lose) + shortName(g.player2) + " Wins!" + return resultIcon(lose) + "Player2 Wins !" } else { - return resultIcon(draw) + " It's a Draw!" + return resultIcon(draw) + " It's a Draw !" } } @@ -162,7 +157,7 @@ func Render(path string) string { sb.WriteString(`# 🎲 **Dice Roller Game** -Welcome to Dice Roller! Challenge your friends to a simple yet exciting dice rolling game. Roll the dice and see who gets the highest score! +Welcome to Dice Roller! Challenge your friends to a simple yet exciting dice rolling game. Roll the dice and see who gets the highest score ! --- @@ -187,7 +182,6 @@ Below are the results from the most recent games. Up to 10 recent games are disp |------|----------|-----------|----------|-----------|-----------| `) - // Display up to 10 recent games maxGames := 10 for n := int(gameId); n > 0 && int(gameId)-n < maxGames; n-- { g, err := getGame(n) @@ -196,12 +190,11 @@ Below are the results from the most recent games. Up to 10 recent games are disp } sb.WriteString(strconv.Itoa(n) + " | " + - shortName(g.player1) + " | " + diceIcon(g.roll1) + " | " + - shortName(g.player2) + " | " + diceIcon(g.roll2) + " | " + + "" + shortName(g.player1) + "" + " | " + diceIcon(g.roll1) + " | " + + "" + shortName(g.player2) + "" + " | " + diceIcon(g.roll2) + " | " + g.status() + "\n") } - // Leaderboard sb.WriteString(` --- @@ -213,7 +206,7 @@ The top players are ranked by performance. Games played against oneself are not `) for i, player := range getLeaderBoard() { - sb.WriteString(ufmt.Sprintf("| %s | %s | %d | %d | %d | %d |\n", + sb.WriteString(ufmt.Sprintf("| %s | **%s** | %d | %d | %d | %d |\n", rankIcon(i+1), shortName(player.addr), player.wins, @@ -223,7 +216,7 @@ The top players are ranked by performance. Games played against oneself are not )) } - sb.WriteString("\n---\n**Good luck and have fun!** πŸŽ‰") + sb.WriteString("\n---\n**Good luck and have fun !** πŸŽ‰") return sb.String() } diff --git a/examples/gno.land/r/demo/games/dice_roller/icon.gno b/examples/gno.land/r/demo/games/dice_roller/icon.gno index 7ccbe61f1a3..3af94f35f64 100644 --- a/examples/gno.land/r/demo/games/dice_roller/icon.gno +++ b/examples/gno.land/r/demo/games/dice_roller/icon.gno @@ -27,12 +27,10 @@ func diceIcon(roll int) string { // resultIcon returns the icon representing the result of a game func resultIcon(result int) string { switch result { - case selfPlay: - return "❌" case win: return "πŸ†" case lose: - return "πŸ†" + return "❌" case draw: return "🀝" default: From 885adef121375bbf43991f61dd880e5fbe815f40 Mon Sep 17 00:00:00 2001 From: linhpn99 Date: Tue, 10 Sep 2024 21:12:56 +0700 Subject: [PATCH 13/22] update --- examples/gno.land/r/demo/games/dice_roller/dice_roller.gno | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/gno.land/r/demo/games/dice_roller/dice_roller.gno b/examples/gno.land/r/demo/games/dice_roller/dice_roller.gno index c039f2e86ac..c531bc75c07 100644 --- a/examples/gno.land/r/demo/games/dice_roller/dice_roller.gno +++ b/examples/gno.land/r/demo/games/dice_roller/dice_roller.gno @@ -176,7 +176,7 @@ Welcome to Dice Roller! Challenge your friends to a simple yet exciting dice rol --- ## **Recent Games**: -Below are the results from the most recent games. Up to 10 recent games are displayed. Games where both players are the same (playing against oneself) are not counted in the leaderboard. +Below are the results from the most recent games. Up to 10 recent games are displayed | Game | Player 1 | 🎲 Roll 1 | Player 2 | 🎲 Roll 2 | πŸ† Winner | |------|----------|-----------|----------|-----------|-----------| From 9306edc48d401cb138df805b69dddac6a4962af3 Mon Sep 17 00:00:00 2001 From: linhpn99 Date: Tue, 10 Sep 2024 21:27:05 +0700 Subject: [PATCH 14/22] update --- examples/gno.land/r/demo/games/dice_roller/dice_roller.gno | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/gno.land/r/demo/games/dice_roller/dice_roller.gno b/examples/gno.land/r/demo/games/dice_roller/dice_roller.gno index c531bc75c07..d1dc74361c2 100644 --- a/examples/gno.land/r/demo/games/dice_roller/dice_roller.gno +++ b/examples/gno.land/r/demo/games/dice_roller/dice_roller.gno @@ -144,8 +144,8 @@ func (g *game) status() string { if g.roll1 > g.roll2 { return resultIcon(win) + " Player1 Wins !" - } else if g.roll1 < g.roll2 { - return resultIcon(lose) + "Player2 Wins !" + } else if g.roll2 < g.roll1 { + return resultIcon(win) + " Player2 Wins !" } else { return resultIcon(draw) + " It's a Draw !" } @@ -170,7 +170,7 @@ Welcome to Dice Roller! Challenge your friends to a simple yet exciting dice rol ## **Scoring Rules**: - **Win** πŸ†: +3 points - **Draw** 🀝: +1 point each -- **Loss**: No points +- **Lose** ❌: No points - **Playing against yourself**: No points or stats changes for you --- From cff6ec1bae379756f7cb289240fdaba3bd060e34 Mon Sep 17 00:00:00 2001 From: linhpn99 Date: Tue, 10 Sep 2024 21:44:00 +0700 Subject: [PATCH 15/22] add rule sorting --- .../r/demo/games/dice_roller/dice_roller.gno | 24 ++++++++++++++----- .../r/demo/games/dice_roller/icon.gno | 2 +- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/examples/gno.land/r/demo/games/dice_roller/dice_roller.gno b/examples/gno.land/r/demo/games/dice_roller/dice_roller.gno index d1dc74361c2..f43cb415038 100644 --- a/examples/gno.land/r/demo/games/dice_roller/dice_roller.gno +++ b/examples/gno.land/r/demo/games/dice_roller/dice_roller.gno @@ -37,7 +37,7 @@ const ( ongoing = iota win draw - lose + loss ) var ( @@ -95,10 +95,10 @@ func Play(idx int) int { if g.roll1 > g.roll2 { player1.updateStats(win) - player2.updateStats(lose) + player2.updateStats(loss) } else if g.roll2 > g.roll1 { player2.updateStats(win) - player1.updateStats(lose) + player1.updateStats(loss) } else { player1.updateStats(draw) player2.updateStats(draw) @@ -247,7 +247,7 @@ func (p *player) updateStats(result int) { case win: p.wins++ p.points += 3 - case lose: + case loss: p.losses++ case draw: p.draws++ @@ -289,9 +289,21 @@ func (r leaderBoard) Len() int { } func (r leaderBoard) Less(i, j int) bool { - return r[i].points > r[j].points + if r[i].points != r[j].points { + return r[i].points > r[j].points + } + + if r[i].wins != r[j].wins { + return r[i].wins > r[j].wins + } + + if r[i].draws != r[j].draws { + return r[i].draws > r[j].draws + } + + return false } func (r leaderBoard) Swap(i, j int) { - r[i].points, r[j].points = r[j].points, r[i].points + r[i], r[j] = r[j], r[i] } diff --git a/examples/gno.land/r/demo/games/dice_roller/icon.gno b/examples/gno.land/r/demo/games/dice_roller/icon.gno index 3af94f35f64..310e452b2f7 100644 --- a/examples/gno.land/r/demo/games/dice_roller/icon.gno +++ b/examples/gno.land/r/demo/games/dice_roller/icon.gno @@ -29,7 +29,7 @@ func resultIcon(result int) string { switch result { case win: return "πŸ†" - case lose: + case loss: return "❌" case draw: return "🀝" From c9e1fbcd4de5448e766aa1b821362183713571bb Mon Sep 17 00:00:00 2001 From: linhpn99 Date: Tue, 10 Sep 2024 21:47:44 +0700 Subject: [PATCH 16/22] wrong condition --- examples/gno.land/r/demo/games/dice_roller/dice_roller.gno | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/gno.land/r/demo/games/dice_roller/dice_roller.gno b/examples/gno.land/r/demo/games/dice_roller/dice_roller.gno index f43cb415038..85ff193daeb 100644 --- a/examples/gno.land/r/demo/games/dice_roller/dice_roller.gno +++ b/examples/gno.land/r/demo/games/dice_roller/dice_roller.gno @@ -144,7 +144,7 @@ func (g *game) status() string { if g.roll1 > g.roll2 { return resultIcon(win) + " Player1 Wins !" - } else if g.roll2 < g.roll1 { + } else if g.roll2 > g.roll1 { return resultIcon(win) + " Player2 Wins !" } else { return resultIcon(draw) + " It's a Draw !" From fe7b3fd301ccbc8157c8f2ac8c61f366e3852619 Mon Sep 17 00:00:00 2001 From: linhpn99 Date: Tue, 10 Sep 2024 21:56:11 +0700 Subject: [PATCH 17/22] update icon --- examples/gno.land/r/demo/games/dice_roller/icon.gno | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/examples/gno.land/r/demo/games/dice_roller/icon.gno b/examples/gno.land/r/demo/games/dice_roller/icon.gno index 310e452b2f7..f17bcc707c0 100644 --- a/examples/gno.land/r/demo/games/dice_roller/icon.gno +++ b/examples/gno.land/r/demo/games/dice_roller/icon.gno @@ -20,13 +20,15 @@ func diceIcon(roll int) string { case 6: return "🎲6" default: - return strconv.Itoa(roll) + return "❓" } } // resultIcon returns the icon representing the result of a game func resultIcon(result int) string { switch result { + case ongoing: + return "πŸ”„" case win: return "πŸ†" case loss: @@ -34,7 +36,7 @@ func resultIcon(result int) string { case draw: return "🀝" default: - return "πŸ”„" + return "❓" } } From 1f08eb79ee1811d3dd806cd34e5c024e921b9a59 Mon Sep 17 00:00:00 2001 From: AnhNPH Date: Fri, 13 Sep 2024 16:18:36 +0700 Subject: [PATCH 18/22] add script to connect adena in realm view --- gno.land/pkg/gnoweb/static/js/realm_help.js | 73 ++++++++++++++++++++- gno.land/pkg/gnoweb/views/realm_help.html | 10 ++- 2 files changed, 80 insertions(+), 3 deletions(-) diff --git a/gno.land/pkg/gnoweb/static/js/realm_help.js b/gno.land/pkg/gnoweb/static/js/realm_help.js index 30cfacd5f59..27e5097d25d 100644 --- a/gno.land/pkg/gnoweb/static/js/realm_help.js +++ b/gno.land/pkg/gnoweb/static/js/realm_help.js @@ -1,4 +1,4 @@ -function main() { +async function main() { // init var myAddr = getMyAddress() u("#my_address").first().value = myAddr; @@ -12,6 +12,49 @@ function main() { var x = u(e.currentTarget).closest("div.func_spec"); updateCommand(x); }); + + async function fetchAccount() { + try { + const response = await adena.GetAccount(); + if (response.code === 0 && response.status === "success") { + const address = response.data.address; + u("#my_address").first().value = address; + setMyAddress(address); + u("div.func_spec").each(function (node, i) { + updateCommand(u(node)); + }); + } + } catch (error) { + console.error("Error fetching account:", error); + } + } + + //check url params have wallet_address or not + var urlParams = new URLSearchParams(window.location.search); + var connectAdena = urlParams.get('connect-adena'); + if (connectAdena) { + if (!window.adena) { + //open adena.app in a new tab if the adena object is not found + window.open("https://adena.app/", "_blank"); + } else { + //the sample code below displays a method provided by Adena that initiates a connection + await adena.AddEstablish("Adena"); + await fetchAccount(); + } + } + //check url params have adena_message or not + var adenaMessage = urlParams.get('adena_message'); + if (adenaMessage) { + //get current url + var currentUrl = window.location.href; + //remove all url params + currentUrl = currentUrl.split("?")[0]; + //redirect to current url + window.location.href = currentUrl; + var message = JSON.parse(atob(adenaMessage)); + await adena.DoContract(message); + } + // special case: when address changes. u("#my_address").on("input", function(e) { var value = u("#my_address").first().value; @@ -99,7 +142,33 @@ function updateCommand(x) { var args = ["gnokey", "broadcast", "-remote", shq(remote), "call.tx"]; var command = args.join(" "); command = command; - shell.append(u("").text(command)).append(u("
")); + shell.append(u("").text(command)).append(u("
")).append(u("
"));; + + // command 4: Sign and broadcast by Adena + var adenaArgs = []; + vals.forEach(function (arg) { + adenaArgs.push(arg); + }); + const message = { + messages: [{ + type: "/vm.m_call", + value: { + caller: myAddr, + send: "", + pkg_path: realmPath, // Đường dαΊ«n gΓ³i Gnoland + func: funcName, // TΓͺn hΓ m + args: adenaArgs + } + }], + gasFee: 1000000, + gasWanted: 2000000 + }; + //convert message to base64 + const messageBase64 = btoa(JSON.stringify(message)); + var currentUrl = window.location.href; + adena_href = currentUrl + "&adena_message=" + messageBase64; + shell.append(u("").text("### SIGN AND BROACAST BY ADENA")).append(u("
")); + shell.append(u("").text("Do Contract")).append(u("
")); } // Jae: why isn't this a library somewhere? diff --git a/gno.land/pkg/gnoweb/views/realm_help.html b/gno.land/pkg/gnoweb/views/realm_help.html index b9c8e119e7a..bc44b8ae942 100644 --- a/gno.land/pkg/gnoweb/views/realm_help.html +++ b/gno.land/pkg/gnoweb/views/realm_help.html @@ -17,7 +17,7 @@
These are the realm's exposed functions ("public smart contracts").

- My address: (see
`gnokey list`)
+ My address: (see `gnokey list` or Connect Adena )


{{ template "func_specs" . }} @@ -27,6 +27,14 @@ {{ template "js" . }} + {{- end -}} From f9e036afbae34a9f50b85f38b3cc8230761d8f4a Mon Sep 17 00:00:00 2001 From: AnhVAR <133180467+AnhVAR@users.noreply.github.com> Date: Fri, 13 Sep 2024 17:51:57 +0700 Subject: [PATCH 19/22] Revert "Implement game roll dice" --- .../r/demo/games/dice_roller/dice_roller.gno | 309 ------------------ .../games/dice_roller/dice_roller_test.gno | 139 -------- .../gno.land/r/demo/games/dice_roller/gno.mod | 11 - .../r/demo/games/dice_roller/icon.gno | 55 ---- 4 files changed, 514 deletions(-) delete mode 100644 examples/gno.land/r/demo/games/dice_roller/dice_roller.gno delete mode 100644 examples/gno.land/r/demo/games/dice_roller/dice_roller_test.gno delete mode 100644 examples/gno.land/r/demo/games/dice_roller/gno.mod delete mode 100644 examples/gno.land/r/demo/games/dice_roller/icon.gno diff --git a/examples/gno.land/r/demo/games/dice_roller/dice_roller.gno b/examples/gno.land/r/demo/games/dice_roller/dice_roller.gno deleted file mode 100644 index 85ff193daeb..00000000000 --- a/examples/gno.land/r/demo/games/dice_roller/dice_roller.gno +++ /dev/null @@ -1,309 +0,0 @@ -package diceroller - -import ( - "errors" - "math/rand" - "sort" - "std" - "strconv" - "strings" - - "gno.land/p/demo/avl" - "gno.land/p/demo/entropy" - "gno.land/p/demo/seqid" - "gno.land/p/demo/ufmt" - "gno.land/r/demo/users" -) - -type ( - // game represents a Dice Roller game between two players - game struct { - player1, player2 std.Address - roll1, roll2 int - } - - // player holds the information about each player including their stats - player struct { - addr std.Address - wins, losses, draws, points int - } - - // leaderBoard is a slice of players, used to sort players by rank - leaderBoard []player -) - -const ( - // Constants to represent game result outcomes - ongoing = iota - win - draw - loss -) - -var ( - games avl.Tree // AVL tree for storing game states - gameId seqid.ID // Sequence ID for games - - players avl.Tree // AVL tree for storing player data - - seed = uint64(entropy.New().Seed()) - r = rand.New(rand.NewPCG(seed, 0xdeadbeef)) -) - -// rollDice generates a random dice roll between 1 and 6 -func rollDice() int { - return r.IntN(6) + 1 -} - -// NewGame initializes a new game with the provided opponent's address -func NewGame(addr std.Address) int { - if !addr.IsValid() { - panic("invalid opponent's address") - } - - games.Set(gameId.Next().String(), &game{ - player1: std.PrevRealm().Addr(), - player2: addr, - }) - - return int(gameId) -} - -// Play allows a player to roll the dice and updates the game state accordingly -func Play(idx int) int { - g, err := getGame(idx) - if err != nil { - panic(err) - } - - roll := rollDice() // Random the player's dice roll - - // Play the game and update the player's roll - if err := g.play(std.PrevRealm().Addr(), roll); err != nil { - panic(err) - } - - // If both players have rolled, update the results and leaderboard - if g.isFinished() { - // If the player is playing against themselves, no points are awarded - if g.player1 == g.player2 { - return roll - } - - player1 := getPlayer(g.player1) - player2 := getPlayer(g.player2) - - if g.roll1 > g.roll2 { - player1.updateStats(win) - player2.updateStats(loss) - } else if g.roll2 > g.roll1 { - player2.updateStats(win) - player1.updateStats(loss) - } else { - player1.updateStats(draw) - player2.updateStats(draw) - } - } - - return roll -} - -// play processes a player's roll and updates their score -func (g *game) play(player std.Address, roll int) error { - if player != g.player1 && player != g.player2 { - return errors.New("invalid player") - } - - if g.isFinished() { - return errors.New("game over") - } - - if player == g.player1 && g.roll1 == 0 { - g.roll1 = roll - return nil - } - - if player == g.player2 && g.roll2 == 0 { - g.roll2 = roll - return nil - } - - return errors.New("already played") -} - -// isFinished checks if the game has ended -func (g *game) isFinished() bool { - return g.roll1 != 0 && g.roll2 != 0 -} - -// checkResult returns the game status as a formatted string -func (g *game) status() string { - if !g.isFinished() { - return resultIcon(ongoing) + " Game still in progress" - } - - if g.roll1 > g.roll2 { - return resultIcon(win) + " Player1 Wins !" - } else if g.roll2 > g.roll1 { - return resultIcon(win) + " Player2 Wins !" - } else { - return resultIcon(draw) + " It's a Draw !" - } -} - -// Render provides a summary of the current state of games and leader board -func Render(path string) string { - var sb strings.Builder - - sb.WriteString(`# 🎲 **Dice Roller Game** - -Welcome to Dice Roller! Challenge your friends to a simple yet exciting dice rolling game. Roll the dice and see who gets the highest score ! - ---- - -## **How to Play**: -1. **Create a game**: Challenge an opponent using [NewGame](diceroller?help&__func=NewGame) -2. **Roll the dice**: Play your turn by rolling a dice using [Play](diceroller?help&__func=Play) - ---- - -## **Scoring Rules**: -- **Win** πŸ†: +3 points -- **Draw** 🀝: +1 point each -- **Lose** ❌: No points -- **Playing against yourself**: No points or stats changes for you - ---- - -## **Recent Games**: -Below are the results from the most recent games. Up to 10 recent games are displayed - -| Game | Player 1 | 🎲 Roll 1 | Player 2 | 🎲 Roll 2 | πŸ† Winner | -|------|----------|-----------|----------|-----------|-----------| -`) - - maxGames := 10 - for n := int(gameId); n > 0 && int(gameId)-n < maxGames; n-- { - g, err := getGame(n) - if err != nil { - continue - } - - sb.WriteString(strconv.Itoa(n) + " | " + - "" + shortName(g.player1) + "" + " | " + diceIcon(g.roll1) + " | " + - "" + shortName(g.player2) + "" + " | " + diceIcon(g.roll2) + " | " + - g.status() + "\n") - } - - sb.WriteString(` ---- - -## **Leaderboard**: -The top players are ranked by performance. Games played against oneself are not counted in the leaderboard - -| Rank | Player | Wins | Losses | Draws | Points | -|------|-----------------------|------|--------|-------|--------| -`) - - for i, player := range getLeaderBoard() { - sb.WriteString(ufmt.Sprintf("| %s | **%s** | %d | %d | %d | %d |\n", - rankIcon(i+1), - shortName(player.addr), - player.wins, - player.losses, - player.draws, - player.points, - )) - } - - sb.WriteString("\n---\n**Good luck and have fun !** πŸŽ‰") - return sb.String() -} - -// shortName returns a shortened name for the given address -func shortName(addr std.Address) string { - user := users.GetUserByAddress(addr) - if user != nil { - return user.Name - } - if len(addr) < 10 { - return string(addr) - } - return string(addr)[:10] + "..." -} - -// getGame retrieves the game state by its ID -func getGame(idx int) (*game, error) { - v, ok := games.Get(seqid.ID(idx).String()) - if !ok { - return nil, errors.New("game not found") - } - return v.(*game), nil -} - -// updateResult updates the player's stats and points based on the game outcome -func (p *player) updateStats(result int) { - switch result { - case win: - p.wins++ - p.points += 3 - case loss: - p.losses++ - case draw: - p.draws++ - p.points++ - } -} - -// getPlayer retrieves a player or initializes a new one if they don't exist -func getPlayer(addr std.Address) *player { - v, ok := players.Get(addr.String()) - if !ok { - player := &player{ - addr: addr, - } - players.Set(addr.String(), player) - return player - } - - return v.(*player) -} - -// getLeaderBoard generates a leaderboard sorted by points -func getLeaderBoard() leaderBoard { - board := leaderBoard{} - players.Iterate("", "", func(key string, value interface{}) bool { - player := value.(*player) - board = append(board, *player) - return false - }) - - sort.Sort(board) - - return board -} - -// Methods for sorting the leaderboard -func (r leaderBoard) Len() int { - return len(r) -} - -func (r leaderBoard) Less(i, j int) bool { - if r[i].points != r[j].points { - return r[i].points > r[j].points - } - - if r[i].wins != r[j].wins { - return r[i].wins > r[j].wins - } - - if r[i].draws != r[j].draws { - return r[i].draws > r[j].draws - } - - return false -} - -func (r leaderBoard) Swap(i, j int) { - r[i], r[j] = r[j], r[i] -} diff --git a/examples/gno.land/r/demo/games/dice_roller/dice_roller_test.gno b/examples/gno.land/r/demo/games/dice_roller/dice_roller_test.gno deleted file mode 100644 index fe6f5400f57..00000000000 --- a/examples/gno.land/r/demo/games/dice_roller/dice_roller_test.gno +++ /dev/null @@ -1,139 +0,0 @@ -package diceroller - -import ( - "std" - "testing" - - "gno.land/p/demo/avl" - "gno.land/p/demo/seqid" - "gno.land/p/demo/testutils" - "gno.land/p/demo/urequire" -) - -var ( - player1 = testutils.TestAddress("alice") - player2 = testutils.TestAddress("bob") - unknownPlayer = testutils.TestAddress("unknown") -) - -// resetGameState resets the game state for testing -func resetGameState() { - games = avl.Tree{} - gameId = seqid.ID(0) - players = avl.Tree{} -} - -// TestNewGame tests the initialization of a new game -func TestNewGame(t *testing.T) { - resetGameState() - - std.TestSetOrigCaller(player1) - gameID := NewGame(player2) - - // Verify that the game has been correctly initialized - g, err := getGame(gameID) - urequire.NoError(t, err) - urequire.Equal(t, player1.String(), g.player1.String()) - urequire.Equal(t, player2.String(), g.player2.String()) - urequire.Equal(t, 0, g.roll1) - urequire.Equal(t, 0, g.roll2) -} - -// TestPlay tests the dice rolling functionality for both players -func TestPlay(t *testing.T) { - resetGameState() - - std.TestSetOrigCaller(player1) - gameID := NewGame(player2) - - g, err := getGame(gameID) - urequire.NoError(t, err) - - // Simulate rolling dice for player 1 - roll1 := Play(gameID) - - // Verify player 1's roll - urequire.NotEqual(t, 0, g.roll1) - urequire.Equal(t, g.roll1, roll1) - urequire.Equal(t, 0, g.roll2) // Player 2 hasn't rolled yet - - // Simulate rolling dice for player 2 - std.TestSetOrigCaller(player2) - roll2 := Play(gameID) - - // Verify player 2's roll - urequire.NotEqual(t, 0, g.roll2) - urequire.Equal(t, g.roll1, roll1) - urequire.Equal(t, g.roll2, roll2) -} - -// TestPlayAgainstSelf tests the scenario where a player plays against themselves -func TestPlayAgainstSelf(t *testing.T) { - resetGameState() - - std.TestSetOrigCaller(player1) - gameID := NewGame(player1) - - // Simulate rolling dice twice by the same player - roll1 := Play(gameID) - roll2 := Play(gameID) - - g, err := getGame(gameID) - urequire.NoError(t, err) - urequire.Equal(t, g.roll1, roll1) - urequire.Equal(t, g.roll2, roll2) -} - -// TestPlayInvalidPlayer tests the scenario where an invalid player tries to play -func TestPlayInvalidPlayer(t *testing.T) { - resetGameState() - - std.TestSetOrigCaller(player1) - gameID := NewGame(player1) - - // Attempt to play as an invalid player - std.TestSetOrigCaller(unknownPlayer) - urequire.PanicsWithMessage(t, "invalid player", func() { - Play(gameID) - }) -} - -// TestPlayAlreadyPlayed tests the scenario where a player tries to play again after already playing -func TestPlayAlreadyPlayed(t *testing.T) { - resetGameState() - - std.TestSetOrigCaller(player1) - gameID := NewGame(player2) - - // Player 1 rolls - Play(gameID) - - // Player 1 tries to roll again - urequire.PanicsWithMessage(t, "already played", func() { - Play(gameID) - }) -} - -// TestPlayBeyondGameEnd tests that playing after both players have finished their rolls fails -func TestPlayBeyondGameEnd(t *testing.T) { - resetGameState() - - std.TestSetOrigCaller(player1) - gameID := NewGame(player2) - - // Play for both players - std.TestSetOrigCaller(player1) - Play(gameID) - std.TestSetOrigCaller(player2) - Play(gameID) - - // Check if the game is over - g, err := getGame(gameID) - urequire.NoError(t, err) - - // Attempt to play more should fail - std.TestSetOrigCaller(player1) - urequire.PanicsWithMessage(t, "game over", func() { - Play(gameID) - }) -} diff --git a/examples/gno.land/r/demo/games/dice_roller/gno.mod b/examples/gno.land/r/demo/games/dice_roller/gno.mod deleted file mode 100644 index 4f1016ad8d4..00000000000 --- a/examples/gno.land/r/demo/games/dice_roller/gno.mod +++ /dev/null @@ -1,11 +0,0 @@ -module gno.land/r/demo/games/diceroller - -require ( - gno.land/p/demo/avl v0.0.0-latest - gno.land/p/demo/entropy v0.0.0-latest - gno.land/p/demo/seqid v0.0.0-latest - gno.land/p/demo/testutils v0.0.0-latest - gno.land/p/demo/ufmt v0.0.0-latest - gno.land/p/demo/urequire v0.0.0-latest - gno.land/r/demo/users v0.0.0-latest -) diff --git a/examples/gno.land/r/demo/games/dice_roller/icon.gno b/examples/gno.land/r/demo/games/dice_roller/icon.gno deleted file mode 100644 index f17bcc707c0..00000000000 --- a/examples/gno.land/r/demo/games/dice_roller/icon.gno +++ /dev/null @@ -1,55 +0,0 @@ -package diceroller - -import ( - "strconv" -) - -// diceIcon returns an icon of the dice roll -func diceIcon(roll int) string { - switch roll { - case 1: - return "🎲1" - case 2: - return "🎲2" - case 3: - return "🎲3" - case 4: - return "🎲4" - case 5: - return "🎲5" - case 6: - return "🎲6" - default: - return "❓" - } -} - -// resultIcon returns the icon representing the result of a game -func resultIcon(result int) string { - switch result { - case ongoing: - return "πŸ”„" - case win: - return "πŸ†" - case loss: - return "❌" - case draw: - return "🀝" - default: - return "❓" - } -} - -// rankIcon returns the icon for a player's rank -func rankIcon(rank int) string { - switch rank { - case 1: - return "πŸ₯‡" - case 2: - return "πŸ₯ˆ" - case 3: - return "πŸ₯‰" - default: - return strconv.Itoa(rank) - } -} From be9cbf32612fa6785a8fea058f9155976c02870c Mon Sep 17 00:00:00 2001 From: AnhNPH Date: Fri, 13 Sep 2024 17:52:11 +0700 Subject: [PATCH 20/22] remove comment --- gno-key.asc | 6 ++++++ gno.land/pkg/gnoweb/views/realm_help.html | 1 - 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 gno-key.asc diff --git a/gno-key.asc b/gno-key.asc new file mode 100644 index 00000000000..6cd9d54d0a3 --- /dev/null +++ b/gno-key.asc @@ -0,0 +1,6 @@ +-----BEGIN TENDERMINT PRIVATE KEY----- + +ChQvdG0uUHJpdktleVNlY3AyNTZrMRIiCiDql7n9235r9oZwkKeoGWVwR5Sfu5Rm +1hf5QFOO/YiGBQ== +=wH/4 +-----END TENDERMINT PRIVATE KEY----- \ No newline at end of file diff --git a/gno.land/pkg/gnoweb/views/realm_help.html b/gno.land/pkg/gnoweb/views/realm_help.html index bc44b8ae942..97c1753c010 100644 --- a/gno.land/pkg/gnoweb/views/realm_help.html +++ b/gno.land/pkg/gnoweb/views/realm_help.html @@ -28,7 +28,6 @@ {{ template "js" . }}