From 13c4e9f7f6000de661006e2a32b54ee740962b6f Mon Sep 17 00:00:00 2001 From: Damir S Date: Sat, 12 Mar 2022 20:33:01 +0300 Subject: [PATCH] [examples] Adds RockPaperScissors example (#715) * Adds rock paper scissors example --- .../games/sources/RockPaperScissors.move | 254 ++++++++++++++++++ .../games/tests/RockPaperScissorsTests.move | 106 ++++++++ 2 files changed, 360 insertions(+) create mode 100644 sui_programmability/examples/games/sources/RockPaperScissors.move create mode 100644 sui_programmability/examples/games/tests/RockPaperScissorsTests.move diff --git a/sui_programmability/examples/games/sources/RockPaperScissors.move b/sui_programmability/examples/games/sources/RockPaperScissors.move new file mode 100644 index 0000000000000..fa8313dd68e05 --- /dev/null +++ b/sui_programmability/examples/games/sources/RockPaperScissors.move @@ -0,0 +1,254 @@ +// This is an idea of a module which will allow some asset to be +// won by playing a rock-paper-scissors (then lizard-spoke) game. +// +// Initial implementation implies so-called commit-reveal scheme +// in which players first submit their commitments +// and then reveal the data that led to these commitments. The +// data is then being verified by one of the parties or a third +// party (depends on implementation and security measures). +// +// In this specific example, the flow is: +// 1. User A creates a Game struct, where he puts a prize asset +// 2. Both users B and C submit their hashes to the game as their +// guesses but don't reveal the actual values yet +// 3. Users B and C submit their salts, so the user A +// can see and prove that the values match, and decides who won the +// round. Asset is then released to the winner or to the game owner +// if nobody won. +// +// TODO: +// - Error codes +// - Status checks +// - If player never revealed the secret +// - If game owner never took or revealed the results (incentives?) + +module Games::RockPaperScissors { + use Sui::ID::{Self, VersionedID}; + use Sui::TxContext::{Self, TxContext}; + use Sui::Transfer::{Self}; + use Std::Vector; + use Std::Hash; + + // -- Gestures and additional consts -- // + + const NONE: u8 = 0; + const ROCK: u8 = 1; + const PAPER: u8 = 2; + const SCISSORS: u8 = 3; + const CHEAT: u8 = 111; + + public fun rock(): u8 { ROCK } + public fun paper(): u8 { PAPER } + public fun scissors(): u8 { SCISSORS } + + // -- Game statuses list -- // + + const STATUS_READY: u8 = 0; + const STATUS_HASH_SUBMISSION: u8 = 1; + const STATUS_HASHES_SUBMITTED: u8 = 2; + const STATUS_REVEALING: u8 = 3; + const STATUS_REVEALED: u8 = 4; + + /// The Prize that's being held inside the [`Game`] object. Should be + /// eventually replaced with some generic T inside the [`Game`]. + struct ThePrize has key, store { + id: VersionedID + } + + /// The main resource of the RockPaperScissors module. Contains all the + /// information about the game state submitted by both players. By default + /// contains empty values and fills as the game progresses. + /// Being destroyed in the end, once [`select_winner`] is called and the game + /// has reached its final state by that time. + struct Game has key { + id: VersionedID, + prize: ThePrize, + player_one: address, + player_two: address, + hash_one: vector, + hash_two: vector, + gesture_one: u8, + gesture_two: u8, + } + + /// Hashed gesture. It is not reveal-able until both players have + /// submitted their moves to the Game. The turn is passed to the + /// game owner who then adds a hash to the Game object. + struct PlayerTurn has key { + id: VersionedID, + hash: vector, + player: address, + } + + /// Secret object which is used to reveal the move. Just like [`PlayerTurn`] + /// it is used to reveal the actual gesture a player has submitted. + struct Secret has key { + id: VersionedID, + salt: vector, + player: address, + } + + /// Shows the current game status. This function is also used in the [`select_winner`] + /// entry point and limits the ability to select a winner, if one of the secrets hasn't + /// been revealed yet. + public fun status(game: &Game): u8 { + let h1_len = Vector::length(&game.hash_one); + let h2_len = Vector::length(&game.hash_two); + + if (game.gesture_one != NONE && game.gesture_two != NONE) { + STATUS_REVEALED + } else if (game.gesture_one != NONE || game.gesture_two != NONE) { + STATUS_REVEALING + } else if (h1_len == 0 && h2_len == 0) { + STATUS_READY + } else if (h1_len != 0 && h2_len != 0) { + STATUS_HASHES_SUBMITTED + } else if (h1_len != 0 || h2_len != 0) { + STATUS_HASH_SUBMISSION + } else { + 0 + } + } + + /// Start a new game at sender address. The only arguments needed are players, the rest + /// is initiated with default/empty values which will be filled later in the game. + /// + /// todo: extend with generics + T as prize + public fun new_game(player_one: address, player_two: address, ctx: &mut TxContext) { + Transfer::transfer(Game { + id: TxContext::new_id(ctx), + prize: ThePrize { id: TxContext::new_id(ctx) }, + player_one, + player_two, + hash_one: vector[], + hash_two: vector[], + gesture_one: NONE, + gesture_two: NONE, + }, TxContext::sender(ctx)); + } + + /// Transfer [`PlayerTurn`] to the game owner. Nobody at this point knows what move + /// is encoded inside the [`hash`] argument. + /// + /// Currently there's no check on whether the game exists. + public fun player_turn(at: address, hash: vector, ctx: &mut TxContext) { + Transfer::transfer(PlayerTurn { + hash, + id: TxContext::new_id(ctx), + player: TxContext::sender(ctx), + }, at); + } + + /// Add a hashed gesture to the game. Store it as a `hash_one` or `hash_two` depending + /// on the player number (one or two) + public fun add_hash(game: &mut Game, cap: PlayerTurn, _ctx: &mut TxContext) { + let PlayerTurn { hash, id, player } = cap; + let status = status(game); + + assert!(status == STATUS_HASH_SUBMISSION || status == STATUS_READY, 0); + assert!(game.player_one == player || game.player_two == player, 0); + + if (player == game.player_one && Vector::length(&game.hash_one) == 0) { + game.hash_one = hash; + } else if (player == game.player_two && Vector::length(&game.hash_two) == 0) { + game.hash_two = hash; + } else { + abort 0 // unreachable!() + }; + + ID::delete(id); + } + + /// Submit a [`Secret`] to the game owner who then matches the hash and saves the + /// gesture in the [`Game`] object. + public fun reveal(at: address, salt: vector, ctx: &mut TxContext) { + Transfer::transfer(Secret { + id: TxContext::new_id(ctx), + salt, + player: TxContext::sender(ctx), + }, at); + } + + /// Use submitted [`Secret`]'s salt to find the gesture played by the player and set it + /// in the [`Game`] object. + /// TODO: think of ways to + public fun match_secret(game: &mut Game, secret: Secret, _ctx: &mut TxContext) { + let Secret { salt, player, id } = secret; + + assert!(player == game.player_one || player == game.player_two, 0); + + if (player == game.player_one) { + game.gesture_one = find_gesture(salt, &game.hash_one); + } else if (player == game.player_two) { + game.gesture_two = find_gesture(salt, &game.hash_two); + }; + + ID::delete(id); + } + + /// The final accord to the game logic. After both secrets have been revealed, + /// the game owner can choose a winner and release the prize. + public fun select_winner(game: Game, ctx: &mut TxContext) { + assert!(status(&game) == STATUS_REVEALED, 0); + + let Game { + id, + prize, + player_one, + player_two, + hash_one: _, + hash_two: _, + gesture_one, + gesture_two, + } = game; + + let p1_wins = play(gesture_one, gesture_two); + let p2_wins = play(gesture_two, gesture_one); + + ID::delete(id); + + // If one of the players wins, he takes the prize. + // If there's a tie, the game owner gets the prize. + if (p1_wins) { + Transfer::transfer(prize, player_one) + } else if (p2_wins) { + Transfer::transfer(prize, player_two) + } else { + Transfer::transfer(prize, TxContext::sender(ctx)) + }; + } + + /// Implement the basic logic of the game. + fun play(one: u8, two: u8): bool { + if (one == ROCK && two == SCISSORS) { true } + else if (one == PAPER && two == ROCK) { true } + else if (one == SCISSORS && two == PAPER) { true } + else if (one != CHEAT && two == CHEAT) { true } + else { false } + } + + /// Hash the salt and the gesture_id and match it against the stored hash. If something + /// matches, the gesture_id is returned, if nothing - player is considered a cheater, and + /// he automatically loses the round. + fun find_gesture(salt: vector, hash: &vector): u8 { + if (hash(ROCK, salt) == *hash) { + ROCK + } else if (hash(PAPER, salt) == *hash) { + PAPER + } else if (hash(SCISSORS, salt) == *hash) { + SCISSORS + } else { + CHEAT + } + } + + /// Internal hashing function to build a [`Secret`] and match it later at the reveal stage. + /// + /// - `salt` argument here is a secret that is only known to the sender. That way we ensure + /// that nobody knows the gesture until the end, but at the same time each player commits + /// to the result with his hash; + fun hash(gesture: u8, salt: vector): vector { + Vector::push_back(&mut salt, gesture); + Hash::sha2_256(salt) + } +} diff --git a/sui_programmability/examples/games/tests/RockPaperScissorsTests.move b/sui_programmability/examples/games/tests/RockPaperScissorsTests.move new file mode 100644 index 0000000000000..1958e9c640c07 --- /dev/null +++ b/sui_programmability/examples/games/tests/RockPaperScissorsTests.move @@ -0,0 +1,106 @@ +#[test_only] +module Games::RockPaperScissorsTests { + use Games::RockPaperScissors::{Self as Game, Game, PlayerTurn, Secret, ThePrize}; + use Sui::TestScenario::{Self}; + use Std::Vector; + use Std::Hash; + + #[test] + public fun play_rock_paper_scissors() { + // So these are our heros. + let the_main_guy = @0xA1C05; + let mr_lizard = @0xA55555; + let mr_spock = @0x590C; + + let scenario = &mut TestScenario::begin(&the_main_guy); + + // Let the game begin! + Game::new_game(mr_spock, mr_lizard, TestScenario::ctx(scenario)); + + // Mr Spock makes his move. He does it secretly and hashes the gesture with a salt + // so that only he knows what it is. + TestScenario::next_tx(scenario, &mr_spock); + { + let hash = hash(Game::rock(), b"my_phaser_never_failed_me!"); + Game::player_turn(the_main_guy, hash, TestScenario::ctx(scenario)); + }; + + // Now it's time for The Main Guy to accept his turn. + TestScenario::next_tx(scenario, &the_main_guy); + { + let game = TestScenario::remove_object(scenario); + let cap = TestScenario::remove_object(scenario); + + assert!(Game::status(&game) == 0, 0); // STATUS_READY + + Game::add_hash(&mut game, cap, TestScenario::ctx(scenario)); + + assert!(Game::status(&game) == 1, 0); // STATUS_HASH_SUBMISSION + + TestScenario::return_object(scenario, game); + }; + + // Same for Mr Lizard. He uses his secret phrase to encode his turn. + TestScenario::next_tx(scenario, &mr_lizard); + { + let hash = hash(Game::scissors(), b"sssssss_you_are_dead!"); + Game::player_turn(the_main_guy, hash, TestScenario::ctx(scenario)); + }; + + TestScenario::next_tx(scenario, &the_main_guy); + { + let game = TestScenario::remove_object(scenario); + let cap = TestScenario::remove_object(scenario); + Game::add_hash(&mut game, cap, TestScenario::ctx(scenario)); + + assert!(Game::status(&game) == 2, 0); // STATUS_HASHES_SUBMITTED + + TestScenario::return_object(scenario, game); + }; + + // Now that both sides made their moves, it's time for Mr Spock and Mr Lizard to + // reveal their secrets. The Main Guy will then be able to determine the winner. Who's + // gonna win The Prize? We'll see in a bit! + TestScenario::next_tx(scenario, &mr_spock); + Game::reveal(the_main_guy, b"my_phaser_never_failed_me!", TestScenario::ctx(scenario)); + + TestScenario::next_tx(scenario, &the_main_guy); + { + let game = TestScenario::remove_object(scenario); + let secret = TestScenario::remove_object(scenario); + Game::match_secret(&mut game, secret, TestScenario::ctx(scenario)); + + assert!(Game::status(&game) == 3, 0); // STATUS_REVEALING + + TestScenario::return_object(scenario, game); + }; + + TestScenario::next_tx(scenario, &mr_lizard); + Game::reveal(the_main_guy, b"sssssss_you_are_dead!", TestScenario::ctx(scenario)); + + // The final step. The Main Guy matches and reveals the secret of the Mr Lizard and + // calls the [`select_winner`] function to release The Prize. + TestScenario::next_tx(scenario, &the_main_guy); + { + let game = TestScenario::remove_object(scenario); + let secret = TestScenario::remove_object(scenario); + Game::match_secret(&mut game, secret, TestScenario::ctx(scenario)); + + assert!(Game::status(&game) == 4, 0); // STATUS_REVEALED + + Game::select_winner(game, TestScenario::ctx(scenario)); + }; + + TestScenario::next_tx(scenario, &mr_spock); + // If it works, then MrSpock is in posession of the prize; + let prize = TestScenario::remove_object(scenario); + // Don't forget to give it back! + TestScenario::return_object(scenario, prize); + } + + // Copy of the hashing function from the main module. + fun hash(gesture: u8, salt: vector): vector { + Vector::push_back(&mut salt, gesture); + Hash::sha2_256(salt) + } +}