diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c72b576..fc89685 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,9 +2,9 @@ name: CI on: pull_request: - branches: [main] + branches: [dev] push: - branches: [main] + branches: [dev] env: CARGO_TERM_COLOR: always diff --git a/17191623856201.jpg b/17191623856201.jpg new file mode 100644 index 0000000..5cf9d50 Binary files /dev/null and b/17191623856201.jpg differ diff --git a/Cargo.lock b/Cargo.lock index 1118be6..8ee0495 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1369,6 +1369,15 @@ dependencies = [ "enum-iterator-derive 1.4.0", ] +[[package]] +name = "enum-iterator" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c280b9e6b3ae19e152d8e31cf47f18389781e119d4013a2a2bb0180e5facc635" +dependencies = [ + "enum-iterator-derive 1.4.0", +] + [[package]] name = "enum-iterator-derive" version = "0.7.0" @@ -3197,6 +3206,7 @@ dependencies = [ name = "pebbles-game-io" version = "0.1.0" dependencies = [ + "enum-iterator 2.1.0", "gmeta", "gstd", "parity-scale-codec", diff --git a/Cargo.toml b/Cargo.toml index e9966ab..78527b3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" edition = "2021" [dependencies] -gstd = { git = "https://github.com/gear-tech/gear.git", tag = "v1.4.1" } +gstd = { git = "https://github.com/gear-tech/gear.git", tag = "v1.4.1",features = ["debug"] } pebbles-game-io.path = "io" [build-dependencies] @@ -12,6 +12,6 @@ gear-wasm-builder = { git = "https://github.com/gear-tech/gear.git", tag = "v1.4 pebbles-game-io.path = "io" [dev-dependencies] -gstd = { git = "https://github.com/gear-tech/gear.git", tag = "v1.4.1" } +gstd = { git = "https://github.com/gear-tech/gear.git", tag = "v1.4.1", features = ["debug"] } gtest = { git = "https://github.com/gear-tech/gear.git", tag = "v1.4.1" } pebbles-game-io.path = "io" diff --git a/README.md b/README.md index d96ece6..55955b1 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,6 @@ pebbles-game The `pebbles-game-io` will contains type definitions for input, output, and internal state data. +https://idea.gear-tech.io/programs/0x849054a5c3fa3dcce136ed3196634d1af055217df03324ba2a0a72756479012a?node=wss%3A%2F%2Ftestnet.vara.network - - +![Alt text](17191623856201.jpg) diff --git a/build.rs b/build.rs index d38a96d..c36cbb9 100644 --- a/build.rs +++ b/build.rs @@ -5,4 +5,4 @@ use pebbles_game_io::PebblesMetadata; fn main() { gear_wasm_builder::build_with_metadata::(); -} \ No newline at end of file +} diff --git a/io/Cargo.toml b/io/Cargo.toml index c815274..e1d8a8a 100644 --- a/io/Cargo.toml +++ b/io/Cargo.toml @@ -9,3 +9,4 @@ gstd = { git = "https://github.com/gear-tech/gear.git", tag = "v1.4.1" } gmeta = { git = "https://github.com/gear-tech/gear.git", tag = "v1.4.1" } parity-scale-codec = { version = "3", default-features = false } scale-info = { version = "2", default-features = false } +enum-iterator = "2.1.0" diff --git a/io/src/lib.rs b/io/src/lib.rs index f688b95..3dcdf1d 100644 --- a/io/src/lib.rs +++ b/io/src/lib.rs @@ -1,6 +1,6 @@ #![no_std] -use gmeta::{In, InOut, Out, Metadata}; +use gmeta::{In, InOut, Metadata, Out}; use gstd::prelude::*; // the metadata to be used by the [IDEA](https://idea.gear-tech.io/programs?node=wss%3A%2F%2Ftestnet.vara.network) portal. @@ -15,8 +15,6 @@ impl Metadata for PebblesMetadata { type Signal = (); } -// When initialising the game, it is necessary to pass some initial information. -// For example, the number of pebbles (N), maximum pebbles to be removed per turn (K), difficulty level. #[derive(Debug, Default, Clone, Encode, Decode, TypeInfo)] pub struct PebblesInit { pub difficulty: DifficultyLevel, @@ -31,9 +29,6 @@ pub enum DifficultyLevel { Hard, } -// It needs to send actions message for every User's move and receive some event from the program. -// The action can be a turn with some count of pebbles to be removed or the give up. -// Also, there is a restart action than resets the game state . #[derive(Debug, Clone, Encode, Decode, TypeInfo)] pub enum PebblesAction { Turn(u32), @@ -45,24 +40,23 @@ pub enum PebblesAction { }, } -// And the event reflects the game state after the User's move: -// either pebbles count removed by the Program or the end of game with the information about the winner. #[derive(Debug, Clone, Encode, Decode, TypeInfo, PartialEq, Eq)] +#[codec(crate = gstd::codec)] +#[scale_info(crate = gstd::scale_info)] pub enum PebblesEvent { CounterTurn(u32), Won(Player), } #[derive(Debug, Default, Clone, Encode, Decode, TypeInfo, PartialEq, Eq)] +#[codec(crate = gstd::codec)] +#[scale_info(crate = gstd::scale_info)] pub enum Player { #[default] User, Program, } -// Internal game state should keep all information related to the current state of the game. -// Some information is set during initialization, the first player is chosen randomly, -// some data are change during the game. #[derive(Debug, Default, Clone, Encode, Decode, TypeInfo)] pub struct GameState { pub pebbles_count: u32, diff --git a/pebbles-state/Cargo.toml b/pebbles-state/Cargo.toml new file mode 100644 index 0000000..d8926fc --- /dev/null +++ b/pebbles-state/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "pebbles-game-state" +version = "0.1.0" +edition = "2021" + +[dependencies] +gstd = { git = "https://github.com/gear-tech/gear.git", tag = "v1.4.1",features = ["debug"] } +gmeta = { git = "https://github.com/gear-tech/gear.git", tag = "v1.4.1" } +pebbles-game-io.path = "io" + +[build-dependencies] +gear-wasm-builder = { git = "https://github.com/gear-tech/gear.git", tag = "v1.4.1" } +pebbles-game-io.path = "io" diff --git a/pebbles-state/build.rs b/pebbles-state/build.rs new file mode 100644 index 0000000..d019586 --- /dev/null +++ b/pebbles-state/build.rs @@ -0,0 +1,3 @@ +fn main() { + gear_wasm_builder::build_metawasm(); +} \ No newline at end of file diff --git a/pebbles-state/src/lib.rs b/pebbles-state/src/lib.rs new file mode 100644 index 0000000..424b9b7 --- /dev/null +++ b/pebbles-state/src/lib.rs @@ -0,0 +1,42 @@ +#[no_std] + +use gmeta::metawasm; +use gstd::{exec, prelude::*}; +use pebbles_game_io::*; + +/// customsize game state function +#[metawasm] +pub mod metafns { + pub type State = GameState; + + pub fn game_state(state: State) -> State { + State { + pebbles_count: state.pebbles_count, + max_pebbles_per_turn: state.max_pebbles_per_turn, + pebbles_remaining: state.pebbles_remaining, + difficulty: state.difficulty, + first_player: state.first_player, + winner: state.winner, + } + } + + pub fn pebbles_count(state: State) -> u32 { + state.pebbles_count + } + pub fn max_pebbles_per_turn(state: State) -> u32 { + state.max_pebbles_per_turn + } + pub fn pebbles_remaining(state: State) -> u32 { + state.pebbles_remaining + } + pub fn get_difficulty(state: State) -> DifficultyLevel { + state.difficulty + } + pub fn first_player(state: State) -> Player { + state.first_player + } + pub fn get_winner(state: State) -> Player { + state.winner + } + +} diff --git a/src/lib.rs b/src/lib.rs index 634a13f..3257d27 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,24 +1,272 @@ #![no_std] - +#![allow(clippy::redundant_pattern_matching)] use gstd::*; use pebbles_game_io::*; static mut PEBBLES_GAME: Option = None; +// There are two difficulty levels in the game: +// DifficultyLevel::Easy and DifficultyLevel::Hard. +// Program should choose the pebbles count to be removed randomly at the easy level, +// and find the best pebbles count (find a winning strategy) at the hard level. + +// Use the following helper function to get a random 32-bit number: +pub fn get_random_u32() -> u32 { + let salt = msg::id(); + let (hash, _num) = exec::random(salt.into()).expect("get_random_u32(): random call failed"); + u32::from_le_bytes([hash[0], hash[1], hash[2], hash[3]]) +} + +// In easy mode, the Program randomly selects between 1 and the maximum number of pebbles that can be removed in each round. +// In hard mode, in order to ensure that the Program achieves ultimate victory as much as possible, +// it is necessary to make the Program obtain the specified pebble number as much as possible each time. + +// O: The current reached pebble serial number, +// A: This program selects the specified pebble serial number as much as possible, +// B: The remaining number of pebbles, +// C: Maximum subtractable quantity per round. +// Formula: A - O = B % (C + 1) +pub fn pebbles_auto_remove(game_state: &GameState) -> u32 { + match game_state.difficulty { + DifficultyLevel::Easy => (get_random_u32() % (game_state.max_pebbles_per_turn)) + 1, + + DifficultyLevel::Hard => { + let peb_rem = game_state.pebbles_remaining; + + if peb_rem <= game_state.max_pebbles_per_turn { + peb_rem + } else { + let ret_peb = peb_rem % (game_state.max_pebbles_per_turn + 1); + // check `return pebbles` is valid or is not zero. + if ret_peb == 0 { + 1 + } else { + ret_peb + } + } + } + } +} + +// Randomly choose who plays is the first player. +pub fn choose_first_player() -> Player { + match get_random_u32() % 2 { + 0 => Player::User, + _ => Player::Program, + } +} + +// Verify the number of input pebbles and the maximum number of pebbles for each round. +pub fn check_pebbles_input( + init_msg_pebbles_count: u32, + init_msg_max_pebbles_per_turn: u32, +) -> bool { + if init_msg_pebbles_count < 1 + || init_msg_max_pebbles_per_turn < 1 + || init_msg_max_pebbles_per_turn >= init_msg_pebbles_count + { + return false; + } + true +} + +// Start or Restart a game. +pub fn restart_game( + init_msg_difficulty: DifficultyLevel, + init_msg_pebbles_count: u32, + init_msg_max_pebbles_per_turn: u32, +) { + // Checks input data for validness + if !check_pebbles_input(init_msg_pebbles_count, init_msg_max_pebbles_per_turn) { + panic!("Invalid input message: Pebbles Count or Max Pebbles per Turn is invalid."); + } + // Chooses the first player using the `choose_first_player()` function + let first_player: Player = choose_first_player(); + + // Fills the `GameState` structure + let mut pebbles_game = GameState { + difficulty: init_msg_difficulty, + pebbles_count: init_msg_pebbles_count, + max_pebbles_per_turn: init_msg_max_pebbles_per_turn, + pebbles_remaining: init_msg_pebbles_count, + first_player: first_player.clone(), + winner: None, + }; + + // Processes the first turn if the first player is Program + if first_player == Player::Program { + let program_take = pebbles_auto_remove(&pebbles_game); + pebbles_game.pebbles_remaining = + pebbles_game.pebbles_remaining.saturating_sub(program_take); + } + + unsafe { PEBBLES_GAME = Some(pebbles_game) }; +} + #[no_mangle] pub extern "C" fn init() { - unimplemented!(); + // Receives `PebblesInit` using the `msg::load` function + let load_init_msg = msg::load::().expect("Unable to load message"); + + restart_game( + load_init_msg.difficulty, + load_init_msg.pebbles_count, + load_init_msg.max_pebbles_per_turn, + ); } #[no_mangle] pub extern "C" fn handle() { - unimplemented!(); + // Receives `PebblesAction` using `msg::load` function + let load_action_msg = msg::load::().expect("Unable to load message"); + + let get_pebbles_game = unsafe { PEBBLES_GAME.get_or_insert(Default::default()) }; + + match load_action_msg { + PebblesAction::GiveUp => { + // The Program is a winner. + get_pebbles_game.winner = Some(Player::Program); + + msg::reply( + PebblesEvent::Won( + get_pebbles_game + .winner + .as_ref() + .expect("The Program Win") + .clone(), + ), + 0, + ) + .expect("Unable to reply GiveUp"); + } + + PebblesAction::Restart { + difficulty, + pebbles_count, + max_pebbles_per_turn, + } => { + // Start or Restart a game. + restart_game(difficulty.clone(), pebbles_count, max_pebbles_per_turn); + + msg::reply( + PebblesInit { + difficulty, + pebbles_count, + max_pebbles_per_turn, + }, + 0, + ) + .expect("Unable to reply Restart"); + } + + PebblesAction::Turn(mut x) => { + // The User round execute + if x > get_pebbles_game.max_pebbles_per_turn { + x = get_pebbles_game.max_pebbles_per_turn; + } + + get_pebbles_game.pebbles_remaining = + get_pebbles_game.pebbles_remaining.saturating_sub(x); + + // Checks input data for validness + if !check_pebbles_input( + get_pebbles_game.pebbles_count, + get_pebbles_game.max_pebbles_per_turn, + ) { + panic!("Invalid PebblesAction User turn message: Pebbles Count or Max Pebbles per Turn is invalid."); + } + + let peb_rem = get_pebbles_game.pebbles_remaining; + + // The reason for writing this is because in practice, + // It has been found that the same `payload` can only be used once in a branch using `msg.reply` + if peb_rem == 0 { + let won_exist = get_pebbles_game.winner.clone(); + + if let Some(_) = &won_exist { + // The Program is a winner. + msg::reply( + PebblesEvent::Won(won_exist.as_ref().expect("Game Over.").clone()), + 0, + ) + .expect("Unable to reply Turn for Winner"); + + exec::leave(); + } else { + // The User is a winner. + get_pebbles_game.winner = Some(Player::User); + + msg::reply( + PebblesEvent::Won( + get_pebbles_game + .winner + .as_ref() + .expect("Game Over.") + .clone(), + ), + 0, + ) + .expect("Unable to reply Turn for Winner"); + + exec::leave(); + // msg::send(ActorId::new(id), get_pebbles_game.clone(), 0).expect("Unable to send"); + } + } else { + msg::reply(PebblesEvent::CounterTurn(peb_rem), 0).expect("Unable to reply"); + // The Program round execute + let program_take = pebbles_auto_remove(get_pebbles_game); + + // Checks input data for validness + if !check_pebbles_input( + get_pebbles_game.pebbles_count, + get_pebbles_game.max_pebbles_per_turn, + ) { + panic!("Invalid PebblesAction Program turn message: Pebbles Count or Max Pebbles per Turn is invalid."); + } + + get_pebbles_game.pebbles_remaining = get_pebbles_game + .pebbles_remaining + .saturating_sub(program_take); + + let peb_rem = get_pebbles_game.pebbles_remaining; + + if peb_rem == 0 { + // The Program is a winner. + get_pebbles_game.winner = Some(Player::Program); + } + } + } + }; } #[no_mangle] pub extern "C" fn state() { - unimplemented!(); -} + let pebbles_game = unsafe { PEBBLES_GAME.take().expect("Error in taking current state") }; + // Checks input data for validness + if !check_pebbles_input( + pebbles_game.pebbles_count, + pebbles_game.max_pebbles_per_turn, + ) { + panic!("Invalid PebblesAction User turn message: Pebbles Count or Max Pebbles per Turn is invalid."); + } + // returns the `GameState` structure using the `msg::reply` function + msg::reply(pebbles_game, 0).expect("Failed to reply state"); +} +#[cfg(test)] +mod tests { + use crate::check_pebbles_input; + use gstd::*; + + #[test] + fn test_check_pebbles_input() { + let res: bool = check_pebbles_input(0, 0); + assert!(!res); + let res: bool = check_pebbles_input(10, 3); + assert!(res); + let res: bool = check_pebbles_input(1, 2); + assert!(!res); + } +} diff --git a/test/basic.rs b/test/basic.rs deleted file mode 100644 index fff8c3a..0000000 --- a/test/basic.rs +++ /dev/null @@ -1,7 +0,0 @@ -use gtest::{Program, System}; -use pebbles_game_io::*; - -#[test] -fn test() { - unimplemented!(); -} diff --git a/tests/basic.rs b/tests/basic.rs new file mode 100644 index 0000000..3200b66 --- /dev/null +++ b/tests/basic.rs @@ -0,0 +1,213 @@ +use crate::scale_info::prelude::time::SystemTime; +use crate::scale_info::prelude::time::UNIX_EPOCH; +use gstd::*; +use gtest::{Log, Program, System}; +use pebbles_game_io::*; + +const ADMIN: u64 = 100; +const MAX_NUMBER_OF_TURNS: u32 = 10; +const MAX_PEBBLES_PER_TURN: u32 = 5; +const PEBBLES_COUNT: u32 = 35; +const DIFFICULTY_EASY: DifficultyLevel = DifficultyLevel::Easy; +const DIFFICULTY_HARD: DifficultyLevel = DifficultyLevel::Hard; + +#[test] +fn success_restart_game() { + let system = System::new(); + + system.init_logger(); + let game = Program::current(&system); + // start game + let game_init_result = game.send( + ADMIN, + PebblesInit { + difficulty: DIFFICULTY_EASY, + pebbles_count: PEBBLES_COUNT, + max_pebbles_per_turn: MAX_PEBBLES_PER_TURN, + }, + ); + + assert!(!game_init_result.main_failed()); + + for i in 1..MAX_NUMBER_OF_TURNS { + // in the third round, perform a restart + if i == 5 { + let game_turn_5_result = game.send( + ADMIN, + PebblesAction::Restart { + difficulty: DIFFICULTY_EASY, + pebbles_count: PEBBLES_COUNT, + max_pebbles_per_turn: MAX_PEBBLES_PER_TURN, + }, + ); + assert!(!game_turn_5_result.main_failed()); + break; + } + + let random_removes = (SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("After restart removes") + .subsec_nanos() + % MAX_PEBBLES_PER_TURN) + + 1; + + let game_user_result = game.send(ADMIN, PebblesAction::Turn(random_removes)); + + assert!(!game_user_result.main_failed()); + } + let random_removes = (SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("Round removes") + .subsec_nanos() + % MAX_PEBBLES_PER_TURN) + + 1; + + let game_user_result = game.send(ADMIN, PebblesAction::Turn(random_removes)); + assert!(!game_user_result.main_failed()); + + let state: GameState = game.read_state(b"").unwrap(); + let pebbles_remaining: u32 = state.pebbles_remaining; + let winner: Option = state.winner.clone(); + + assert_ne!(pebbles_remaining, 0); + assert_eq!(winner, None); +} + +#[test] +fn success_giveup() { + let system = System::new(); + + system.init_logger(); + let game = Program::current(&system); + + let game_init_result = game.send( + ADMIN, + PebblesInit { + difficulty: DIFFICULTY_EASY, + pebbles_count: PEBBLES_COUNT, + max_pebbles_per_turn: MAX_PEBBLES_PER_TURN, + }, + ); + assert!(!game_init_result.main_failed()); + + for i in 1..MAX_NUMBER_OF_TURNS { + if i == 5 { + let user_giveup_result = game.send(ADMIN, PebblesAction::GiveUp); + + let program_win_log = Log::builder() + .dest(ADMIN) + .payload(PebblesEvent::Won(Player::Program)); + assert!(user_giveup_result.contains(&program_win_log)); + + break; + } + + let random_removes = (SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("After user giveup removes") + .subsec_nanos() + % MAX_PEBBLES_PER_TURN) + + 1; + + let user_choice = random_removes; + let game_user_result = game.send(ADMIN, PebblesAction::Turn(user_choice)); + + assert!(!game_user_result.main_failed()) + } + let state: GameState = game.read_state(b"").unwrap(); + let pebbles_remaining: u32 = state.pebbles_remaining; + let winner: Player = state.winner.as_ref().expect("The Program win").clone(); + + assert_ne!(pebbles_remaining, 0); + assert_eq!(winner, Player::Program); +} + +#[test] +fn success_run_game_with_difficulty_easy() { + let system = System::new(); + + system.init_logger(); + let game = Program::current(&system); + + let game_init_result = game.send( + ADMIN, + PebblesInit { + difficulty: DIFFICULTY_EASY, + pebbles_count: PEBBLES_COUNT, + max_pebbles_per_turn: MAX_PEBBLES_PER_TURN, + }, + ); + assert!(!game_init_result.main_failed()); + + for _ in 1..MAX_NUMBER_OF_TURNS { + let random_removes = (SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("REASON") + .subsec_nanos() + % MAX_PEBBLES_PER_TURN) + + 1; + + let game_user_result = game.send(ADMIN, PebblesAction::Turn(random_removes)); + + assert!(!game_user_result.main_failed()); + + let state: GameState = game.read_state(b"").unwrap(); + let pebbles_remaining: u32 = state.pebbles_remaining; + + if pebbles_remaining == 0 { + break; + } + } + + let state: GameState = game.read_state(b"").unwrap(); + let pebbles_remaining: u32 = state.pebbles_remaining; + let winner: Player = state.winner.as_ref().expect("REASON").clone(); + + assert_eq!(pebbles_remaining, 0); + assert!(winner == Player::Program || winner == Player::User); +} + +#[test] +fn success_run_game_with_difficulty_hard() { + let system = System::new(); + + system.init_logger(); + let game = Program::current(&system); + + let game_init_result = game.send( + ADMIN, + PebblesInit { + difficulty: DIFFICULTY_HARD, + pebbles_count: PEBBLES_COUNT, + max_pebbles_per_turn: MAX_PEBBLES_PER_TURN, + }, + ); + assert!(!game_init_result.main_failed()); + + for _ in 1..MAX_NUMBER_OF_TURNS { + let random_removes = (SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("REASON") + .subsec_nanos() + % MAX_PEBBLES_PER_TURN) + + 1; + + let game_user_result = game.send(ADMIN, PebblesAction::Turn(random_removes)); + + assert!(!game_user_result.main_failed()); + + let state: GameState = game.read_state(b"").unwrap(); + let pebbles_remaining: u32 = state.pebbles_remaining; + + if pebbles_remaining == 0 { + break; + } + } + + let state: GameState = game.read_state(b"").unwrap(); + let pebbles_remaining: u32 = state.pebbles_remaining; + let winner: Player = state.winner.as_ref().expect("REASON").clone(); + + assert_eq!(pebbles_remaining, 0); + assert!(winner == Player::Program || winner == Player::User); +}