forked from MystenLabs/sui
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[examples] Adds RockPaperScissors example (MystenLabs#715)
* Adds rock paper scissors example
- Loading branch information
Showing
2 changed files
with
360 additions
and
0 deletions.
There are no files selected for viewing
254 changes: 254 additions & 0 deletions
254
sui_programmability/examples/games/sources/RockPaperScissors.move
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
106
sui_programmability/examples/games/tests/RockPaperScissorsTests.move
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} |