diff --git a/src/components/player.cairo b/src/components/player.cairo index 69b706411..128b308f2 100644 --- a/src/components/player.cairo +++ b/src/components/player.cairo @@ -1,4 +1,5 @@ use starknet::ContractAddress; +use rollyourown::PlayerState; #[derive(Component, Copy, Drop, Serde)] struct Player { @@ -10,6 +11,7 @@ struct Player { cash: u128, health: u8, turns_remaining: usize, + state: PlayerState, } #[generate_trait] @@ -22,6 +24,9 @@ impl PlayerImpl of PlayerTrait { if self.turns_remaining == 0 { return false; } + if self.state != PlayerState::Normal { + return false; + } true } diff --git a/src/components/risks.cairo b/src/components/risks.cairo index e58486940..11586ea62 100644 --- a/src/components/risks.cairo +++ b/src/components/risks.cairo @@ -18,36 +18,29 @@ struct Risks { #[key] location_id: felt252, travel: u8, - hurt: u8, - mugged: u8, - arrested: u8, + run: u8, } #[generate_trait] impl RisksImpl of RisksTrait { #[inline(always)] - fn travel(ref self: Risks, seed: felt252) -> (bool, TravelResult) { - let mut seed = seed; - let mut health_loss = 0; - let mut arrested = false; - let mut mugged = false; - let mut event_occured = false; - - if occurs(seed, self.travel) { - seed = pedersen::pedersen(seed, seed); - event_occured = true; - - // TEMP: for testing, mugging is only risk - mugged = true; - } + fn travel(ref self: Risks, seed: felt252) -> bool { + occurs(seed, self.travel) + } - (event_occured, TravelResult { arrested, mugged, health_loss }) + fn run(ref self: Risks, seed: felt252) -> bool { + occurs(seed, self.run) } } fn occurs(seed: felt252, likelihood: u8) -> bool { + if likelihood == 0 { + return false; + } + let seed: u256 = seed.into(); let result: u128 = seed.low % 100; + (result <= likelihood.into()) } @@ -55,25 +48,20 @@ fn occurs(seed: felt252, likelihood: u8) -> bool { #[available_gas(1000000)] fn test_never_occurs() { let seed = pedersen::pedersen(1, 1); - let mut risks = Risks { game_id: 0, location_id: 0, travel: 0, hurt: 0, mugged: 0, arrested: 0, }; - let (event_occured, result) = risks.travel(seed); + let mut risks = Risks { game_id: 0, location_id: 0, travel: 0, run: 0 }; + let event = risks.travel(seed); - assert(!event_occured, 'event occured'); - assert(result.health_loss == 0, 'health_loss occured'); - assert(!result.mugged, 'was mugged'); - assert(!result.arrested, 'was arrested'); + assert(event == bool::False, 'event occured'); } #[test] #[available_gas(1000000)] fn test_always_occurs() { let seed = pedersen::pedersen(1, 1); - let mut risks = Risks { - game_id: 0, location_id: 0, travel: 100, hurt: 100, mugged: 100, arrested: 100, - }; - let (event_occured, result) = risks.travel(seed); + let mut risks = Risks { game_id: 0, location_id: 0, travel: 100, run: 0 }; + let event = risks.travel(seed); - assert(event_occured, 'event did not occur'); + assert(event == bool::True, 'event did not occur'); } #[test] diff --git a/src/constants.cairo b/src/constants.cairo index 8b1ed7aee..8ec1c8587 100644 --- a/src/constants.cairo +++ b/src/constants.cairo @@ -1,9 +1,10 @@ const SCALING_FACTOR: u128 = 10_000; -const TRAVEL_RISK: u8 = 30; -const HURT_RISK: u8 = 0; -const MUGGED_RISK: u8 = 0; -const ARRESTED_RISK: u8 = 50; +const TRAVEL_RISK: u8 = 30; // 30% chance of mugged +const RUN_CHANCE: u8 = 30; // 30% chance of successfully getting away + +const RUN_PENALTY: u8 = 30; // 30% of cash lost +const PAY_PENALTY: u8 = 10; // 10% of cash lost // max drug price is $300 // min drug price is $2 diff --git a/src/lib.cairo b/src/lib.cairo index 77ed33837..715267717 100644 --- a/src/lib.cairo +++ b/src/lib.cairo @@ -7,3 +7,31 @@ mod utils; #[cfg(test)] mod tests; +#[derive(Copy, Drop, Serde, PartialEq)] +enum PlayerState { + Normal: (), + BeingMugged: (), + BeingArrested: (), +} + +impl StorageSizePlayerState of dojo::StorageSize { + #[inline(always)] + fn unpacked_size() -> usize { + 1 + } + + #[inline(always)] + fn packed_size() -> usize { + 2 + } +} + +impl PlayerStatePrintImpl of core::debug::PrintTrait { + fn print(self: PlayerState) { + match self { + PlayerState::Normal(()) => 0.print(), + PlayerState::BeingMugged(()) => 1.print(), + PlayerState::BeingArrested(()) => 2.print(), + } + } +} diff --git a/src/systems.cairo b/src/systems.cairo index 02d2bdf30..ce14a12b4 100644 --- a/src/systems.cairo +++ b/src/systems.cairo @@ -2,4 +2,5 @@ mod create; mod join; mod trade; mod travel; -mod player; +mod set_name; +mod decide; diff --git a/src/systems/create.cairo b/src/systems/create.cairo index 3904bbfc0..30c8bea63 100644 --- a/src/systems/create.cairo +++ b/src/systems/create.cairo @@ -9,6 +9,7 @@ mod create_game { use dojo::world::Context; + use rollyourown::PlayerState; use rollyourown::components::name::Name; use rollyourown::components::game::Game; use rollyourown::components::player::Player; @@ -17,8 +18,8 @@ mod create_game { use rollyourown::components::drug::{Drug, DrugTrait}; use rollyourown::components::location::{Location, LocationTrait}; use rollyourown::constants::{ - SCALING_FACTOR, TRAVEL_RISK, HURT_RISK, MUGGED_RISK, ARRESTED_RISK, MIN_CASH, MAX_CASH, - MIN_QUANITTY, MAX_QUANTITY, STARTING_CASH + SCALING_FACTOR, TRAVEL_RISK, RUN_CHANCE, MIN_CASH, MAX_CASH, MIN_QUANITTY, MAX_QUANTITY, + STARTING_CASH }; use rollyourown::utils::random; @@ -52,7 +53,7 @@ mod create_game { let game_id = ctx.world.uuid(); // game entity - set!( + set !( ctx.world, (Game { game_id, @@ -68,7 +69,7 @@ mod create_game { let seed = starknet::get_tx_info().unbox().transaction_hash; let location_id = LocationTrait::random(seed); // player entity - set!( + set !( ctx.world, ( Player { @@ -77,7 +78,8 @@ mod create_game { location_id, cash: STARTING_CASH, health: 100, - turns_remaining: max_turns + turns_remaining: max_turns, + state: PlayerState::Normal(()), }, ) ); @@ -89,15 +91,10 @@ mod create_game { match locations.pop_front() { Option::Some(location_id) => { //set location entity - set!( + set !( ctx.world, (Risks { - game_id, - location_id: *location_id, - travel: TRAVEL_RISK, - hurt: HURT_RISK, - mugged: MUGGED_RISK, - arrested: ARRESTED_RISK + game_id, location_id: *location_id, travel: TRAVEL_RISK, run: RUN_CHANCE }) ); @@ -108,14 +105,13 @@ mod create_game { loop { match drugs.pop_front() { Option::Some(drug_id) => { - // HACK: temp hack to get some randomness seed = pedersen::pedersen(seed, *drug_id); let market_cash = random(seed, MIN_CASH, MAX_CASH); let rand = random(seed, MIN_QUANITTY.into(), MAX_QUANTITY.into()); let market_quantity: usize = rand.try_into().unwrap(); //set market entity - set!( + set !( ctx.world, (Market { game_id, @@ -139,13 +135,14 @@ mod create_game { }; // emit player joined - emit!(ctx.world, PlayerJoined { game_id, player_id: ctx.origin, location_id: location_id }); + emit !( + ctx.world, PlayerJoined { game_id, player_id: ctx.origin, location_id: location_id } + ); // emit game created - emit!( - ctx.world, GameCreated { - game_id, creator: ctx.origin, start_time, max_players, max_turns - } + emit !( + ctx.world, + GameCreated { game_id, creator: ctx.origin, start_time, max_players, max_turns } ); (game_id, ctx.origin) diff --git a/src/systems/decide.cairo b/src/systems/decide.cairo new file mode 100644 index 000000000..114ff705e --- /dev/null +++ b/src/systems/decide.cairo @@ -0,0 +1,90 @@ +#[system] +mod decide { + use array::ArrayTrait; + use box::BoxTrait; + use traits::Into; + use starknet::ContractAddress; + + use dojo::world::Context; + use rollyourown::PlayerState; + use rollyourown::constants::{RUN_PENALTY, PAY_PENALTY}; + use rollyourown::components::game::{Game, GameTrait}; + use rollyourown::components::risks::{Risks, RisksTrait}; + use rollyourown::components::player::{Player, PlayerTrait}; + + #[derive(Copy, Drop, Serde, PartialEq)] + enum Action { + Pay: (), + Run: (), + } + + #[derive(Copy, Drop, Serde, PartialEq)] + enum Result { + Paid: (), + GotAway: (), + Mugged: (), + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + Decision: Decision, + Consequence: Consequence, + } + + #[derive(Drop, starknet::Event)] + struct Decision { + game_id: u32, + player_id: ContractAddress, + action: Action, + } + + #[derive(Drop, starknet::Event)] + struct Consequence { + game_id: u32, + player_id: ContractAddress, + result: Result + } + + fn execute(ctx: Context, game_id: u32, action: Action, next_location_id: felt252) { + let game = get !(ctx.world, game_id, Game); + assert(game.tick(), 'game cannot progress'); + + let player_id = ctx.origin; + let mut player = get !(ctx.world, (game_id, player_id).into(), Player); + assert(player.state != PlayerState::Normal(()), 'player response not needed'); + + let result = match action { + Action::Pay => { + emit !(ctx.world, Decision { game_id, player_id, action: Action::Pay }); + + player.cash -= 1; + Result::Paid(()) + }, + Action::Run => { + emit !(ctx.world, Decision { game_id, player_id, action: Action::Run }); + + let mut risks = get !(ctx.world, (game_id, player.location_id).into(), Risks); + let seed = starknet::get_tx_info().unbox().transaction_hash; + let got_away = risks.run(seed); + + match got_away { + bool::False => { + player.cash -= 1; + Result::Mugged(()) + }, + bool::True => { + Result::GotAway(()) + } + } + }, + }; + + player.state = PlayerState::Normal(()); + player.location_id = next_location_id; + player.turns_remaining -= 1; + set !(ctx.world, (player)); + + emit !(ctx.world, Consequence { game_id, player_id, result }); + } +} diff --git a/src/systems/join.cairo b/src/systems/join.cairo index 7b1697aba..c38c55c76 100644 --- a/src/systems/join.cairo +++ b/src/systems/join.cairo @@ -7,6 +7,7 @@ mod join_game { use dojo::world::Context; + use rollyourown::PlayerState; use rollyourown::components::game::Game; use rollyourown::components::player::Player; use rollyourown::components::location::{Location, LocationTrait}; @@ -28,7 +29,7 @@ mod join_game { fn execute(ctx: Context, game_id: u32) -> ContractAddress { let block_info = starknet::get_block_info().unbox(); - let game = get!(ctx.world, game_id, (Game)); + let game = get !(ctx.world, game_id, (Game)); assert(!game.is_finished, 'game is finished'); assert(game.max_players > game.num_players, 'game is full'); assert(game.start_time >= block_info.block_timestamp, 'already started'); @@ -36,7 +37,7 @@ mod join_game { let seed = starknet::get_tx_info().unbox().transaction_hash; let location_id = LocationTrait::random(seed); // spawn player into game - set!( + set !( ctx.world, (Player { game_id, @@ -44,12 +45,13 @@ mod join_game { location_id, cash: STARTING_CASH, health: 100, - turns_remaining: game.max_turns + turns_remaining: game.max_turns, + state: PlayerState::Normal(()), }) ); // update num players joined - set!( + set !( ctx.world, (Game { game_id, @@ -63,7 +65,9 @@ mod join_game { ); // emit player joined - emit!(ctx.world, PlayerJoined { game_id, player_id: ctx.origin, location_id: location_id }); + emit !( + ctx.world, PlayerJoined { game_id, player_id: ctx.origin, location_id: location_id } + ); ctx.origin } diff --git a/src/systems/player.cairo b/src/systems/set_name.cairo similarity index 73% rename from src/systems/player.cairo rename to src/systems/set_name.cairo index 1bf91d1b6..dc47276dd 100644 --- a/src/systems/player.cairo +++ b/src/systems/set_name.cairo @@ -8,6 +8,6 @@ mod set_name { use rollyourown::components::name::Name; fn execute(ctx: Context, game_id: u32, player_name: felt252) { - set!(ctx.world, (Name { game_id, player_id: ctx.origin, short_string: player_name, })) + set !(ctx.world, (Name { game_id, player_id: ctx.origin, short_string: player_name, })) } } diff --git a/src/systems/trade.cairo b/src/systems/trade.cairo index e328b624f..622d71144 100644 --- a/src/systems/trade.cairo +++ b/src/systems/trade.cairo @@ -40,14 +40,14 @@ mod buy { ctx: Context, game_id: u32, location_id: felt252, drug_id: felt252, quantity: usize ) { let player_id = ctx.origin; - let game = get!(ctx.world, game_id, (Game)); + let game = get !(ctx.world, game_id, (Game)); assert(game.tick(), 'cannot progress'); - let mut player = get!(ctx.world, (game_id, player_id).into(), Player); + let mut player = get !(ctx.world, (game_id, player_id).into(), Player); assert(player.location_id == location_id, 'player is not at location'); assert(player.can_continue(), 'player cannot trade'); - let mut market = get!(ctx.world, (game_id, location_id, drug_id).into(), Market); + let mut market = get !(ctx.world, (game_id, location_id, drug_id).into(), Market); let cost = market.buy(quantity); assert(cost < player.cash, 'not enough cash'); @@ -55,20 +55,20 @@ mod buy { // update market market.cash += cost; market.quantity -= quantity; - set!(ctx.world, (market)); + set !(ctx.world, (market)); // update player player.cash -= cost; - set!(ctx.world, (player)); + set !(ctx.world, (player)); - let mut player_drug = get!(ctx.world, (game_id, player_id, drug_id).into(), Drug); + let mut player_drug = get !(ctx.world, (game_id, player_id, drug_id).into(), Drug); player_drug.game_id = game_id; player_drug.player_id = player_id; player_drug.drug_id = drug_id; player_drug.quantity += quantity; - set!(ctx.world, (player_drug)); + set !(ctx.world, (player_drug)); - emit!(ctx.world, Bought { game_id, player_id, drug_id, quantity, cost }); + emit !(ctx.world, Bought { game_id, player_id, drug_id, quantity, cost }); } } @@ -107,31 +107,31 @@ mod sell { ctx: Context, game_id: u32, location_id: felt252, drug_id: felt252, quantity: usize ) { let player_id = ctx.origin; - let game = get!(ctx.world, game_id, Game); + let game = get !(ctx.world, game_id, Game); assert(game.tick(), 'cannot progress'); - let mut player = get!(ctx.world, (game_id, player_id).into(), Player); + let mut player = get !(ctx.world, (game_id, player_id).into(), Player); assert(player.location_id == location_id, 'player is not at location'); - let mut drug = get!(ctx.world, (game_id, player_id, drug_id).into(), Drug); + let mut drug = get !(ctx.world, (game_id, player_id, drug_id).into(), Drug); assert(drug.quantity >= quantity, 'not enough drugs to sell'); - let mut market = get!(ctx.world, (game_id, location_id, drug_id).into(), Market); + let mut market = get !(ctx.world, (game_id, location_id, drug_id).into(), Market); let payout = market.sell(quantity); // update market market.quantity += quantity; market.cash -= payout; - set!(ctx.world, (market)); + set !(ctx.world, (market)); // update player player.cash += payout; - set!(ctx.world, (player)); + set !(ctx.world, (player)); drug.quantity -= quantity; - set!(ctx.world, (drug)); + set !(ctx.world, (drug)); - emit!(ctx.world, Sold { game_id, player_id, drug_id, quantity, payout }); + emit !(ctx.world, Sold { game_id, player_id, drug_id, quantity, payout }); } } diff --git a/src/systems/travel.cairo b/src/systems/travel.cairo index 5757f50c9..33bbf03f5 100644 --- a/src/systems/travel.cairo +++ b/src/systems/travel.cairo @@ -7,7 +7,7 @@ mod travel { use dojo::world::Context; - + use rollyourown::PlayerState; use rollyourown::components::{game::{Game, GameTrait}, location::Location}; use rollyourown::components::player::{Player, PlayerTrait}; use rollyourown::components::risks::{Risks, RisksTrait, TravelResult}; @@ -31,9 +31,7 @@ mod travel { struct RandomEvent { game_id: u32, player_id: ContractAddress, - health_loss: u8, - mugged: bool, - arrested: bool + player_state: PlayerState, } @@ -42,42 +40,40 @@ mod travel { // 3. Update the players location to the next_location_id. // 4. Update the new locations supply based on random events. fn execute(ctx: Context, game_id: u32, next_location_id: felt252) -> bool { - let game = get!(ctx.world, game_id, Game); + let game = get !(ctx.world, game_id, Game); assert(game.tick(), 'game cannot progress'); let player_id = ctx.origin; - let mut player = get!(ctx.world, (game_id, player_id).into(), Player); + let mut player = get !(ctx.world, (game_id, player_id).into(), Player); assert(player.can_continue(), 'player cannot travel'); assert(player.location_id != next_location_id, 'already at location'); - let mut risks = get!(ctx.world, (game_id, next_location_id).into(), Risks); + let mut risks = get !(ctx.world, (game_id, next_location_id).into(), Risks); let seed = starknet::get_tx_info().unbox().transaction_hash; - let (event_occured, result) = risks.travel(seed); - if event_occured { - emit!(ctx.world, RandomEvent { - game_id, - player_id, - health_loss: result.health_loss, - mugged: result.mugged, - arrested: result.arrested, - }); - } + if risks.travel(seed) { + player.state = PlayerState::BeingMugged(()); + set !(ctx.world, (player)); + + emit !( + ctx.world, + RandomEvent { game_id, player_id, player_state: PlayerState::BeingMugged(()) } + ); - // If mugged, player loses half their cash - if result.mugged { - player.cash /= 2; + return true; } - // update player player.location_id = next_location_id; player.turns_remaining -= 1; - set!(ctx.world, (player)); + set !(ctx.world, (player)); - emit!(ctx.world, Traveled { + emit !( + ctx.world, + Traveled { game_id, player_id, from_location: player.location_id, to_location: next_location_id - }); + } + ); - event_occured + false } } diff --git a/src/tests/create.cairo b/src/tests/create.cairo index f26380896..8daf2fd9c 100644 --- a/src/tests/create.cairo +++ b/src/tests/create.cairo @@ -23,11 +23,12 @@ use rollyourown::components::drug::{drug, Drug}; use rollyourown::components::location::Location; use rollyourown::components::risks::{risks, Risks}; use rollyourown::components::name::{name, Name}; +use rollyourown::systems::decide::decide; use rollyourown::systems::travel::travel; use rollyourown::systems::trade::{buy, sell}; use rollyourown::systems::join::join_game; use rollyourown::systems::create::create_game; -use rollyourown::systems::player::set_name; +use rollyourown::systems::set_name::set_name; use rollyourown::constants::SCALING_FACTOR; const START_TIME: u64 = 0; @@ -54,6 +55,7 @@ fn spawn_game() -> (ContractAddress, u32, felt252) { systems.append(buy::TEST_CLASS_HASH); systems.append(sell::TEST_CLASS_HASH); systems.append(set_name::TEST_CLASS_HASH); + systems.append(decide::TEST_CLASS_HASH); let world = spawn_test_world(components, systems); @@ -67,7 +69,7 @@ fn spawn_game() -> (ContractAddress, u32, felt252) { let (game_id, player_id) = serde::Serde::<(u32, felt252)>::deserialize(ref res) .expect('spawn deserialization failed'); - let game = get!(world, game_id, (Game)); + let game = get !(world, game_id, (Game)); assert(game.start_time == START_TIME, 'start time mismatch'); assert(game.max_players == MAX_PLAYERS, 'max players mismatch'); assert(game.max_turns == MAX_TURNS, 'max turns mismatch'); @@ -88,7 +90,7 @@ fn spawn_player(world_address: ContractAddress, game_id: felt252) -> felt252 { let player_id = serde::Serde::::deserialize(ref res) .expect('spawn deserialization failed'); - let player = get!(world, (game_id, player_id).into(), (Player)); + let player = get !(world, (game_id, player_id).into(), (Player)); assert(player.health == 100, 'health mismatch'); assert(player.cash == 100 * SCALING_FACTOR, 'cash mismatch'); @@ -102,13 +104,13 @@ fn test_create_game() { let (world_address, game_id, player_id) = spawn_game(); let world = IWorldDispatcher { contract_address: world_address }; - let brooklyn_risks = get!(world, (game_id, 'Brooklyn').into(), (Risks)); + let brooklyn_risks = get !(world, (game_id, 'Brooklyn').into(), (Risks)); assert(brooklyn_risks.location_id == 'Brooklyn', 'not Brooklyn location'); - let queens_risks = get!(world, (game_id, 'Queens').into(), (Risks)); + let queens_risks = get !(world, (game_id, 'Queens').into(), (Risks)); assert(queens_risks.location_id == 'Queens', 'not Queens location'); - let player = get!(world, (game_id, player_id).into(), (Player)); + let player = get !(world, (game_id, player_id).into(), (Player)); assert(player.turns_remaining == 10, 'wrong Player turns remaining'); } diff --git a/src/tests/player.cairo b/src/tests/player.cairo index d48d2a62c..99eba9769 100644 --- a/src/tests/player.cairo +++ b/src/tests/player.cairo @@ -26,7 +26,7 @@ fn test_set_name() { world.execute('set_name', set_name_calldata); - let name = get!(world, (game_id, player_id), (Name)); + let name = get !(world, (game_id, player_id), (Name)); assert(name.short_string == 'Rambo', 'incorrect name'); } @@ -48,9 +48,9 @@ fn test_join_game() { let player_id = serde::Serde::::deserialize(ref res) .expect('spawn deserialization failed'); - let game = get!(world, game_id, (Game)); + let game = get !(world, game_id, (Game)); assert(game.num_players == 2, 'incorrect num players'); - let player = get!(world, (game_id, alice).into(), (Player)); + let player = get !(world, (game_id, alice).into(), (Player)); assert(player.turns_remaining == 10, 'player did not join'); } diff --git a/src/tests/travel.cairo b/src/tests/travel.cairo index 308dd8a8c..444e91b7b 100644 --- a/src/tests/travel.cairo +++ b/src/tests/travel.cairo @@ -11,12 +11,13 @@ use dojo::world::{IWorldDispatcher, IWorldDispatcherTrait}; use dojo::test_utils::spawn_test_world; +use rollyourown::PlayerState; use rollyourown::components::player::Player; use rollyourown::tests::create::{spawn_game, spawn_player}; #[test] #[available_gas(110000000)] -fn test_travel() { +fn test_travel_and_decision() { let (world_address, game_id, player_id) = spawn_game(); // creator auto joins let world = IWorldDispatcher { contract_address: world_address }; @@ -27,16 +28,17 @@ fn test_travel() { world.execute('travel', travel_calldata); - let player = get!(world, (game_id, player_id).into(), (Player)); - assert(player.location_id == brooklyn_id, 'incorrect travel'); + let player = get !(world, (game_id, player_id).into(), (Player)); + assert(player.state == PlayerState::BeingMugged(()), 'incorrect state'); + assert(player.location_id != brooklyn_id, 'should not have traveled'); - let queens_id = 'Queens'; - let mut travel_calldata = array::ArrayTrait::::new(); - travel_calldata.append(game_id.into()); - travel_calldata.append(queens_id); + let mut decision_calldata = array::ArrayTrait::::new(); + decision_calldata.append(game_id.into()); + decision_calldata.append(0.into()); // 0 = pay + decision_calldata.append(brooklyn_id); - world.execute('travel', travel_calldata); + world.execute('decide', decision_calldata); - let player = get!(world, (game_id, player_id).into(), (Player)); - assert(player.location_id == queens_id, 'incorrect travel'); + let player = get !(world, (game_id, player_id).into(), (Player)); + assert(player.location_id == brooklyn_id, 'should have traveled'); }