diff --git a/docs/examples/Gaming/racingcars.md b/docs/examples/Gaming/racingcars.md index 03417f5..6d0b1d1 100644 --- a/docs/examples/Gaming/racingcars.md +++ b/docs/examples/Gaming/racingcars.md @@ -35,33 +35,31 @@ Everyone can play the game via this link - [Play Racing Cars](https://racing.var > More information about this can be found in the [README](https://github.com/gear-foundation/dapps/blob/master/frontend/apps/racing-car-game/README.md) directory of the frontend. ## Implementation details - +To implement this game, the [CarRacesService](https://github.com/gear-foundation/dapps/tree/master/contracts/car-races/app/src/services/mod.rs) was developed, which contains all the core game functionality. ### Program description The program contains the following information -```rust title="car-races/src/lib.rs" -pub struct Contract { - pub admins: Vec, - pub strategy_ids: Vec, - pub games: HashMap, - pub msg_id_to_game_id: HashMap, - pub config: Config, - pub messages_allowed: bool, +```rust title="car-races/app/src/services/mod.rs" +pub struct ContractData { + admins: Vec, + strategy_ids: Vec, + games: HashMap, + messages_allowed: bool, + dns_info: Option<(ActorId, String)>, } ``` * `admins` - game admins * `strategy_ids` - program strategy ids * `games` - game information for each player -* `msg_id_to_game_id` - this field is responsible for tracking strategy id reply messages -* `config` - game configuration * `messages_allowed` - access to playability +* `dns_info` - optional field that stores the [dDNS](../Infra/dein.md) address and the program name. Where `Game` is defined as follows: -```rust title="car-races/io/src/lib.rs" +```rust title="car-races/app/src/services/game.rs" pub struct Game { pub cars: BTreeMap, pub car_ids: Vec, @@ -72,7 +70,7 @@ pub struct Game { pub last_time_step: u64, } ``` -```rust title="car-races/io/src/lib.rs" +```rust title="car-races/app/src/services/game.rs" pub struct Car { pub position: u32, pub speed: u32, @@ -83,417 +81,228 @@ pub struct Car { ### Initialization -To initialize the game program, it only needs to be passed the game configuration - -```rust title="car-races/src/lib.rs" -#[no_mangle] -extern fn init() { - let init_msg: GameInit = msg::load().expect("Unable to load the message"); - - unsafe { - CONTRACT = Some(Contract { - admins: vec![msg::source()], - config: init_msg.config, - games: HashMap::with_capacity(20_000), - msg_id_to_game_id: HashMap::with_capacity(5_000), - ..Default::default() - }); +To initialize the game program, the game configuration and the optional DNS address and name must be provided. + +```rust title="car-races/src/app/services/mod.rs" +pub async fn init( + config: InitConfig, + dns_id_and_name: Option<(ActorId, String)>, + ) { + unsafe { + DATA = Some(ContractData { + admins: vec![exec_context.actor_id()], + games: HashMap::with_capacity(20_000), + dns_info: dns_id_and_name.clone(), + ..Default::default() + }); + CONFIG = Some(config.config); + } + if let Some((id, name)) = dns_id_and_name { + let request = [ + "Dns".encode(), + "AddNewProgram".to_string().encode(), + (name, exec::program_id()).encode(), + ] + .concat(); + + msg::send_bytes_with_gas_for_reply(id, request, 5_000_000_000, 0, 0) + .expect("Error in sending message") + .await + .expect("Error in `AddNewProgram`"); + } } -} ``` -```rust title="car-races/io/src/lib.rs" -pub struct GameInit { - pub config: Config, -} -// -pub struct Config { - pub gas_to_remove_game: u64, - pub initial_speed: u32, - pub min_speed: u32, - pub max_speed: u32, - pub gas_for_round: u64, - pub time_interval: u32, - pub max_distance: u32, - pub time: u32, - pub time_for_game_storage: u64, -} +### Service functions ``` -* `gas_to_remove_game` - gas to delete a game using delayed messages -* `initial_speed` - initial speed of the cars -* `min_speed` - the minimum speed to which the car can decelerate -* `max_speed` - the maximum speed to which the car can accelerate -* `gas_for_round` - gas for one round -* `time_interval` - time after which the game should be deleted using delayed messages -* `max_distance` - race distance -* `time` - the time the car travels each turn -* `time_for_game_storage` - game data storage time - -### Action - -```rust title="car-races/io/src/lib.rs" -pub enum GameAction { - AddAdmin(ActorId), - RemoveAdmin(ActorId), - AddStrategyIds { - car_ids: Vec, - }, - StartGame, - Play { - account: ActorId, - }, - PlayerMove { - strategy_action: StrategyAction, - }, - UpdateConfig { - gas_to_remove_game: Option, - initial_speed: Option, - min_speed: Option, - max_speed: Option, - gas_for_round: Option, - time_interval: Option, - time_for_game_storage: Option, - }, - RemoveGameInstance { - account_id: ActorId, - }, - RemoveGameInstances { - players_ids: Option>, - }, - AllowMessages(bool), -} +service CarRacesService { + + // Admin actions + //---------------------------------- + + AddAdmin : (admin: actor_id) -> null; + RemoveAdmin : (admin: actor_id) -> null; + UpdateConfig : (config: ServicesConfig) -> null; + AllowMessages : (messages_allowed: bool) -> null; + Kill : (inheritor: actor_id) -> null; + AddStrategyIds : (car_ids: vec actor_id) -> null; + RemoveGameInstance : (account: actor_id) -> null; + RemoveInstances : (player_ids: opt vec actor_id) -> null; + + // Player (game-related) actions + //---------------------------------- + + StartGame : (session_for_account: opt actor_id) -> null; + PlayerMove : (strategy_move: StrategyAction, session_for_account: opt actor_id) -> null; + + // Queries (read-only operations) + //---------------------------------- + + query Admins : () -> vec actor_id; + query AllGames : () -> vec struct { actor_id, Game }; + query ConfigState : () -> ServicesConfig; + query DnsInfo : () -> opt struct { actor_id, str }; + query Game : (account_id: actor_id) -> opt Game; + query MessagesAllowed : () -> bool; + query StrategyIds : () -> vec actor_id; + + // Events + //---------------------------------- + + events { + RoundInfo: RoundInfo; + Killed: struct { inheritor: actor_id }; + } +}; ``` -### Reply - -```rust title="car-races/io/src/lib.rs" -pub enum GameReply { - GameStarted, - NotEnoughGas, - GameFinished, - GasReserved, - StrategyAdded, - PlayersMove, +### Logic +Before starting the game, the admin must first send a message to enable or disable message processing for the game: +```rust title="car-races/app/src/services/mod.rs" +pub fn allow_messages(&mut self, messages_allowed: bool) { + let msg_src = msg::source(); + assert!(self.data().admins.contains(&msg_src), "Not admin"); + self.data_mut().messages_allowed = messages_allowed; } ``` - -### Logic - -Before starting the game, the program must specify the address of the opponent machines' programs using the command `GameAction::AddStrategyIds{car_ids: Vec}`, but this is only available to the admin. - -```rust title="car-races/src/lib.rs" -fn add_strategy_ids(&mut self, car_ids: Vec) { - assert!(self.admins.contains(&msg::source()), "You are not admin"); - - assert!(car_ids.len() == 2, "There must be 2 strategies of cars"); - self.strategy_ids = car_ids; +After that, the admin needs to add two strategy contracts that will play against the user: + +```rust title="car-races/app/src/services/mod.rs" +pub fn add_strategy_ids(&mut self, car_ids: Vec) { + let msg_src = msg::source(); + assert!(self.data().messages_allowed, "Message processing suspended"); + assert!(self.data().admins.contains(&msg_src), "Not admin"); + assert_eq!(car_ids.len(), 2, "Must be two strategies"); + self.data_mut().strategy_ids = car_ids; } ``` - -After successfully adding programs, the game can be initialized using the command `GameAction::StartGame` - -```rust title="car-races/src/lib.rs" -fn start_game(&mut self) { - let player = msg::source(); +This function, `start_game`, initializes a new game session for a player, supporting signless/gasless [sessions](https://github.com/gear-foundation/signless-gasless-session-service), which allows players to participate in games without requiring them to sign each action or pay gas fees. You can read more about signless/gasless sessions [here](/docs/about/features/gassignless.md). + +```rust title="car-races/app/src/services/mod.rs" +pub fn start_game(&mut self, session_for_account: Option) { + // Ensure that message processing is allowed before starting the game. + assert!(self.data().messages_allowed, "Message processing suspended"); + + let msg_src = msg::source(); + let sessions = SessionStorage::get_session_map(); + + // Determine the player, either from the session or the message source. + let player = get_player( + sessions, + &msg_src, + &session_for_account, + ActionsForSession::StartGame, + ); let last_time_step = exec::block_timestamp(); + let strategy_ids = self.data().strategy_ids.clone(); - let game = if let Some(game) = self.games.get_mut(&player) { - if game.state != GameState::Finished { - panic!("Please complete the game"); - } + // Check if the player already has a game; if finished, reset it, otherwise create a new one. + let game = if let Some(game) = self.data_mut().games.get_mut(&player) { + assert!(game.state == GameState::Finished, "Game already started"); game.current_round = 0; game.result = None; game.last_time_step = last_time_step; game } else { - self.games.entry(player).or_insert_with(|| Game { + self.data_mut().games.entry(player).or_insert_with(|| Game { last_time_step, ..Default::default() }) }; - game.car_ids = vec![player, self.strategy_ids[0], self.strategy_ids[1]]; + // Initialize the cars with the player and two strategy contracts. + game.car_ids = vec![player, strategy_ids[0], strategy_ids[1]]; let initial_state = Car { position: 0, - speed: self.config.initial_speed, + speed: config().initial_speed, car_actions: Vec::new(), round_result: None, }; game.cars.insert(player, initial_state.clone()); - game.cars - .insert(self.strategy_ids[0], initial_state.clone()); - game.cars.insert(self.strategy_ids[1], initial_state); + game.cars.insert(strategy_ids[0], initial_state.clone()); + game.cars.insert(strategy_ids[1], initial_state); + // Set the game state to allow player actions. game.state = GameState::PlayerAction; - msg::reply(GameReply::GameStarted, 0).expect("Error during reply"); } ``` +The function `player_move` handles the player's move in the game and ensures proper state transitions and actions based on the player's input. It also supports signless/gasless sessions to enhance player interaction without requiring manual signing or gas fees. -It's now possible to make a move with the command `GameAction::PlayerMove {strategy_action: StrategyAction}` This action saves the player's move and changes the game state, after which the move is passed to the bots and a message is sent to the bot's address to make the move. - -```rust title="car-races/src/lib.rs" -fn player_move(&mut self, strategy_move: StrategyAction) { - let player = msg::source(); - let game = self.get_game(&player); - - assert_eq!( - game.state, - GameState::PlayerAction, - "Not time for the player" - ); - match strategy_move { - StrategyAction::BuyAcceleration => { - game.buy_acceleration(); - } - StrategyAction::BuyShell => { - game.buy_shell(); - } - StrategyAction::Skip => {} - } - - game.state = GameState::Race; - game.last_time_step = exec::block_timestamp(); - let num_of_cars = game.car_ids.len() as u8; - - game.current_turn = (game.current_turn + 1) % num_of_cars; - let car_id = game.get_current_car_id(); - - let msg_id = msg::send_with_gas( - car_id, - CarAction::YourTurn(game.cars.clone()), - self.config.gas_for_round, - 0, - ) - .expect("Error in sending a message"); - - self.msg_id_to_game_id.insert(msg_id, player); -} -``` - -Messages from the machine program are received in the `handle_reply()` function, and the game state changes based on the bot's move. - -```rust title="car-races/src/lib.rs" -#[no_mangle] -extern fn handle_reply() { - let reply_to = msg::reply_to().expect("Unable to get the msg id"); - let contract = unsafe { CONTRACT.as_mut().expect("The game is not initialized") }; - - let game_id = contract - .msg_id_to_game_id - .remove(&reply_to) - .expect("Unexpected reply"); - - let game = contract - .games - .get_mut(&game_id) - .expect("Unexpected: Game does not exist"); - - let bytes = msg::load_bytes().expect("Unable to load bytes"); - // car eliminated from race for wrong payload - if let Ok(strategy) = StrategyAction::decode(&mut &bytes[..]) { - match strategy { - StrategyAction::BuyAcceleration => { - game.buy_acceleration(); - } - StrategyAction::BuyShell => { - game.buy_shell(); - } - StrategyAction::Skip => {} - } - } else { - // car eliminated from race for wrong payload - let current_car_id = game.get_current_car_id(); - game.car_ids.retain(|car_id| *car_id != current_car_id); - } - let num_of_cars = game.car_ids.len() as u8; - - game.current_turn = (game.current_turn + 1) % num_of_cars; - - // if one round is made, then we update the positions of the cars - // and send a message about the new position of the fields - if game.current_turn == 0 { - game.current_round = game.current_round.saturating_add(1); - game.update_positions(&contract.config); - } - - msg::send(exec::program_id(), GameAction::Play { account: game_id }, 0) - .expect("Error in sending a msg"); -} -``` - -At the end of `handle_reply()`, the `GameAction::Play` is called, which can only be called by the program itself. This action is needed to track the game status and send a message to another bot. - -```rust title="car-races/src/lib.rs" -fn play(&mut self, account: &ActorId) { - assert_eq!( - msg::source(), - exec::program_id(), - "Only program can send this message" +```rust title="car-races/app/src/services/mod.rs" +pub async fn player_move( + &mut self, + strategy_move: StrategyAction, + session_for_account: Option, +) { + // Ensure that message processing is allowed before processing the move. + assert!(self.data().messages_allowed, "Message processing suspended"); + + let msg_src = msg::source(); + let sessions = SessionStorage::get_session_map(); + + // Determine the player based on the message source or session. + let player = get_player( + sessions, + &msg_src, + &session_for_account, + ActionsForSession::Move, ); - let game = self.get_game(account); - - if game.state == GameState::Finished { - let result = game.result.clone(); - let cars = game.cars.clone(); - let car_ids = game.car_ids.clone(); - self.send_messages(account); - send_message_round_info(&car_ids[0], &cars, &result); - return; - } - if game.current_turn == 0 { - game.state = GameState::PlayerAction; - let result = game.result.clone(); - let cars = game.cars.clone(); - let car_ids = game.car_ids.clone(); - send_message_round_info(&car_ids[0], &cars, &result); - return; - } - - let car_id = game.get_current_car_id(); - - let msg_id = msg::send(car_id, CarAction::YourTurn(game.cars.clone()), 0) - .expect("Error in sending a message"); + // Retrieve the current game for the player. + let game = self.get_game(&player); - self.msg_id_to_game_id.insert(msg_id, *account); -} -``` -However, if the bots have already made their move, a message is sent to the player informing them of the end of the round and game information. + // Handle the player's move within an asynchronous event handler. + event_or_panic_async!(self, || async move { + // Validate the current game state. + game.verify_game_state()?; -```rust title="car-races/src/lib.rs" -fn send_message_round_info( - account: &ActorId, - cars_info: &BTreeMap, - result: &Option, -) { - let mut cars = Vec::new(); - for (car_id, info) in cars_info.iter() { - cars.push((*car_id, info.position, info.round_result.clone())); - } - msg::send( - *account, - RoundInfo { - cars, - result: result.clone(), - }, - 0, - ) - .expect("Unable to send the message about round info"); -} -``` + // Apply the player's strategy move. + game.apply_strategy_move(strategy_move); -If the game is completed, a **delayed message** will also be sent to delete the game from the program state - -```rust title="car-races/src/lib.rs" -fn send_messages(account: &ActorId, config: &Config) { - msg::send_with_gas_delayed( - exec::program_id(), - GameAction::RemoveGameInstance { - account_id: *account, - }, - config.gas_to_remove_game, - 0, - config.time_interval, - ) - .expect("Error in sending message"); -} -``` + // Transition the game state to the race phase and update the timestamp. + game.state = GameState::Race; + game.last_time_step = exec::block_timestamp(); + let num_of_cars = game.car_ids.len() as u8; -## Program metadata and state -Metadata interface description: + // Update the turn to the next car. + game.current_turn = (game.current_turn + 1) % num_of_cars; -```rust title="car-races/io/src/lib.rs" -pub struct ContractMetadata; + let mut round_info: Option = None; -impl Metadata for ContractMetadata { - type Init = In; - type Handle = InOut; - type Others = InOut<(), RoundInfo>; - type Reply = (); - type Signal = (); - type State = InOut; -} -``` -One of Gear's features is reading partial states. - -```rust title="car-races/io/src/lib.rs" -pub enum StateQuery { - Admins, - StrategyIds, - Game { account_id: ActorId }, - AllGames, - MsgIdToGameId, - Config, - MessagesAllowed, -} -``` + // Continue processing car turns until the player can act or the game finishes. + while !game.is_player_action_or_finished() { + game.process_car_turn().await?; + + // After all cars have acted, the game returns to the player. + if game.current_turn == 0 { + game.state = GameState::PlayerAction; + game.current_round = game.current_round.saturating_add(1); -```rust title="car-races/io/src/lib.rs" -pub enum StateReply { - Admins(Vec), - StrategyIds(Vec), - Game(Option), - AllGames(Vec<(ActorId, Game)>), - MsgIdToGameId(Vec<(MessageId, ActorId)>), - WaitingMsgs(Vec<(MessageId, MessageId)>), - Config(Config), - MessagesAllowed(bool), -} + // Update the positions of the cars after the round. + game.update_positions(); -``` + // Create round info to track the current state of the game. + round_info = Some(create_round_info(game)); -To display the program state information, the `state()` function is used: - -```rust title="car-races/src/lib.rs" -#[no_mangle] -extern fn state() { - let Contract { - admins, - strategy_ids, - games, - msg_id_to_game_id, - config, - messages_allowed, - } = unsafe { CONTRACT.take().expect("Failed to get state") }; - let query: StateQuery = msg::load().expect("Unable to load the state query"); - - match query { - StateQuery::Admins => { - msg::reply(StateReply::Admins(admins), 0).expect("Unable to share the state"); - } - StateQuery::StrategyIds => { - msg::reply(StateReply::StrategyIds(strategy_ids), 0) - .expect("Unable to share the state"); - } - StateQuery::Game { account_id } => { - let game = games.get(&account_id).cloned(); - msg::reply(StateReply::Game(game), 0).expect("Unable to share the state"); - } - StateQuery::AllGames => { - msg::reply(StateReply::AllGames(games.into_iter().collect()), 0) - .expect("Unable to share the state"); - } - StateQuery::MsgIdToGameId => { - msg::reply( - StateReply::MsgIdToGameId(msg_id_to_game_id.into_iter().collect()), - 0, - ) - .expect("Unable to share the state"); - } - StateQuery::Config => { - msg::reply(StateReply::Config(config), 0).expect("Unable to share the state"); + // If the game is finished, a delayed message is sent to remove the game instance. + if game.state == GameState::Finished { + send_msg_to_remove_game_instance(player); + } + } } - StateQuery::MessagesAllowed => { - msg::reply(StateReply::MessagesAllowed(messages_allowed), 0) - .expect("Unable to share the state"); + + // Return the round info as an event or handle an unexpected state. + match round_info { + Some(info) => Ok(Event::RoundInfo(info)), + None => Err(Error::UnexpectedState), } - } + }) } ``` @@ -501,6 +310,6 @@ extern fn state() { The source code of this example of Racing Cars Game program and the example of an implementation of its testing is available on [gear-foundation/dapp/contracts/car-races](https://github.com/gear-foundation/dapps/tree/master/contracts/car-races). -See also an example of the program testing implementation based on `gtest`: [gear-foundation/dapps/car-races/tests](https://github.com/gear-foundation/dapps/tree/master/contracts/car-races/tests). +See also an example of the program testing implementation based on `gtest`: [gear-foundation/dapps/car-races/tests](https://github.com/gear-foundation/dapps/tree/master/contracts/car-races/app/tests). For more details about testing programs written on Vara, refer to the [Program Testing](/docs/build/testing) article.