Skip to content

Commit

Permalink
[examples] Adds RockPaperScissors example (MystenLabs#715)
Browse files Browse the repository at this point in the history
* Adds rock paper scissors example
  • Loading branch information
damirka authored Mar 12, 2022
1 parent cf03fe4 commit 13c4e9f
Show file tree
Hide file tree
Showing 2 changed files with 360 additions and 0 deletions.
254 changes: 254 additions & 0 deletions sui_programmability/examples/games/sources/RockPaperScissors.move
Original file line number Diff line number Diff line change
@@ -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<u8>,
hash_two: vector<u8>,
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<u8>,
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<u8>,
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<u8>, 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<u8>, 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<u8>, hash: &vector<u8>): 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<u8>): vector<u8> {
Vector::push_back(&mut salt, gesture);
Hash::sha2_256(salt)
}
}
106 changes: 106 additions & 0 deletions sui_programmability/examples/games/tests/RockPaperScissorsTests.move
Original file line number Diff line number Diff line change
@@ -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<Game>(scenario);
let cap = TestScenario::remove_object<PlayerTurn>(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<Game>(scenario);
let cap = TestScenario::remove_object<PlayerTurn>(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<Game>(scenario);
let secret = TestScenario::remove_object<Secret>(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<Game>(scenario);
let secret = TestScenario::remove_object<Secret>(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<ThePrize>(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<u8>): vector<u8> {
Vector::push_back(&mut salt, gesture);
Hash::sha2_256(salt)
}
}

0 comments on commit 13c4e9f

Please sign in to comment.