Vara-Man Game
Vara-Man is a classic arcade game where the player must collect all the coins in a maze within a set time and avoid enemies.
The maze consists of three types of zones. The green zone is a rest area where enemies cannot enter, but it contains no coins. The blue zone is where players must collect available coins and avoid enemies. This zone offers many opportunities to evade enemies. The red zone, smaller in size, has fewer turns for escaping enemies, but it contains gold coins that provide more game points.
The game has two modes: single-player, where a player can independently play and refine their skills.
In the multiplayer mode, a tournament can be initiated where up to eight players can compete. The tournament winner receives the entire prize pool. One player creates a game lobby and sets the tournament properties - its duration, difficulty, and an entry fee that each player must pay upon entering the game, which constitutes the total prize fund. Other players join the game using the Vara address of the player who created the game. The entry fee can be set to zero, in which case the prize fund will also be empty.
The source code is available on GitHub. This article describes the program interface, data structure, basic functions and explains their purpose. It can be used as is or modified to suit your own scenarios. The game has several reward modes, one of which requires connecting the fungible token program that needs to be uploaded separately.
Anyone can play the game via this link - Play VaraMan (VARA tokens are requred for gas fees).
How to run
- Build a program
Additional details regarding this matter can be located within the README directory of the program.
- Upload the program to the Vara Network Testnet
Further details regarding the process of program uploading can be located within the Getting Started section.
- Build and run user interface
More information about this can be found in the README directory of the frontend.
- Optional.
In case of a reward mode in the form of fungible tokens , build the program as described in the README and upload the program to the Vara Network Testnet. After that it will be necessary to send a message
VaraManAction::ChangeStatus
to the program to put it in statusStartedWithFungibleToken { ft_address }
, where the address of the loaded program should be specified.
Implementation details
The VaraMan program contains the following information:
struct VaraMan {
games: HashMap<ActorId, GameInstance>,
players: HashMap<ActorId, Player>,
status: Status,
config: Config,
admins: Vec<ActorId>,
}
games
- this field contains the addresses of the players and information about their gamesplayers
- information about the player according to his addressstatus
- program statusconfig
- program configurationadmins
- admins addresses
Where the structure of the GameInstance
and the GameInstance
is defined as follows
pub struct GameInstance {
pub level: Level,
pub gold_coins: u64,
pub silver_coins: u64,
}
level
- level of difficulty (Easy/Medium/Hard)gold_coins
- number of gold coins collectedsilver_coins
- number of silver coins collected
pub struct Player {
pub name: String,
pub lives: u64,
pub claimed_gold_coins: u64,
pub claimed_silver_coins: u64,
}
name
- player's namelives
- number of livesclaimed_gold_coins
- number of gold coins earnedclaimed_silver_coins
- number of silver coins earned
The program has several modes:
pub enum Status {
#[default]
Paused,
StartedUnrewarded,
StartedWithFungibleToken {
ft_address: ActorId,
},
StartedWithNativeToken,
}
- Paused - this status means that the game is suspended
- StartedUnrewarded - the mode in which the reward is not given
- StartedWithFungibleToken - the reward is given in the form of fungible tokens (the fungible token program must be uploaded separately)
- StartedWithNativeToken - the reward is given in the form of native tokens
Initialization
To initialize the game program, it only needs to be passed game configuration
pub struct VaraManInit {
pub config: Config,
}
pub struct Config {
pub one_coin_in_value: u64,
pub tokens_per_gold_coin_easy: u64,
pub tokens_per_silver_coin_easy: u64,
pub tokens_per_gold_coin_medium: u64,
pub tokens_per_silver_coin_medium: u64,
pub tokens_per_gold_coin_hard: u64,
pub tokens_per_silver_coin_hard: u64,
pub gold_coins: u64,
pub silver_coins: u64,
pub number_of_lives: u64,
}
one_coin_in_value
- the price of one coin in the value systemtokens_per_gold_coin_easy
- gold coin price at easy leveltokens_per_silver_coin_easy
- silver coin price at easy leveltokens_per_gold_coin_medium
- gold coin price at medium leveltokens_per_silver_coin_medium
- silver coin price at medium leveltokens_per_gold_coin_hard
- gold coin price at hard leveltokens_per_silver_coin_hard
- silver coin price at hard levelgold_coins
- the maximum number of gold coins earnedsilver_coins
- the maximum number of silver coins earnednumber_of_lives
- the number of lives given to the players
Action
pub enum VaraManAction {
StartGame { level: Level },
RegisterPlayer { name: String },
ClaimReward { silver_coins: u64, gold_coins: u64 },
ChangeStatus(Status),
ChangeConfig(Config),
AddAdmin(ActorId),
}
Event
pub enum VaraManEvent {
GameStarted,
RewardClaimed {
player_address: ActorId,
silver_coins: u64,
gold_coins: u64,
},
AdminAdded(ActorId),
PlayerRegistered(ActorId),
StatusChanged(Status),
ConfigChanged(Config),
Error(String),
}
Logic
Before starting the game, it is necessary to register the player by specifying the player's nickname. Registration can reply with error if the game status is on Paused
, the nickname is a empty string or the player is already registered.
VaraManAction::RegisterPlayer { name } => {
let actor_id = msg::source();
if vara_man.status == Status::Paused {
return Err(VaraManError::WrongStatus);
}
if name.is_empty() {
return Err(VaraManError::EmptyName);
}
if vara_man.players.contains_key(&actor_id) {
Err(VaraManError::AlreadyRegistered)
} else {
vara_man.players.insert(
actor_id,
Player {
name,
lives: vara_man.config.number_of_lives,
claimed_gold_coins: 0,
claimed_silver_coins: 0,
},
);
Ok(VaraManEvent::PlayerRegistered(actor_id))
}
}
If the registration was successful, a game VaraManAction::StartGame
message can be sent. The start of the game may end with an error if the status is on Paused
, the player is not registered yet, player already start game or lives is ended.
VaraManAction::StartGame { level } => {
let player_address = msg::source();
if vara_man.status == Status::Paused {
return Err(VaraManError::WrongStatus);
}
let Some(player) = vara_man.players.get_mut(&player_address) else {
return Err(VaraManError::NotRegistered);
};
if vara_man.games.get(&player_address).is_some() {
return Err(VaraManError::AlreadyStartGame);
};
if !player.is_have_lives() && !vara_man.admins.contains(&player_address) {
return Err(VaraManError::LivesEnded);
}
vara_man.games.insert(
player_address,
GameInstance {
level,
gold_coins: vara_man.config.gold_coins,
silver_coins: vara_man.config.silver_coins,
is_claimed: false,
},
);
Ok(VaraManEvent::GameStarted)
}
When the player ends the game, the frontend will send a message about how many coins the player has earned to convert them to value and send them to the player. This message may end in an error if the player is not started the game, the status is on Paused
, the player is not registered yet, the number of coins exceeds the allowed number of coins or transfer of the value failed.
VaraManAction::ClaimReward {
silver_coins,
gold_coins,
} => {
let player_address = msg::source();
if let Some(game) = vara_man.games.get(&player_address) {
// Check that game is not paused
if vara_man.status == Status::Paused {
return Err(VaraManError::GameIsPaused);
}
// Check that player is registered
let Some(player) = vara_man.players.get_mut(&player_address) else {
return Err(VaraManError::NotRegistered);
};
// Check passed coins range
if silver_coins > game.silver_coins || gold_coins > game.gold_coins {
return Err(VaraManError::AmountGreaterThanAllowed);
}
let (tokens_per_gold_coin, tokens_per_silver_coin) = vara_man
.config
.get_tokens_per_gold_coin_for_level(game.level);
if vara_man.status == Status::StartedWithNativeToken {
let native_tokens_amount = vara_man
.config
.one_coin_in_value
.checked_mul(tokens_per_gold_coin)
.expect("Math overflow!")
.checked_mul(gold_coins)
.expect("Math overflow!")
.checked_add(
vara_man
.config
.one_coin_in_value
.checked_mul(tokens_per_silver_coin)
.expect("Math overflow!")
.checked_mul(silver_coins)
.expect("Math overflow!"),
)
.expect("Math overflow!");
if msg::send(player_address, 0u8, native_tokens_amount as u128).is_err() {
return Err(VaraManError::TransferNativeTokenFailed);
}
} else if let Status::StartedWithFungibleToken { ft_address } = vara_man.status {
let fungible_tokens_amount = gold_coins
.checked_mul(tokens_per_gold_coin)
.expect("Math overflow!")
.checked_add(
silver_coins
.checked_mul(tokens_per_silver_coin)
.expect("Math overflow!"),
)
.expect("Math overflow!");
let transfer_response: FTEvent = msg::send_for_reply_as(
ft_address,
FTAction::Transfer {
from: exec::program_id(),
to: player_address,
amount: fungible_tokens_amount.into(),
},
0,
0,
)
.expect("Error in sending a message")
.await
.expect("Error in transfer Fungible Token");
}
player.claimed_gold_coins = player
.claimed_gold_coins
.checked_add(gold_coins)
.expect("Math overflow!");
player.claimed_silver_coins = player
.claimed_silver_coins
.checked_add(silver_coins)
.expect("Math overflow!");
vara_man.games.remove(&player_address);
if vara_man.status != Status::StartedUnrewarded
&& !vara_man.admins.contains(&player_address)
{
player.lives -= 1;
}
Ok(VaraManEvent::GameFinished {
player_address,
silver_coins,
gold_coins,
})
} else {
Err(VaraManError::GameDoesNotExist)
}
}
In the case of Status::StartedWithNativeToken
the coins are converted into naive tokens and sent to the player, in case of Status::StartedWithFungibleToken
a message is sent to the fungible token program for transferring tokens to the player's address. After the transfer is successful the game is removed and the number of lives is reduced by one ( in the Status::StartedUnrewarded
mode, lives are not taken away).
Program metadata and state
Metadata interface description:
pub struct VaraManMetadata;
impl Metadata for VaraManMetadata {
type Init = In<VaraManInit>;
type Handle = InOut<VaraManAction, Result<VaraManEvent, VaraManError>>;
type Others = ();
type Reply = ();
type Signal = ();
type State = InOut<StateQuery, StateReply>;
}
One of Gear's features is reading partial states.
pub enum StateQuery {
All,
AllGames,
AllPlayers,
Game { player_address: ActorId },
Player { player_address: ActorId },
Config,
Admins,
Status,
}